Failed Conditions
Pull Request — master (#6593)
by Thomas
68:53 queued 58:31
created

EntityGenerator::parseTokensInEntityFile()   D

Complexity

Conditions 18
Paths 86

Size

Total Lines 47
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 33
CRAP Score 18.0082

Importance

Changes 0
Metric Value
dl 0
loc 47
ccs 33
cts 34
cp 0.9706
rs 4.9205
c 0
b 0
f 0
cc 18
eloc 33
nc 86
nop 1
crap 18.0082

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/*
3
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the MIT license. For more information, see
17
 * <http://www.doctrine-project.org>.
18
 */
19
20
namespace Doctrine\ORM\Tools;
21
22
use Doctrine\Common\Collections\Collection;
23
use Doctrine\Common\Util\Inflector;
24
use Doctrine\DBAL\Types\Type;
25
use Doctrine\ORM\Mapping\ClassMetadataInfo;
26
27
/**
28
 * Generic class used to generate PHP5 entity classes from ClassMetadataInfo instances.
29
 *
30
 *     [php]
31
 *     $classes = $em->getClassMetadataFactory()->getAllMetadata();
32
 *
33
 *     $generator = new \Doctrine\ORM\Tools\EntityGenerator();
34
 *     $generator->setGenerateAnnotations(true);
35
 *     $generator->setGenerateStubMethods(true);
36
 *     $generator->setRegenerateEntityIfExists(false);
37
 *     $generator->setUpdateEntityIfExists(true);
38
 *     $generator->generate($classes, '/path/to/generate/entities');
39
 *
40
 *
41
 * @link    www.doctrine-project.org
42
 * @since   2.0
43
 * @author  Benjamin Eberlei <[email protected]>
44
 * @author  Guilherme Blanco <[email protected]>
45
 * @author  Jonathan Wage <[email protected]>
46
 * @author  Roman Borschel <[email protected]>
47
 */
48
class EntityGenerator
49
{
50
    /**
51
     * Specifies class fields should be protected.
52
     */
53
    const FIELD_VISIBLE_PROTECTED = 'protected';
54
55
    /**
56
     * Specifies class fields should be private.
57
     */
58
    const FIELD_VISIBLE_PRIVATE = 'private';
59
60
    /**
61
     * @var bool
62
     */
63
    protected $backupExisting = true;
64
65
    /**
66
     * The extension to use for written php files.
67
     *
68
     * @var string
69
     */
70
    protected $extension = '.php';
71
72
    /**
73
     * Whether or not the current ClassMetadataInfo instance is new or old.
74
     *
75
     * @var boolean
76
     */
77
    protected $isNew = true;
78
79
    /**
80
     * @var array
81
     */
82
    protected $staticReflection = [];
83
84
    /**
85
     * Number of spaces to use for indention in generated code.
86
     */
87
    protected $numSpaces = 4;
88
89
    /**
90
     * The actual spaces to use for indention.
91
     *
92
     * @var string
93
     */
94
    protected $spaces = '    ';
95
96
    /**
97
     * The class all generated entities should extend.
98
     *
99
     * @var string
100
     */
101
    protected $classToExtend;
102
103
    /**
104
     * Whether or not to generation annotations.
105
     *
106
     * @var boolean
107
     */
108
    protected $generateAnnotations = false;
109
110
    /**
111
     * @var string
112
     */
113
    protected $annotationsPrefix = '';
114
115
    /**
116
     * Whether or not to generate sub methods.
117
     *
118
     * @var boolean
119
     */
120
    protected $generateEntityStubMethods = false;
121
122
    /**
123
     * Whether or not to update the entity class if it exists already.
124
     *
125
     * @var boolean
126
     */
127
    protected $updateEntityIfExists = false;
128
129
    /**
130
     * Whether or not to re-generate entity class if it exists already.
131
     *
132
     * @var boolean
133
     */
134
    protected $regenerateEntityIfExists = false;
135
136
    /**
137
     * Visibility of the field
138
     *
139
     * @var string
140
     */
141
    protected $fieldVisibility = 'private';
142
143
    /**
144
     * Whether or not to make generated embeddables immutable.
145
     *
146
     * @var boolean.
147
     */
148
    protected $embeddablesImmutable = false;
149
150
    /**
151
     * Hash-map for handle types.
152
     *
153
     * @var array
154
     */
155
    protected $typeAlias = [
156
        Type::DATETIMETZ    => '\DateTime',
157
        Type::DATETIME      => '\DateTime',
158
        Type::DATE          => '\DateTime',
159
        Type::TIME          => '\DateTime',
160
        Type::OBJECT        => '\stdClass',
161
        Type::INTEGER       => 'int',
162
        Type::BIGINT        => 'int',
163
        Type::SMALLINT      => 'int',
164
        Type::TEXT          => 'string',
165
        Type::BLOB          => 'string',
166
        Type::DECIMAL       => 'string',
167
        Type::JSON_ARRAY    => 'array',
168
        Type::SIMPLE_ARRAY  => 'array',
169
        Type::BOOLEAN       => 'bool',
170
    ];
171
172
    /**
173
     * Hash-map to handle generator types string.
174
     *
175
     * @var array
176
     */
177
    protected static $generatorStrategyMap = [
178
        ClassMetadataInfo::GENERATOR_TYPE_AUTO      => 'AUTO',
179
        ClassMetadataInfo::GENERATOR_TYPE_SEQUENCE  => 'SEQUENCE',
180
        ClassMetadataInfo::GENERATOR_TYPE_TABLE     => 'TABLE',
181
        ClassMetadataInfo::GENERATOR_TYPE_IDENTITY  => 'IDENTITY',
182
        ClassMetadataInfo::GENERATOR_TYPE_NONE      => 'NONE',
183
        ClassMetadataInfo::GENERATOR_TYPE_UUID      => 'UUID',
184
        ClassMetadataInfo::GENERATOR_TYPE_CUSTOM    => 'CUSTOM'
185
    ];
186
187
    /**
188
     * Hash-map to handle the change tracking policy string.
189
     *
190
     * @var array
191
     */
192
    protected static $changeTrackingPolicyMap = [
193
        ClassMetadataInfo::CHANGETRACKING_DEFERRED_IMPLICIT  => 'DEFERRED_IMPLICIT',
194
        ClassMetadataInfo::CHANGETRACKING_DEFERRED_EXPLICIT  => 'DEFERRED_EXPLICIT',
195
        ClassMetadataInfo::CHANGETRACKING_NOTIFY             => 'NOTIFY',
196
    ];
197
198
    /**
199
     * Hash-map to handle the inheritance type string.
200
     *
201
     * @var array
202
     */
203
    protected static $inheritanceTypeMap = [
204
        ClassMetadataInfo::INHERITANCE_TYPE_NONE            => 'NONE',
205
        ClassMetadataInfo::INHERITANCE_TYPE_JOINED          => 'JOINED',
206
        ClassMetadataInfo::INHERITANCE_TYPE_SINGLE_TABLE    => 'SINGLE_TABLE',
207
        ClassMetadataInfo::INHERITANCE_TYPE_TABLE_PER_CLASS => 'TABLE_PER_CLASS',
208
    ];
209
210
    /**
211
     * @var string
212
     */
213
    protected static $classTemplate =
214
'<?php
215
216
<namespace>
217
<useStatement>
218
<entityAnnotation>
219
<entityClassName>
220
{
221
<entityBody>
222
}
223
';
224
225
    /**
226
     * @var string
227
     */
228
    protected static $getMethodTemplate =
229
'/**
230
 * <description>
231
 *
232
 * @return <variableType>
233
 */
234
public function <methodName>()
235
{
236
<spaces>return $this-><fieldName>;
237
}';
238
239
    /**
240
     * @var string
241
     */
242
    protected static $setMethodTemplate =
243
'/**
244
 * <description>
245
 *
246
 * @param <variableType> $<variableName>
247
 *
248
 * @return <entity>
249
 */
250
public function <methodName>(<methodTypeHint>$<variableName><variableDefault>)
251
{
252
<spaces>$this-><fieldName> = $<variableName>;
253
254
<spaces>return $this;
255
}';
256
257
    /**
258
     * @var string
259
     */
260
    protected static $addMethodTemplate =
261
'/**
262
 * <description>
263
 *
264
 * @param <variableType> $<variableName>
265
 *
266
 * @return <entity>
267
 */
268
public function <methodName>(<methodTypeHint>$<variableName>)
269
{
270
<spaces>$this-><fieldName>[] = $<variableName>;
271
272
<spaces>return $this;
273
}';
274
275
    /**
276
     * @var string
277
     */
278
    protected static $removeMethodTemplate =
279
'/**
280
 * <description>
281
 *
282
 * @param <variableType> $<variableName>
283
 *
284
 * @return boolean TRUE if this collection contained the specified element, FALSE otherwise.
285
 */
286
public function <methodName>(<methodTypeHint>$<variableName>)
287
{
288
<spaces>return $this-><fieldName>->removeElement($<variableName>);
289
}';
290
291
    /**
292
     * @var string
293
     */
294
    protected static $lifecycleCallbackMethodTemplate =
295
'/**
296
 * @<name>
297
 */
298
public function <methodName>()
299
{
300
<spaces>// Add your code here
301
}';
302
303
    /**
304
     * @var string
305
     */
306
    protected static $constructorMethodTemplate =
307
'/**
308
 * Constructor
309
 */
310
public function __construct()
311
{
312
<spaces><collections>
313
}
314
';
315
316
    /**
317
     * @var string
318
     */
319
    protected static $embeddableConstructorMethodTemplate =
320
'/**
321
 * Constructor
322
 *
323
 * <paramTags>
324
 */
325
public function __construct(<params>)
326
{
327
<spaces><fields>
328
}
329
';
330
331
    /**
332
     * Constructor.
333
     */
334 9
    public function __construct()
335
    {
336 9
        if (version_compare(\Doctrine\Common\Version::VERSION, '2.2.0-DEV', '>=')) {
337 9
            $this->annotationsPrefix = 'ORM\\';
338
        }
339 9
    }
340
341
    /**
342
     * Generates and writes entity classes for the given array of ClassMetadataInfo instances.
343
     *
344
     * @param array  $metadatas
345
     * @param string $outputDirectory
346
     *
347
     * @return void
348
     */
349
    public function generate(array $metadatas, $outputDirectory): void
350
    {
351
        foreach ($metadatas as $metadata) {
352
            $this->writeEntityClass($metadata, $outputDirectory);
353
        }
354
    }
355
356
    /**
357
     * Generates and writes entity class to disk for the given ClassMetadataInfo instance.
358
     *
359
     * @param ClassMetadataInfo $metadata
360
     * @param string            $outputDirectory
361
     *
362
     * @return void
363
     *
364
     * @throws \RuntimeException
365
     */
366
    public function writeEntityClass(ClassMetadataInfo $metadata, $outputDirectory): void
367
    {
368
        $path = $outputDirectory . '/' . str_replace('\\', DIRECTORY_SEPARATOR, $metadata->name) . $this->extension;
369
        $dir = dirname($path);
370
371
        if ( ! is_dir($dir)) {
372
            mkdir($dir, 0775, true);
373
        }
374
375
        $this->isNew = ! file_exists($path) || $this->regenerateEntityIfExists;
376
377
        if ( ! $this->isNew) {
378
            $this->parseTokensInEntityFile(file_get_contents($path));
379
        } else {
380
            $this->staticReflection[$metadata->name] = ['properties' => [], 'methods' => []];
381
        }
382
383
        if ($this->backupExisting && file_exists($path)) {
384
            $backupPath = dirname($path) . DIRECTORY_SEPARATOR . basename($path) . "~";
385
            if (!copy($path, $backupPath)) {
386
                throw new \RuntimeException("Attempt to backup overwritten entity file but copy operation failed.");
387
            }
388
        }
389
390
        // If entity doesn't exist or we're re-generating the entities entirely
391
        if ($this->isNew) {
392
            file_put_contents($path, $this->generateEntityClass($metadata));
393
        // If entity exists and we're allowed to update the entity class
394
        } elseif ($this->updateEntityIfExists) {
395
            file_put_contents($path, $this->generateUpdatedEntityClass($metadata, $path));
396
        }
397
        chmod($path, 0664);
398
    }
399
400
    /**
401
     * Generates a PHP5 Doctrine 2 entity class from the given ClassMetadataInfo instance.
402
     *
403
     * @param ClassMetadataInfo $metadata
404
     *
405
     * @return string
406
     */
407 1
    public function generateEntityClass(ClassMetadataInfo $metadata): string
408
    {
409
        $placeHolders = [
410 1
            '<namespace>',
411
            '<useStatement>',
412
            '<entityAnnotation>',
413
            '<entityClassName>',
414
            '<entityBody>'
415
        ];
416
417
        $replacements = [
418 1
            $this->generateEntityNamespace($metadata),
419 1
            $this->generateEntityUse(),
420 1
            $this->generateEntityDocBlock($metadata),
421
            $this->generateEntityClassName($metadata),
422
            $this->generateEntityBody($metadata)
423
        ];
424
425
        $code = str_replace($placeHolders, $replacements, static::$classTemplate);
426
427
        return str_replace('<spaces>', $this->spaces, $code);
428
    }
429
430
    /**
431
     * Generates the updated code for the given ClassMetadataInfo and entity at path.
432
     *
433
     * @param ClassMetadataInfo $metadata
434
     * @param string            $path
435
     *
436
     * @return string
437
     */
438
    public function generateUpdatedEntityClass(ClassMetadataInfo $metadata, $path): string
439
    {
440
        $currentCode = file_get_contents($path);
441
442
        $body = $this->generateEntityBody($metadata);
443
        $body = str_replace('<spaces>', $this->spaces, $body);
444
        $last = strrpos($currentCode, '}');
445
446
        return substr($currentCode, 0, $last) . $body . ($body ? "\n" : '') . "}\n";
447
    }
448
449
    /**
450
     * Sets the number of spaces the exported class should have.
451
     *
452
     * @param integer $numSpaces
453
     *
454
     * @return void
455
     */
456
    public function setNumSpaces($numSpaces): void
457
    {
458
        $this->spaces = str_repeat(' ', $numSpaces);
459
        $this->numSpaces = $numSpaces;
460
    }
461
462
    /**
463
     * Sets the extension to use when writing php files to disk.
464
     *
465
     * @param string $extension
466
     *
467
     * @return void
468
     */
469
    public function setExtension($extension): void
470
    {
471
        $this->extension = $extension;
472
    }
473
474
    /**
475
     * Sets the name of the class the generated classes should extend from.
476
     *
477
     * @param string $classToExtend
478
     *
479
     * @return void
480
     */
481
    public function setClassToExtend($classToExtend): void
482
    {
483
        $this->classToExtend = $classToExtend;
484
    }
485
486
    /**
487
     * Sets whether or not to generate annotations for the entity.
488
     *
489
     * @param bool $bool
490
     *
491
     * @return void
492
     */
493 9
    public function setGenerateAnnotations($bool): void
494
    {
495 9
        $this->generateAnnotations = $bool;
496 9
    }
497
498
    /**
499
     * Sets the class fields visibility for the entity (can either be private or protected).
500
     *
501
     * @param bool $visibility
502
     *
503
     * @return void
504
     *
505
     * @throws \InvalidArgumentException
506
     */
507 8
    public function setFieldVisibility($visibility): void
508
    {
509 8
        if ($visibility !== static::FIELD_VISIBLE_PRIVATE && $visibility !== static::FIELD_VISIBLE_PROTECTED) {
510
            throw new \InvalidArgumentException('Invalid provided visibility (only private and protected are allowed): ' . $visibility);
511
        }
512
513 8
        $this->fieldVisibility = $visibility;
514 8
    }
515
516
    /**
517
     * Sets whether or not to generate immutable embeddables.
518
     *
519
     * @param boolean $embeddablesImmutable
520
     */
521
    public function setEmbeddablesImmutable($embeddablesImmutable): void
522
    {
523
        $this->embeddablesImmutable = (boolean) $embeddablesImmutable;
524
    }
525
526
    /**
527
     * Sets an annotation prefix.
528
     *
529
     * @param string $prefix
530
     *
531
     * @return void
532
     */
533 9
    public function setAnnotationPrefix($prefix): void
534
    {
535 9
        $this->annotationsPrefix = $prefix;
536 9
    }
537
538
    /**
539
     * Sets whether or not to try and update the entity if it already exists.
540
     *
541
     * @param bool $bool
542
     *
543
     * @return void
544
     */
545 9
    public function setUpdateEntityIfExists($bool): void
546
    {
547 9
        $this->updateEntityIfExists = $bool;
548 9
    }
549
550
    /**
551
     * Sets whether or not to regenerate the entity if it exists.
552
     *
553
     * @param bool $bool
554
     *
555
     * @return void
556
     */
557 9
    public function setRegenerateEntityIfExists($bool): void
558
    {
559 9
        $this->regenerateEntityIfExists = $bool;
560 9
    }
561
562
    /**
563
     * Sets whether or not to generate stub methods for the entity.
564
     *
565
     * @param bool $bool
566
     *
567
     * @return void
568
     */
569 9
    public function setGenerateStubMethods($bool): void
570
    {
571 9
        $this->generateEntityStubMethods = $bool;
572 9
    }
573
574
    /**
575
     * Should an existing entity be backed up if it already exists?
576
     *
577
     * @param bool $bool
578
     *
579
     * @return void
580
     */
581
    public function setBackupExisting($bool): void
582
    {
583
        $this->backupExisting = $bool;
584
    }
585
586
    /**
587
     * @param string $type
588
     *
589
     * @return string
590
     */
591
    protected function getType($type): string
592
    {
593
        if (isset($this->typeAlias[$type])) {
594
            return $this->typeAlias[$type];
595
        }
596
597
        return $type;
598
    }
599
600
    /**
601
     * @param ClassMetadataInfo $metadata
602
     *
603
     * @return string
604
     */
605 1
    protected function generateEntityNamespace(ClassMetadataInfo $metadata): ?string
606
    {
607 1
        if ($this->hasNamespace($metadata)) {
608 1
            return 'namespace ' . $this->getNamespace($metadata) .';';
609
        }
610
    }
611
612 1
    protected function generateEntityUse(): ?string
613
    {
614 1
        if ($this->generateAnnotations) {
615 1
            return "\n".'use Doctrine\ORM\Mapping as ORM;'."\n";
616
        } else {
617
            return "";
618
        }
619
    }
620
621
    /**
622
     * @param ClassMetadataInfo $metadata
623
     *
624
     * @return string
625
     */
626
    protected function generateEntityClassName(ClassMetadataInfo $metadata): string
627
    {
628
        return 'class ' . $this->getClassName($metadata) .
629
            ($this->extendsClass() ? ' extends ' . $this->getClassToExtendName() : null);
630
    }
631
632
    /**
633
     * @param ClassMetadataInfo $metadata
634
     *
635
     * @return string
636
     */
637
    protected function generateEntityBody(ClassMetadataInfo $metadata): string
638
    {
639
        $fieldMappingProperties = $this->generateEntityFieldMappingProperties($metadata);
640
        $embeddedProperties = $this->generateEntityEmbeddedProperties($metadata);
641
        $associationMappingProperties = $this->generateEntityAssociationMappingProperties($metadata);
642
        $stubMethods = $this->generateEntityStubMethods ? $this->generateEntityStubMethods($metadata) : null;
643
        $lifecycleCallbackMethods = $this->generateEntityLifecycleCallbackMethods($metadata);
644
645
        $code = [];
646
647
        if ($fieldMappingProperties) {
648
            $code[] = $fieldMappingProperties;
649
        }
650
651
        if ($embeddedProperties) {
652
            $code[] = $embeddedProperties;
653
        }
654
655
        if ($associationMappingProperties) {
656
            $code[] = $associationMappingProperties;
657
        }
658
659
        $code[] = $this->generateEntityConstructor($metadata);
660
661
        if ($stubMethods) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $stubMethods of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
662
            $code[] = $stubMethods;
663
        }
664
665
        if ($lifecycleCallbackMethods) {
666
            $code[] = $lifecycleCallbackMethods;
667
        }
668
669
        return implode("\n", $code);
670
    }
671
672
    /**
673
     * @param ClassMetadataInfo $metadata
674
     *
675
     * @return string
676
     */
677
    protected function generateEntityConstructor(ClassMetadataInfo $metadata): string
678
    {
679
        if ($this->hasMethod('__construct', $metadata)) {
680
            return '';
681
        }
682
683
        if ($metadata->isEmbeddedClass && $this->embeddablesImmutable) {
684
            return $this->generateEmbeddableConstructor($metadata);
685
        }
686
687
        $collections = [];
688
689
        foreach ($metadata->associationMappings as $mapping) {
690
            if ($mapping['type'] & ClassMetadataInfo::TO_MANY) {
691
                $collections[] = '$this->'.$mapping['fieldName'].' = new \Doctrine\Common\Collections\ArrayCollection();';
692
            }
693
        }
694
695
        if ($collections) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $collections of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
696
            return $this->prefixCodeWithSpaces(str_replace("<collections>", implode("\n".$this->spaces, $collections), static::$constructorMethodTemplate));
697
        }
698
699
        return '';
700
    }
