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
20namespace Doctrine\ORM\Tools;
21
22use Doctrine\Common\Collections\Collection;
23use Doctrine\Common\Inflector\Inflector;
24use Doctrine\DBAL\Types\Type;
25use Doctrine\ORM\Mapping\ClassMetadataInfo;
26use ReflectionClass;
27use const E_USER_DEPRECATED;
28use function str_replace;
29use function trigger_error;
30use function var_export;
31
32/**
33 * Generic class used to generate PHP5 entity classes from ClassMetadataInfo instances.
34 *
35 *     [php]
36 *     $classes = $em->getClassMetadataFactory()->getAllMetadata();
37 *
38 *     $generator = new \Doctrine\ORM\Tools\EntityGenerator();
39 *     $generator->setGenerateAnnotations(true);
40 *     $generator->setGenerateStubMethods(true);
41 *     $generator->setRegenerateEntityIfExists(false);
42 *     $generator->setUpdateEntityIfExists(true);
43 *     $generator->generate($classes, '/path/to/generate/entities');
44 *
45 *
46 * @link    www.doctrine-project.org
47 * @since   2.0
48 * @author  Benjamin Eberlei <kontakt@beberlei.de>
49 * @author  Guilherme Blanco <guilhermeblanco@hotmail.com>
50 * @author  Jonathan Wage <jonwage@gmail.com>
51 * @author  Roman Borschel <roman@code-factory.org>
52 *
53 * @deprecated 2.7 This class is being removed from the ORM and won't have any replacement
54 */
55class EntityGenerator
56{
57    /**
58     * Specifies class fields should be protected.
59     */
60    const FIELD_VISIBLE_PROTECTED = 'protected';
61
62    /**
63     * Specifies class fields should be private.
64     */
65    const FIELD_VISIBLE_PRIVATE = 'private';
66
67    /**
68     * @var bool
69     */
70    protected $backupExisting = true;
71
72    /**
73     * The extension to use for written php files.
74     *
75     * @var string
76     */
77    protected $extension = '.php';
78
79    /**
80     * Whether or not the current ClassMetadataInfo instance is new or old.
81     *
82     * @var boolean
83     */
84    protected $isNew = true;
85
86    /**
87     * @var array
88     */
89    protected $staticReflection = [];
90
91    /**
92     * Number of spaces to use for indention in generated code.
93     */
94    protected $numSpaces = 4;
95
96    /**
97     * The actual spaces to use for indention.
98     *
99     * @var string
100     */
101    protected $spaces = '    ';
102
103    /**
104     * The class all generated entities should extend.
105     *
106     * @var string
107     */
108    protected $classToExtend;
109
110    /**
111     * Whether or not to generation annotations.
112     *
113     * @var boolean
114     */
115    protected $generateAnnotations = false;
116
117    /**
118     * @var string
119     */
120    protected $annotationsPrefix = '';
121
122    /**
123     * Whether or not to generate sub methods.
124     *
125     * @var boolean
126     */
127    protected $generateEntityStubMethods = false;
128
129    /**
130     * Whether or not to update the entity class if it exists already.
131     *
132     * @var boolean
133     */
134    protected $updateEntityIfExists = false;
135
136    /**
137     * Whether or not to re-generate entity class if it exists already.
138     *
139     * @var boolean
140     */
141    protected $regenerateEntityIfExists = false;
142
143    /**
144     * Visibility of the field
145     *
146     * @var string
147     */
148    protected $fieldVisibility = 'private';
149
150    /**
151     * Whether or not to make generated embeddables immutable.
152     *
153     * @var bool
154     */
155    protected $embeddablesImmutable = false;
156
157    /**
158     * Hash-map for handle types.
159     *
160     * @var array
161     */
162    protected $typeAlias = [
163        Type::DATETIMETZ    => '\DateTime',
164        Type::DATETIME      => '\DateTime',
165        Type::DATE          => '\DateTime',
166        Type::TIME          => '\DateTime',
167        Type::OBJECT        => '\stdClass',
168        Type::INTEGER       => 'int',
169        Type::BIGINT        => 'int',
170        Type::SMALLINT      => 'int',
171        Type::TEXT          => 'string',
172        Type::BLOB          => 'string',
173        Type::DECIMAL       => 'string',
174        Type::GUID          => 'string',
175        Type::JSON_ARRAY    => 'array',
176        Type::SIMPLE_ARRAY  => 'array',
177        Type::BOOLEAN       => 'bool',
178    ];
179
180    /**
181     * Hash-map to handle generator types string.
182     *
183     * @var array
184     */
185    protected static $generatorStrategyMap = [
186        ClassMetadataInfo::GENERATOR_TYPE_AUTO      => 'AUTO',
187        ClassMetadataInfo::GENERATOR_TYPE_SEQUENCE  => 'SEQUENCE',
188        ClassMetadataInfo::GENERATOR_TYPE_TABLE     => 'TABLE',
189        ClassMetadataInfo::GENERATOR_TYPE_IDENTITY  => 'IDENTITY',
190        ClassMetadataInfo::GENERATOR_TYPE_NONE      => 'NONE',
191        ClassMetadataInfo::GENERATOR_TYPE_UUID      => 'UUID',
192        ClassMetadataInfo::GENERATOR_TYPE_CUSTOM    => 'CUSTOM'
193    ];
194
195    /**
196     * Hash-map to handle the change tracking policy string.
197     *
198     * @var array
199     */
200    protected static $changeTrackingPolicyMap = [
201        ClassMetadataInfo::CHANGETRACKING_DEFERRED_IMPLICIT  => 'DEFERRED_IMPLICIT',
202        ClassMetadataInfo::CHANGETRACKING_DEFERRED_EXPLICIT  => 'DEFERRED_EXPLICIT',
203        ClassMetadataInfo::CHANGETRACKING_NOTIFY             => 'NOTIFY',
204    ];
205
206    /**
207     * Hash-map to handle the inheritance type string.
208     *
209     * @var array
210     */
211    protected static $inheritanceTypeMap = [
212        ClassMetadataInfo::INHERITANCE_TYPE_NONE            => 'NONE',
213        ClassMetadataInfo::INHERITANCE_TYPE_JOINED          => 'JOINED',
214        ClassMetadataInfo::INHERITANCE_TYPE_SINGLE_TABLE    => 'SINGLE_TABLE',
215        ClassMetadataInfo::INHERITANCE_TYPE_TABLE_PER_CLASS => 'TABLE_PER_CLASS',
216    ];
217
218    /**
219     * @var string
220     */
221    protected static $classTemplate =
222'<?php
223
224<namespace>
225<useStatement>
226<entityAnnotation>
227<entityClassName>
228{
229<entityBody>
230}
231';
232
233    /**
234     * @var string
235     */
236    protected static $getMethodTemplate =
237'/**
238 * <description>
239 *
240 * @return <variableType>
241 */
242public function <methodName>()
243{
244<spaces>return $this-><fieldName>;
245}';
246
247    /**
248     * @var string
249     */
250    protected static $setMethodTemplate =
251'/**
252 * <description>
253 *
254 * @param <variableType> $<variableName>
255 *
256 * @return <entity>
257 */
258public function <methodName>(<methodTypeHint>$<variableName><variableDefault>)
259{
260<spaces>$this-><fieldName> = $<variableName>;
261
262<spaces>return $this;
263}';
264
265    /**
266     * @var string
267     */
268    protected static $addMethodTemplate =
269'/**
270 * <description>
271 *
272 * @param <variableType> $<variableName>
273 *
274 * @return <entity>
275 */
276public function <methodName>(<methodTypeHint>$<variableName>)
277{
278<spaces>$this-><fieldName>[] = $<variableName>;
279
280<spaces>return $this;
281}';
282
283    /**
284     * @var string
285     */
286    protected static $removeMethodTemplate =
287'/**
288 * <description>
289 *
290 * @param <variableType> $<variableName>
291 *
292 * @return boolean TRUE if this collection contained the specified element, FALSE otherwise.
293 */
294public function <methodName>(<methodTypeHint>$<variableName>)
295{
296<spaces>return $this-><fieldName>->removeElement($<variableName>);
297}';
298
299    /**
300     * @var string
301     */
302    protected static $lifecycleCallbackMethodTemplate =
303'/**
304 * @<name>
305 */
306public function <methodName>()
307{
308<spaces>// Add your code here
309}';
310
311    /**
312     * @var string
313     */
314    protected static $constructorMethodTemplate =
315'/**
316 * Constructor
317 */
318public function __construct()
319{
320<spaces><collections>
321}
322';
323
324    /**
325     * @var string
326     */
327    protected static $embeddableConstructorMethodTemplate =
328'/**
329 * Constructor
330 *
331 * <paramTags>
332 */
333public function __construct(<params>)
334{
335<spaces><fields>
336}
337';
338
339    /**
340     * Constructor.
341     */
342    public function __construct()
343    {
344        @trigger_error(self::class . ' is deprecated and will be removed in Doctrine ORM 3.0', E_USER_DEPRECATED);
345
346        $this->annotationsPrefix = 'ORM\\';
347    }
348
349    /**
350     * Generates and writes entity classes for the given array of ClassMetadataInfo instances.
351     *
352     * @param array  $metadatas
353     * @param string $outputDirectory
354     *
355     * @return void
356     */
357    public function generate(array $metadatas, $outputDirectory)
358    {
359        foreach ($metadatas as $metadata) {
360            $this->writeEntityClass($metadata, $outputDirectory);
361        }
362    }
363
364    /**
365     * Generates and writes entity class to disk for the given ClassMetadataInfo instance.
366     *
367     * @param ClassMetadataInfo $metadata
368     * @param string            $outputDirectory
369     *
370     * @return void
371     *
372     * @throws \RuntimeException
373     */
374    public function writeEntityClass(ClassMetadataInfo $metadata, $outputDirectory)
375    {
376        $path = $outputDirectory . '/' . str_replace('\\', DIRECTORY_SEPARATOR, $metadata->name) . $this->extension;
377        $dir = dirname($path);
378
379        if ( ! is_dir($dir)) {
380            mkdir($dir, 0775, true);
381        }
382
383        $this->isNew = ! file_exists($path) || $this->regenerateEntityIfExists;
384
385        if ( ! $this->isNew) {
386            $this->parseTokensInEntityFile(file_get_contents($path));
387        } else {
388            $this->staticReflection[$metadata->name] = ['properties' => [], 'methods' => []];
389        }
390
391        if ($this->backupExisting && file_exists($path)) {
392            $backupPath = dirname($path) . DIRECTORY_SEPARATOR . basename($path) . "~";
393            if (!copy($path, $backupPath)) {
394                throw new \RuntimeException("Attempt to backup overwritten entity file but copy operation failed.");
395            }
396        }
397
398        // If entity doesn't exist or we're re-generating the entities entirely
399        if ($this->isNew) {
400            file_put_contents($path, $this->generateEntityClass($metadata));
401        // If entity exists and we're allowed to update the entity class
402        } elseif ($this->updateEntityIfExists) {
403            file_put_contents($path, $this->generateUpdatedEntityClass($metadata, $path));
404        }
405        chmod($path, 0664);
406    }
407
408    /**
409     * Generates a PHP5 Doctrine 2 entity class from the given ClassMetadataInfo instance.
410     *
411     * @param ClassMetadataInfo $metadata
412     *
413     * @return string
414     */
415    public function generateEntityClass(ClassMetadataInfo $metadata)
416    {
417        $placeHolders = [
418            '<namespace>',
419            '<useStatement>',
420            '<entityAnnotation>',
421            '<entityClassName>',
422            '<entityBody>'
423        ];
424
425        $replacements = [
426            $this->generateEntityNamespace($metadata),
427            $this->generateEntityUse(),
428            $this->generateEntityDocBlock($metadata),
429            $this->generateEntityClassName($metadata),
430            $this->generateEntityBody($metadata)
431        ];
432
433        $code = str_replace($placeHolders, $replacements, static::$classTemplate);
434
435        return str_replace('<spaces>', $this->spaces, $code);
436    }
437
438    /**
439     * Generates the updated code for the given ClassMetadataInfo and entity at path.
440     *
441     * @param ClassMetadataInfo $metadata
442     * @param string            $path
443     *
444     * @return string
445     */
446    public function generateUpdatedEntityClass(ClassMetadataInfo $metadata, $path)
447    {
448        $currentCode = file_get_contents($path);
449
450        $body = $this->generateEntityBody($metadata);
451        $body = str_replace('<spaces>', $this->spaces, $body);
452        $last = strrpos($currentCode, '}');
453
454        return substr($currentCode, 0, $last) . $body . ($body ? "\n" : '') . "}\n";
455    }
456
457    /**
458     * Sets the number of spaces the exported class should have.
459     *
460     * @param integer $numSpaces
461     *
462     * @return void
463     */
464    public function setNumSpaces($numSpaces)
465    {
466        $this->spaces = str_repeat(' ', $numSpaces);
467        $this->numSpaces = $numSpaces;
468    }
469
470    /**
471     * Sets the extension to use when writing php files to disk.
472     *
473     * @param string $extension
474     *
475     * @return void
476     */
477    public function setExtension($extension)
478    {
479        $this->extension = $extension;
480    }
481
482    /**
483     * Sets the name of the class the generated classes should extend from.
484     *
485     * @param string $classToExtend
486     *
487     * @return void
488     */
489    public function setClassToExtend($classToExtend)
490    {
491        $this->classToExtend = $classToExtend;
492    }
493
494    /**
495     * Sets whether or not to generate annotations for the entity.
496     *
497     * @param bool $bool
498     *
499     * @return void
500     */
501    public function setGenerateAnnotations($bool)
502    {
503        $this->generateAnnotations = $bool;
504    }
505
506    /**
507     * Sets the class fields visibility for the entity (can either be private or protected).
508     *
509     * @param string $visibility
510     *
511     * @return void
512     *
513     * @throws \InvalidArgumentException
514     *
515     * @psalm-param self::FIELD_VISIBLE_*
516     */
517    public function setFieldVisibility($visibility)
518    {
519        if ($visibility !== static::FIELD_VISIBLE_PRIVATE && $visibility !== static::FIELD_VISIBLE_PROTECTED) {
520            throw new \InvalidArgumentException('Invalid provided visibility (only private and protected are allowed): ' . $visibility);
521        }
522
523        $this->fieldVisibility = $visibility;
524    }
525
526    /**
527     * Sets whether or not to generate immutable embeddables.
528     *
529     * @param boolean $embeddablesImmutable
530     */
531    public function setEmbeddablesImmutable($embeddablesImmutable)
532    {
533        $this->embeddablesImmutable = (boolean) $embeddablesImmutable;
534    }
535
536    /**
537     * Sets an annotation prefix.
538     *
539     * @param string $prefix
540     *
541     * @return void
542     */
543    public function setAnnotationPrefix($prefix)
544    {
545        $this->annotationsPrefix = $prefix;
546    }
547
548    /**
549     * Sets whether or not to try and update the entity if it already exists.
550     *
551     * @param bool $bool
552     *
553     * @return void
554     */
555    public function setUpdateEntityIfExists($bool)
556    {
557        $this->updateEntityIfExists = $bool;
558    }
559
560    /**
561     * Sets whether or not to regenerate the entity if it exists.
562     *
563     * @param bool $bool
564     *
565     * @return void
566     */
567    public function setRegenerateEntityIfExists($bool)
568    {
569        $this->regenerateEntityIfExists = $bool;
570    }
571
572    /**
573     * Sets whether or not to generate stub methods for the entity.
574     *
575     * @param bool $bool
576     *
577     * @return void
578     */
579    public function setGenerateStubMethods($bool)
580    {
581        $this->generateEntityStubMethods = $bool;
582    }
583
584    /**
585     * Should an existing entity be backed up if it already exists?
586     *
587     * @param bool $bool
588     *
589     * @return void
590     */
591    public function setBackupExisting($bool)
592    {
593        $this->backupExisting = $bool;
594    }
595
596    /**
597     * @param string $type
598     *
599     * @return string
600     */
601    protected function getType($type)
602    {
603        if (isset($this->typeAlias[$type])) {
604            return $this->typeAlias[$type];
605        }
606
607        return $type;
608    }
609
610    /**
611     * @param ClassMetadataInfo $metadata
612     *
613     * @return string
614     */
615    protected function generateEntityNamespace(ClassMetadataInfo $metadata)
616    {
617        if (! $this->hasNamespace($metadata)) {
618            return '';
619        }
620
621        return 'namespace ' . $this->getNamespace($metadata) .';';
622    }
623
624    /**
625     * @return string
626     */
627    protected function generateEntityUse()
628    {
629        if (! $this->generateAnnotations) {
630            return '';
631        }
632
633        return "\n".'use Doctrine\ORM\Mapping as ORM;'."\n";
634    }
635
636    /**
637     * @param ClassMetadataInfo $metadata
638     *
639     * @return string
640     */
641    protected function generateEntityClassName(ClassMetadataInfo $metadata)
642    {
643        return 'class ' . $this->getClassName($metadata) .
644            ($this->extendsClass() ? ' extends ' . $this->getClassToExtendName() : null);
645    }
646
647    /**
648     * @param ClassMetadataInfo $metadata
649     *
650     * @return string
651     */
652    protected function generateEntityBody(ClassMetadataInfo $metadata)
653    {
654        $fieldMappingProperties = $this->generateEntityFieldMappingProperties($metadata);
655        $embeddedProperties = $this->generateEntityEmbeddedProperties($metadata);
656        $associationMappingProperties = $this->generateEntityAssociationMappingProperties($metadata);
657        $stubMethods = $this->generateEntityStubMethods ? $this->generateEntityStubMethods($metadata) : null;
658        $lifecycleCallbackMethods = $this->generateEntityLifecycleCallbackMethods($metadata);
659
660        $code = [];
661
662        if ($fieldMappingProperties) {
663            $code[] = $fieldMappingProperties;
664        }
665
666        if ($embeddedProperties) {
667            $code[] = $embeddedProperties;
668        }
669
670        if ($associationMappingProperties) {
671            $code[] = $associationMappingProperties;
672        }
673
674        $code[] = $this->generateEntityConstructor($metadata);
675
676        if ($stubMethods) {
677            $code[] = $stubMethods;
678        }
679
680        if ($lifecycleCallbackMethods) {
681            $code[] = $lifecycleCallbackMethods;
682        }
683
684        return implode("\n", $code);
685    }
686
687    /**
688     * @param ClassMetadataInfo $metadata
689     *
690     * @return string
691     */
692    protected function generateEntityConstructor(ClassMetadataInfo $metadata)
693    {
694        if ($this->hasMethod('__construct', $metadata)) {
695            return '';
696        }
697
698        if ($metadata->isEmbeddedClass && $this->embeddablesImmutable) {
699            return $this->generateEmbeddableConstructor($metadata);
700        }
701
702        $collections = [];
703
704        foreach ($metadata->associationMappings as $mapping) {
705            if ($mapping['type'] & ClassMetadataInfo::TO_MANY) {
706                $collections[] = '$this->'.$mapping['fieldName'].' = new \Doctrine\Common\Collections\ArrayCollection();';
707            }
708        }
709
710        if ($collections) {
711            return $this->prefixCodeWithSpaces(str_replace("<collections>", implode("\n".$this->spaces, $collections), static::$constructorMethodTemplate));
712        }
713
714        return '';
715    }
716
717    /**
718     * @param ClassMetadataInfo $metadata
719     *
720     * @return string
721     */
722    private function generateEmbeddableConstructor(ClassMetadataInfo $metadata)
723    {
724        $paramTypes = [];
725        $paramVariables = [];
726        $params = [];
727        $fields = [];
728
729        // Resort fields to put optional fields at the end of the method signature.
730        $requiredFields = [];
731        $optionalFields = [];
732
733        foreach ($metadata->fieldMappings as $fieldMapping) {
734            if (empty($fieldMapping['nullable'])) {
735                $requiredFields[] = $fieldMapping;
736
737                continue;
738            }
739
740            $optionalFields[] = $fieldMapping;
741        }
742
743        $fieldMappings = array_merge($requiredFields, $optionalFields);
744
745        foreach ($metadata->embeddedClasses as $fieldName => $embeddedClass) {
746            $paramType = '\\' . ltrim($embeddedClass['class'], '\\');
747            $paramVariable = '$' . $fieldName;
748
749            $paramTypes[] = $paramType;
750            $paramVariables[] = $paramVariable;
751            $params[] = $paramType . ' ' . $paramVariable;
752            $fields[] = '$this->' . $fieldName . ' = ' . $paramVariable . ';';
753        }
754
755        foreach ($fieldMappings as $fieldMapping) {
756            if (isset($fieldMapping['declaredField'], $metadata->embeddedClasses[$fieldMapping['declaredField']])) {
757                continue;
758            }
759
760            $paramTypes[] = $this->getType($fieldMapping['type']) . (!empty($fieldMapping['nullable']) ? '|null' : '');
761            $param = '$' . $fieldMapping['fieldName'];
762            $paramVariables[] = $param;
763
764            if ($fieldMapping['type'] === 'datetime') {
765                $param = $this->getType($fieldMapping['type']) . ' ' . $param;
766            }
767
768            if (!empty($fieldMapping['nullable'])) {
769                $param .= ' = null';
770            }
771
772            $params[] = $param;
773
774            $fields[] = '$this->' . $fieldMapping['fieldName'] . ' = $' . $fieldMapping['fieldName'] . ';';
775        }
776
777        $maxParamTypeLength = max(array_map('strlen', $paramTypes));
778        $paramTags = array_map(
779            function ($type, $variable) use ($maxParamTypeLength) {
780                return '@param ' . $type . str_repeat(' ', $maxParamTypeLength - strlen($type) + 1) . $variable;
781            },
782            $paramTypes,
783            $paramVariables
784        );
785
786        // Generate multi line constructor if the signature exceeds 120 characters.
787        if (array_sum(array_map('strlen', $params)) + count($params) * 2 + 29 > 120) {
788            $delimiter = "\n" . $this->spaces;
789            $params = $delimiter . implode(',' . $delimiter, $params) . "\n";
790        } else {
791            $params = implode(', ', $params);
792        }
793
794        $replacements = [
795            '<paramTags>' => implode("\n * ", $paramTags),
796            '<params>'    => $params,
797            '<fields>'    => implode("\n" . $this->spaces, $fields),
798        ];
799
800        $constructor = str_replace(
801            array_keys($replacements),
802            array_values($replacements),
803            static::$embeddableConstructorMethodTemplate
804        );
805
806        return $this->prefixCodeWithSpaces($constructor);
807    }
808
809    /**
810     * @todo this won't work if there is a namespace in brackets and a class outside of it.
811     *
812     * @param string $src
813     *
814     * @return void
815     */
816    protected function parseTokensInEntityFile($src)
817    {
818        $tokens = token_get_all($src);
819        $tokensCount = count($tokens);
820        $lastSeenNamespace = '';
821        $lastSeenClass = false;
822
823        $inNamespace = false;
824        $inClass = false;
825
826        for ($i = 0; $i < $tokensCount; $i++) {
827            $token = $tokens[$i];
828            if (in_array($token[0], [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT], true)) {
829                continue;
830            }
831
832            if ($inNamespace) {
833                if (in_array($token[0], [T_NS_SEPARATOR, T_STRING], true)) {
834                    $lastSeenNamespace .= $token[1];
835                } elseif (is_string($token) && in_array($token, [';', '{'], true)) {
836                    $inNamespace = false;
837                }
838            }
839
840            if ($inClass) {
841                $inClass = false;
842                $lastSeenClass = $lastSeenNamespace . ($lastSeenNamespace ? '\\' : '') . $token[1];
843                $this->staticReflection[$lastSeenClass]['properties'] = [];
844                $this->staticReflection[$lastSeenClass]['methods'] = [];
845            }
846
847            if (T_NAMESPACE === $token[0]) {
848                $lastSeenNamespace = '';
849                $inNamespace = true;
850            } elseif (T_CLASS === $token[0] && T_DOUBLE_COLON !== $tokens[$i-1][0]) {
851                $inClass = true;
852            } elseif (T_FUNCTION === $token[0]) {
853                if (T_STRING === $tokens[$i+2][0]) {
854                    $this->staticReflection[$lastSeenClass]['methods'][] = strtolower($tokens[$i+2][1]);
855                } elseif ($tokens[$i+2] == '&' && T_STRING === $tokens[$i+3][0]) {
856                    $this->staticReflection[$lastSeenClass]['methods'][] = strtolower($tokens[$i+3][1]);
857                }
858            } elseif (in_array($token[0], [T_VAR, T_PUBLIC, T_PRIVATE, T_PROTECTED], true) && T_FUNCTION !== $tokens[$i+2][0]) {
859                $this->staticReflection[$lastSeenClass]['properties'][] = substr($tokens[$i+2][1], 1);
860            }
861        }
862    }
863
864    /**
865     * @param string            $property
866     * @param ClassMetadataInfo $metadata
867     *
868     * @return bool
869     */
870    protected function hasProperty($property, ClassMetadataInfo $metadata)
871    {
872        if ($this->extendsClass() || (!$this->isNew && class_exists($metadata->name))) {
873            // don't generate property if its already on the base class.
874            $reflClass = new \ReflectionClass($this->getClassToExtend() ?: $metadata->name);
875            if ($reflClass->hasProperty($property)) {
876                return true;
877            }
878        }
879
880        // check traits for existing property
881        foreach ($this->getTraits($metadata) as $trait) {
882            if ($trait->hasProperty($property)) {
883                return true;
884            }
885        }
886
887        return (
888            isset($this->staticReflection[$metadata->name]) &&
889            in_array($property, $this->staticReflection[$metadata->name]['properties'], true)
890        );
891    }
892
893    /**
894     * @param string            $method
895     * @param ClassMetadataInfo $metadata
896     *
897     * @return bool
898     */
899    protected function hasMethod($method, ClassMetadataInfo $metadata)
900    {
901        if ($this->extendsClass() || (!$this->isNew && class_exists($metadata->name))) {
902            // don't generate method if its already on the base class.
903            $reflClass = new \ReflectionClass($this->getClassToExtend() ?: $metadata->name);
904
905            if ($reflClass->hasMethod($method)) {
906                return true;
907            }
908        }
909
910        // check traits for existing method
911        foreach ($this->getTraits($metadata) as $trait) {
912            if ($trait->hasMethod($method)) {
913                return true;
914            }
915        }
916
917        return (
918            isset($this->staticReflection[$metadata->name]) &&
919            in_array(strtolower($method), $this->staticReflection[$metadata->name]['methods'], true)
920        );
921    }
922
923    /**
924     * @param ClassMetadataInfo $metadata
925     *
926     * @return ReflectionClass[]
927     *
928     * @throws \ReflectionException
929     *
930     * @psalm-return array<trait-string, ReflectionClass>
931     */
932    protected function getTraits(ClassMetadataInfo $metadata)
933    {
934        if (! ($metadata->reflClass !== null || class_exists($metadata->name))) {
935            return [];
936        }
937
938        $reflClass = $metadata->reflClass ?? new \ReflectionClass($metadata->name);
939
940        $traits = [];
941
942        while ($reflClass !== false) {
943            $traits = array_merge($traits, $reflClass->getTraits());
944
945            $reflClass = $reflClass->getParentClass();
946        }
947
948        return $traits;
949    }
950
951    /**
952     * @param ClassMetadataInfo $metadata
953     *
954     * @return bool
955     */
956    protected function hasNamespace(ClassMetadataInfo $metadata)
957    {
958        return (bool) strpos($metadata->name, '\\');
959    }
960
961    /**
962     * @return bool
963     */
964    protected function extendsClass()
965    {
966        return (bool) $this->classToExtend;
967    }
968
969    /**
970     * @return string
971     */
972    protected function getClassToExtend()
973    {
974        return $this->classToExtend;
975    }
976
977    /**
978     * @return string
979     */
980    protected function getClassToExtendName()
981    {
982        $refl = new \ReflectionClass($this->getClassToExtend());
983
984        return '\\' . $refl->getName();
985    }
986
987    /**
988     * @param ClassMetadataInfo $metadata
989     *
990     * @return string
991     */
992    protected function getClassName(ClassMetadataInfo $metadata)
993    {
994        return ($pos = strrpos($metadata->name, '\\'))
995            ? substr($metadata->name, $pos + 1, strlen($metadata->name)) : $metadata->name;
996    }
997
998    /**
999     * @param ClassMetadataInfo $metadata
1000     *
1001     * @return string
1002     */
1003    protected function getNamespace(ClassMetadataInfo $metadata)
1004    {
1005        return substr($metadata->name, 0, strrpos($metadata->name, '\\'));
1006    }
1007
1008    /**
1009     * @param ClassMetadataInfo $metadata
1010     *
1011     * @return string
1012     */
1013    protected function generateEntityDocBlock(ClassMetadataInfo $metadata)
1014    {
1015        $lines = [];
1016        $lines[] = '/**';
1017        $lines[] = ' * ' . $this->getClassName($metadata);
1018
1019        if ($this->generateAnnotations) {
1020            $lines[] = ' *';
1021
1022            $methods = [
1023                'generateTableAnnotation',
1024                'generateInheritanceAnnotation',
1025                'generateDiscriminatorColumnAnnotation',
1026                'generateDiscriminatorMapAnnotation',
1027                'generateEntityAnnotation',
1028                'generateEntityListenerAnnotation',
1029            ];
1030
1031            foreach ($methods as $method) {
1032                if ($code = $this->$method($metadata)) {
1033                    $lines[] = ' * ' . $code;
1034                }
1035            }
1036
1037            if (isset($metadata->lifecycleCallbacks) && $metadata->lifecycleCallbacks) {
1038                $lines[] = ' * @' . $this->annotationsPrefix . 'HasLifecycleCallbacks';
1039            }
1040        }
1041
1042        $lines[] = ' */';
1043
1044        return implode("\n", $lines);
1045    }
1046
1047    /**
1048     * @param ClassMetadataInfo $metadata
1049     *
1050     * @return string
1051     */
1052    protected function generateEntityAnnotation(ClassMetadataInfo $metadata)
1053    {
1054        $prefix = '@' . $this->annotationsPrefix;
1055
1056        if ($metadata->isEmbeddedClass) {
1057            return $prefix . 'Embeddable';
1058        }
1059
1060        $customRepository = $metadata->customRepositoryClassName
1061            ? '(repositoryClass="' . $metadata->customRepositoryClassName . '")'
1062            : '';
1063
1064        return $prefix . ($metadata->isMappedSuperclass ? 'MappedSuperclass' : 'Entity') . $customRepository;
1065    }
1066
1067    /**
1068     * @param ClassMetadataInfo $metadata
1069     *
1070     * @return string
1071     */
1072    protected function generateTableAnnotation(ClassMetadataInfo $metadata)
1073    {
1074        if ($metadata->isEmbeddedClass) {
1075            return '';
1076        }
1077
1078        $table = [];
1079
1080        if (isset($metadata->table['schema'])) {
1081            $table[] = 'schema="' . $metadata->table['schema'] . '"';
1082        }
1083
1084        if (isset($metadata->table['name'])) {
1085            $table[] = 'name="' . $metadata->table['name'] . '"';
1086        }
1087
1088        if (isset($metadata->table['options']) && $metadata->table['options']) {
1089            $table[] = 'options={' . $this->exportTableOptions((array) $metadata->table['options']) . '}';
1090        }
1091
1092        if (isset($metadata->table['uniqueConstraints']) && $metadata->table['uniqueConstraints']) {
1093            $constraints = $this->generateTableConstraints('UniqueConstraint', $metadata->table['uniqueConstraints']);
1094            $table[] = 'uniqueConstraints={' . $constraints . '}';
1095        }
1096
1097        if (isset($metadata->table['indexes']) && $metadata->table['indexes']) {
1098            $constraints = $this->generateTableConstraints('Index', $metadata->table['indexes']);
1099            $table[] = 'indexes={' . $constraints . '}';
1100        }
1101
1102        return '@' . $this->annotationsPrefix . 'Table(' . implode(', ', $table) . ')';
1103    }
1104
1105    /**
1106     * @param string $constraintName
1107     * @param array  $constraints
1108     *
1109     * @return string
1110     */
1111    protected function generateTableConstraints($constraintName, array $constraints)
1112    {
1113        $annotations = [];
1114        foreach ($constraints as $name => $constraint) {
1115            $columns = [];
1116            foreach ($constraint['columns'] as $column) {
1117                $columns[] = '"' . $column . '"';
1118            }
1119            $annotations[] = '@' . $this->annotationsPrefix . $constraintName . '(name="' . $name . '", columns={' . implode(', ', $columns) . '})';
1120        }
1121
1122        return implode(', ', $annotations);
1123    }
1124
1125    /**
1126     * @param ClassMetadataInfo $metadata
1127     *
1128     * @return string
1129     */
1130    protected function generateInheritanceAnnotation(ClassMetadataInfo $metadata)
1131    {
1132        if ($metadata->inheritanceType === ClassMetadataInfo::INHERITANCE_TYPE_NONE) {
1133            return '';
1134        }
1135
1136        return '@' . $this->annotationsPrefix . 'InheritanceType("'.$this->getInheritanceTypeString($metadata->inheritanceType).'")';
1137    }
1138
1139    /**
1140     * @param ClassMetadataInfo $metadata
1141     *
1142     * @return string
1143     */
1144    protected function generateDiscriminatorColumnAnnotation(ClassMetadataInfo $metadata)
1145    {
1146        if ($metadata->inheritanceType === ClassMetadataInfo::INHERITANCE_TYPE_NONE) {
1147            return '';
1148        }
1149
1150        $discrColumn = $metadata->discriminatorColumn;
1151        $columnDefinition = 'name="' . $discrColumn['name']
1152            . '", type="' . $discrColumn['type']
1153            . '", length=' . $discrColumn['length'];
1154
1155        return '@' . $this->annotationsPrefix . 'DiscriminatorColumn(' . $columnDefinition . ')';
1156    }
1157
1158    /**
1159     * @param ClassMetadataInfo $metadata
1160     *
1161     * @return string|null
1162     */
1163    protected function generateDiscriminatorMapAnnotation(ClassMetadataInfo $metadata)
1164    {
1165        if ($metadata->inheritanceType === ClassMetadataInfo::INHERITANCE_TYPE_NONE) {
1166            return null;
1167        }
1168
1169        $inheritanceClassMap = [];
1170
1171        foreach ($metadata->discriminatorMap as $type => $class) {
1172            $inheritanceClassMap[] = '"' . $type . '" = "' . $class . '"';
1173        }
1174
1175        return '@' . $this->annotationsPrefix . 'DiscriminatorMap({' . implode(', ', $inheritanceClassMap) . '})';
1176    }
1177
1178    /**
1179     * @param ClassMetadataInfo $metadata
1180     *
1181     * @return string
1182     */
1183    protected function generateEntityStubMethods(ClassMetadataInfo $metadata)
1184    {
1185        $methods = [];
1186
1187        foreach ($metadata->fieldMappings as $fieldMapping) {
1188            if (isset($fieldMapping['declaredField'], $metadata->embeddedClasses[$fieldMapping['declaredField']])) {
1189                continue;
1190            }
1191
1192            $nullableField = $this->nullableFieldExpression($fieldMapping);
1193
1194            if ((!$metadata->isEmbeddedClass || !$this->embeddablesImmutable)
1195                && (!isset($fieldMapping['id']) || ! $fieldMapping['id'] || $metadata->generatorType === ClassMetadataInfo::GENERATOR_TYPE_NONE)
1196                && $code = $this->generateEntityStubMethod($metadata, 'set', $fieldMapping['fieldName'], $fieldMapping['type'], $nullableField)
1197            ) {
1198                $methods[] = $code;
1199            }
1200
1201            if ($code = $this->generateEntityStubMethod($metadata, 'get', $fieldMapping['fieldName'], $fieldMapping['type'], $nullableField)) {
1202                $methods[] = $code;
1203            }
1204        }
1205
1206        foreach ($metadata->embeddedClasses as $fieldName => $embeddedClass) {
1207            if (isset($embeddedClass['declaredField'])) {
1208                continue;
1209            }
1210
1211            if ( ! $metadata->isEmbeddedClass || ! $this->embeddablesImmutable) {
1212                if ($code = $this->generateEntityStubMethod($metadata, 'set', $fieldName, $embeddedClass['class'])) {
1213                    $methods[] = $code;
1214                }
1215            }
1216
1217            if ($code = $this->generateEntityStubMethod($metadata, 'get', $fieldName, $embeddedClass['class'])) {
1218                $methods[] = $code;
1219            }
1220        }
1221
1222        foreach ($metadata->associationMappings as $associationMapping) {
1223            if ($associationMapping['type'] & ClassMetadataInfo::TO_ONE) {
1224                $nullable = $this->isAssociationIsNullable($associationMapping) ? 'null' : null;
1225                if ($code = $this->generateEntityStubMethod($metadata, 'set', $associationMapping['fieldName'], $associationMapping['targetEntity'], $nullable)) {
1226                    $methods[] = $code;
1227                }
1228                if ($code = $this->generateEntityStubMethod($metadata, 'get', $associationMapping['fieldName'], $associationMapping['targetEntity'], $nullable)) {
1229                    $methods[] = $code;
1230                }
1231            } elseif ($associationMapping['type'] & ClassMetadataInfo::TO_MANY) {
1232                if ($code = $this->generateEntityStubMethod($metadata, 'add', $associationMapping['fieldName'], $associationMapping['targetEntity'])) {
1233                    $methods[] = $code;
1234                }
1235                if ($code = $this->generateEntityStubMethod($metadata, 'remove', $associationMapping['fieldName'], $associationMapping['targetEntity'])) {
1236                    $methods[] = $code;
1237                }
1238                if ($code = $this->generateEntityStubMethod($metadata, 'get', $associationMapping['fieldName'], Collection::class)) {
1239                    $methods[] = $code;
1240                }
1241            }
1242        }
1243
1244        return implode("\n\n", $methods);
1245    }
1246
1247    /**
1248     * @param array $associationMapping
1249     *
1250     * @return bool
1251     */
1252    protected function isAssociationIsNullable(array $associationMapping)
1253    {
1254        if (isset($associationMapping['id']) && $associationMapping['id']) {
1255            return false;
1256        }
1257
1258        if (isset($associationMapping['joinColumns'])) {
1259            $joinColumns = $associationMapping['joinColumns'];
1260        } else {
1261            //@todo there is no way to retrieve targetEntity metadata
1262            $joinColumns = [];
1263        }
1264
1265        foreach ($joinColumns as $joinColumn) {
1266            if (isset($joinColumn['nullable']) && !$joinColumn['nullable']) {
1267                return false;
1268            }
1269        }
1270
1271        return true;
1272    }
1273
1274    /**
1275     * @param ClassMetadataInfo $metadata
1276     *
1277     * @return string
1278     */
1279    protected function generateEntityLifecycleCallbackMethods(ClassMetadataInfo $metadata)
1280    {
1281        if (empty($metadata->lifecycleCallbacks)) {
1282            return '';
1283        }
1284
1285        $methods = [];
1286
1287        foreach ($metadata->lifecycleCallbacks as $name => $callbacks) {
1288            foreach ($callbacks as $callback) {
1289                $methods[] = $this->generateLifecycleCallbackMethod($name, $callback, $metadata);
1290            }
1291        }
1292
1293        return implode("\n\n", array_filter($methods));
1294    }
1295
1296    /**
1297     * @param ClassMetadataInfo $metadata
1298     *
1299     * @return string
1300     */
1301    protected function generateEntityAssociationMappingProperties(ClassMetadataInfo $metadata)
1302    {
1303        $lines = [];
1304
1305        foreach ($metadata->associationMappings as $associationMapping) {
1306            if ($this->hasProperty($associationMapping['fieldName'], $metadata)) {
1307                continue;
1308            }
1309
1310            $lines[] = $this->generateAssociationMappingPropertyDocBlock($associationMapping, $metadata);
1311            $lines[] = $this->spaces . $this->fieldVisibility . ' $' . $associationMapping['fieldName']
1312                     . ($associationMapping['type'] == 'manyToMany' ? ' = array()' : null) . ";\n";
1313        }
1314
1315        return implode("\n", $lines);
1316    }
1317
1318    /**
1319     * @param ClassMetadataInfo $metadata
1320     *
1321     * @return string
1322     */
1323    protected function generateEntityFieldMappingProperties(ClassMetadataInfo $metadata)
1324    {
1325        $lines = [];
1326
1327        foreach ($metadata->fieldMappings as $fieldMapping) {
1328            if (isset($fieldMapping['declaredField'], $metadata->embeddedClasses[$fieldMapping['declaredField']]) ||
1329                $this->hasProperty($fieldMapping['fieldName'], $metadata) ||
1330                $metadata->isInheritedField($fieldMapping['fieldName'])
1331            ) {
1332                continue;
1333            }
1334
1335            $defaultValue = '';
1336            if (isset($fieldMapping['options']['default'])) {
1337                if ($fieldMapping['type'] === 'boolean' && $fieldMapping['options']['default'] === '1') {
1338                    $defaultValue = ' = true';
1339                } else {
1340                    $defaultValue = ' = ' . var_export($fieldMapping['options']['default'], true);
1341                }
1342            }
1343
1344            $lines[] = $this->generateFieldMappingPropertyDocBlock($fieldMapping, $metadata);
1345            $lines[] = $this->spaces . $this->fieldVisibility . ' $' . $fieldMapping['fieldName'] . $defaultValue . ";\n";
1346        }
1347
1348        return implode("\n", $lines);
1349    }
1350
1351    /**
1352     * @param ClassMetadataInfo $metadata
1353     *
1354     * @return string
1355     */
1356    protected function generateEntityEmbeddedProperties(ClassMetadataInfo $metadata)
1357    {
1358        $lines = [];
1359
1360        foreach ($metadata->embeddedClasses as $fieldName => $embeddedClass) {
1361            if (isset($embeddedClass['declaredField']) || $this->hasProperty($fieldName, $metadata)) {
1362                continue;
1363            }
1364
1365            $lines[] = $this->generateEmbeddedPropertyDocBlock($embeddedClass);
1366            $lines[] = $this->spaces . $this->fieldVisibility . ' $' . $fieldName . ";\n";
1367        }
1368
1369        return implode("\n", $lines);
1370    }
1371
1372    /**
1373     * @param ClassMetadataInfo $metadata
1374     * @param string            $type
1375     * @param string            $fieldName
1376     * @param string|null       $typeHint
1377     * @param string|null       $defaultValue
1378     *
1379     * @return string
1380     */
1381    protected function generateEntityStubMethod(ClassMetadataInfo $metadata, $type, $fieldName, $typeHint = null, $defaultValue = null)
1382    {
1383        $methodName = $type . Inflector::classify($fieldName);
1384        $variableName = Inflector::camelize($fieldName);
1385        if (in_array($type, ["add", "remove"])) {
1386            $methodName = Inflector::singularize($methodName);
1387            $variableName = Inflector::singularize($variableName);
1388        }
1389
1390        if ($this->hasMethod($methodName, $metadata)) {
1391            return '';
1392        }
1393        $this->staticReflection[$metadata->name]['methods'][] = strtolower($methodName);
1394
1395        $var = sprintf('%sMethodTemplate', $type);
1396        $template = static::$$var;
1397
1398        $methodTypeHint = null;
1399        $types          = Type::getTypesMap();
1400        $variableType   = $typeHint ? $this->getType($typeHint) : null;
1401
1402        if ($typeHint && ! isset($types[$typeHint])) {
1403            $variableType   =  '\\' . ltrim($variableType, '\\');
1404            $methodTypeHint =  '\\' . $typeHint . ' ';
1405        }
1406
1407        $replacements = [
1408          '<description>'       => ucfirst($type) . ' ' . $variableName . '.',
1409          '<methodTypeHint>'    => $methodTypeHint,
1410          '<variableType>'      => $variableType . (null !== $defaultValue ? ('|' . $defaultValue) : ''),
1411          '<variableName>'      => $variableName,
1412          '<methodName>'        => $methodName,
1413          '<fieldName>'         => $fieldName,
1414          '<variableDefault>'   => ($defaultValue !== null ) ? (' = ' . $defaultValue) : '',
1415          '<entity>'            => $this->getClassName($metadata)
1416        ];
1417
1418        $method = str_replace(
1419            array_keys($replacements),
1420            array_values($replacements),
1421            $template
1422        );
1423
1424        return $this->prefixCodeWithSpaces($method);
1425    }
1426
1427    /**
1428     * @param string            $name
1429     * @param string            $methodName
1430     * @param ClassMetadataInfo $metadata
1431     *
1432     * @return string
1433     */
1434    protected function generateLifecycleCallbackMethod($name, $methodName, ClassMetadataInfo $metadata)
1435    {
1436        if ($this->hasMethod($methodName, $metadata)) {
1437            return '';
1438        }
1439
1440        $this->staticReflection[$metadata->name]['methods'][] = $methodName;
1441
1442        $replacements = [
1443            '<name>'        => $this->annotationsPrefix . ucfirst($name),
1444            '<methodName>'  => $methodName,
1445        ];
1446
1447        $method = str_replace(
1448            array_keys($replacements),
1449            array_values($replacements),
1450            static::$lifecycleCallbackMethodTemplate
1451        );
1452
1453        return $this->prefixCodeWithSpaces($method);
1454    }
1455
1456    /**
1457     * @param array $joinColumn
1458     *
1459     * @return string
1460     */
1461    protected function generateJoinColumnAnnotation(array $joinColumn)
1462    {
1463        $joinColumnAnnot = [];
1464
1465        if (isset($joinColumn['name'])) {
1466            $joinColumnAnnot[] = 'name="' . $joinColumn['name'] . '"';
1467        }
1468
1469        if (isset($joinColumn['referencedColumnName'])) {
1470            $joinColumnAnnot[] = 'referencedColumnName="' . $joinColumn['referencedColumnName'] . '"';
1471        }
1472
1473        if (isset($joinColumn['unique']) && $joinColumn['unique']) {
1474            $joinColumnAnnot[] = 'unique=true';
1475        }
1476
1477        if (isset($joinColumn['nullable'])) {
1478            $joinColumnAnnot[] = 'nullable=' . ($joinColumn['nullable'] ? 'true' : 'false');
1479        }
1480
1481        if (isset($joinColumn['onDelete'])) {
1482            $joinColumnAnnot[] = 'onDelete="' . ($joinColumn['onDelete'] . '"');
1483        }
1484
1485        if (isset($joinColumn['columnDefinition'])) {
1486            $joinColumnAnnot[] = 'columnDefinition="' . $joinColumn['columnDefinition'] . '"';
1487        }
1488
1489        return '@' . $this->annotationsPrefix . 'JoinColumn(' . implode(', ', $joinColumnAnnot) . ')';
1490    }
1491
1492    /**
1493     * @param array             $associationMapping
1494     * @param ClassMetadataInfo $metadata
1495     *
1496     * @return string
1497     */
1498    protected function generateAssociationMappingPropertyDocBlock(array $associationMapping, ClassMetadataInfo $metadata)
1499    {
1500        $lines = [];
1501        $lines[] = $this->spaces . '/**';
1502
1503        if ($associationMapping['type'] & ClassMetadataInfo::TO_MANY) {
1504            $lines[] = $this->spaces . ' * @var \Doctrine\Common\Collections\Collection';
1505        } else {
1506            $lines[] = $this->spaces . ' * @var \\' . ltrim($associationMapping['targetEntity'], '\\');
1507        }
1508
1509        if ($this->generateAnnotations) {
1510            $lines[] = $this->spaces . ' *';
1511
1512            if (isset($associationMapping['id']) && $associationMapping['id']) {
1513                $lines[] = $this->spaces . ' * @' . $this->annotationsPrefix . 'Id';
1514
1515                if ($generatorType = $this->getIdGeneratorTypeString($metadata->generatorType)) {
1516                    $lines[] = $this->spaces . ' * @' . $this->annotationsPrefix . 'GeneratedValue(strategy="' . $generatorType . '")';
1517                }
1518            }
1519
1520            $type = null;
1521            switch ($associationMapping['type']) {
1522                case ClassMetadataInfo::ONE_TO_ONE:
1523                    $type = 'OneToOne';
1524                    break;
1525                case ClassMetadataInfo::MANY_TO_ONE:
1526                    $type = 'ManyToOne';
1527                    break;
1528                case ClassMetadataInfo::ONE_TO_MANY:
1529                    $type = 'OneToMany';
1530                    break;
1531                case ClassMetadataInfo::MANY_TO_MANY:
1532                    $type = 'ManyToMany';
1533                    break;
1534            }
1535            $typeOptions = [];
1536
1537            if (isset($associationMapping['targetEntity'])) {
1538                $typeOptions[] = 'targetEntity="' . $associationMapping['targetEntity'] . '"';
1539            }
1540
1541            if (isset($associationMapping['inversedBy'])) {
1542                $typeOptions[] = 'inversedBy="' . $associationMapping['inversedBy'] . '"';
1543            }
1544
1545            if (isset($associationMapping['mappedBy'])) {
1546                $typeOptions[] = 'mappedBy="' . $associationMapping['mappedBy'] . '"';
1547            }
1548
1549            if ($associationMapping['cascade']) {
1550                $cascades = [];
1551
1552                if ($associationMapping['isCascadePersist']) $cascades[] = '"persist"';
1553                if ($associationMapping['isCascadeRemove']) $cascades[] = '"remove"';
1554                if ($associationMapping['isCascadeDetach']) $cascades[] = '"detach"';
1555                if ($associationMapping['isCascadeMerge']) $cascades[] = '"merge"';
1556                if ($associationMapping['isCascadeRefresh']) $cascades[] = '"refresh"';
1557
1558                if (count($cascades) === 5) {
1559                    $cascades = ['"all"'];
1560                }
1561
1562                $typeOptions[] = 'cascade={' . implode(',', $cascades) . '}';
1563            }
1564
1565            if (isset($associationMapping['orphanRemoval']) && $associationMapping['orphanRemoval']) {
1566                $typeOptions[] = 'orphanRemoval=true';
1567            }
1568
1569            if (isset($associationMapping['fetch']) && $associationMapping['fetch'] !== ClassMetadataInfo::FETCH_LAZY) {
1570                $fetchMap = [
1571                    ClassMetadataInfo::FETCH_EXTRA_LAZY => 'EXTRA_LAZY',
1572                    ClassMetadataInfo::FETCH_EAGER      => 'EAGER',
1573                ];
1574
1575                $typeOptions[] = 'fetch="' . $fetchMap[$associationMapping['fetch']] . '"';
1576            }
1577
1578            $lines[] = $this->spaces . ' * @' . $this->annotationsPrefix . '' . $type . '(' . implode(', ', $typeOptions) . ')';
1579
1580            if (isset($associationMapping['joinColumns']) && $associationMapping['joinColumns']) {
1581                $lines[] = $this->spaces . ' * @' . $this->annotationsPrefix . 'JoinColumns({';
1582
1583                $joinColumnsLines = [];
1584
1585                foreach ($associationMapping['joinColumns'] as $joinColumn) {
1586                    if ($joinColumnAnnot = $this->generateJoinColumnAnnotation($joinColumn)) {
1587                        $joinColumnsLines[] = $this->spaces . ' *   ' . $joinColumnAnnot;
1588                    }
1589                }
1590
1591                $lines[] = implode(",\n", $joinColumnsLines);
1592                $lines[] = $this->spaces . ' * })';
1593            }
1594
1595            if (isset($associationMapping['joinTable']) && $associationMapping['joinTable']) {
1596                $joinTable = [];
1597                $joinTable[] = 'name="' . $associationMapping['joinTable']['name'] . '"';
1598
1599                if (isset($associationMapping['joinTable']['schema'])) {
1600                    $joinTable[] = 'schema="' . $associationMapping['joinTable']['schema'] . '"';
1601                }
1602
1603                $lines[] = $this->spaces . ' * @' . $this->annotationsPrefix . 'JoinTable(' . implode(', ', $joinTable) . ',';
1604                $lines[] = $this->spaces . ' *   joinColumns={';
1605
1606                $joinColumnsLines = [];
1607
1608                foreach ($associationMapping['joinTable']['joinColumns'] as $joinColumn) {
1609                    $joinColumnsLines[] = $this->spaces . ' *     ' . $this->generateJoinColumnAnnotation($joinColumn);
1610                }
1611
1612                $lines[] = implode(",". PHP_EOL, $joinColumnsLines);
1613                $lines[] = $this->spaces . ' *   },';
1614                $lines[] = $this->spaces . ' *   inverseJoinColumns={';
1615
1616                $inverseJoinColumnsLines = [];
1617
1618                foreach ($associationMapping['joinTable']['inverseJoinColumns'] as $joinColumn) {
1619                    $inverseJoinColumnsLines[] = $this->spaces . ' *     ' . $this->generateJoinColumnAnnotation($joinColumn);
1620                }
1621
1622                $lines[] = implode(",". PHP_EOL, $inverseJoinColumnsLines);
1623                $lines[] = $this->spaces . ' *   }';
1624                $lines[] = $this->spaces . ' * )';
1625            }
1626
1627            if (isset($associationMapping['orderBy'])) {
1628                $lines[] = $this->spaces . ' * @' . $this->annotationsPrefix . 'OrderBy({';
1629
1630                foreach ($associationMapping['orderBy'] as $name => $direction) {
1631                    $lines[] = $this->spaces . ' *     "' . $name . '"="' . $direction . '",';
1632                }
1633
1634                $lines[count($lines) - 1] = substr($lines[count($lines) - 1], 0, strlen($lines[count($lines) - 1]) - 1);
1635                $lines[] = $this->spaces . ' * })';
1636            }
1637        }
1638
1639        $lines[] = $this->spaces . ' */';
1640
1641        return implode("\n", $lines);
1642    }
1643
1644    /**
1645     * @param array             $fieldMapping
1646     * @param ClassMetadataInfo $metadata
1647     *
1648     * @return string
1649     */
1650    protected function generateFieldMappingPropertyDocBlock(array $fieldMapping, ClassMetadataInfo $metadata)
1651    {
1652        $lines = [];
1653        $lines[] = $this->spaces . '/**';
1654        $lines[] = $this->spaces . ' * @var '
1655            . $this->getType($fieldMapping['type'])
1656            . ($this->nullableFieldExpression($fieldMapping) ? '|null' : '');
1657
1658        if ($this->generateAnnotations) {
1659            $lines[] = $this->spaces . ' *';
1660
1661            $column = [];
1662            if (isset($fieldMapping['columnName'])) {
1663                $column[] = 'name="' . $fieldMapping['columnName'] . '"';
1664            }
1665
1666            if (isset($fieldMapping['type'])) {
1667                $column[] = 'type="' . $fieldMapping['type'] . '"';
1668            }
1669
1670            if (isset($fieldMapping['length'])) {
1671                $column[] = 'length=' . $fieldMapping['length'];
1672            }
1673
1674            if (isset($fieldMapping['precision'])) {
1675                $column[] = 'precision=' .  $fieldMapping['precision'];
1676            }
1677
1678            if (isset($fieldMapping['scale'])) {
1679                $column[] = 'scale=' . $fieldMapping['scale'];
1680            }
1681
1682            if (isset($fieldMapping['nullable'])) {
1683                $column[] = 'nullable=' .  var_export($fieldMapping['nullable'], true);
1684            }
1685
1686            $options = [];
1687
1688            if (isset($fieldMapping['options']['default']) && $fieldMapping['options']['default']) {
1689                $options[] = '"default"="' . $fieldMapping['options']['default'] .'"';
1690            }
1691
1692            if (isset($fieldMapping['options']['unsigned']) && $fieldMapping['options']['unsigned']) {
1693                $options[] = '"unsigned"=true';
1694            }
1695
1696            if (isset($fieldMapping['options']['fixed']) && $fieldMapping['options']['fixed']) {
1697                $options[] = '"fixed"=true';
1698            }
1699
1700            if (isset($fieldMapping['options']['comment']) && $fieldMapping['options']['comment']) {
1701                $options[] = '"comment"="' . str_replace('"', '""', $fieldMapping['options']['comment']) . '"';
1702            }
1703
1704            if (isset($fieldMapping['options']['collation']) && $fieldMapping['options']['collation']) {
1705                $options[] = '"collation"="' . $fieldMapping['options']['collation'] .'"';
1706            }
1707
1708            if (isset($fieldMapping['options']['check']) && $fieldMapping['options']['check']) {
1709                $options[] = '"check"="' . $fieldMapping['options']['check'] .'"';
1710            }
1711
1712            if ($options) {
1713                $column[] = 'options={'.implode(',', $options).'}';
1714            }
1715
1716            if (isset($fieldMapping['columnDefinition'])) {
1717                $column[] = 'columnDefinition="' . $fieldMapping['columnDefinition'] . '"';
1718            }
1719
1720            if (isset($fieldMapping['unique'])) {
1721                $column[] = 'unique=' . var_export($fieldMapping['unique'], true);
1722            }
1723
1724            $lines[] = $this->spaces . ' * @' . $this->annotationsPrefix . 'Column(' . implode(', ', $column) . ')';
1725
1726            if (isset($fieldMapping['id']) && $fieldMapping['id']) {
1727                $lines[] = $this->spaces . ' * @' . $this->annotationsPrefix . 'Id';
1728
1729                if ($generatorType = $this->getIdGeneratorTypeString($metadata->generatorType)) {
1730                    $lines[] = $this->spaces.' * @' . $this->annotationsPrefix . 'GeneratedValue(strategy="' . $generatorType . '")';
1731                }
1732
1733                if ($metadata->sequenceGeneratorDefinition) {
1734                    $sequenceGenerator = [];
1735
1736                    if (isset($metadata->sequenceGeneratorDefinition['sequenceName'])) {
1737                        $sequenceGenerator[] = 'sequenceName="' . $metadata->sequenceGeneratorDefinition['sequenceName'] . '"';
1738                    }
1739
1740                    if (isset($metadata->sequenceGeneratorDefinition['allocationSize'])) {
1741                        $sequenceGenerator[] = 'allocationSize=' . $metadata->sequenceGeneratorDefinition['allocationSize'];
1742                    }
1743
1744                    if (isset($metadata->sequenceGeneratorDefinition['initialValue'])) {
1745                        $sequenceGenerator[] = 'initialValue=' . $metadata->sequenceGeneratorDefinition['initialValue'];
1746                    }
1747
1748                    $lines[] = $this->spaces . ' * @' . $this->annotationsPrefix . 'SequenceGenerator(' . implode(', ', $sequenceGenerator) . ')';
1749                }
1750            }
1751
1752            if (isset($fieldMapping['version']) && $fieldMapping['version']) {
1753                $lines[] = $this->spaces . ' * @' . $this->annotationsPrefix . 'Version';
1754            }
1755        }
1756
1757        $lines[] = $this->spaces . ' */';
1758
1759        return implode("\n", $lines);
1760    }
1761
1762    /**
1763     * @param array $embeddedClass
1764     *
1765     * @return string
1766     */
1767    protected function generateEmbeddedPropertyDocBlock(array $embeddedClass)
1768    {
1769        $lines = [];
1770        $lines[] = $this->spaces . '/**';
1771        $lines[] = $this->spaces . ' * @var \\' . ltrim($embeddedClass['class'], '\\');
1772
1773        if ($this->generateAnnotations) {
1774            $lines[] = $this->spaces . ' *';
1775
1776            $embedded = ['class="' . $embeddedClass['class'] . '"'];
1777
1778            if (isset($embeddedClass['columnPrefix'])) {
1779                if (is_string($embeddedClass['columnPrefix'])) {
1780                    $embedded[] = 'columnPrefix="' . $embeddedClass['columnPrefix'] . '"';
1781                } else {
1782                    $embedded[] = 'columnPrefix=' . var_export($embeddedClass['columnPrefix'], true);
1783                }
1784            }
1785
1786            $lines[] = $this->spaces . ' * @' .
1787                $this->annotationsPrefix . 'Embedded(' . implode(', ', $embedded) . ')';
1788        }
1789
1790        $lines[] = $this->spaces . ' */';
1791
1792        return implode("\n", $lines);
1793    }
1794
1795    private function generateEntityListenerAnnotation(ClassMetadataInfo $metadata): string
1796    {
1797        if (0 === \count($metadata->entityListeners)) {
1798            return '';
1799        }
1800
1801        $processedClasses = [];
1802        foreach ($metadata->entityListeners as $event => $eventListeners) {
1803            foreach ($eventListeners as $eventListener) {
1804                $processedClasses[] = '"' . $eventListener['class'] . '"';
1805            }
1806        }
1807
1808        return \sprintf(
1809            '%s%s({%s})',
1810            '@' . $this->annotationsPrefix,
1811            'EntityListeners',
1812            \implode(',', \array_unique($processedClasses))
1813        );
1814    }
1815
1816    /**
1817     * @param string $code
1818     * @param int    $num
1819     *
1820     * @return string
1821     */
1822    protected function prefixCodeWithSpaces($code, $num = 1)
1823    {
1824        $lines = explode("\n", $code);
1825
1826        foreach ($lines as $key => $value) {
1827            if ( ! empty($value)) {
1828                $lines[$key] = str_repeat($this->spaces, $num) . $lines[$key];
1829            }
1830        }
1831
1832        return implode("\n", $lines);
1833    }
1834
1835    /**
1836     * @param integer $type The inheritance type used by the class and its subclasses.
1837     *
1838     * @return string The literal string for the inheritance type.
1839     *
1840     * @throws \InvalidArgumentException When the inheritance type does not exist.
1841     */
1842    protected function getInheritanceTypeString($type)
1843    {
1844        if ( ! isset(static::$inheritanceTypeMap[$type])) {
1845            throw new \InvalidArgumentException(sprintf('Invalid provided InheritanceType: %s', $type));
1846        }
1847
1848        return static::$inheritanceTypeMap[$type];
1849    }
1850
1851    /**
1852     * @param integer $type The policy used for change-tracking for the mapped class.
1853     *
1854     * @return string The literal string for the change-tracking type.
1855     *
1856     * @throws \InvalidArgumentException When the change-tracking type does not exist.
1857     */
1858    protected function getChangeTrackingPolicyString($type)
1859    {
1860        if ( ! isset(static::$changeTrackingPolicyMap[$type])) {
1861            throw new \InvalidArgumentException(sprintf('Invalid provided ChangeTrackingPolicy: %s', $type));
1862        }
1863
1864        return static::$changeTrackingPolicyMap[$type];
1865    }
1866
1867    /**
1868     * @param integer $type The generator to use for the mapped class.
1869     *
1870     * @return string The literal string for the generator type.
1871     *
1872     * @throws \InvalidArgumentException    When the generator type does not exist.
1873     */
1874    protected function getIdGeneratorTypeString($type)
1875    {
1876        if ( ! isset(static::$generatorStrategyMap[$type])) {
1877            throw new \InvalidArgumentException(sprintf('Invalid provided IdGeneratorType: %s', $type));
1878        }
1879
1880        return static::$generatorStrategyMap[$type];
1881    }
1882
1883    /**
1884     * @param array $fieldMapping
1885     *
1886     * @return string|null
1887     */
1888    private function nullableFieldExpression(array $fieldMapping)
1889    {
1890        if (isset($fieldMapping['nullable']) && true === $fieldMapping['nullable']) {
1891            return 'null';
1892        }
1893
1894        return null;
1895    }
1896
1897    /**
1898     * Exports (nested) option elements.
1899     *
1900     * @param array $options
1901     *
1902     * @return string
1903     */
1904    private function exportTableOptions(array $options)
1905    {
1906        $optionsStr = [];
1907
1908        foreach ($options as $name => $option) {
1909            if (is_array($option)) {
1910                $optionsStr[] = '"' . $name . '"={' . $this->exportTableOptions($option) . '}';
1911            } else {
1912                $optionsStr[] = '"' . $name . '"="' . (string) $option . '"';
1913            }
1914        }
1915
1916        return implode(',', $optionsStr);
1917    }
1918}
1919