vendor/doctrine/orm/src/Mapping/Driver/AttributeDriver.php line 111

  1. <?php
  2. declare(strict_types=1);
  3. namespace Doctrine\ORM\Mapping\Driver;
  4. use Doctrine\Deprecations\Deprecation;
  5. use Doctrine\ORM\Events;
  6. use Doctrine\ORM\Mapping;
  7. use Doctrine\ORM\Mapping\Builder\EntityListenerBuilder;
  8. use Doctrine\ORM\Mapping\ClassMetadata;
  9. use Doctrine\ORM\Mapping\MappingAttribute;
  10. use Doctrine\ORM\Mapping\MappingException;
  11. use Doctrine\Persistence\Mapping\ClassMetadata as PersistenceClassMetadata;
  12. use Doctrine\Persistence\Mapping\Driver\ColocatedMappingDriver;
  13. use LogicException;
  14. use ReflectionClass;
  15. use ReflectionMethod;
  16. use ReflectionProperty;
  17. use function assert;
  18. use function class_exists;
  19. use function constant;
  20. use function defined;
  21. use function get_class;
  22. use const PHP_VERSION_ID;
  23. class AttributeDriver extends CompatibilityAnnotationDriver
  24. {
  25.     use ColocatedMappingDriver;
  26.     use ReflectionBasedDriver;
  27.     private const ENTITY_ATTRIBUTE_CLASSES = [
  28.         Mapping\Entity::class => 1,
  29.         Mapping\MappedSuperclass::class => 2,
  30.     ];
  31.     /**
  32.      * @deprecated override isTransient() instead of overriding this property
  33.      *
  34.      * @var array<class-string<MappingAttribute>, int>
  35.      */
  36.     protected $entityAnnotationClasses self::ENTITY_ATTRIBUTE_CLASSES;
  37.     /**
  38.      * The attribute reader.
  39.      *
  40.      * @internal this property will be private in 3.0
  41.      *
  42.      * @var AttributeReader
  43.      */
  44.     protected $reader;
  45.     /** @param array<string> $paths */
  46.     public function __construct(array $pathsbool $reportFieldsWhereDeclared false)
  47.     {
  48.         if (PHP_VERSION_ID 80000) {
  49.             throw new LogicException(
  50.                 'The attribute metadata driver cannot be enabled on PHP 7. Please upgrade to PHP 8 or choose a different'
  51.                 ' metadata driver.'
  52.             );
  53.         }
  54.         $this->reader = new AttributeReader();
  55.         $this->addPaths($paths);
  56.         if ($this->entityAnnotationClasses !== self::ENTITY_ATTRIBUTE_CLASSES) {
  57.             Deprecation::trigger(
  58.                 'doctrine/orm',
  59.                 'https://github.com/doctrine/orm/pull/10204',
  60.                 'Changing the value of %s::$entityAnnotationClasses is deprecated and will have no effect in Doctrine ORM 3.0.',
  61.                 self::class
  62.             );
  63.         }
  64.         if (! $reportFieldsWhereDeclared) {
  65.             Deprecation::trigger(
  66.                 'doctrine/orm',
  67.                 'https://github.com/doctrine/orm/pull/10455',
  68.                 'In ORM 3.0, the AttributeDriver will report fields for the classes where they are declared. This may uncover invalid mapping configurations. To opt into the new mode today, set the "reportFieldsWhereDeclared" constructor parameter to true.',
  69.                 self::class
  70.             );
  71.         }
  72.         $this->reportFieldsWhereDeclared $reportFieldsWhereDeclared;
  73.     }
  74.     /**
  75.      * Retrieve the current annotation reader
  76.      *
  77.      * @deprecated no replacement planned.
  78.      *
  79.      * @return AttributeReader
  80.      */
  81.     public function getReader()
  82.     {
  83.         Deprecation::trigger(
  84.             'doctrine/orm',
  85.             'https://github.com/doctrine/orm/pull/9587',
  86.             '%s is deprecated with no replacement',
  87.             __METHOD__
  88.         );
  89.         return $this->reader;
  90.     }
  91.     /**
  92.      * {@inheritDoc}
  93.      */
  94.     public function isTransient($className)
  95.     {
  96.         $classAttributes $this->reader->getClassAttributes(new ReflectionClass($className));
  97.         foreach ($classAttributes as $a) {
  98.             $attr $a instanceof RepeatableAttributeCollection $a[0] : $a;
  99.             if (isset($this->entityAnnotationClasses[get_class($attr)])) {
  100.                 return false;
  101.             }
  102.         }
  103.         return true;
  104.     }
  105.     /**
  106.      * {@inheritDoc}
  107.      *
  108.      * @psalm-param class-string<T> $className
  109.      * @psalm-param ClassMetadata<T> $metadata
  110.      *
  111.      * @template T of object
  112.      */
  113.     public function loadMetadataForClass($classNamePersistenceClassMetadata $metadata): void
  114.     {
  115.         $reflectionClass $metadata->getReflectionClass()
  116.             // this happens when running attribute driver in combination with
  117.             // static reflection services. This is not the nicest fix
  118.             ?? new ReflectionClass($metadata->name);
  119.         $classAttributes $this->reader->getClassAttributes($reflectionClass);
  120.         // Evaluate Entity attribute
  121.         if (isset($classAttributes[Mapping\Entity::class])) {
  122.             $entityAttribute $classAttributes[Mapping\Entity::class];
  123.             if ($entityAttribute->repositoryClass !== null) {
  124.                 $metadata->setCustomRepositoryClass($entityAttribute->repositoryClass);
  125.             }
  126.             if ($entityAttribute->readOnly) {
  127.                 $metadata->markReadOnly();
  128.             }
  129.         } elseif (isset($classAttributes[Mapping\MappedSuperclass::class])) {
  130.             $mappedSuperclassAttribute $classAttributes[Mapping\MappedSuperclass::class];
  131.             $metadata->setCustomRepositoryClass($mappedSuperclassAttribute->repositoryClass);
  132.             $metadata->isMappedSuperclass true;
  133.         } elseif (isset($classAttributes[Mapping\Embeddable::class])) {
  134.             $metadata->isEmbeddedClass true;
  135.         } else {
  136.             throw MappingException::classIsNotAValidEntityOrMappedSuperClass($className);
  137.         }
  138.         $primaryTable = [];
  139.         if (isset($classAttributes[Mapping\Table::class])) {
  140.             $tableAnnot             $classAttributes[Mapping\Table::class];
  141.             $primaryTable['name']   = $tableAnnot->name;
  142.             $primaryTable['schema'] = $tableAnnot->schema;
  143.             if ($tableAnnot->options) {
  144.                 $primaryTable['options'] = $tableAnnot->options;
  145.             }
  146.         }
  147.         if (isset($classAttributes[Mapping\Index::class])) {
  148.             foreach ($classAttributes[Mapping\Index::class] as $idx => $indexAnnot) {
  149.                 $index = [];
  150.                 if (! empty($indexAnnot->columns)) {
  151.                     $index['columns'] = $indexAnnot->columns;
  152.                 }
  153.                 if (! empty($indexAnnot->fields)) {
  154.                     $index['fields'] = $indexAnnot->fields;
  155.                 }
  156.                 if (
  157.                     isset($index['columns'], $index['fields'])
  158.                     || (
  159.                         ! isset($index['columns'])
  160.                         && ! isset($index['fields'])
  161.                     )
  162.                 ) {
  163.                     throw MappingException::invalidIndexConfiguration(
  164.                         $className,
  165.                         (string) ($indexAnnot->name ?? $idx)
  166.                     );
  167.                 }
  168.                 if (! empty($indexAnnot->flags)) {
  169.                     $index['flags'] = $indexAnnot->flags;
  170.                 }
  171.                 if (! empty($indexAnnot->options)) {
  172.                     $index['options'] = $indexAnnot->options;
  173.                 }
  174.                 if (! empty($indexAnnot->name)) {
  175.                     $primaryTable['indexes'][$indexAnnot->name] = $index;
  176.                 } else {
  177.                     $primaryTable['indexes'][] = $index;
  178.                 }
  179.             }
  180.         }
  181.         if (isset($classAttributes[Mapping\UniqueConstraint::class])) {
  182.             foreach ($classAttributes[Mapping\UniqueConstraint::class] as $idx => $uniqueConstraintAnnot) {
  183.                 $uniqueConstraint = [];
  184.                 if (! empty($uniqueConstraintAnnot->columns)) {
  185.                     $uniqueConstraint['columns'] = $uniqueConstraintAnnot->columns;
  186.                 }
  187.                 if (! empty($uniqueConstraintAnnot->fields)) {
  188.                     $uniqueConstraint['fields'] = $uniqueConstraintAnnot->fields;
  189.                 }
  190.                 if (
  191.                     isset($uniqueConstraint['columns'], $uniqueConstraint['fields'])
  192.                     || (
  193.                         ! isset($uniqueConstraint['columns'])
  194.                         && ! isset($uniqueConstraint['fields'])
  195.                     )
  196.                 ) {
  197.                     throw MappingException::invalidUniqueConstraintConfiguration(
  198.                         $className,
  199.                         (string) ($uniqueConstraintAnnot->name ?? $idx)
  200.                     );
  201.                 }
  202.                 if (! empty($uniqueConstraintAnnot->options)) {
  203.                     $uniqueConstraint['options'] = $uniqueConstraintAnnot->options;
  204.                 }
  205.                 if (! empty($uniqueConstraintAnnot->name)) {
  206.                     $primaryTable['uniqueConstraints'][$uniqueConstraintAnnot->name] = $uniqueConstraint;
  207.                 } else {
  208.                     $primaryTable['uniqueConstraints'][] = $uniqueConstraint;
  209.                 }
  210.             }
  211.         }
  212.         $metadata->setPrimaryTable($primaryTable);
  213.         // Evaluate #[Cache] attribute
  214.         if (isset($classAttributes[Mapping\Cache::class])) {
  215.             $cacheAttribute $classAttributes[Mapping\Cache::class];
  216.             $cacheMap       = [
  217.                 'region' => $cacheAttribute->region,
  218.                 'usage'  => constant('Doctrine\ORM\Mapping\ClassMetadata::CACHE_USAGE_' $cacheAttribute->usage),
  219.             ];
  220.             $metadata->enableCache($cacheMap);
  221.         }
  222.         // Evaluate InheritanceType attribute
  223.         if (isset($classAttributes[Mapping\InheritanceType::class])) {
  224.             $inheritanceTypeAttribute $classAttributes[Mapping\InheritanceType::class];
  225.             $metadata->setInheritanceType(
  226.                 constant('Doctrine\ORM\Mapping\ClassMetadata::INHERITANCE_TYPE_' $inheritanceTypeAttribute->value)
  227.             );
  228.             if ($metadata->inheritanceType !== ClassMetadata::INHERITANCE_TYPE_NONE) {
  229.                 // Evaluate DiscriminatorColumn attribute
  230.                 if (isset($classAttributes[Mapping\DiscriminatorColumn::class])) {
  231.                     $discrColumnAttribute $classAttributes[Mapping\DiscriminatorColumn::class];
  232.                     $columnDef = [
  233.                         'name' => isset($discrColumnAttribute->name) ? (string) $discrColumnAttribute->name null,
  234.                         'type' => isset($discrColumnAttribute->type) ? (string) $discrColumnAttribute->type 'string',
  235.                         'length' => isset($discrColumnAttribute->length) ? (int) $discrColumnAttribute->length 255,
  236.                         'columnDefinition' => isset($discrColumnAttribute->columnDefinition) ? (string) $discrColumnAttribute->columnDefinition null,
  237.                         'enumType' => isset($discrColumnAttribute->enumType) ? (string) $discrColumnAttribute->enumType null,
  238.                     ];
  239.                     if ($discrColumnAttribute->options) {
  240.                         $columnDef['options'] = (array) $discrColumnAttribute->options;
  241.                     }
  242.                     $metadata->setDiscriminatorColumn($columnDef);
  243.                 } else {
  244.                     $metadata->setDiscriminatorColumn(['name' => 'dtype''type' => 'string''length' => 255]);
  245.                 }
  246.                 // Evaluate DiscriminatorMap attribute
  247.                 if (isset($classAttributes[Mapping\DiscriminatorMap::class])) {
  248.                     $discrMapAttribute $classAttributes[Mapping\DiscriminatorMap::class];
  249.                     $metadata->setDiscriminatorMap($discrMapAttribute->value);
  250.                 }
  251.             }
  252.         }
  253.         // Evaluate DoctrineChangeTrackingPolicy attribute
  254.         if (isset($classAttributes[Mapping\ChangeTrackingPolicy::class])) {
  255.             $changeTrackingAttribute $classAttributes[Mapping\ChangeTrackingPolicy::class];
  256.             $metadata->setChangeTrackingPolicy(constant('Doctrine\ORM\Mapping\ClassMetadata::CHANGETRACKING_' $changeTrackingAttribute->value));
  257.         }
  258.         foreach ($reflectionClass->getProperties() as $property) {
  259.             assert($property instanceof ReflectionProperty);
  260.             if ($this->isRepeatedPropertyDeclaration($property$metadata)) {
  261.                 continue;
  262.             }
  263.             $mapping              = [];
  264.             $mapping['fieldName'] = $property->name;
  265.             // Evaluate #[Cache] attribute
  266.             $cacheAttribute $this->reader->getPropertyAttribute($propertyMapping\Cache::class);
  267.             if ($cacheAttribute !== null) {
  268.                 assert($cacheAttribute instanceof Mapping\Cache);
  269.                 $mapping['cache'] = $metadata->getAssociationCacheDefaults(
  270.                     $mapping['fieldName'],
  271.                     [
  272.                         'usage'  => (int) constant('Doctrine\ORM\Mapping\ClassMetadata::CACHE_USAGE_' $cacheAttribute->usage),
  273.                         'region' => $cacheAttribute->region,
  274.                     ]
  275.                 );
  276.             }
  277.             // Check for JoinColumn/JoinColumns attributes
  278.             $joinColumns = [];
  279.             $joinColumnAttributes $this->reader->getPropertyAttributeCollection($propertyMapping\JoinColumn::class);
  280.             foreach ($joinColumnAttributes as $joinColumnAttribute) {
  281.                 $joinColumns[] = $this->joinColumnToArray($joinColumnAttribute);
  282.             }
  283.             // Field can only be attributed with one of:
  284.             // Column, OneToOne, OneToMany, ManyToOne, ManyToMany, Embedded
  285.             $columnAttribute     $this->reader->getPropertyAttribute($propertyMapping\Column::class);
  286.             $oneToOneAttribute   $this->reader->getPropertyAttribute($propertyMapping\OneToOne::class);
  287.             $oneToManyAttribute  $this->reader->getPropertyAttribute($propertyMapping\OneToMany::class);
  288.             $manyToOneAttribute  $this->reader->getPropertyAttribute($propertyMapping\ManyToOne::class);
  289.             $manyToManyAttribute $this->reader->getPropertyAttribute($propertyMapping\ManyToMany::class);
  290.             $embeddedAttribute   $this->reader->getPropertyAttribute($propertyMapping\Embedded::class);
  291.             if ($columnAttribute !== null) {
  292.                 $mapping $this->columnToArray($property->name$columnAttribute);
  293.                 if ($this->reader->getPropertyAttribute($propertyMapping\Id::class)) {
  294.                     $mapping['id'] = true;
  295.                 }
  296.                 $generatedValueAttribute $this->reader->getPropertyAttribute($propertyMapping\GeneratedValue::class);
  297.                 if ($generatedValueAttribute !== null) {
  298.                     $metadata->setIdGeneratorType(constant('Doctrine\ORM\Mapping\ClassMetadata::GENERATOR_TYPE_' $generatedValueAttribute->strategy));
  299.                 }
  300.                 if ($this->reader->getPropertyAttribute($propertyMapping\Version::class)) {
  301.                     $metadata->setVersionMapping($mapping);
  302.                 }
  303.                 $metadata->mapField($mapping);
  304.                 // Check for SequenceGenerator/TableGenerator definition
  305.                 $seqGeneratorAttribute    $this->reader->getPropertyAttribute($propertyMapping\SequenceGenerator::class);
  306.                 $customGeneratorAttribute $this->reader->getPropertyAttribute($propertyMapping\CustomIdGenerator::class);
  307.                 if ($seqGeneratorAttribute !== null) {
  308.                     $metadata->setSequenceGeneratorDefinition(
  309.                         [
  310.                             'sequenceName' => $seqGeneratorAttribute->sequenceName,
  311.                             'allocationSize' => $seqGeneratorAttribute->allocationSize,
  312.                             'initialValue' => $seqGeneratorAttribute->initialValue,
  313.                         ]
  314.                     );
  315.                 } elseif ($customGeneratorAttribute !== null) {
  316.                     $metadata->setCustomGeneratorDefinition(
  317.                         [
  318.                             'class' => $customGeneratorAttribute->class,
  319.                         ]
  320.                     );
  321.                 }
  322.             } elseif ($oneToOneAttribute !== null) {
  323.                 if ($this->reader->getPropertyAttribute($propertyMapping\Id::class)) {
  324.                     $mapping['id'] = true;
  325.                 }
  326.                 $mapping['targetEntity']  = $oneToOneAttribute->targetEntity;
  327.                 $mapping['joinColumns']   = $joinColumns;
  328.                 $mapping['mappedBy']      = $oneToOneAttribute->mappedBy;
  329.                 $mapping['inversedBy']    = $oneToOneAttribute->inversedBy;
  330.                 $mapping['cascade']       = $oneToOneAttribute->cascade;
  331.                 $mapping['orphanRemoval'] = $oneToOneAttribute->orphanRemoval;
  332.                 $mapping['fetch']         = $this->getFetchMode($className$oneToOneAttribute->fetch);
  333.                 $metadata->mapOneToOne($mapping);
  334.             } elseif ($oneToManyAttribute !== null) {
  335.                 $mapping['mappedBy']      = $oneToManyAttribute->mappedBy;
  336.                 $mapping['targetEntity']  = $oneToManyAttribute->targetEntity;
  337.                 $mapping['cascade']       = $oneToManyAttribute->cascade;
  338.                 $mapping['indexBy']       = $oneToManyAttribute->indexBy;
  339.                 $mapping['orphanRemoval'] = $oneToManyAttribute->orphanRemoval;
  340.                 $mapping['fetch']         = $this->getFetchMode($className$oneToManyAttribute->fetch);
  341.                 $orderByAttribute $this->reader->getPropertyAttribute($propertyMapping\OrderBy::class);
  342.                 if ($orderByAttribute !== null) {
  343.                     $mapping['orderBy'] = $orderByAttribute->value;
  344.                 }
  345.                 $metadata->mapOneToMany($mapping);
  346.             } elseif ($manyToOneAttribute !== null) {
  347.                 $idAttribute $this->reader->getPropertyAttribute($propertyMapping\Id::class);
  348.                 if ($idAttribute !== null) {
  349.                     $mapping['id'] = true;
  350.                 }
  351.                 $mapping['joinColumns']  = $joinColumns;
  352.                 $mapping['cascade']      = $manyToOneAttribute->cascade;
  353.                 $mapping['inversedBy']   = $manyToOneAttribute->inversedBy;
  354.                 $mapping['targetEntity'] = $manyToOneAttribute->targetEntity;
  355.                 $mapping['fetch']        = $this->getFetchMode($className$manyToOneAttribute->fetch);
  356.                 $metadata->mapManyToOne($mapping);
  357.             } elseif ($manyToManyAttribute !== null) {
  358.                 $joinTable          = [];
  359.                 $joinTableAttribute $this->reader->getPropertyAttribute($propertyMapping\JoinTable::class);
  360.                 if ($joinTableAttribute !== null) {
  361.                     $joinTable = [
  362.                         'name' => $joinTableAttribute->name,
  363.                         'schema' => $joinTableAttribute->schema,
  364.                     ];
  365.                     if ($joinTableAttribute->options) {
  366.                         $joinTable['options'] = $joinTableAttribute->options;
  367.                     }
  368.                     foreach ($joinTableAttribute->joinColumns as $joinColumn) {
  369.                         $joinTable['joinColumns'][] = $this->joinColumnToArray($joinColumn);
  370.                     }
  371.                     foreach ($joinTableAttribute->inverseJoinColumns as $joinColumn) {
  372.                         $joinTable['inverseJoinColumns'][] = $this->joinColumnToArray($joinColumn);
  373.                     }
  374.                 }
  375.                 foreach ($this->reader->getPropertyAttributeCollection($propertyMapping\JoinColumn::class) as $joinColumn) {
  376.                     $joinTable['joinColumns'][] = $this->joinColumnToArray($joinColumn);
  377.                 }
  378.                 foreach ($this->reader->getPropertyAttributeCollection($propertyMapping\InverseJoinColumn::class) as $joinColumn) {
  379.                     $joinTable['inverseJoinColumns'][] = $this->joinColumnToArray($joinColumn);
  380.                 }
  381.                 $mapping['joinTable']     = $joinTable;
  382.                 $mapping['targetEntity']  = $manyToManyAttribute->targetEntity;
  383.                 $mapping['mappedBy']      = $manyToManyAttribute->mappedBy;
  384.                 $mapping['inversedBy']    = $manyToManyAttribute->inversedBy;
  385.                 $mapping['cascade']       = $manyToManyAttribute->cascade;
  386.                 $mapping['indexBy']       = $manyToManyAttribute->indexBy;
  387.                 $mapping['orphanRemoval'] = $manyToManyAttribute->orphanRemoval;
  388.                 $mapping['fetch']         = $this->getFetchMode($className$manyToManyAttribute->fetch);
  389.                 $orderByAttribute $this->reader->getPropertyAttribute($propertyMapping\OrderBy::class);
  390.                 if ($orderByAttribute !== null) {
  391.                     $mapping['orderBy'] = $orderByAttribute->value;
  392.                 }
  393.                 $metadata->mapManyToMany($mapping);
  394.             } elseif ($embeddedAttribute !== null) {
  395.                 $mapping['class']        = $embeddedAttribute->class;
  396.                 $mapping['columnPrefix'] = $embeddedAttribute->columnPrefix;
  397.                 $metadata->mapEmbedded($mapping);
  398.             }
  399.         }
  400.         // Evaluate AssociationOverrides attribute
  401.         if (isset($classAttributes[Mapping\AssociationOverrides::class])) {
  402.             $associationOverride $classAttributes[Mapping\AssociationOverrides::class];
  403.             foreach ($associationOverride->overrides as $associationOverride) {
  404.                 $override  = [];
  405.                 $fieldName $associationOverride->name;
  406.                 // Check for JoinColumn/JoinColumns attributes
  407.                 if ($associationOverride->joinColumns) {
  408.                     $joinColumns = [];
  409.                     foreach ($associationOverride->joinColumns as $joinColumn) {
  410.                         $joinColumns[] = $this->joinColumnToArray($joinColumn);
  411.                     }
  412.                     $override['joinColumns'] = $joinColumns;
  413.                 }
  414.                 if ($associationOverride->inverseJoinColumns) {
  415.                     $joinColumns = [];
  416.                     foreach ($associationOverride->inverseJoinColumns as $joinColumn) {
  417.                         $joinColumns[] = $this->joinColumnToArray($joinColumn);
  418.                     }
  419.                     $override['inverseJoinColumns'] = $joinColumns;
  420.                 }
  421.                 // Check for JoinTable attributes
  422.                 if ($associationOverride->joinTable) {
  423.                     $joinTableAnnot $associationOverride->joinTable;
  424.                     $joinTable      = [
  425.                         'name'      => $joinTableAnnot->name,
  426.                         'schema'    => $joinTableAnnot->schema,
  427.                         'joinColumns' => $override['joinColumns'] ?? [],
  428.                         'inverseJoinColumns' => $override['inverseJoinColumns'] ?? [],
  429.                     ];
  430.                     unset($override['joinColumns'], $override['inverseJoinColumns']);
  431.                     $override['joinTable'] = $joinTable;
  432.                 }
  433.                 // Check for inversedBy
  434.                 if ($associationOverride->inversedBy) {
  435.                     $override['inversedBy'] = $associationOverride->inversedBy;
  436.                 }
  437.                 // Check for `fetch`
  438.                 if ($associationOverride->fetch) {
  439.                     $override['fetch'] = constant(ClassMetadata::class . '::FETCH_' $associationOverride->fetch);
  440.                 }
  441.                 $metadata->setAssociationOverride($fieldName$override);
  442.             }
  443.         }
  444.         // Evaluate AttributeOverrides attribute
  445.         if (isset($classAttributes[Mapping\AttributeOverrides::class])) {
  446.             $attributeOverridesAnnot $classAttributes[Mapping\AttributeOverrides::class];
  447.             foreach ($attributeOverridesAnnot->overrides as $attributeOverride) {
  448.                 $mapping $this->columnToArray($attributeOverride->name$attributeOverride->column);
  449.                 $metadata->setAttributeOverride($attributeOverride->name$mapping);
  450.             }
  451.         }
  452.         // Evaluate EntityListeners attribute
  453.         if (isset($classAttributes[Mapping\EntityListeners::class])) {
  454.             $entityListenersAttribute $classAttributes[Mapping\EntityListeners::class];
  455.             foreach ($entityListenersAttribute->value as $item) {
  456.                 $listenerClassName $metadata->fullyQualifiedClassName($item);
  457.                 if (! class_exists($listenerClassName)) {
  458.                     throw MappingException::entityListenerClassNotFound($listenerClassName$className);
  459.                 }
  460.                 $hasMapping    false;
  461.                 $listenerClass = new ReflectionClass($listenerClassName);
  462.                 foreach ($listenerClass->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
  463.                     assert($method instanceof ReflectionMethod);
  464.                     // find method callbacks.
  465.                     $callbacks  $this->getMethodCallbacks($method);
  466.                     $hasMapping $hasMapping ?: ! empty($callbacks);
  467.                     foreach ($callbacks as $value) {
  468.                         $metadata->addEntityListener($value[1], $listenerClassName$value[0]);
  469.                     }
  470.                 }
  471.                 // Evaluate the listener using naming convention.
  472.                 if (! $hasMapping) {
  473.                     EntityListenerBuilder::bindEntityListener($metadata$listenerClassName);
  474.                 }
  475.             }
  476.         }
  477.         // Evaluate #[HasLifecycleCallbacks] attribute
  478.         if (isset($classAttributes[Mapping\HasLifecycleCallbacks::class])) {
  479.             foreach ($reflectionClass->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
  480.                 assert($method instanceof ReflectionMethod);
  481.                 foreach ($this->getMethodCallbacks($method) as $value) {
  482.                     $metadata->addLifecycleCallback($value[0], $value[1]);
  483.                 }
  484.             }
  485.         }
  486.     }
  487.     /**
  488.      * Attempts to resolve the fetch mode.
  489.      *
  490.      * @param class-string $className The class name.
  491.      * @param string       $fetchMode The fetch mode.
  492.      *
  493.      * @return ClassMetadata::FETCH_* The fetch mode as defined in ClassMetadata.
  494.      *
  495.      * @throws MappingException If the fetch mode is not valid.
  496.      */
  497.     private function getFetchMode(string $classNamestring $fetchMode): int
  498.     {
  499.         if (! defined('Doctrine\ORM\Mapping\ClassMetadata::FETCH_' $fetchMode)) {
  500.             throw MappingException::invalidFetchMode($className$fetchMode);
  501.         }
  502.         return constant('Doctrine\ORM\Mapping\ClassMetadata::FETCH_' $fetchMode);
  503.     }
  504.     /**
  505.      * Attempts to resolve the generated mode.
  506.      *
  507.      * @throws MappingException If the fetch mode is not valid.
  508.      */
  509.     private function getGeneratedMode(string $generatedMode): int
  510.     {
  511.         if (! defined('Doctrine\ORM\Mapping\ClassMetadata::GENERATED_' $generatedMode)) {
  512.             throw MappingException::invalidGeneratedMode($generatedMode);
  513.         }
  514.         return constant('Doctrine\ORM\Mapping\ClassMetadata::GENERATED_' $generatedMode);
  515.     }
  516.     /**
  517.      * Parses the given method.
  518.      *
  519.      * @return list<array{string, string}>
  520.      * @psalm-return list<array{string, (Events::*)}>
  521.      */
  522.     private function getMethodCallbacks(ReflectionMethod $method): array
  523.     {
  524.         $callbacks  = [];
  525.         $attributes $this->reader->getMethodAttributes($method);
  526.         foreach ($attributes as $attribute) {
  527.             if ($attribute instanceof Mapping\PrePersist) {
  528.                 $callbacks[] = [$method->nameEvents::prePersist];
  529.             }
  530.             if ($attribute instanceof Mapping\PostPersist) {
  531.                 $callbacks[] = [$method->nameEvents::postPersist];
  532.             }
  533.             if ($attribute instanceof Mapping\PreUpdate) {
  534.                 $callbacks[] = [$method->nameEvents::preUpdate];
  535.             }
  536.             if ($attribute instanceof Mapping\PostUpdate) {
  537.                 $callbacks[] = [$method->nameEvents::postUpdate];
  538.             }
  539.             if ($attribute instanceof Mapping\PreRemove) {
  540.                 $callbacks[] = [$method->nameEvents::preRemove];
  541.             }
  542.             if ($attribute instanceof Mapping\PostRemove) {
  543.                 $callbacks[] = [$method->nameEvents::postRemove];
  544.             }
  545.             if ($attribute instanceof Mapping\PostLoad) {
  546.                 $callbacks[] = [$method->nameEvents::postLoad];
  547.             }
  548.             if ($attribute instanceof Mapping\PreFlush) {
  549.                 $callbacks[] = [$method->nameEvents::preFlush];
  550.             }
  551.         }
  552.         return $callbacks;
  553.     }
  554.     /**
  555.      * Parse the given JoinColumn as array
  556.      *
  557.      * @param Mapping\JoinColumn|Mapping\InverseJoinColumn $joinColumn
  558.      *
  559.      * @return mixed[]
  560.      * @psalm-return array{
  561.      *                   name: string|null,
  562.      *                   unique: bool,
  563.      *                   nullable: bool,
  564.      *                   onDelete: mixed,
  565.      *                   columnDefinition: string|null,
  566.      *                   referencedColumnName: string,
  567.      *                   options?: array<string, mixed>
  568.      *               }
  569.      */
  570.     private function joinColumnToArray($joinColumn): array
  571.     {
  572.         $mapping = [
  573.             'name' => $joinColumn->name,
  574.             'unique' => $joinColumn->unique,
  575.             'nullable' => $joinColumn->nullable,
  576.             'onDelete' => $joinColumn->onDelete,
  577.             'columnDefinition' => $joinColumn->columnDefinition,
  578.             'referencedColumnName' => $joinColumn->referencedColumnName,
  579.         ];
  580.         if ($joinColumn->options) {
  581.             $mapping['options'] = $joinColumn->options;
  582.         }
  583.         return $mapping;
  584.     }
  585.     /**
  586.      * Parse the given Column as array
  587.      *
  588.      * @return mixed[]
  589.      * @psalm-return array{
  590.      *                   fieldName: string,
  591.      *                   type: mixed,
  592.      *                   scale: int,
  593.      *                   length: int,
  594.      *                   unique: bool,
  595.      *                   nullable: bool,
  596.      *                   precision: int,
  597.      *                   enumType?: class-string,
  598.      *                   options?: mixed[],
  599.      *                   columnName?: string,
  600.      *                   columnDefinition?: string
  601.      *               }
  602.      */
  603.     private function columnToArray(string $fieldNameMapping\Column $column): array
  604.     {
  605.         $mapping = [
  606.             'fieldName' => $fieldName,
  607.             'type'      => $column->type,
  608.             'scale'     => $column->scale,
  609.             'length'    => $column->length,
  610.             'unique'    => $column->unique,
  611.             'nullable'  => $column->nullable,
  612.             'precision' => $column->precision,
  613.         ];
  614.         if ($column->options) {
  615.             $mapping['options'] = $column->options;
  616.         }
  617.         if (isset($column->name)) {
  618.             $mapping['columnName'] = $column->name;
  619.         }
  620.         if (isset($column->columnDefinition)) {
  621.             $mapping['columnDefinition'] = $column->columnDefinition;
  622.         }
  623.         if ($column->updatable === false) {
  624.             $mapping['notUpdatable'] = true;
  625.         }
  626.         if ($column->insertable === false) {
  627.             $mapping['notInsertable'] = true;
  628.         }
  629.         if ($column->generated !== null) {
  630.             $mapping['generated'] = $this->getGeneratedMode($column->generated);
  631.         }
  632.         if ($column->enumType) {
  633.             $mapping['enumType'] = $column->enumType;
  634.         }
  635.         return $mapping;
  636.     }
  637. }