701
702
    /**
703
     * @param ClassMetadataInfo $metadata
704
     *
705
     * @return string
706
     */
707
    private function generateEmbeddableConstructor(ClassMetadataInfo $metadata): string
708
    {
709
        $paramTypes = [];
710
        $paramVariables = [];
711
        $params = [];
712
        $fields = [];
713
714
        // Resort fields to put optional fields at the end of the method signature.
715
        $requiredFields = [];
716
        $optionalFields = [];
717
718
        foreach ($metadata->fieldMappings as $fieldMapping) {
719
            if (empty($fieldMapping['nullable'])) {
720
                $requiredFields[] = $fieldMapping;
721
722
                continue;
723
            }
724
725
            $optionalFields[] = $fieldMapping;
726
        }
727
728
        $fieldMappings = array_merge($requiredFields, $optionalFields);
729
730
        foreach ($metadata->embeddedClasses as $fieldName => $embeddedClass) {
731
            $paramType = '\\' . ltrim($embeddedClass['class'], '\\');
732
            $paramVariable = '$' . $fieldName;
733
734
            $paramTypes[] = $paramType;
735
            $paramVariables[] = $paramVariable;
736
            $params[] = $paramType . ' ' . $paramVariable;
737
            $fields[] = '$this->' . $fieldName . ' = ' . $paramVariable . ';';
738
        }
739
740
        foreach ($fieldMappings as $fieldMapping) {
741
            if (isset($fieldMapping['declaredField']) &&
742
                isset($metadata->embeddedClasses[$fieldMapping['declaredField']])
743
            ) {
744
                continue;
745
            }
746
747
            $paramTypes[] = $this->getType($fieldMapping['type']) . (!empty($fieldMapping['nullable']) ? '|null' : '');
748
            $param = '$' . $fieldMapping['fieldName'];
749
            $paramVariables[] = $param;
750
751
            if ($fieldMapping['type'] === 'datetime') {
752
                $param = $this->getType($fieldMapping['type']) . ' ' . $param;
753
            }
754
755
            if (!empty($fieldMapping['nullable'])) {
756
                $param .= ' = null';
757
            }
758
759
            $params[] = $param;
760
761
            $fields[] = '$this->' . $fieldMapping['fieldName'] . ' = $' . $fieldMapping['fieldName'] . ';';
762
        }
763
764
        $maxParamTypeLength = max(array_map('strlen', $paramTypes));
765
        $paramTags = array_map(
766
            function ($type, $variable) use ($maxParamTypeLength) {
767
                return '@param ' . $type . str_repeat(' ', $maxParamTypeLength - strlen($type) + 1) . $variable;
768
            },
769
            $paramTypes,
770
            $paramVariables
771
        );
772
773
        // Generate multi line constructor if the signature exceeds 120 characters.
774
        if (array_sum(array_map('strlen', $params)) + count($params) * 2 + 29 > 120) {
775
            $delimiter = "\n" . $this->spaces;
776
            $params = $delimiter . implode(',' . $delimiter, $params) . "\n";
777
        } else {
778
            $params = implode(', ', $params);
779
        }
780
781
        $replacements = [
782
            '<paramTags>' => implode("\n * ", $paramTags),
783
            '<params>'    => $params,
784
            '<fields>'    => implode("\n" . $this->spaces, $fields),
785
        ];
786
787
        $constructor = str_replace(
788
            array_keys($replacements),
789
            array_values($replacements),
790
            static::$embeddableConstructorMethodTemplate
791
        );
792
793
        return $this->prefixCodeWithSpaces($constructor);
794
    }
795
796
    /**
797
     * @todo this won't work if there is a namespace in brackets and a class outside of it.
798
     *
799
     * @param string $src
800
     *
801
     * @return void
802
     */
803 5
    protected function parseTokensInEntityFile($src): void
804
    {
805 5
        $tokens = token_get_all($src);
806 5
        $tokensCount = count($tokens);
807 5
        $lastSeenNamespace = '';
808 5
        $lastSeenClass = false;
809
810 5
        $inNamespace = false;
811 5
        $inClass = false;
812
813 5
        for ($i = 0; $i < $tokensCount; $i++) {
814 5
            $token = $tokens[$i];
815 5
            if (in_array($token[0], [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT], true)) {
816 5
                continue;
817
            }
818
819 5
            if ($inNamespace) {
820 5
                if (in_array($token[0], [T_NS_SEPARATOR, T_STRING], true)) {
821 5
                    $lastSeenNamespace .= $token[1];
822 5
                } elseif (is_string($token) && in_array($token, [';', '{'], true)) {
823 5
                    $inNamespace = false;
824
                }
825
            }
826
827 5
            if ($inClass) {
828 5
                $inClass = false;
829 5
                $lastSeenClass = $lastSeenNamespace . ($lastSeenNamespace ? '\\' : '') . $token[1];
830 5
                $this->staticReflection[$lastSeenClass]['properties'] = [];
831 5
                $this->staticReflection[$lastSeenClass]['methods'] = [];
832
            }
833
834 5
            if (T_NAMESPACE === $token[0]) {
835 5
                $lastSeenNamespace = '';
836 5
                $inNamespace = true;
837 5
            } elseif (T_CLASS === $token[0] && T_DOUBLE_COLON !== $tokens[$i-1][0]) {
838 5
                $inClass = true;
839 5
            } elseif (T_FUNCTION === $token[0]) {
840 1
                if (T_STRING === $tokens[$i+2][0]) {
841 1
                    $this->staticReflection[$lastSeenClass]['methods'][] = strtolower($tokens[$i+2][1]);
842
                } elseif ($tokens[$i+2] == '&' && T_STRING === $tokens[$i+3][0]) {
843 1
                    $this->staticReflection[$lastSeenClass]['methods'][] = strtolower($tokens[$i+3][1]);
844
                }
845 5
            } elseif (in_array($token[0], [T_VAR, T_PUBLIC, T_PRIVATE, T_PROTECTED], true) && T_FUNCTION !== $tokens[$i+2][0]) {
846 1
                $this->staticReflection[$lastSeenClass]['properties'][] = substr($tokens[$i+2][1], 1);
847
            }
848
        }
849 5
    }
850
851
    /**
852
     * @param string            $property
853
     * @param ClassMetadataInfo $metadata
854
     *
855
     * @return bool
856
     */
857
    protected function hasProperty($property, ClassMetadataInfo $metadata): bool
858
    {
859
        if ($this->extendsClass() || (!$this->isNew && class_exists($metadata->name))) {
860
            // don't generate property if its already on the base class.
861
            $reflClass = new \ReflectionClass($this->getClassToExtend() ?: $metadata->name);
862
            if ($reflClass->hasProperty($property)) {
863
                return true;
864
            }
865
        }
866
867
        // check traits for existing property
868
        foreach ($this->getTraits($metadata) as $trait) {
869
            if ($trait->hasProperty($property)) {
870
                return true;
871
            }
872
        }
873
874
        return (
875
            isset($this->staticReflection[$metadata->name]) &&
876
            in_array($property, $this->staticReflection[$metadata->name]['properties'], true)
877
        );
878
    }
879
880
    /**
881
     * @param string            $method
882
     * @param ClassMetadataInfo $metadata
883
     *
884
     * @return bool
885
     */
886
    protected function hasMethod($method, ClassMetadataInfo $metadata): bool
887
    {
888
        if ($this->extendsClass() || (!$this->isNew && class_exists($metadata->name))) {
889
            // don't generate method if its already on the base class.
890
            $reflClass = new \ReflectionClass($this->getClassToExtend() ?: $metadata->name);
891
892
            if ($reflClass->hasMethod($method)) {
893
                return true;
894
            }
895
        }
896
897
        // check traits for existing method
898
        foreach ($this->getTraits($metadata) as $trait) {
899
            if ($trait->hasMethod($method)) {
900
                return true;
901
            }
902
        }
903
904
        return (
905
            isset($this->staticReflection[$metadata->name]) &&
906
            in_array(strtolower($method), $this->staticReflection[$metadata->name]['methods'], true)
907
        );
908
    }
909
910
    /**
911
     * @param ClassMetadataInfo $metadata
912
     *
913
     * @return array
914
     */
915
    protected function getTraits(ClassMetadataInfo $metadata): array
916
    {
917
        if (! ($metadata->reflClass !== null || class_exists($metadata->name))) {
918
            return [];
919
        }
920
921
        $reflClass = $metadata->reflClass === null
922
            ? new \ReflectionClass($metadata->name)
923
            : $metadata->reflClass;
924
925
        $traits = [];
926
927
        while ($reflClass !== false) {
928
            $traits = array_merge($traits, $reflClass->getTraits());
929
930
            $reflClass = $reflClass->getParentClass();
931
        }
932
933
        return $traits;
934
    }
935
936
    /**
937
     * @param ClassMetadataInfo $metadata
938
     *
939
     * @return bool
940
     */
941 1
    protected function hasNamespace(ClassMetadataInfo $metadata): bool
942
    {
943 1
        return (bool) strpos($metadata->name, '\\');
944
    }
945
946
    /**
947
     * @return bool
948
     */
949
    protected function extendsClass(): bool
950
    {
951
        return (bool) $this->classToExtend;
952
    }
953
954
    /**
955
     * @return string
956
     */
957
    protected function getClassToExtend(): string
958
    {
959
        return $this->classToExtend;
960
    }
961
962
    /**
963
     * @return string
964
     */
965
    protected function getClassToExtendName(): string
966
    {
967
        $refl = new \ReflectionClass($this->getClassToExtend());
968
969
        return '\\' . $refl->getName();
0 ignored issues
show
Bug introduced by
Consider using $refl->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
970
    }
971
972
    /**
973
     * @param ClassMetadataInfo $metadata
974
     *
975
     * @return string
976
     */
977 1
    protected function getClassName(ClassMetadataInfo $metadata): string
978
    {
979 1
        return ($pos = strrpos($metadata->name, '\\'))
980 1
            ? substr($metadata->name, $pos + 1, strlen($metadata->name)) : $metadata->name;
981
    }
982
983
    /**
984
     * @param ClassMetadataInfo $metadata
985
     *
986
     * @return string
987
     */
988 1
    protected function getNamespace(ClassMetadataInfo $metadata): string
989
    {
990 1
        return substr($metadata->name, 0, strrpos($metadata->name, '\\'));
991
    }
992
993
    /**
994
     * @param ClassMetadataInfo $metadata
995
     *
996
     * @return string
997
     */
998 1
    protected function generateEntityDocBlock(ClassMetadataInfo $metadata): string
999
    {
1000 1
        $lines = [];
1001 1
        $lines[] = '/**';
1002 1
        $lines[] = ' * ' . $this->getClassName($metadata);
1003
1004 1
        if ($this->generateAnnotations) {
1005 1
            $lines[] = ' *';
1006
1007
            $methods = [
1008 1
                'generateTableAnnotation',
1009
                'generateInheritanceAnnotation',
1010
                'generateDiscriminatorColumnAnnotation',
1011
                'generateDiscriminatorMapAnnotation',
1012
                'generateEntityAnnotation',
1013
                'generateEntityListenerAnnotation',
1014
            ];
1015
1016 1
            foreach ($methods as $method) {
1017 1
                if ($code = $this->$method($metadata)) {
1018 1
                    $lines[] = ' * ' . $code;
1019
                }
1020
            }
1021
1022
            if (isset($metadata->lifecycleCallbacks) && $metadata->lifecycleCallbacks) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $metadata->lifecycleCallbacks of type array[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1023
                $lines[] = ' * @' . $this->annotationsPrefix . 'HasLifecycleCallbacks';
1024
            }
1025
        }
1026
1027
        $lines[] = ' */';
1028
1029
        return implode("\n", $lines);
1030
    }
1031
1032
    /**
1033
     * @param ClassMetadataInfo $metadata
1034
     *
1035
     * @return string
1036
     */
1037
    protected function generateEntityAnnotation(ClassMetadataInfo $metadata): string
1038
    {
1039
        $prefix = '@' . $this->annotationsPrefix;
1040
1041
        if ($metadata->isEmbeddedClass) {
1042
            return $prefix . 'Embeddable';
1043
        }
1044
1045
        $customRepository = $metadata->customRepositoryClassName
1046
            ? '(repositoryClass="' . $metadata->customRepositoryClassName . '")'
1047
            : '';
1048
1049
        return $prefix . ($metadata->isMappedSuperclass ? 'MappedSuperclass' : 'Entity') . $customRepository;
1050
    }
1051
1052
    /**
1053
     * @param ClassMetadataInfo $metadata
1054
     *
1055
     * @return string
1056
     */
1057 1
    protected function generateTableAnnotation(ClassMetadataInfo $metadata): string
1058
    {
1059 1
        if ($metadata->isEmbeddedClass) {
1060
            return '';
1061
        }
1062
1063 1
        $table = [];
1064
1065 1
        if (isset($metadata->table['schema'])) {
1066
            $table[] = 'schema="' . $metadata->table['schema'] . '"';
1067
        }
1068
1069 1
        if (isset($metadata->table['name'])) {
1070 1
            $table[] = 'name="' . $metadata->table['name'] . '"';
1071
        }
1072
1073 1
        if (isset($metadata->table['options']) && $metadata->table['options']) {
1074 1
            $table[] = 'options={' . $this->exportTableOptions((array) $metadata->table['options']) . '}';
1075
        }
1076
1077 1
        if (isset($metadata->table['uniqueConstraints']) && $metadata->table['uniqueConstraints']) {
1078
            $constraints = $this->generateTableConstraints('UniqueConstraint', $metadata->table['uniqueConstraints']);
1079
            $table[] = 'uniqueConstraints={' . $constraints . '}';
1080
        }
1081
1082 1
        if (isset($metadata->table['indexes']) && $metadata->table['indexes']) {
1083
            $constraints = $this->generateTableConstraints('Index', $metadata->table['indexes']);
1084
            $table[] = 'indexes={' . $constraints . '}';
1085
        }
1086
1087 1
        return '@' . $this->annotationsPrefix . 'Table(' . implode(', ', $table) . ')';
1088
    }
1089
1090
    /**
1091
     * @param string $constraintName
1092
     * @param array  $constraints
1093
     *
1094
     * @return string
1095
     */
1096
    protected function generateTableConstraints($constraintName, array $constraints): string
1097
    {
1098
        $annotations = [];
1099
        foreach ($constraints as $name => $constraint) {
1100
            $columns = [];
1101
            foreach ($constraint['columns'] as $column) {
1102
                $columns[] = '"' . $column . '"';
1103
            }
1104
            $annotations[] = '@' . $this->annotationsPrefix . $constraintName . '(name="' . $name . '", columns={' . implode(', ', $columns) . '})';
1105
        }
1106
1107
        return implode(', ', $annotations);
1108
    }
1109
1110
    /**
1111
     * @param ClassMetadataInfo $metadata
1112
     *
1113
     * @return string
1114
     */
1115 1
    protected function generateInheritanceAnnotation(ClassMetadataInfo $metadata): ?string
1116
    {
1117 1
        if ($metadata->inheritanceType != ClassMetadataInfo::INHERITANCE_TYPE_NONE) {
1118
            return '@' . $this->annotationsPrefix . 'InheritanceType("'.$this->getInheritanceTypeString($metadata->inheritanceType).'")';
1119
        }
1120 1
    }
1121
1122
    /**
1123
     * @param ClassMetadataInfo $metadata
1124
     *
1125
     * @return string
1126
     */
1127
    protected function generateDiscriminatorColumnAnnotation(ClassMetadataInfo $metadata): ?string
1128
    {
1129
        if ($metadata->inheritanceType != ClassMetadataInfo::INHERITANCE_TYPE_NONE) {
1130
            $discrColumn = $metadata->discriminatorColumn;
1131
            $columnDefinition = 'name="' . $discrColumn['name']
1132
                . '", type="' . $discrColumn['type']
1133
                . '", length=' . $discrColumn['length'];
1134
1135
            return '@' . $this->annotationsPrefix . 'DiscriminatorColumn(' . $columnDefinition . ')';
1136
        }
1137
    }
1138
1139
    /**
1140
     * @param ClassMetadataInfo $metadata
1141
     *
1142
     * @return string
1143
     */
1144
    protected function generateDiscriminatorMapAnnotation(ClassMetadataInfo $metadata): ?string
1145
    {
1146
        if ($metadata->inheritanceType != ClassMetadataInfo::INHERITANCE_TYPE_NONE) {
1147
            $inheritanceClassMap = [];
1148
1149
            foreach ($metadata->discriminatorMap as $type => $class) {
1150
                $inheritanceClassMap[] .= '"' . $type . '" = "' . $class . '"';
1151
            }
1152
1153
            return '@' . $this->annotationsPrefix . 'DiscriminatorMap({' . implode(', ', $inheritanceClassMap) . '})';
1154
        }
1155
    }
1156
1157
    /**
1158
     * @param ClassMetadataInfo $metadata
1159
     *
1160
     * @return string
1161
     */
1162
    protected function generateEntityStubMethods(ClassMetadataInfo $metadata): string
1163
    {
1164
        $methods = [];
1165
1166
        foreach ($metadata->fieldMappings as $fieldMapping) {
1167
            if (isset($fieldMapping['declaredField']) &&
1168
                isset($metadata->embeddedClasses[$fieldMapping['declaredField']])
1169
            ) {
1170
                continue;
1171
            }
1172
1173
            $nullableField = $this->nullableFieldExpression($fieldMapping);
1174
1175
            if (( ! isset($fieldMapping['id']) ||
1176
                    ! $fieldMapping['id'] ||
1177
                    $metadata->generatorType == ClassMetadataInfo::GENERATOR_TYPE_NONE
1178
                ) && (! $metadata->isEmbeddedClass || ! $this->embeddablesImmutable)
1179
                && $code = $this->generateEntityStubMethod($metadata, 'set', $fieldMapping['fieldName'], $fieldMapping['type'], $nullableField)
1180
            ) {
1181
                $methods[] = $code;
1182
            }
1183
1184
            if ($code = $this->generateEntityStubMethod($metadata, 'get', $fieldMapping['fieldName'], $fieldMapping['type'], $nullableField)) {
1185
                $methods[] = $code;
1186
            }
1187
        }
1188
1189
        foreach ($metadata->embeddedClasses as $fieldName => $embeddedClass) {
1190
            if (isset($embeddedClass['declaredField'])) {
1191
                continue;
1192
            }
1193
1194
            if ( ! $metadata->isEmbeddedClass || ! $this->embeddablesImmutable) {
1195
                if ($code = $this->generateEntityStubMethod($metadata, 'set', $fieldName, $embeddedClass['class'])) {
1196
                    $methods[] = $code;
1197
                }
1198
            }
1199
1200
            if ($code = $this->generateEntityStubMethod($metadata, 'get', $fieldName, $embeddedClass['class'])) {
1201
                $methods[] = $code;
1202
            }
1203
        }
1204
1205
        foreach ($metadata->associationMappings as $associationMapping) {
1206
            if ($associationMapping['type'] & ClassMetadataInfo::TO_ONE) {
1207
                $nullable = $this->isAssociationIsNullable($associationMapping) ? 'null' : null;
1208
                if ($code = $this->generateEntityStubMethod($metadata, 'set', $associationMapping['fieldName'], $associationMapping['targetEntity'], $nullable)) {
1209
                    $methods[] = $code;
1210
                }
1211
                if ($code = $this->generateEntityStubMethod($metadata, 'get', $associationMapping['fieldName'], $associationMapping['targetEntity'], $nullable)) {
1212
                    $methods[] = $code;
1213
                }
1214
            } elseif ($associationMapping['type'] & ClassMetadataInfo::TO_MANY) {
1215
                if ($code = $this->generateEntityStubMethod($metadata, 'add', $associationMapping['fieldName'], $associationMapping['targetEntity'])) {
1216
                    $methods[] = $code;
1217
                }
1218
                if ($code = $this->generateEntityStubMethod($metadata, 'remove', $associationMapping['fieldName'], $associationMapping['targetEntity'])) {
1219
                    $methods[] = $code;
1220
                }
1221
                if ($code = $this->generateEntityStubMethod($metadata, 'get', $associationMapping['fieldName'], Collection::class)) {
1222
                    $methods[] = $code;
1223
                }
1224
            }
1225
        }
1226
1227
        return implode("\n\n", $methods);
1228
    }
1229
1230
    /**
1231
     * @param array $associationMapping
1232
     *
1233
     * @return bool
1234
     */
1235
    protected function isAssociationIsNullable(array $associationMapping): bool
1236
    {
1237
        if (isset($associationMapping['id']) && $associationMapping['id']) {
1238
            return false;
1239
        }
1240
1241
        if (isset($associationMapping['joinColumns'])) {
1242
            $joinColumns = $associationMapping['joinColumns'];
1243
        } else {
1244
            //@todo there is no way to retrieve targetEntity metadata
1245
            $joinColumns = [];
1246
        }
1247
1248
        foreach ($joinColumns as $joinColumn) {
1249
            if (isset($joinColumn['nullable']) && !$joinColumn['nullable']) {
1250
                return false;
1251
            }
1252
        }
1253
1254
        return true;
1255
    }
1256
1257
    /**
1258
     * @param ClassMetadataInfo $metadata
1259
     *
1260
     * @return string
1261
     */
1262
    protected function generateEntityLifecycleCallbackMethods(ClassMetadataInfo $metadata): string
1263
    {
1264
        if (empty($metadata->lifecycleCallbacks)) {
1265
            return '';
1266
        }
1267
1268
        $methods = [];
1269
1270
        foreach ($metadata->lifecycleCallbacks as $name => $callbacks) {
1271
            foreach ($callbacks as $callback) {
1272
                $methods[] = $this->generateLifecycleCallbackMethod($name, $callback, $metadata);
1273
            }
1274
        }
1275
1276
        return implode("\n\n", array_filter($methods));
1277
    }
1278
1279
    /**
1280
     * @param ClassMetadataInfo $metadata
1281
     *
1282
     * @return string
1283
     */
1284
    protected function generateEntityAssociationMappingProperties(ClassMetadataInfo $metadata): string
1285
    {
1286
        $lines = [];
1287
1288
        foreach ($metadata->associationMappings as $associationMapping) {
1289
            if ($this->hasProperty($associationMapping['fieldName'], $metadata)) {
1290
                continue;
1291
            }
1292
1293
            $lines[] = $this->generateAssociationMappingPropertyDocBlock($associationMapping, $metadata);
1294
            $lines[] = $this->spaces . $this->fieldVisibility . ' $' . $associationMapping['fieldName']
1295
                     . ($associationMapping['type'] == 'manyToMany' ? ' = array()' : null) . ";\n";
1296
        }
1297
1298
        return implode("\n", $lines);
1299
    }
1300
1301
    /**
1302
     * @param ClassMetadataInfo $metadata
1303
     *
1304
     * @return string
1305
     */
1306
    protected function generateEntityFieldMappingProperties(ClassMetadataInfo $metadata): string
1307
    {
1308
        $lines = [];
1309
1310
        foreach ($metadata->fieldMappings as $fieldMapping) {
1311
            if ($this->hasProperty($fieldMapping['fieldName'], $metadata) ||
1312
                $metadata->isInheritedField($fieldMapping['fieldName']) ||
1313
                (
1314
                    isset($fieldMapping['declaredField']) &&
1315
                    isset($metadata->embeddedClasses[$fieldMapping['declaredField']])
1316
                )
1317
            ) {
1318
                continue;
1319
            }
1320
1321
            $lines[] = $this->generateFieldMappingPropertyDocBlock($fieldMapping, $metadata);
1322
            $lines[] = $this->spaces . $this->fieldVisibility . ' $' . $fieldMapping['fieldName']
1323
                     . (isset($fieldMapping['options']['default']) ? ' = ' . var_export($fieldMapping['options']['default'], true) : null) . ";\n";
1324
        }
1325
1326
        return implode("\n", $lines);
1327
    }
1328
1329
    /**
1330
     * @param ClassMetadataInfo $metadata
1331
     *
1332
     * @return string
1333
     */
1334
    protected function generateEntityEmbeddedProperties(ClassMetadataInfo $metadata): string
1335
    {
1336
        $lines = [];
1337
1338
        foreach ($metadata->embeddedClasses as $fieldName => $embeddedClass) {
1339
            if (isset($embeddedClass['declaredField']) || $this->hasProperty($fieldName, $metadata)) {
1340
                continue;
1341
            }
1342
1343
            $lines[] = $this->generateEmbeddedPropertyDocBlock($embeddedClass);
1344
            $lines[] = $this->spaces . $this->fieldVisibility . ' $' . $fieldName . ";\n";
1345
        }
1346
1347
        return implode("\n", $lines);
1348
    }
1349
1350
    /**
1351
     * @param ClassMetadataInfo $metadata
1352
     * @param string            $type
1353
     * @param string            $fieldName
1354
     * @param string|null       $typeHint
1355
     * @param string|null       $defaultValue
1356
     *
1357
     * @return string
1358
     */
1359
    protected function generateEntityStubMethod(ClassMetadataInfo $metadata, $type, $fieldName, $typeHint = null, $defaultValue = null): string
1360
    {
1361
        $methodName = $type . Inflector::classify($fieldName);
1362
        $variableName = Inflector::camelize($fieldName);
1363
        if (in_array($type, ["add", "remove"])) {
1364
            $methodName = Inflector::singularize($methodName);
1365
            $variableName = Inflector::singularize($variableName);
1366
        }
1367
1368
        if ($this->hasMethod($methodName, $metadata)) {
1369
            return '';
1370
        }
1371
        $this->staticReflection[$metadata->name]['methods'][] = strtolower($methodName);
1372
1373
        $var = sprintf('%sMethodTemplate', $type);
1374
        $template = static::$$var;
1375
1376
        $methodTypeHint = null;
1377
        $types          = Type::getTypesMap();
1378
        $variableType   = $typeHint ? $this->getType($typeHint) : null;
1379
1380
        if ($typeHint && ! isset($types[$typeHint])) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $typeHint of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1381
            $variableType   =  '\\' . ltrim($variableType, '\\');
1382
            $methodTypeHint =  '\\' . $typeHint . ' ';
1383
        }
1384
1385
        $replacements = [
1386
          '<description>'       => ucfirst($type) . ' ' . $variableName . '.',
1387
          '<methodTypeHint>'    => $methodTypeHint,
1388
          '<variableType>'      => $variableType . (null !== $defaultValue ? ('|' . $defaultValue) : ''),
1389
          '<variableName>'      => $variableName,
1390
          '<methodName>'        => $methodName,
1391
          '<fieldName>'         => $fieldName,
1392
          '<variableDefault>'   => ($defaultValue !== null ) ? (' = ' . $defaultValue) : '',
1393
          '<entity>'            => $this->getClassName($metadata)
1394
        ];
1395
1396
        $method = str_replace(
1397
            array_keys($replacements),
1398
            array_values($replacements),
1399
            $template
1400
        );
1401
1402
        return $this->prefixCodeWithSpaces($method);
1403
    }
1404
1405
    /**
1406
     * @param string            $name
1407
     * @param string            $methodName
1408
     * @param ClassMetadataInfo $metadata
1409
     *
1410
     * @return string
1411
     */
1412
    protected function generateLifecycleCallbackMethod($name, $methodName, ClassMetadataInfo $metadata): string
1413
    {
1414
        if ($this->hasMethod($methodName, $metadata)) {
1415
            return '';
1416
        }
1417
        $this->staticReflection[$metadata->name]['methods'][] = $methodName;
1418
1419
        $replacements = [
1420
            '<name>'        => $this->annotationsPrefix . ucfirst($name),
1421
            '<methodName>'  => $methodName,
1422
        ];
1423
1424
        $method = str_replace(
1425
            array_keys($replacements),
1426
            array_values($replacements),
1427
            static::$lifecycleCallbackMethodTemplate
1428
        );
1429
1430
        return $this->prefixCodeWithSpaces($method);
1431
    }
1432
1433
    /**
1434
     * @param array $joinColumn
1435
     *
1436
     * @return string
1437
     */
1438
    protected function generateJoinColumnAnnotation(array $joinColumn): string
1439
    {
1440
        $joinColumnAnnot = [];
1441
1442
        if (isset($joinColumn['name'])) {
1443
            $joinColumnAnnot[] = 'name="' . $joinColumn['name'] . '"';
1444
        }
1445
1446
        if (isset($joinColumn['referencedColumnName'])) {
1447
            $joinColumnAnnot[] = 'referencedColumnName="' . $joinColumn['referencedColumnName'] . '"';
1448
        }
1449
1450
        if (isset($joinColumn['unique']) && $joinColumn['unique']) {
1451
            $joinColumnAnnot[] = 'unique=' . ($joinColumn['unique'] ? 'true' : 'false');
1452
        }
1453
1454
        if (isset($joinColumn['nullable'])) {
1455
            $joinColumnAnnot[] = 'nullable=' . ($joinColumn['nullable'] ? 'true' : 'false');
1456
        }
1457
1458
        if (isset($joinColumn['onDelete'])) {
1459
            $joinColumnAnnot[] = 'onDelete="' . ($joinColumn['onDelete'] . '"');
1460
        }
1461
1462
        if (isset($joinColumn['columnDefinition'])) {
1463
            $joinColumnAnnot[] = 'columnDefinition="' . $joinColumn['columnDefinition'] . '"';
1464
        }
1465
1466
        return '@' . $this->annotationsPrefix . 'JoinColumn(' . implode(', ', $joinColumnAnnot) . ')';
1467
    }
1468
1469
    /**
1470
     * @param array             $associationMapping
1471
     * @param ClassMetadataInfo $metadata
1472
     *
1473
     * @return string
1474
     */
1475
    protected function generateAssociationMappingPropertyDocBlock(array $associationMapping, ClassMetadataInfo $metadata): string
1476
    {
1477
        $lines = [];
1478
        $lines[] = $this->spaces . '/**';
1479
1480
        if ($associationMapping['type'] & ClassMetadataInfo::TO_MANY) {
1481
            $lines[] = $this->spaces . ' * @var \Doctrine\Common\Collections\Collection';
1482
        } else {
1483
            $lines[] = $this->spaces . ' * @var \\' . ltrim($associationMapping['targetEntity'], '\\');
1484
        }
1485
1486
        if ($this->generateAnnotations) {
1487
            $lines[] = $this->spaces . ' *';
1488
1489
            if (isset($associationMapping['id']) && $associationMapping['id']) {
1490
                $lines[] = $this->spaces . ' * @' . $this->annotationsPrefix . 'Id';
1491
1492
                if ($generatorType = $this->getIdGeneratorTypeString($metadata->generatorType)) {
1493
                    $lines[] = $this->spaces . ' * @' . $this->annotationsPrefix . 'GeneratedValue(strategy="' . $generatorType . '")';
1494
                }
1495
            }
1496
1497
            $type = null;
1498
            switch ($associationMapping['type']) {
1499
                case ClassMetadataInfo::ONE_TO_ONE:
1500
                    $type = 'OneToOne';
1501
                    break;
1502
                case ClassMetadataInfo::MANY_TO_ONE:
1503
                    $type = 'ManyToOne';
1504
                    break;
1505
                case ClassMetadataInfo::ONE_TO_MANY:
1506
                    $type = 'OneToMany';
1507
                    break;
1508
                case ClassMetadataInfo::MANY_TO_MANY:
1509
                    $type = 'ManyToMany';
1510
                    break;
1511
            }
1512
            $typeOptions = [];
1513
1514
            if (isset($associationMapping['targetEntity'])) {
1515
                $typeOptions[] = 'targetEntity="' . $associationMapping['targetEntity'] . '"';
1516
            }
1517
1518
            if (isset($associationMapping['inversedBy'])) {
1519
                $typeOptions[] = 'inversedBy="' . $associationMapping['inversedBy'] . '"';
1520
            }
1521
1522
            if (isset($associationMapping['mappedBy'])) {
1523
                $typeOptions[] = 'mappedBy="' . $associationMapping['mappedBy'] . '"';
1524
            }
1525
1526
            if ($associationMapping['cascade']) {
1527
                $cascades = [];
1528
1529
                if ($associationMapping['isCascadePersist']) $cascades[] = '"persist"';
1530
                if ($associationMapping['isCascadeRemove']) $cascades[] = '"remove"';
1531
                if ($associationMapping['isCascadeDetach']) $cascades[] = '"detach"';
1532
                if ($associationMapping['isCascadeMerge']) $cascades[] = '"merge"';
1533
                if ($associationMapping['isCascadeRefresh']) $cascades[] = '"refresh"';
1534
1535
                if (count($cascades) === 5) {
1536
                    $cascades = ['"all"'];
1537
                }
1538
1539
                $typeOptions[] = 'cascade={' . implode(',', $cascades) . '}';
1540
            }
1541
1542
            if (isset($associationMapping['orphanRemoval']) && $associationMapping['orphanRemoval']) {
1543
                $typeOptions[] = 'orphanRemoval=' . ($associationMapping['orphanRemoval'] ? 'true' : 'false');
1544
            }
1545
1546
            if (isset($associationMapping['fetch']) && $associationMapping['fetch'] !== ClassMetadataInfo::FETCH_LAZY) {
1547
                $fetchMap = [
1548
                    ClassMetadataInfo::FETCH_EXTRA_LAZY => 'EXTRA_LAZY',
1549
                    ClassMetadataInfo::FETCH_EAGER      => 'EAGER',
1550
                ];
1551
1552
                $typeOptions[] = 'fetch="' . $fetchMap[$associationMapping['fetch']] . '"';
1553
            }
1554
1555
            $lines[] = $this->spaces . ' * @' . $this->annotationsPrefix . '' . $type . '(' . implode(', ', $typeOptions) . ')';
1556
1557
            if (isset($associationMapping['joinColumns']) && $associationMapping['joinColumns']) {
1558
                $lines[] = $this->spaces . ' * @' . $this->annotationsPrefix . 'JoinColumns({';
1559
1560
                $joinColumnsLines = [];
1561
1562
                foreach ($associationMapping['joinColumns'] as $joinColumn) {
1563
                    if ($joinColumnAnnot = $this->generateJoinColumnAnnotation($joinColumn)) {
1564
                        $joinColumnsLines[] = $this->spaces . ' *   ' . $joinColumnAnnot;
1565
                    }
1566
                }
1567
1568
                $lines[] = implode(",\n", $joinColumnsLines);
1569
                $lines[] = $this->spaces . ' * })';
1570
            }
1571
1572
            if (isset($associationMapping['joinTable']) && $associationMapping['joinTable']) {
1573
                $joinTable = [];
1574
                $joinTable[] = 'name="' . $associationMapping['joinTable']['name'] . '"';
1575
1576
                if (isset($associationMapping['joinTable']['schema'])) {
1577
                    $joinTable[] = 'schema="' . $associationMapping['joinTable']['schema'] . '"';
1578
                }
1579
1580
                $lines[] = $this->spaces . ' * @' . $this->annotationsPrefix . 'JoinTable(' . implode(', ', $joinTable) . ',';
1581
                $lines[] = $this->spaces . ' *   joinColumns={';
1582
1583
                $joinColumnsLines = [];
1584
1585
                foreach ($associationMapping['joinTable']['joinColumns'] as $joinColumn) {
1586
                    $joinColumnsLines[] = $this->spaces . ' *     ' . $this->generateJoinColumnAnnotation($joinColumn);
1587
                }
1588
1589
                $lines[] = implode(",". PHP_EOL, $joinColumnsLines);
1590
                $lines[] = $this->spaces . ' *   },';
1591
                $lines[] = $this->spaces . ' *   inverseJoinColumns={';
1592
1593
                $inverseJoinColumnsLines = [];
1594
1595
                foreach ($associationMapping['joinTable']['inverseJoinColumns'] as $joinColumn) {
1596
                    $inverseJoinColumnsLines[] = $this->spaces . ' *     ' . $this->generateJoinColumnAnnotation($joinColumn);
1597
                }
1598
1599
                $lines[] = implode(",". PHP_EOL, $inverseJoinColumnsLines);
1600
                $lines[] = $this->spaces . ' *   }';
1601
                $lines[] = $this->spaces . ' * )';
1602
            }
1603
1604
            if (isset($associationMapping['orderBy'])) {
1605
                $lines[] = $this->spaces . ' * @' . $this->annotationsPrefix . 'OrderBy({';
1606
1607
                foreach ($associationMapping['orderBy'] as $name => $direction) {
1608
                    $lines[] = $this->spaces . ' *     "' . $name . '"="' . $direction . '",';
1609
                }
1610
1611
                $lines[count($lines) - 1] = substr($lines[count($lines) - 1], 0, strlen($lines[count($lines) - 1]) - 1);
1612
                $lines[] = $this->spaces . ' * })';
1613
            }
1614
        }
1615
1616
        $lines[] = $this->spaces . ' */';
1617
1618
        return implode("\n", $lines);
1619
    }
1620
1621
    /**
1622
     * @param array             $fieldMapping
1623
     * @param ClassMetadataInfo $metadata
1624
     *
1625
     * @return string
1626
     */
1627
    protected function generateFieldMappingPropertyDocBlock(array $fieldMapping, ClassMetadataInfo $metadata): string
1628
    {
1629
        $lines = [];
1630
        $lines[] = $this->spaces . '/**';
1631
        $lines[] = $this->spaces . ' * @var '
1632
            . $this->getType($fieldMapping['type'])
1633
            . ($this->nullableFieldExpression($fieldMapping) ? '|null' : '');
1634
1635
        if ($this->generateAnnotations) {
1636
            $lines[] = $this->spaces . ' *';
1637
1638
            $column = [];
1639
            if (isset($fieldMapping['columnName'])) {
1640
                $column[] = 'name="' . $fieldMapping['columnName'] . '"';
1641
            }
1642
1643
            if (isset($fieldMapping['type'])) {
1644
                $column[] = 'type="' . $fieldMapping['type'] . '"';
1645
            }
1646
1647
            if (isset($fieldMapping['length'])) {
1648
                $column[] = 'length=' . $fieldMapping['length'];
1649
            }
1650
1651
            if (isset($fieldMapping['precision'])) {
1652
                $column[] = 'precision=' .  $fieldMapping['precision'];
1653
            }
1654
1655
            if (isset($fieldMapping['scale'])) {
1656
                $column[] = 'scale=' . $fieldMapping['scale'];
1657
            }
1658
1659
            if (isset($fieldMapping['nullable'])) {
1660
                $column[] = 'nullable=' .  var_export($fieldMapping['nullable'], true);
1661
            }
1662
1663
            $options = [];
1664
1665
            if (isset($fieldMapping['options']['unsigned']) && $fieldMapping['options']['unsigned']) {
1666
                $options[] = '"unsigned"=true';
1667
            }
1668
1669
            if ($options) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $options of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1670
                $column[] = 'options={'.implode(',', $options).'}';
1671
            }
1672
1673
            if (isset($fieldMapping['columnDefinition'])) {
1674
                $column[] = 'columnDefinition="' . $fieldMapping['columnDefinition'] . '"';
1675
            }
1676
1677
            if (isset($fieldMapping['unique'])) {
1678
                $column[] = 'unique=' . var_export($fieldMapping['unique'], true);
1679
            }
1680
1681
            $lines[] = $this->spaces . ' * @' . $this->annotationsPrefix . 'Column(' . implode(', ', $column) . ')';
1682
1683
            if (isset($fieldMapping['id']) && $fieldMapping['id']) {
1684
                $lines[] = $this->spaces . ' * @' . $this->annotationsPrefix . 'Id';
1685
1686
                if ($generatorType = $this->getIdGeneratorTypeString($metadata->generatorType)) {
1687
                    $lines[] = $this->spaces.' * @' . $this->annotationsPrefix . 'GeneratedValue(strategy="' . $generatorType . '")';
1688
                }
1689
1690
                if ($metadata->sequenceGeneratorDefinition) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $metadata->sequenceGeneratorDefinition of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1691
                    $sequenceGenerator = [];
1692
1693
                    if (isset($metadata->sequenceGeneratorDefinition['sequenceName'])) {
1694
                        $sequenceGenerator[] = 'sequenceName="' . $metadata->sequenceGeneratorDefinition['sequenceName'] . '"';
1695
                    }
1696
1697
                    if (isset($metadata->sequenceGeneratorDefinition['allocationSize'])) {
1698
                        $sequenceGenerator[] = 'allocationSize=' . $metadata->sequenceGeneratorDefinition['allocationSize'];
1699
                    }
1700
1701
                    if (isset($metadata->sequenceGeneratorDefinition['initialValue'])) {
1702
                        $sequenceGenerator[] = 'initialValue=' . $metadata->sequenceGeneratorDefinition['initialValue'];
1703
                    }
1704
1705
                    $lines[] = $this->spaces . ' * @' . $this->annotationsPrefix . 'SequenceGenerator(' . implode(', ', $sequenceGenerator) . ')';
1706
                }
1707
            }
1708
1709
            if (isset($fieldMapping['version']) && $fieldMapping['version']) {
1710
                $lines[] = $this->spaces . ' * @' . $this->annotationsPrefix . 'Version';
1711
            }
1712
        }
1713
1714
        $lines[] = $this->spaces . ' */';
1715
1716
        return implode("\n", $lines);
1717
    }
1718
1719
    /**
1720
     * @param array $embeddedClass
1721
     *
1722
     * @return string
1723
     */
1724
    protected function generateEmbeddedPropertyDocBlock(array $embeddedClass): string
1725
    {
1726
        $lines = [];
1727
        $lines[] = $this->spaces . '/**';
1728
        $lines[] = $this->spaces . ' * @var \\' . ltrim($embeddedClass['class'], '\\');
1729
1730
        if ($this->generateAnnotations) {
1731
            $lines[] = $this->spaces . ' *';
1732
1733
            $embedded = ['class="' . $embeddedClass['class'] . '"'];
1734
1735
            if (isset($embeddedClass['columnPrefix'])) {
1736
                if (is_string($embeddedClass['columnPrefix'])) {
1737
                    $embedded[] = 'columnPrefix="' . $embeddedClass['columnPrefix'] . '"';
1738
                } else {
1739
                    $embedded[] = 'columnPrefix=' . var_export($embeddedClass['columnPrefix'], true);
1740
                }
1741
            }
1742
1743
            $lines[] = $this->spaces . ' * @' .
1744
                $this->annotationsPrefix . 'Embedded(' . implode(', ', $embedded) . ')';
1745
        }
1746
1747
        $lines[] = $this->spaces . ' */';
1748
1749
        return implode("\n", $lines);
1750
    }
1751
1752
    /**
1753
     * @param ClassMetadataInfo $metadata
1754
     *
1755
     * @return string
1756
     */
1757
    protected function generateEntityListenerAnnotation(ClassMetadataInfo $metadata): string
1758
    {
1759
        if (0 === \count($metadata->entityListeners)) {
1760
            return '';
1761
        }
1762
1763
        $processedClasses = [];
1764
        foreach ($metadata->entityListeners as $event => $eventListeners) {
1765
            foreach ($eventListeners as $eventListener) {
1766
                $processedClasses[] = '"' . $eventListener['class'] . '"';
1767
            }
1768
        }
1769
1770
        return \sprintf(
1771
            '%s%s({%s})',
1772
            '@' . $this->annotationsPrefix,
1773
            'EntityListeners',
1774
            \implode(',', \array_unique($processedClasses))
1775
        );
1776
    }
1777
1778
    /**
1779
     * @param string $code
1780
     * @param int    $num
1781
     *
1782
     * @return string
1783
     */
1784
    protected function prefixCodeWithSpaces($code, $num = 1): string
1785
    {
1786
        $lines = explode("\n", $code);
1787
1788
        foreach ($lines as $key => $value) {
1789
            if ( ! empty($value)) {
1790
                $lines[$key] = str_repeat($this->spaces, $num) . $lines[$key];
1791
            }
1792
        }
1793
1794
        return implode("\n", $lines);
1795
    }
1796
1797
    /**
1798
     * @param integer $type The inheritance type used by the class and its subclasses.
1799
     *
1800
     * @return string The literal string for the inheritance type.
1801
     *
1802
     * @throws \InvalidArgumentException When the inheritance type does not exist.
1803
     */
1804 1
    protected function getInheritanceTypeString($type): string
1805
    {
1806 1
        if ( ! isset(static::$inheritanceTypeMap[$type])) {
1807 1
            throw new \InvalidArgumentException(sprintf('Invalid provided InheritanceType: %s', $type));
1808
        }
1809
1810 1
        return static::$inheritanceTypeMap[$type];
1811
    }
1812
1813
    /**
1814
     * @param integer $type The policy used for change-tracking for the mapped class.
1815
     *
1816
     * @return string The literal string for the change-tracking type.
1817
     *
1818
     * @throws \InvalidArgumentException When the change-tracking type does not exist.
1819
     */
1820 1
    protected function getChangeTrackingPolicyString($type): string
1821
    {
1822 1
        if ( ! isset(static::$changeTrackingPolicyMap[$type])) {
1823 1
            throw new \InvalidArgumentException(sprintf('Invalid provided ChangeTrackingPolicy: %s', $type));
1824
        }
1825
1826 1
        return static::$changeTrackingPolicyMap[$type];
1827
    }
1828
1829
    /**
1830
     * @param integer $type The generator to use for the mapped class.
1831
     *
1832
     * @return string The literal string for the generator type.
1833
     *
1834
     * @throws \InvalidArgumentException    When the generator type does not exist.
1835
     */
1836 1
    protected function getIdGeneratorTypeString($type): string
1837
    {
1838 1
        if ( ! isset(static::$generatorStrategyMap[$type])) {
1839 1
            throw new \InvalidArgumentException(sprintf('Invalid provided IdGeneratorType: %s', $type));
1840
        }
1841
1842 1
        return static::$generatorStrategyMap[$type];
1843
    }
1844
1845
    /**
1846
     * @param array $fieldMapping
1847
     *
1848
     * @return string|null
1849
     */
1850
    private function nullableFieldExpression(array $fieldMapping): ?string
1851
    {
1852
        if (isset($fieldMapping['nullable']) && true === $fieldMapping['nullable']) {
1853
            return 'null';
1854
        }
1855
1856
        return null;
1857
    }
1858
1859
    /**
1860
     * Exports (nested) option elements.
1861
     *
1862
     * @param array $options
1863
     *
1864
     * @return string
1865
     */
1866 1
    private function exportTableOptions(array $options): string
1867
    {
1868 1
        $optionsStr = [];
1869
1870 1
        foreach ($options as $name => $option) {
1871 1
            if (is_array($option)) {
1872 1
                $optionsStr[] = '"' . $name . '"={' . $this->exportTableOptions($option) . '}';
1873
            } else {
1874 1
                $optionsStr[] = '"' . $name . '"="' . (string) $option . '"';
1875
            }
1876
        }
1877
1878 1
        return implode(',', $optionsStr);
1879
    }
1880
}
1881