>
+ */
+ public array $entityListeners = [];
+
+ /**
+ * READ-ONLY: The association mappings of this class.
+ *
+ * The mapping definition array supports the following keys:
+ *
+ * - fieldName (string)
+ * The name of the field in the entity the association is mapped to.
+ *
+ * - targetEntity (string)
+ * The class name of the target entity. If it is fully-qualified it is used as is.
+ * If it is a simple, unqualified class name the namespace is assumed to be the same
+ * as the namespace of the source entity.
+ *
+ * - mappedBy (string, required for bidirectional associations)
+ * The name of the field that completes the bidirectional association on the owning side.
+ * This key must be specified on the inverse side of a bidirectional association.
+ *
+ * - inversedBy (string, required for bidirectional associations)
+ * The name of the field that completes the bidirectional association on the inverse side.
+ * This key must be specified on the owning side of a bidirectional association.
+ *
+ * - cascade (array, optional)
+ * The names of persistence operations to cascade on the association. The set of possible
+ * values are: "persist", "remove", "detach", "merge", "refresh", "all" (implies all others).
+ *
+ * - orderBy (array, one-to-many/many-to-many only)
+ * A map of field names (of the target entity) to sorting directions (ASC/DESC).
+ * Example: array('priority' => 'desc')
+ *
+ * - fetch (integer, optional)
+ * The fetching strategy to use for the association, usually defaults to FETCH_LAZY.
+ * Possible values are: ClassMetadata::FETCH_EAGER, ClassMetadata::FETCH_LAZY.
+ *
+ * - joinTable (array, optional, many-to-many only)
+ * Specification of the join table and its join columns (foreign keys).
+ * Only valid for many-to-many mappings. Note that one-to-many associations can be mapped
+ * through a join table by simply mapping the association as many-to-many with a unique
+ * constraint on the join table.
+ *
+ * - indexBy (string, optional, to-many only)
+ * Specification of a field on target-entity that is used to index the collection by.
+ * This field HAS to be either the primary key or a unique column. Otherwise the collection
+ * does not contain all the entities that are actually related.
+ *
+ * A join table definition has the following structure:
+ *
+ * array(
+ * 'name' => ,
+ * 'joinColumns' => array(),
+ * 'inverseJoinColumns' => array()
+ * )
+ *
+ *
+ * @psalm-var array
+ */
+ public array $associationMappings = [];
+
+ /**
+ * READ-ONLY: Flag indicating whether the identifier/primary key of the class is composite.
+ */
+ public bool $isIdentifierComposite = false;
+
+ /**
+ * READ-ONLY: Flag indicating whether the identifier/primary key contains at least one foreign key association.
+ *
+ * This flag is necessary because some code blocks require special treatment of this cases.
+ */
+ public bool $containsForeignIdentifier = false;
+
+ /**
+ * READ-ONLY: Flag indicating whether the identifier/primary key contains at least one ENUM type.
+ *
+ * This flag is necessary because some code blocks require special treatment of this cases.
+ */
+ public bool $containsEnumIdentifier = false;
+
+ /**
+ * READ-ONLY: The ID generator used for generating IDs for this class.
+ *
+ * @todo Remove!
+ */
+ public AbstractIdGenerator $idGenerator;
+
+ /**
+ * READ-ONLY: The definition of the sequence generator of this class. Only used for the
+ * SEQUENCE generation strategy.
+ *
+ * The definition has the following structure:
+ *
+ * array(
+ * 'sequenceName' => 'name',
+ * 'allocationSize' => '20',
+ * 'initialValue' => '1'
+ * )
+ *
+ *
+ * @var array|null
+ * @psalm-var array{sequenceName: string, allocationSize: string, initialValue: string, quoted?: mixed}|null
+ * @todo Merge with tableGeneratorDefinition into generic generatorDefinition
+ */
+ public array|null $sequenceGeneratorDefinition = null;
+
/**
- * Repeating the ClassMetadataInfo constructor to infer correctly the template with PHPStan
+ * READ-ONLY: The policy used for change-tracking on entities of this class.
+ */
+ public int $changeTrackingPolicy = self::CHANGETRACKING_DEFERRED_IMPLICIT;
+
+ /**
+ * READ-ONLY: A Flag indicating whether one or more columns of this class
+ * have to be reloaded after insert / update operations.
+ */
+ public bool $requiresFetchAfterChange = false;
+
+ /**
+ * READ-ONLY: A flag for whether or not instances of this class are to be versioned
+ * with optimistic locking.
+ */
+ public bool $isVersioned = false;
+
+ /**
+ * READ-ONLY: The name of the field which is used for versioning in optimistic locking (if any).
+ */
+ public string|null $versionField = null;
+
+ /** @var mixed[]|null */
+ public array|null $cache = null;
+
+ /**
+ * The ReflectionClass instance of the mapped class.
+ *
+ * @var ReflectionClass|null
+ */
+ public ReflectionClass|null $reflClass = null;
+
+ /**
+ * Is this entity marked as "read-only"?
+ *
+ * That means it is never considered for change-tracking in the UnitOfWork. It is a very helpful performance
+ * optimization for entities that are immutable, either in your domain or through the relation database
+ * (coming from a view, or a history table for example).
+ */
+ public bool $isReadOnly = false;
+
+ /**
+ * NamingStrategy determining the default column and table names.
+ */
+ protected NamingStrategy $namingStrategy;
+
+ /**
+ * The ReflectionProperty instances of the mapped class.
*
- * @see https://github.com/doctrine/orm/issues/8709
+ * @var array
+ */
+ public array $reflFields = [];
+
+ private InstantiatorInterface|null $instantiator = null;
+
+ private TypedFieldMapper $typedFieldMapper;
+
+ /**
+ * Initializes a new ClassMetadata instance that will hold the object-relational mapping
+ * metadata of the class with the given name.
*
- * @param string $entityName The name of the entity class the new instance is used for.
- * @psalm-param class-string $entityName
+ * @param string $name The name of the entity class the new instance is used for.
+ * @psalm-param class-string $name
*/
- public function __construct($entityName, NamingStrategy|null $namingStrategy = null, TypedFieldMapper|null $typedFieldMapper = null)
+ public function __construct(public $name, NamingStrategy|null $namingStrategy = null, TypedFieldMapper|null $typedFieldMapper = null)
{
- parent::__construct($entityName, $namingStrategy, $typedFieldMapper);
+ $this->rootEntityName = $name;
+ $this->namingStrategy = $namingStrategy ?? new DefaultNamingStrategy();
+ $this->instantiator = new Instantiator();
+ $this->typedFieldMapper = $typedFieldMapper ?? new DefaultTypedFieldMapper();
+ }
+
+ /**
+ * Gets the ReflectionProperties of the mapped class.
+ *
+ * @return ReflectionProperty[]|null[] An array of ReflectionProperty instances.
+ * @psalm-return array
+ */
+ public function getReflectionProperties(): array
+ {
+ return $this->reflFields;
+ }
+
+ /**
+ * Gets a ReflectionProperty for a specific field of the mapped class.
+ */
+ public function getReflectionProperty(string $name): ReflectionProperty|null
+ {
+ return $this->reflFields[$name];
+ }
+
+ /**
+ * Gets the ReflectionProperty for the single identifier field.
+ *
+ * @throws BadMethodCallException If the class has a composite identifier.
+ */
+ public function getSingleIdReflectionProperty(): ReflectionProperty|null
+ {
+ if ($this->isIdentifierComposite) {
+ throw new BadMethodCallException('Class ' . $this->name . ' has a composite identifier.');
+ }
+
+ return $this->reflFields[$this->identifier[0]];
+ }
+
+ /**
+ * Extracts the identifier values of an entity of this class.
+ *
+ * For composite identifiers, the identifier values are returned as an array
+ * with the same order as the field order in {@link identifier}.
+ *
+ * @param object $entity
+ *
+ * @return array
+ */
+ public function getIdentifierValues($entity): array
+ {
+ if ($this->isIdentifierComposite) {
+ $id = [];
+
+ foreach ($this->identifier as $idField) {
+ $value = $this->reflFields[$idField]->getValue($entity);
+
+ if ($value !== null) {
+ $id[$idField] = $value;
+ }
+ }
+
+ return $id;
+ }
+
+ $id = $this->identifier[0];
+ $value = $this->reflFields[$id]->getValue($entity);
+
+ if ($value === null) {
+ return [];
+ }
+
+ return [$id => $value];
+ }
+
+ /**
+ * Populates the entity identifier of an entity.
+ *
+ * @psalm-param array $id
+ *
+ * @todo Rename to assignIdentifier()
+ */
+ public function setIdentifierValues(object $entity, array $id): void
+ {
+ foreach ($id as $idField => $idValue) {
+ $this->reflFields[$idField]->setValue($entity, $idValue);
+ }
+ }
+
+ /**
+ * Sets the specified field to the specified value on the given entity.
+ */
+ public function setFieldValue(object $entity, string $field, mixed $value): void
+ {
+ $this->reflFields[$field]->setValue($entity, $value);
+ }
+
+ /**
+ * Gets the specified field's value off the given entity.
+ */
+ public function getFieldValue(object $entity, string $field): mixed
+ {
+ return $this->reflFields[$field]->getValue($entity);
+ }
+
+ /**
+ * Creates a string representation of this instance.
+ *
+ * @return string The string representation of this instance.
+ *
+ * @todo Construct meaningful string representation.
+ */
+ public function __toString(): string
+ {
+ return self::class . '@' . spl_object_id($this);
+ }
+
+ /**
+ * Determines which fields get serialized.
+ *
+ * It is only serialized what is necessary for best unserialization performance.
+ * That means any metadata properties that are not set or empty or simply have
+ * their default value are NOT serialized.
+ *
+ * Parts that are also NOT serialized because they can not be properly unserialized:
+ * - reflClass (ReflectionClass)
+ * - reflFields (ReflectionProperty array)
+ *
+ * @return string[] The names of all the fields that should be serialized.
+ */
+ public function __sleep()
+ {
+ // This metadata is always serialized/cached.
+ $serialized = [
+ 'associationMappings',
+ 'columnNames', //TODO: 3.0 Remove this. Can use fieldMappings[$fieldName]['columnName']
+ 'fieldMappings',
+ 'fieldNames',
+ 'embeddedClasses',
+ 'identifier',
+ 'isIdentifierComposite', // TODO: REMOVE
+ 'name',
+ 'namespace', // TODO: REMOVE
+ 'table',
+ 'rootEntityName',
+ 'idGenerator', //TODO: Does not really need to be serialized. Could be moved to runtime.
+ ];
+
+ // The rest of the metadata is only serialized if necessary.
+ if ($this->changeTrackingPolicy !== self::CHANGETRACKING_DEFERRED_IMPLICIT) {
+ $serialized[] = 'changeTrackingPolicy';
+ }
+
+ if ($this->customRepositoryClassName) {
+ $serialized[] = 'customRepositoryClassName';
+ }
+
+ if ($this->inheritanceType !== self::INHERITANCE_TYPE_NONE) {
+ $serialized[] = 'inheritanceType';
+ $serialized[] = 'discriminatorColumn';
+ $serialized[] = 'discriminatorValue';
+ $serialized[] = 'discriminatorMap';
+ $serialized[] = 'parentClasses';
+ $serialized[] = 'subClasses';
+ }
+
+ if ($this->generatorType !== self::GENERATOR_TYPE_NONE) {
+ $serialized[] = 'generatorType';
+ if ($this->generatorType === self::GENERATOR_TYPE_SEQUENCE) {
+ $serialized[] = 'sequenceGeneratorDefinition';
+ }
+ }
+
+ if ($this->isMappedSuperclass) {
+ $serialized[] = 'isMappedSuperclass';
+ }
+
+ if ($this->isEmbeddedClass) {
+ $serialized[] = 'isEmbeddedClass';
+ }
+
+ if ($this->containsForeignIdentifier) {
+ $serialized[] = 'containsForeignIdentifier';
+ }
+
+ if ($this->containsEnumIdentifier) {
+ $serialized[] = 'containsEnumIdentifier';
+ }
+
+ if ($this->isVersioned) {
+ $serialized[] = 'isVersioned';
+ $serialized[] = 'versionField';
+ }
+
+ if ($this->lifecycleCallbacks) {
+ $serialized[] = 'lifecycleCallbacks';
+ }
+
+ if ($this->entityListeners) {
+ $serialized[] = 'entityListeners';
+ }
+
+ if ($this->isReadOnly) {
+ $serialized[] = 'isReadOnly';
+ }
+
+ if ($this->customGeneratorDefinition) {
+ $serialized[] = 'customGeneratorDefinition';
+ }
+
+ if ($this->cache) {
+ $serialized[] = 'cache';
+ }
+
+ if ($this->requiresFetchAfterChange) {
+ $serialized[] = 'requiresFetchAfterChange';
+ }
+
+ return $serialized;
+ }
+
+ /**
+ * Creates a new instance of the mapped class, without invoking the constructor.
+ */
+ public function newInstance(): object
+ {
+ return $this->instantiator->instantiate($this->name);
+ }
+
+ /**
+ * Restores some state that can not be serialized/unserialized.
+ */
+ public function wakeupReflection(ReflectionService $reflService): void
+ {
+ // Restore ReflectionClass and properties
+ $this->reflClass = $reflService->getClass($this->name);
+ $this->instantiator = $this->instantiator ?: new Instantiator();
+
+ $parentReflFields = [];
+
+ foreach ($this->embeddedClasses as $property => $embeddedClass) {
+ if (isset($embeddedClass['declaredField'])) {
+ $childProperty = $this->getAccessibleProperty(
+ $reflService,
+ $this->embeddedClasses[$embeddedClass['declaredField']]['class'],
+ $embeddedClass['originalField'],
+ );
+ assert($childProperty !== null);
+ $parentReflFields[$property] = new ReflectionEmbeddedProperty(
+ $parentReflFields[$embeddedClass['declaredField']],
+ $childProperty,
+ $this->embeddedClasses[$embeddedClass['declaredField']]['class'],
+ );
+
+ continue;
+ }
+
+ $fieldRefl = $this->getAccessibleProperty(
+ $reflService,
+ $embeddedClass['declared'] ?? $this->name,
+ $property,
+ );
+
+ $parentReflFields[$property] = $fieldRefl;
+ $this->reflFields[$property] = $fieldRefl;
+ }
+
+ foreach ($this->fieldMappings as $field => $mapping) {
+ if (isset($mapping['declaredField']) && isset($parentReflFields[$mapping['declaredField']])) {
+ $childProperty = $this->getAccessibleProperty($reflService, $mapping['originalClass'], $mapping['originalField']);
+ assert($childProperty !== null);
+
+ if (isset($mapping['enumType'])) {
+ $childProperty = new ReflectionEnumProperty(
+ $childProperty,
+ $mapping['enumType'],
+ );
+ }
+
+ $this->reflFields[$field] = new ReflectionEmbeddedProperty(
+ $parentReflFields[$mapping['declaredField']],
+ $childProperty,
+ $mapping['originalClass'],
+ );
+ continue;
+ }
+
+ $this->reflFields[$field] = isset($mapping['declared'])
+ ? $this->getAccessibleProperty($reflService, $mapping['declared'], $field)
+ : $this->getAccessibleProperty($reflService, $this->name, $field);
+
+ if (isset($mapping['enumType']) && $this->reflFields[$field] !== null) {
+ $this->reflFields[$field] = new ReflectionEnumProperty(
+ $this->reflFields[$field],
+ $mapping['enumType'],
+ );
+ }
+ }
+
+ foreach ($this->associationMappings as $field => $mapping) {
+ $this->reflFields[$field] = isset($mapping['declared'])
+ ? $this->getAccessibleProperty($reflService, $mapping['declared'], $field)
+ : $this->getAccessibleProperty($reflService, $this->name, $field);
+ }
+ }
+
+ /**
+ * Initializes a new ClassMetadata instance that will hold the object-relational mapping
+ * metadata of the class with the given name.
+ *
+ * @param ReflectionService $reflService The reflection service.
+ */
+ public function initializeReflection(ReflectionService $reflService): void
+ {
+ $this->reflClass = $reflService->getClass($this->name);
+ $this->namespace = $reflService->getClassNamespace($this->name);
+
+ if ($this->reflClass) {
+ $this->name = $this->rootEntityName = $this->reflClass->getName();
+ }
+
+ $this->table['name'] = $this->namingStrategy->classToTableName($this->name);
+ }
+
+ /**
+ * Validates Identifier.
+ *
+ * @throws MappingException
+ */
+ public function validateIdentifier(): void
+ {
+ if ($this->isMappedSuperclass || $this->isEmbeddedClass) {
+ return;
+ }
+
+ // Verify & complete identifier mapping
+ if (! $this->identifier) {
+ throw MappingException::identifierRequired($this->name);
+ }
+
+ if ($this->usesIdGenerator() && $this->isIdentifierComposite) {
+ throw MappingException::compositeKeyAssignedIdGeneratorRequired($this->name);
+ }
+ }
+
+ /**
+ * Validates association targets actually exist.
+ *
+ * @throws MappingException
+ */
+ public function validateAssociations(): void
+ {
+ foreach ($this->associationMappings as $mapping) {
+ if (
+ ! class_exists($mapping['targetEntity'])
+ && ! interface_exists($mapping['targetEntity'])
+ && ! trait_exists($mapping['targetEntity'])
+ ) {
+ throw MappingException::invalidTargetEntityClass($mapping['targetEntity'], $this->name, $mapping['fieldName']);
+ }
+ }
+ }
+
+ /**
+ * Validates lifecycle callbacks.
+ *
+ * @throws MappingException
+ */
+ public function validateLifecycleCallbacks(ReflectionService $reflService): void
+ {
+ foreach ($this->lifecycleCallbacks as $callbacks) {
+ foreach ($callbacks as $callbackFuncName) {
+ if (! $reflService->hasPublicMethod($this->name, $callbackFuncName)) {
+ throw MappingException::lifecycleCallbackMethodNotFound($this->name, $callbackFuncName);
+ }
+ }
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * Can return null when using static reflection, in violation of the LSP
+ */
+ public function getReflectionClass()
+ {
+ return $this->reflClass;
+ }
+
+ /** @psalm-param array{usage?: mixed, region?: mixed} $cache */
+ public function enableCache(array $cache): void
+ {
+ if (! isset($cache['usage'])) {
+ $cache['usage'] = self::CACHE_USAGE_READ_ONLY;
+ }
+
+ if (! isset($cache['region'])) {
+ $cache['region'] = strtolower(str_replace('\\', '_', $this->rootEntityName));
+ }
+
+ $this->cache = $cache;
+ }
+
+ /** @psalm-param array{usage?: int, region?: string} $cache */
+ public function enableAssociationCache(string $fieldName, array $cache): void
+ {
+ $this->associationMappings[$fieldName]['cache'] = $this->getAssociationCacheDefaults($fieldName, $cache);
+ }
+
+ /**
+ * @psalm-param array{usage?: int, region?: string|null} $cache
+ *
+ * @return int[]|string[]
+ * @psalm-return array{usage: int, region: string|null}
+ */
+ public function getAssociationCacheDefaults(string $fieldName, array $cache): array
+ {
+ if (! isset($cache['usage'])) {
+ $cache['usage'] = $this->cache['usage'] ?? self::CACHE_USAGE_READ_ONLY;
+ }
+
+ if (! isset($cache['region'])) {
+ $cache['region'] = strtolower(str_replace('\\', '_', $this->rootEntityName)) . '__' . $fieldName;
+ }
+
+ return $cache;
+ }
+
+ /**
+ * Sets the change tracking policy used by this class.
+ */
+ public function setChangeTrackingPolicy(int $policy): void
+ {
+ $this->changeTrackingPolicy = $policy;
+ }
+
+ /**
+ * Whether the change tracking policy of this class is "deferred explicit".
+ */
+ public function isChangeTrackingDeferredExplicit(): bool
+ {
+ return $this->changeTrackingPolicy === self::CHANGETRACKING_DEFERRED_EXPLICIT;
+ }
+
+ /**
+ * Whether the change tracking policy of this class is "deferred implicit".
+ */
+ public function isChangeTrackingDeferredImplicit(): bool
+ {
+ return $this->changeTrackingPolicy === self::CHANGETRACKING_DEFERRED_IMPLICIT;
+ }
+
+ /**
+ * Whether the change tracking policy of this class is "notify".
+ */
+ public function isChangeTrackingNotify(): bool
+ {
+ return $this->changeTrackingPolicy === self::CHANGETRACKING_NOTIFY;
+ }
+
+ /**
+ * Checks whether a field is part of the identifier/primary key field(s).
+ */
+ public function isIdentifier(string $fieldName): bool
+ {
+ if (! $this->identifier) {
+ return false;
+ }
+
+ if (! $this->isIdentifierComposite) {
+ return $fieldName === $this->identifier[0];
+ }
+
+ return in_array($fieldName, $this->identifier, true);
+ }
+
+ public function isUniqueField(string $fieldName): bool
+ {
+ $mapping = $this->getFieldMapping($fieldName);
+
+ return $mapping !== false && isset($mapping['unique']) && $mapping['unique'];
+ }
+
+ public function isNullable(string $fieldName): bool
+ {
+ $mapping = $this->getFieldMapping($fieldName);
+
+ return $mapping !== false && isset($mapping['nullable']) && $mapping['nullable'];
+ }
+
+ /**
+ * Gets a column name for a field name.
+ * If the column name for the field cannot be found, the given field name
+ * is returned.
+ */
+ public function getColumnName(string $fieldName): string
+ {
+ return $this->columnNames[$fieldName] ?? $fieldName;
+ }
+
+ /**
+ * Gets the mapping of a (regular) field that holds some data but not a
+ * reference to another object.
+ *
+ * @return mixed[] The field mapping.
+ * @psalm-return FieldMapping
+ *
+ * @throws MappingException
+ */
+ public function getFieldMapping(string $fieldName): array
+ {
+ if (! isset($this->fieldMappings[$fieldName])) {
+ throw MappingException::mappingNotFound($this->name, $fieldName);
+ }
+
+ return $this->fieldMappings[$fieldName];
+ }
+
+ /**
+ * Gets the mapping of an association.
+ *
+ * @see ClassMetadata::$associationMappings
+ *
+ * @param string $fieldName The field name that represents the association in
+ * the object model.
+ *
+ * @return mixed[] The mapping.
+ * @psalm-return AssociationMapping
+ *
+ * @throws MappingException
+ */
+ public function getAssociationMapping(string $fieldName): array
+ {
+ if (! isset($this->associationMappings[$fieldName])) {
+ throw MappingException::mappingNotFound($this->name, $fieldName);
+ }
+
+ return $this->associationMappings[$fieldName];
+ }
+
+ /**
+ * Gets all association mappings of the class.
+ *
+ * @psalm-return array
+ */
+ public function getAssociationMappings(): array
+ {
+ return $this->associationMappings;
+ }
+
+ /**
+ * Gets the field name for a column name.
+ * If no field name can be found the column name is returned.
+ *
+ * @return string The column alias.
+ */
+ public function getFieldName(string $columnName): string
+ {
+ return $this->fieldNames[$columnName] ?? $columnName;
+ }
+
+ /**
+ * Checks whether given property has type
+ */
+ private function isTypedProperty(string $name): bool
+ {
+ return isset($this->reflClass)
+ && $this->reflClass->hasProperty($name)
+ && $this->reflClass->getProperty($name)->hasType();
+ }
+
+ /**
+ * Validates & completes the given field mapping based on typed property.
+ *
+ * @param array{fieldName: string, type?: mixed} $mapping The field mapping to validate & complete.
+ *
+ * @return array{fieldName: string, enumType?: string, type?: mixed} The updated mapping.
+ */
+ private function validateAndCompleteTypedFieldMapping(array $mapping): array
+ {
+ $field = $this->reflClass->getProperty($mapping['fieldName']);
+
+ $mapping = $this->typedFieldMapper->validateAndComplete($mapping, $field);
+
+ return $mapping;
+ }
+
+ /**
+ * Validates & completes the basic mapping information based on typed property.
+ *
+ * @param array{type: self::ONE_TO_ONE|self::MANY_TO_ONE|self::ONE_TO_MANY|self::MANY_TO_MANY, fieldName: string, targetEntity?: class-string} $mapping The mapping.
+ *
+ * @return mixed[] The updated mapping.
+ */
+ private function validateAndCompleteTypedAssociationMapping(array $mapping): array
+ {
+ $type = $this->reflClass->getProperty($mapping['fieldName'])->getType();
+
+ if ($type === null || ($mapping['type'] & self::TO_ONE) === 0) {
+ return $mapping;
+ }
+
+ if (! isset($mapping['targetEntity']) && $type instanceof ReflectionNamedType) {
+ $mapping['targetEntity'] = $type->getName();
+ }
+
+ return $mapping;
+ }
+
+ /**
+ * Validates & completes the given field mapping.
+ *
+ * @psalm-param array{
+ * fieldName?: string,
+ * columnName?: string,
+ * id?: bool,
+ * generated?: int,
+ * enumType?: class-string,
+ * } $mapping The field mapping to validate & complete.
+ *
+ * @return mixed[] The updated mapping.
+ *
+ * @throws MappingException
+ */
+ protected function validateAndCompleteFieldMapping(array $mapping): array
+ {
+ // Check mandatory fields
+ if (! isset($mapping['fieldName']) || ! $mapping['fieldName']) {
+ throw MappingException::missingFieldName($this->name);
+ }
+
+ if ($this->isTypedProperty($mapping['fieldName'])) {
+ $mapping = $this->validateAndCompleteTypedFieldMapping($mapping);
+ }
+
+ if (! isset($mapping['type'])) {
+ // Default to string
+ $mapping['type'] = 'string';
+ }
+
+ // Complete fieldName and columnName mapping
+ if (! isset($mapping['columnName'])) {
+ $mapping['columnName'] = $this->namingStrategy->propertyToColumnName($mapping['fieldName'], $this->name);
+ }
+
+ if ($mapping['columnName'][0] === '`') {
+ $mapping['columnName'] = trim($mapping['columnName'], '`');
+ $mapping['quoted'] = true;
+ }
+
+ $this->columnNames[$mapping['fieldName']] = $mapping['columnName'];
+
+ if (isset($this->fieldNames[$mapping['columnName']]) || ($this->discriminatorColumn && $this->discriminatorColumn['name'] === $mapping['columnName'])) {
+ throw MappingException::duplicateColumnName($this->name, $mapping['columnName']);
+ }
+
+ $this->fieldNames[$mapping['columnName']] = $mapping['fieldName'];
+
+ // Complete id mapping
+ if (isset($mapping['id']) && $mapping['id'] === true) {
+ if ($this->versionField === $mapping['fieldName']) {
+ throw MappingException::cannotVersionIdField($this->name, $mapping['fieldName']);
+ }
+
+ if (! in_array($mapping['fieldName'], $this->identifier, true)) {
+ $this->identifier[] = $mapping['fieldName'];
+ }
+
+ // Check for composite key
+ if (! $this->isIdentifierComposite && count($this->identifier) > 1) {
+ $this->isIdentifierComposite = true;
+ }
+ }
+
+ if (isset($mapping['generated'])) {
+ if (! in_array($mapping['generated'], [self::GENERATED_NEVER, self::GENERATED_INSERT, self::GENERATED_ALWAYS])) {
+ throw MappingException::invalidGeneratedMode($mapping['generated']);
+ }
+
+ if ($mapping['generated'] === self::GENERATED_NEVER) {
+ unset($mapping['generated']);
+ }
+ }
+
+ if (isset($mapping['enumType'])) {
+ if (! enum_exists($mapping['enumType'])) {
+ throw MappingException::nonEnumTypeMapped($this->name, $mapping['fieldName'], $mapping['enumType']);
+ }
+
+ if (! empty($mapping['id'])) {
+ $this->containsEnumIdentifier = true;
+ }
+ }
+
+ return $mapping;
+ }
+
+ /**
+ * Validates & completes the basic mapping information that is common to all
+ * association mappings (one-to-one, many-ot-one, one-to-many, many-to-many).
+ *
+ * @psalm-param array $mapping The mapping.
+ *
+ * @return mixed[] The updated mapping.
+ * @psalm-return array{
+ * mappedBy: mixed|null,
+ * inversedBy: mixed|null,
+ * isOwningSide: bool,
+ * sourceEntity: class-string,
+ * targetEntity: string,
+ * fieldName: mixed,
+ * fetch: mixed,
+ * cascade: array,
+ * isCascadeRemove: bool,
+ * isCascadePersist: bool,
+ * isCascadeRefresh: bool,
+ * isCascadeMerge: bool,
+ * isCascadeDetach: bool,
+ * type: int,
+ * originalField: string,
+ * originalClass: class-string,
+ * ?orphanRemoval: bool
+ * }
+ *
+ * @throws MappingException If something is wrong with the mapping.
+ */
+ protected function _validateAndCompleteAssociationMapping(array $mapping)
+ {
+ if (! isset($mapping['mappedBy'])) {
+ $mapping['mappedBy'] = null;
+ }
+
+ if (! isset($mapping['inversedBy'])) {
+ $mapping['inversedBy'] = null;
+ }
+
+ $mapping['isOwningSide'] = true; // assume owning side until we hit mappedBy
+
+ if (empty($mapping['indexBy'])) {
+ unset($mapping['indexBy']);
+ }
+
+ // If targetEntity is unqualified, assume it is in the same namespace as
+ // the sourceEntity.
+ $mapping['sourceEntity'] = $this->name;
+
+ if ($this->isTypedProperty($mapping['fieldName'])) {
+ $mapping = $this->validateAndCompleteTypedAssociationMapping($mapping);
+ }
+
+ if (isset($mapping['targetEntity'])) {
+ $mapping['targetEntity'] = $this->fullyQualifiedClassName($mapping['targetEntity']);
+ $mapping['targetEntity'] = ltrim($mapping['targetEntity'], '\\');
+ }
+
+ if (($mapping['type'] & self::MANY_TO_ONE) > 0 && isset($mapping['orphanRemoval']) && $mapping['orphanRemoval']) {
+ throw MappingException::illegalOrphanRemoval($this->name, $mapping['fieldName']);
+ }
+
+ // Complete id mapping
+ if (isset($mapping['id']) && $mapping['id'] === true) {
+ if (isset($mapping['orphanRemoval']) && $mapping['orphanRemoval']) {
+ throw MappingException::illegalOrphanRemovalOnIdentifierAssociation($this->name, $mapping['fieldName']);
+ }
+
+ if (! in_array($mapping['fieldName'], $this->identifier, true)) {
+ if (isset($mapping['joinColumns']) && count($mapping['joinColumns']) >= 2) {
+ throw MappingException::cannotMapCompositePrimaryKeyEntitiesAsForeignId(
+ $mapping['targetEntity'],
+ $this->name,
+ $mapping['fieldName'],
+ );
+ }
+
+ $this->identifier[] = $mapping['fieldName'];
+ $this->containsForeignIdentifier = true;
+ }
+
+ // Check for composite key
+ if (! $this->isIdentifierComposite && count($this->identifier) > 1) {
+ $this->isIdentifierComposite = true;
+ }
+
+ if ($this->cache && ! isset($mapping['cache'])) {
+ throw NonCacheableEntityAssociation::fromEntityAndField(
+ $this->name,
+ $mapping['fieldName'],
+ );
+ }
+ }
+
+ // Mandatory attributes for both sides
+ // Mandatory: fieldName, targetEntity
+ if (! isset($mapping['fieldName']) || ! $mapping['fieldName']) {
+ throw MappingException::missingFieldName($this->name);
+ }
+
+ if (! isset($mapping['targetEntity'])) {
+ throw MappingException::missingTargetEntity($mapping['fieldName']);
+ }
+
+ // Mandatory and optional attributes for either side
+ if (! $mapping['mappedBy']) {
+ if (isset($mapping['joinTable']) && $mapping['joinTable']) {
+ if (isset($mapping['joinTable']['name']) && $mapping['joinTable']['name'][0] === '`') {
+ $mapping['joinTable']['name'] = trim($mapping['joinTable']['name'], '`');
+ $mapping['joinTable']['quoted'] = true;
+ }
+ }
+ } else {
+ $mapping['isOwningSide'] = false;
+ }
+
+ if (isset($mapping['id']) && $mapping['id'] === true && $mapping['type'] & self::TO_MANY) {
+ throw MappingException::illegalToManyIdentifierAssociation($this->name, $mapping['fieldName']);
+ }
+
+ // Fetch mode. Default fetch mode to LAZY, if not set.
+ if (! isset($mapping['fetch'])) {
+ $mapping['fetch'] = self::FETCH_LAZY;
+ }
+
+ // Cascades
+ $cascades = isset($mapping['cascade']) ? array_map('strtolower', $mapping['cascade']) : [];
+
+ $allCascades = ['remove', 'persist', 'refresh', 'merge', 'detach'];
+ if (in_array('all', $cascades, true)) {
+ $cascades = $allCascades;
+ } elseif (count($cascades) !== count(array_intersect($cascades, $allCascades))) {
+ throw MappingException::invalidCascadeOption(
+ array_diff($cascades, $allCascades),
+ $this->name,
+ $mapping['fieldName'],
+ );
+ }
+
+ $mapping['cascade'] = $cascades;
+ $mapping['isCascadeRemove'] = in_array('remove', $cascades, true);
+ $mapping['isCascadePersist'] = in_array('persist', $cascades, true);
+ $mapping['isCascadeRefresh'] = in_array('refresh', $cascades, true);
+ $mapping['isCascadeMerge'] = in_array('merge', $cascades, true);
+ $mapping['isCascadeDetach'] = in_array('detach', $cascades, true);
+
+ return $mapping;
+ }
+
+ /**
+ * Validates & completes a one-to-one association mapping.
+ *
+ * @psalm-param array $mapping The mapping to validate & complete.
+ *
+ * @return mixed[] The validated & completed mapping.
+ * @psalm-return array{isOwningSide: mixed, orphanRemoval: bool, isCascadeRemove: bool}
+ * @psalm-return array{
+ * mappedBy: mixed|null,
+ * inversedBy: mixed|null,
+ * isOwningSide: bool,
+ * sourceEntity: class-string,
+ * targetEntity: string,
+ * fieldName: mixed,
+ * fetch: mixed,
+ * cascade: array,
+ * isCascadeRemove: bool,
+ * isCascadePersist: bool,
+ * isCascadeRefresh: bool,
+ * isCascadeMerge: bool,
+ * isCascadeDetach: bool,
+ * type: int,
+ * originalField: string,
+ * originalClass: class-string,
+ * joinColumns?: array{0: array{name: string, referencedColumnName: string}}|mixed,
+ * id?: mixed,
+ * sourceToTargetKeyColumns?: array,
+ * joinColumnFieldNames?: array,
+ * targetToSourceKeyColumns?: array,
+ * orphanRemoval: bool
+ * }
+ *
+ * @throws RuntimeException
+ * @throws MappingException
+ */
+ protected function _validateAndCompleteOneToOneMapping(array $mapping)
+ {
+ $mapping = $this->_validateAndCompleteAssociationMapping($mapping);
+
+ if (isset($mapping['joinColumns']) && $mapping['joinColumns']) {
+ $mapping['isOwningSide'] = true;
+ }
+
+ if ($mapping['isOwningSide']) {
+ if (empty($mapping['joinColumns'])) {
+ // Apply default join column
+ $mapping['joinColumns'] = [
+ [
+ 'name' => $this->namingStrategy->joinColumnName($mapping['fieldName'], $this->name),
+ 'referencedColumnName' => $this->namingStrategy->referenceColumnName(),
+ ],
+ ];
+ }
+
+ $uniqueConstraintColumns = [];
+
+ foreach ($mapping['joinColumns'] as &$joinColumn) {
+ if ($mapping['type'] === self::ONE_TO_ONE && ! $this->isInheritanceTypeSingleTable()) {
+ if (count($mapping['joinColumns']) === 1) {
+ if (empty($mapping['id'])) {
+ $joinColumn['unique'] = true;
+ }
+ } else {
+ $uniqueConstraintColumns[] = $joinColumn['name'];
+ }
+ }
+
+ if (empty($joinColumn['name'])) {
+ $joinColumn['name'] = $this->namingStrategy->joinColumnName($mapping['fieldName'], $this->name);
+ }
+
+ if (empty($joinColumn['referencedColumnName'])) {
+ $joinColumn['referencedColumnName'] = $this->namingStrategy->referenceColumnName();
+ }
+
+ if ($joinColumn['name'][0] === '`') {
+ $joinColumn['name'] = trim($joinColumn['name'], '`');
+ $joinColumn['quoted'] = true;
+ }
+
+ if ($joinColumn['referencedColumnName'][0] === '`') {
+ $joinColumn['referencedColumnName'] = trim($joinColumn['referencedColumnName'], '`');
+ $joinColumn['quoted'] = true;
+ }
+
+ $mapping['sourceToTargetKeyColumns'][$joinColumn['name']] = $joinColumn['referencedColumnName'];
+ $mapping['joinColumnFieldNames'][$joinColumn['name']] = $joinColumn['fieldName'] ?? $joinColumn['name'];
+ }
+
+ if ($uniqueConstraintColumns) {
+ if (! $this->table) {
+ throw new RuntimeException('ClassMetadata::setTable() has to be called before defining a one to one relationship.');
+ }
+
+ $this->table['uniqueConstraints'][$mapping['fieldName'] . '_uniq'] = ['columns' => $uniqueConstraintColumns];
+ }
+
+ $mapping['targetToSourceKeyColumns'] = array_flip($mapping['sourceToTargetKeyColumns']);
+ }
+
+ $mapping['orphanRemoval'] = isset($mapping['orphanRemoval']) && $mapping['orphanRemoval'];
+ $mapping['isCascadeRemove'] = $mapping['orphanRemoval'] || $mapping['isCascadeRemove'];
+
+ if ($mapping['orphanRemoval']) {
+ unset($mapping['unique']);
+ }
+
+ if (isset($mapping['id']) && $mapping['id'] === true && ! $mapping['isOwningSide']) {
+ throw MappingException::illegalInverseIdentifierAssociation($this->name, $mapping['fieldName']);
+ }
+
+ return $mapping;
+ }
+
+ /**
+ * Validates & completes a one-to-many association mapping.
+ *
+ * @psalm-param array $mapping The mapping to validate and complete.
+ *
+ * @return mixed[] The validated and completed mapping.
+ * @psalm-return array{
+ * mappedBy: mixed,
+ * inversedBy: mixed,
+ * isOwningSide: bool,
+ * sourceEntity: string,
+ * targetEntity: string,
+ * fieldName: mixed,
+ * fetch: int|mixed,
+ * cascade: array,
+ * isCascadeRemove: bool,
+ * isCascadePersist: bool,
+ * isCascadeRefresh: bool,
+ * isCascadeMerge: bool,
+ * isCascadeDetach: bool,
+ * orphanRemoval: bool
+ * }
+ *
+ * @throws MappingException
+ * @throws InvalidArgumentException
+ */
+ protected function _validateAndCompleteOneToManyMapping(array $mapping)
+ {
+ $mapping = $this->_validateAndCompleteAssociationMapping($mapping);
+
+ // OneToMany-side MUST be inverse (must have mappedBy)
+ if (! isset($mapping['mappedBy'])) {
+ throw MappingException::oneToManyRequiresMappedBy($this->name, $mapping['fieldName']);
+ }
+
+ $mapping['orphanRemoval'] = isset($mapping['orphanRemoval']) && $mapping['orphanRemoval'];
+ $mapping['isCascadeRemove'] = $mapping['orphanRemoval'] || $mapping['isCascadeRemove'];
+
+ $this->assertMappingOrderBy($mapping);
+
+ return $mapping;
+ }
+
+ /**
+ * Validates & completes a many-to-many association mapping.
+ *
+ * @psalm-param array $mapping The mapping to validate & complete.
+ * @psalm-param array $mapping The mapping to validate & complete.
+ *
+ * @return mixed[] The validated & completed mapping.
+ * @psalm-return array{
+ * mappedBy: mixed,
+ * inversedBy: mixed,
+ * isOwningSide: bool,
+ * sourceEntity: class-string,
+ * targetEntity: string,
+ * fieldName: mixed,
+ * fetch: mixed,
+ * cascade: array,
+ * isCascadeRemove: bool,
+ * isCascadePersist: bool,
+ * isCascadeRefresh: bool,
+ * isCascadeMerge: bool,
+ * isCascadeDetach: bool,
+ * type: int,
+ * originalField: string,
+ * originalClass: class-string,
+ * joinTable?: array{inverseJoinColumns: mixed}|mixed,
+ * joinTableColumns?: list,
+ * isOnDeleteCascade?: true,
+ * relationToSourceKeyColumns?: array,
+ * relationToTargetKeyColumns?: array,
+ * orphanRemoval: bool
+ * }
+ *
+ * @throws InvalidArgumentException
+ */
+ protected function _validateAndCompleteManyToManyMapping(array $mapping)
+ {
+ $mapping = $this->_validateAndCompleteAssociationMapping($mapping);
+
+ if ($mapping['isOwningSide']) {
+ // owning side MUST have a join table
+ if (! isset($mapping['joinTable']['name'])) {
+ $mapping['joinTable']['name'] = $this->namingStrategy->joinTableName($mapping['sourceEntity'], $mapping['targetEntity'], $mapping['fieldName']);
+ }
+
+ $selfReferencingEntityWithoutJoinColumns = $mapping['sourceEntity'] === $mapping['targetEntity']
+ && (! (isset($mapping['joinTable']['joinColumns']) || isset($mapping['joinTable']['inverseJoinColumns'])));
+
+ if (! isset($mapping['joinTable']['joinColumns'])) {
+ $mapping['joinTable']['joinColumns'] = [
+ [
+ 'name' => $this->namingStrategy->joinKeyColumnName($mapping['sourceEntity'], $selfReferencingEntityWithoutJoinColumns ? 'source' : null),
+ 'referencedColumnName' => $this->namingStrategy->referenceColumnName(),
+ 'onDelete' => 'CASCADE',
+ ],
+ ];
+ }
+
+ if (! isset($mapping['joinTable']['inverseJoinColumns'])) {
+ $mapping['joinTable']['inverseJoinColumns'] = [
+ [
+ 'name' => $this->namingStrategy->joinKeyColumnName($mapping['targetEntity'], $selfReferencingEntityWithoutJoinColumns ? 'target' : null),
+ 'referencedColumnName' => $this->namingStrategy->referenceColumnName(),
+ 'onDelete' => 'CASCADE',
+ ],
+ ];
+ }
+
+ $mapping['joinTableColumns'] = [];
+
+ foreach ($mapping['joinTable']['joinColumns'] as &$joinColumn) {
+ if (empty($joinColumn['name'])) {
+ $joinColumn['name'] = $this->namingStrategy->joinKeyColumnName($mapping['sourceEntity'], $joinColumn['referencedColumnName']);
+ }
+
+ if (empty($joinColumn['referencedColumnName'])) {
+ $joinColumn['referencedColumnName'] = $this->namingStrategy->referenceColumnName();
+ }
+
+ if ($joinColumn['name'][0] === '`') {
+ $joinColumn['name'] = trim($joinColumn['name'], '`');
+ $joinColumn['quoted'] = true;
+ }
+
+ if ($joinColumn['referencedColumnName'][0] === '`') {
+ $joinColumn['referencedColumnName'] = trim($joinColumn['referencedColumnName'], '`');
+ $joinColumn['quoted'] = true;
+ }
+
+ if (isset($joinColumn['onDelete']) && strtolower($joinColumn['onDelete']) === 'cascade') {
+ $mapping['isOnDeleteCascade'] = true;
+ }
+
+ $mapping['relationToSourceKeyColumns'][$joinColumn['name']] = $joinColumn['referencedColumnName'];
+ $mapping['joinTableColumns'][] = $joinColumn['name'];
+ }
+
+ foreach ($mapping['joinTable']['inverseJoinColumns'] as &$inverseJoinColumn) {
+ if (empty($inverseJoinColumn['name'])) {
+ $inverseJoinColumn['name'] = $this->namingStrategy->joinKeyColumnName($mapping['targetEntity'], $inverseJoinColumn['referencedColumnName']);
+ }
+
+ if (empty($inverseJoinColumn['referencedColumnName'])) {
+ $inverseJoinColumn['referencedColumnName'] = $this->namingStrategy->referenceColumnName();
+ }
+
+ if ($inverseJoinColumn['name'][0] === '`') {
+ $inverseJoinColumn['name'] = trim($inverseJoinColumn['name'], '`');
+ $inverseJoinColumn['quoted'] = true;
+ }
+
+ if ($inverseJoinColumn['referencedColumnName'][0] === '`') {
+ $inverseJoinColumn['referencedColumnName'] = trim($inverseJoinColumn['referencedColumnName'], '`');
+ $inverseJoinColumn['quoted'] = true;
+ }
+
+ if (isset($inverseJoinColumn['onDelete']) && strtolower($inverseJoinColumn['onDelete']) === 'cascade') {
+ $mapping['isOnDeleteCascade'] = true;
+ }
+
+ $mapping['relationToTargetKeyColumns'][$inverseJoinColumn['name']] = $inverseJoinColumn['referencedColumnName'];
+ $mapping['joinTableColumns'][] = $inverseJoinColumn['name'];
+ }
+ }
+
+ $mapping['orphanRemoval'] = isset($mapping['orphanRemoval']) && $mapping['orphanRemoval'];
+
+ $this->assertMappingOrderBy($mapping);
+
+ return $mapping;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getIdentifierFieldNames(): array
+ {
+ return $this->identifier;
+ }
+
+ /**
+ * Gets the name of the single id field. Note that this only works on
+ * entity classes that have a single-field pk.
+ *
+ * @throws MappingException If the class doesn't have an identifier or it has a composite primary key.
+ */
+ public function getSingleIdentifierFieldName(): string
+ {
+ if ($this->isIdentifierComposite) {
+ throw MappingException::singleIdNotAllowedOnCompositePrimaryKey($this->name);
+ }
+
+ if (! isset($this->identifier[0])) {
+ throw MappingException::noIdDefined($this->name);
+ }
+
+ return $this->identifier[0];
+ }
+
+ /**
+ * Gets the column name of the single id column. Note that this only works on
+ * entity classes that have a single-field pk.
+ *
+ * @throws MappingException If the class doesn't have an identifier or it has a composite primary key.
+ */
+ public function getSingleIdentifierColumnName(): string
+ {
+ return $this->getColumnName($this->getSingleIdentifierFieldName());
+ }
+
+ /**
+ * INTERNAL:
+ * Sets the mapped identifier/primary key fields of this class.
+ * Mainly used by the ClassMetadataFactory to assign inherited identifiers.
+ *
+ * @psalm-param list $identifier
+ */
+ public function setIdentifier(array $identifier): void
+ {
+ $this->identifier = $identifier;
+ $this->isIdentifierComposite = (count($this->identifier) > 1);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getIdentifier(): array
+ {
+ return $this->identifier;
+ }
+
+ public function hasField(string $fieldName): bool
+ {
+ return isset($this->fieldMappings[$fieldName]) || isset($this->embeddedClasses[$fieldName]);
+ }
+
+ /**
+ * Gets an array containing all the column names.
+ *
+ * @psalm-param list|null $fieldNames
+ *
+ * @return mixed[]
+ * @psalm-return list
+ */
+ public function getColumnNames(array|null $fieldNames = null): array
+ {
+ if ($fieldNames === null) {
+ return array_keys($this->fieldNames);
+ }
+
+ return array_values(array_map([$this, 'getColumnName'], $fieldNames));
+ }
+
+ /**
+ * Returns an array with all the identifier column names.
+ *
+ * @psalm-return list
+ */
+ public function getIdentifierColumnNames(): array
+ {
+ $columnNames = [];
+
+ foreach ($this->identifier as $idProperty) {
+ if (isset($this->fieldMappings[$idProperty])) {
+ $columnNames[] = $this->fieldMappings[$idProperty]['columnName'];
+
+ continue;
+ }
+
+ // Association defined as Id field
+ $joinColumns = $this->associationMappings[$idProperty]['joinColumns'];
+ $assocColumnNames = array_map(static fn ($joinColumn) => $joinColumn['name'], $joinColumns);
+
+ $columnNames = array_merge($columnNames, $assocColumnNames);
+ }
+
+ return $columnNames;
+ }
+
+ /**
+ * Sets the type of Id generator to use for the mapped class.
+ *
+ * @psalm-param self::GENERATOR_TYPE_* $generatorType
+ */
+ public function setIdGeneratorType(int $generatorType): void
+ {
+ $this->generatorType = $generatorType;
+ }
+
+ /**
+ * Checks whether the mapped class uses an Id generator.
+ */
+ public function usesIdGenerator(): bool
+ {
+ return $this->generatorType !== self::GENERATOR_TYPE_NONE;
+ }
+
+ public function isInheritanceTypeNone(): bool
+ {
+ return $this->inheritanceType === self::INHERITANCE_TYPE_NONE;
+ }
+
+ /**
+ * Checks whether the mapped class uses the JOINED inheritance mapping strategy.
+ *
+ * @return bool TRUE if the class participates in a JOINED inheritance mapping,
+ * FALSE otherwise.
+ */
+ public function isInheritanceTypeJoined(): bool
+ {
+ return $this->inheritanceType === self::INHERITANCE_TYPE_JOINED;
+ }
+
+ /**
+ * Checks whether the mapped class uses the SINGLE_TABLE inheritance mapping strategy.
+ *
+ * @return bool TRUE if the class participates in a SINGLE_TABLE inheritance mapping,
+ * FALSE otherwise.
+ */
+ public function isInheritanceTypeSingleTable(): bool
+ {
+ return $this->inheritanceType === self::INHERITANCE_TYPE_SINGLE_TABLE;
+ }
+
+ /**
+ * Checks whether the mapped class uses the TABLE_PER_CLASS inheritance mapping strategy.
+ *
+ * @return bool TRUE if the class participates in a TABLE_PER_CLASS inheritance mapping,
+ * FALSE otherwise.
+ */
+ public function isInheritanceTypeTablePerClass(): bool
+ {
+ return $this->inheritanceType === self::INHERITANCE_TYPE_TABLE_PER_CLASS;
+ }
+
+ /**
+ * Checks whether the class uses an identity column for the Id generation.
+ */
+ public function isIdGeneratorIdentity(): bool
+ {
+ return $this->generatorType === self::GENERATOR_TYPE_IDENTITY;
+ }
+
+ /**
+ * Checks whether the class uses a sequence for id generation.
+ *
+ * @psalm-assert-if-true !null $this->sequenceGeneratorDefinition
+ */
+ public function isIdGeneratorSequence(): bool
+ {
+ return $this->generatorType === self::GENERATOR_TYPE_SEQUENCE;
+ }
+
+ /**
+ * Checks whether the class has a natural identifier/pk (which means it does
+ * not use any Id generator.
+ */
+ public function isIdentifierNatural(): bool
+ {
+ return $this->generatorType === self::GENERATOR_TYPE_NONE;
+ }
+
+ /**
+ * Gets the type of a field.
+ *
+ * @todo 3.0 Remove this. PersisterHelper should fix it somehow
+ */
+ public function getTypeOfField(string $fieldName): string|null
+ {
+ return isset($this->fieldMappings[$fieldName])
+ ? $this->fieldMappings[$fieldName]['type']
+ : null;
+ }
+
+ /**
+ * Gets the name of the primary table.
+ */
+ public function getTableName(): string
+ {
+ return $this->table['name'];
+ }
+
+ /**
+ * Gets primary table's schema name.
+ */
+ public function getSchemaName(): string|null
+ {
+ return $this->table['schema'] ?? null;
+ }
+
+ /**
+ * Gets the table name to use for temporary identifier tables of this class.
+ */
+ public function getTemporaryIdTableName(): string
+ {
+ // replace dots with underscores because PostgreSQL creates temporary tables in a special schema
+ return str_replace('.', '_', $this->getTableName() . '_id_tmp');
+ }
+
+ /**
+ * Sets the mapped subclasses of this class.
+ *
+ * @psalm-param list $subclasses The names of all mapped subclasses.
+ */
+ public function setSubclasses(array $subclasses): void
+ {
+ foreach ($subclasses as $subclass) {
+ $this->subClasses[] = $this->fullyQualifiedClassName($subclass);
+ }
+ }
+
+ /**
+ * Sets the parent class names.
+ * Assumes that the class names in the passed array are in the order:
+ * directParent -> directParentParent -> directParentParentParent ... -> root.
+ *
+ * @psalm-param list $classNames
+ */
+ public function setParentClasses(array $classNames): void
+ {
+ $this->parentClasses = $classNames;
+
+ if (count($classNames) > 0) {
+ $this->rootEntityName = array_pop($classNames);
+ }
+ }
+
+ /**
+ * Sets the inheritance type used by the class and its subclasses.
+ *
+ * @psalm-param self::INHERITANCE_TYPE_* $type
+ *
+ * @throws MappingException
+ */
+ public function setInheritanceType(int $type): void
+ {
+ if (! $this->isInheritanceType($type)) {
+ throw MappingException::invalidInheritanceType($this->name, $type);
+ }
+
+ $this->inheritanceType = $type;
+ }
+
+ /**
+ * Sets the association to override association mapping of property for an entity relationship.
+ *
+ * @psalm-param array $overrideMapping
+ *
+ * @throws MappingException
+ */
+ public function setAssociationOverride(string $fieldName, array $overrideMapping): void
+ {
+ if (! isset($this->associationMappings[$fieldName])) {
+ throw MappingException::invalidOverrideFieldName($this->name, $fieldName);
+ }
+
+ $mapping = $this->associationMappings[$fieldName];
+
+ //if (isset($mapping['inherited']) && (count($overrideMapping) !== 1 || ! isset($overrideMapping['fetch']))) {
+ // TODO: Deprecate overriding the fetch mode via association override for 3.0,
+ // users should do this with a listener and a custom attribute/annotation
+ // TODO: Enable this exception in 2.8
+ //throw MappingException::illegalOverrideOfInheritedProperty($this->name, $fieldName);
+ //}
+
+ if (isset($overrideMapping['joinColumns'])) {
+ $mapping['joinColumns'] = $overrideMapping['joinColumns'];
+ }
+
+ if (isset($overrideMapping['inversedBy'])) {
+ $mapping['inversedBy'] = $overrideMapping['inversedBy'];
+ }
+
+ if (isset($overrideMapping['joinTable'])) {
+ $mapping['joinTable'] = $overrideMapping['joinTable'];
+ }
+
+ if (isset($overrideMapping['fetch'])) {
+ $mapping['fetch'] = $overrideMapping['fetch'];
+ }
+
+ $mapping['joinColumnFieldNames'] = null;
+ $mapping['joinTableColumns'] = null;
+ $mapping['sourceToTargetKeyColumns'] = null;
+ $mapping['relationToSourceKeyColumns'] = null;
+ $mapping['relationToTargetKeyColumns'] = null;
+
+ switch ($mapping['type']) {
+ case self::ONE_TO_ONE:
+ $mapping = $this->_validateAndCompleteOneToOneMapping($mapping);
+ break;
+ case self::ONE_TO_MANY:
+ $mapping = $this->_validateAndCompleteOneToManyMapping($mapping);
+ break;
+ case self::MANY_TO_ONE:
+ $mapping = $this->_validateAndCompleteOneToOneMapping($mapping);
+ break;
+ case self::MANY_TO_MANY:
+ $mapping = $this->_validateAndCompleteManyToManyMapping($mapping);
+ break;
+ }
+
+ $this->associationMappings[$fieldName] = $mapping;
+ }
+
+ /**
+ * Sets the override for a mapped field.
+ *
+ * @psalm-param array $overrideMapping
+ *
+ * @throws MappingException
+ */
+ public function setAttributeOverride(string $fieldName, array $overrideMapping): void
+ {
+ if (! isset($this->fieldMappings[$fieldName])) {
+ throw MappingException::invalidOverrideFieldName($this->name, $fieldName);
+ }
+
+ $mapping = $this->fieldMappings[$fieldName];
+
+ //if (isset($mapping['inherited'])) {
+ // TODO: Enable this exception in 2.8
+ //throw MappingException::illegalOverrideOfInheritedProperty($this->name, $fieldName);
+ //}
+
+ if (isset($mapping['id'])) {
+ $overrideMapping['id'] = $mapping['id'];
+ }
+
+ if (! isset($overrideMapping['type'])) {
+ $overrideMapping['type'] = $mapping['type'];
+ }
+
+ if (! isset($overrideMapping['fieldName'])) {
+ $overrideMapping['fieldName'] = $mapping['fieldName'];
+ }
+
+ if ($overrideMapping['type'] !== $mapping['type']) {
+ throw MappingException::invalidOverrideFieldType($this->name, $fieldName);
+ }
+
+ unset($this->fieldMappings[$fieldName]);
+ unset($this->fieldNames[$mapping['columnName']]);
+ unset($this->columnNames[$mapping['fieldName']]);
+
+ $overrideMapping = $this->validateAndCompleteFieldMapping($overrideMapping);
+
+ $this->fieldMappings[$fieldName] = $overrideMapping;
+ }
+
+ /**
+ * Checks whether a mapped field is inherited from an entity superclass.
+ */
+ public function isInheritedField(string $fieldName): bool
+ {
+ return isset($this->fieldMappings[$fieldName]['inherited']);
+ }
+
+ /**
+ * Checks if this entity is the root in any entity-inheritance-hierarchy.
+ */
+ public function isRootEntity(): bool
+ {
+ return $this->name === $this->rootEntityName;
+ }
+
+ /**
+ * Checks whether a mapped association field is inherited from a superclass.
+ */
+ public function isInheritedAssociation(string $fieldName): bool
+ {
+ return isset($this->associationMappings[$fieldName]['inherited']);
+ }
+
+ public function isInheritedEmbeddedClass(string $fieldName): bool
+ {
+ return isset($this->embeddedClasses[$fieldName]['inherited']);
+ }
+
+ /**
+ * Sets the name of the primary table the class is mapped to.
+ *
+ * @deprecated Use {@link setPrimaryTable}.
+ */
+ public function setTableName(string $tableName): void
+ {
+ $this->table['name'] = $tableName;
+ }
+
+ /**
+ * Sets the primary table definition. The provided array supports the
+ * following structure:
+ *
+ * name => (optional, defaults to class name)
+ * indexes => array of indexes (optional)
+ * uniqueConstraints => array of constraints (optional)
+ *
+ * If a key is omitted, the current value is kept.
+ *
+ * @psalm-param array $table The table description.
+ */
+ public function setPrimaryTable(array $table): void
+ {
+ if (isset($table['name'])) {
+ // Split schema and table name from a table name like "myschema.mytable"
+ if (str_contains($table['name'], '.')) {
+ [$this->table['schema'], $table['name']] = explode('.', $table['name'], 2);
+ }
+
+ if ($table['name'][0] === '`') {
+ $table['name'] = trim($table['name'], '`');
+ $this->table['quoted'] = true;
+ }
+
+ $this->table['name'] = $table['name'];
+ }
+
+ if (isset($table['quoted'])) {
+ $this->table['quoted'] = $table['quoted'];
+ }
+
+ if (isset($table['schema'])) {
+ $this->table['schema'] = $table['schema'];
+ }
+
+ if (isset($table['indexes'])) {
+ $this->table['indexes'] = $table['indexes'];
+ }
+
+ if (isset($table['uniqueConstraints'])) {
+ $this->table['uniqueConstraints'] = $table['uniqueConstraints'];
+ }
+
+ if (isset($table['options'])) {
+ $this->table['options'] = $table['options'];
+ }
+ }
+
+ /**
+ * Checks whether the given type identifies an inheritance type.
+ */
+ private function isInheritanceType(int $type): bool
+ {
+ return $type === self::INHERITANCE_TYPE_NONE ||
+ $type === self::INHERITANCE_TYPE_SINGLE_TABLE ||
+ $type === self::INHERITANCE_TYPE_JOINED ||
+ $type === self::INHERITANCE_TYPE_TABLE_PER_CLASS;
+ }
+
+ /**
+ * Adds a mapped field to the class.
+ *
+ * @psalm-param array $mapping The field mapping.
+ *
+ * @throws MappingException
+ */
+ public function mapField(array $mapping): void
+ {
+ $mapping = $this->validateAndCompleteFieldMapping($mapping);
+ $this->assertFieldNotMapped($mapping['fieldName']);
+
+ if (isset($mapping['generated'])) {
+ $this->requiresFetchAfterChange = true;
+ }
+
+ $this->fieldMappings[$mapping['fieldName']] = $mapping;
+ }
+
+ /**
+ * INTERNAL:
+ * Adds an association mapping without completing/validating it.
+ * This is mainly used to add inherited association mappings to derived classes.
+ *
+ * @psalm-param AssociationMapping $mapping
+ *
+ * @throws MappingException
+ */
+ public function addInheritedAssociationMapping(array $mapping/*, $owningClassName = null*/): void
+ {
+ if (isset($this->associationMappings[$mapping['fieldName']])) {
+ throw MappingException::duplicateAssociationMapping($this->name, $mapping['fieldName']);
+ }
+
+ $this->associationMappings[$mapping['fieldName']] = $mapping;
+ }
+
+ /**
+ * INTERNAL:
+ * Adds a field mapping without completing/validating it.
+ * This is mainly used to add inherited field mappings to derived classes.
+ *
+ * @psalm-param array $fieldMapping
+ */
+ public function addInheritedFieldMapping(array $fieldMapping): void
+ {
+ $this->fieldMappings[$fieldMapping['fieldName']] = $fieldMapping;
+ $this->columnNames[$fieldMapping['fieldName']] = $fieldMapping['columnName'];
+ $this->fieldNames[$fieldMapping['columnName']] = $fieldMapping['fieldName'];
+ }
+
+ /**
+ * Adds a one-to-one mapping.
+ *
+ * @param array $mapping The mapping.
+ */
+ public function mapOneToOne(array $mapping): void
+ {
+ $mapping['type'] = self::ONE_TO_ONE;
+
+ $mapping = $this->_validateAndCompleteOneToOneMapping($mapping);
+
+ $this->_storeAssociationMapping($mapping);
+ }
+
+ /**
+ * Adds a one-to-many mapping.
+ *
+ * @psalm-param array $mapping The mapping.
+ */
+ public function mapOneToMany(array $mapping): void
+ {
+ $mapping['type'] = self::ONE_TO_MANY;
+
+ $mapping = $this->_validateAndCompleteOneToManyMapping($mapping);
+
+ $this->_storeAssociationMapping($mapping);
+ }
+
+ /**
+ * Adds a many-to-one mapping.
+ *
+ * @psalm-param array $mapping The mapping.
+ */
+ public function mapManyToOne(array $mapping): void
+ {
+ $mapping['type'] = self::MANY_TO_ONE;
+
+ // A many-to-one mapping is essentially a one-one backreference
+ $mapping = $this->_validateAndCompleteOneToOneMapping($mapping);
+
+ $this->_storeAssociationMapping($mapping);
+ }
+
+ /**
+ * Adds a many-to-many mapping.
+ *
+ * @psalm-param array $mapping The mapping.
+ */
+ public function mapManyToMany(array $mapping): void
+ {
+ $mapping['type'] = self::MANY_TO_MANY;
+
+ $mapping = $this->_validateAndCompleteManyToManyMapping($mapping);
+
+ $this->_storeAssociationMapping($mapping);
+ }
+
+ /**
+ * Stores the association mapping.
+ *
+ * @psalm-param array $assocMapping
+ *
+ * @throws MappingException
+ */
+ protected function _storeAssociationMapping(array $assocMapping): void
+ {
+ $sourceFieldName = $assocMapping['fieldName'];
+
+ $this->assertFieldNotMapped($sourceFieldName);
+
+ $this->associationMappings[$sourceFieldName] = $assocMapping;
+ }
+
+ /**
+ * Registers a custom repository class for the entity class.
+ *
+ * @param string|null $repositoryClassName The class name of the custom mapper.
+ * @psalm-param class-string|null $repositoryClassName
+ */
+ public function setCustomRepositoryClass(string|null $repositoryClassName): void
+ {
+ $this->customRepositoryClassName = $this->fullyQualifiedClassName($repositoryClassName);
+ }
+
+ /**
+ * Dispatches the lifecycle event of the given entity to the registered
+ * lifecycle callbacks and lifecycle listeners.
+ *
+ * @deprecated Deprecated since version 2.4 in favor of \Doctrine\ORM\Event\ListenersInvoker
+ *
+ * @param string $lifecycleEvent The lifecycle event.
+ */
+ public function invokeLifecycleCallbacks(string $lifecycleEvent, object $entity): void
+ {
+ foreach ($this->lifecycleCallbacks[$lifecycleEvent] as $callback) {
+ $entity->$callback();
+ }
+ }
+
+ /**
+ * Whether the class has any attached lifecycle listeners or callbacks for a lifecycle event.
+ */
+ public function hasLifecycleCallbacks(string $lifecycleEvent): bool
+ {
+ return isset($this->lifecycleCallbacks[$lifecycleEvent]);
+ }
+
+ /**
+ * Gets the registered lifecycle callbacks for an event.
+ *
+ * @return string[]
+ * @psalm-return list
+ */
+ public function getLifecycleCallbacks(string $event): array
+ {
+ return $this->lifecycleCallbacks[$event] ?? [];
+ }
+
+ /**
+ * Adds a lifecycle callback for entities of this class.
+ */
+ public function addLifecycleCallback(string $callback, string $event): void
+ {
+ if ($this->isEmbeddedClass) {
+ Deprecation::trigger(
+ 'doctrine/orm',
+ 'https://github.com/doctrine/orm/pull/8381',
+ 'Registering lifecycle callback %s on Embedded class %s is not doing anything and will throw exception in 3.0',
+ $event,
+ $this->name,
+ );
+ }
+
+ if (isset($this->lifecycleCallbacks[$event]) && in_array($callback, $this->lifecycleCallbacks[$event], true)) {
+ return;
+ }
+
+ $this->lifecycleCallbacks[$event][] = $callback;
+ }
+
+ /**
+ * Sets the lifecycle callbacks for entities of this class.
+ * Any previously registered callbacks are overwritten.
+ *
+ * @psalm-param array> $callbacks
+ */
+ public function setLifecycleCallbacks(array $callbacks): void
+ {
+ $this->lifecycleCallbacks = $callbacks;
+ }
+
+ /**
+ * Adds a entity listener for entities of this class.
+ *
+ * @param string $eventName The entity lifecycle event.
+ * @param string $class The listener class.
+ * @param string $method The listener callback method.
+ *
+ * @throws MappingException
+ */
+ public function addEntityListener(string $eventName, string $class, string $method): void
+ {
+ $class = $this->fullyQualifiedClassName($class);
+
+ $listener = [
+ 'class' => $class,
+ 'method' => $method,
+ ];
+
+ if (! class_exists($class)) {
+ throw MappingException::entityListenerClassNotFound($class, $this->name);
+ }
+
+ if (! method_exists($class, $method)) {
+ throw MappingException::entityListenerMethodNotFound($class, $method, $this->name);
+ }
+
+ if (isset($this->entityListeners[$eventName]) && in_array($listener, $this->entityListeners[$eventName], true)) {
+ throw MappingException::duplicateEntityListener($class, $method, $this->name);
+ }
+
+ $this->entityListeners[$eventName][] = $listener;
+ }
+
+ /**
+ * Sets the discriminator column definition.
+ *
+ * @see getDiscriminatorColumn()
+ *
+ * @param mixed[]|null $columnDef
+ * @psalm-param array{name: string|null, fieldName?: string, type?: string, length?: int, columnDefinition?: string|null, enumType?: class-string|null}|null $columnDef
+ *
+ * @throws MappingException
+ */
+ public function setDiscriminatorColumn(array|null $columnDef): void
+ {
+ if ($columnDef !== null) {
+ if (! isset($columnDef['name'])) {
+ throw MappingException::nameIsMandatoryForDiscriminatorColumns($this->name);
+ }
+
+ if (isset($this->fieldNames[$columnDef['name']])) {
+ throw MappingException::duplicateColumnName($this->name, $columnDef['name']);
+ }
+
+ if (! isset($columnDef['fieldName'])) {
+ $columnDef['fieldName'] = $columnDef['name'];
+ }
+
+ if (! isset($columnDef['type'])) {
+ $columnDef['type'] = 'string';
+ }
+
+ if (in_array($columnDef['type'], ['boolean', 'array', 'object', 'datetime', 'time', 'date'], true)) {
+ throw MappingException::invalidDiscriminatorColumnType($this->name, $columnDef['type']);
+ }
+
+ $this->discriminatorColumn = $columnDef;
+ }
+ }
+
+ /**
+ * @return array
+ * @psalm-return DiscriminatorColumnMapping
+ */
+ final public function getDiscriminatorColumn(): array
+ {
+ if ($this->discriminatorColumn === null) {
+ throw new LogicException('The discriminator column was not set.');
+ }
+
+ return $this->discriminatorColumn;
+ }
+
+ /**
+ * Sets the discriminator values used by this class.
+ * Used for JOINED and SINGLE_TABLE inheritance mapping strategies.
+ *
+ * @param array $map
+ */
+ public function setDiscriminatorMap(array $map): void
+ {
+ foreach ($map as $value => $className) {
+ $this->addDiscriminatorMapClass($value, $className);
+ }
+ }
+
+ /**
+ * Adds one entry of the discriminator map with a new class and corresponding name.
+ *
+ * @throws MappingException
+ */
+ public function addDiscriminatorMapClass(int|string $name, string $className): void
+ {
+ $className = $this->fullyQualifiedClassName($className);
+ $className = ltrim($className, '\\');
+
+ $this->discriminatorMap[$name] = $className;
+
+ if ($this->name === $className) {
+ $this->discriminatorValue = $name;
+
+ return;
+ }
+
+ if (! (class_exists($className) || interface_exists($className))) {
+ throw MappingException::invalidClassInDiscriminatorMap($className, $this->name);
+ }
+
+ if (is_subclass_of($className, $this->name) && ! in_array($className, $this->subClasses, true)) {
+ $this->subClasses[] = $className;
+ }
+ }
+
+ public function hasAssociation(string $fieldName): bool
+ {
+ return isset($this->associationMappings[$fieldName]);
+ }
+
+ public function isSingleValuedAssociation(string $fieldName): bool
+ {
+ return isset($this->associationMappings[$fieldName])
+ && ($this->associationMappings[$fieldName]['type'] & self::TO_ONE);
+ }
+
+ public function isCollectionValuedAssociation(string $fieldName): bool
+ {
+ return isset($this->associationMappings[$fieldName])
+ && ! ($this->associationMappings[$fieldName]['type'] & self::TO_ONE);
+ }
+
+ /**
+ * Is this an association that only has a single join column?
+ */
+ public function isAssociationWithSingleJoinColumn(string $fieldName): bool
+ {
+ return isset($this->associationMappings[$fieldName])
+ && isset($this->associationMappings[$fieldName]['joinColumns'][0])
+ && ! isset($this->associationMappings[$fieldName]['joinColumns'][1]);
+ }
+
+ /**
+ * Returns the single association join column (if any).
+ *
+ * @throws MappingException
+ */
+ public function getSingleAssociationJoinColumnName(string $fieldName): string
+ {
+ if (! $this->isAssociationWithSingleJoinColumn($fieldName)) {
+ throw MappingException::noSingleAssociationJoinColumnFound($this->name, $fieldName);
+ }
+
+ return $this->associationMappings[$fieldName]['joinColumns'][0]['name'];
+ }
+
+ /**
+ * Returns the single association referenced join column name (if any).
+ *
+ * @throws MappingException
+ */
+ public function getSingleAssociationReferencedJoinColumnName(string $fieldName): string
+ {
+ if (! $this->isAssociationWithSingleJoinColumn($fieldName)) {
+ throw MappingException::noSingleAssociationJoinColumnFound($this->name, $fieldName);
+ }
+
+ return $this->associationMappings[$fieldName]['joinColumns'][0]['referencedColumnName'];
+ }
+
+ /**
+ * Used to retrieve a fieldname for either field or association from a given column.
+ *
+ * This method is used in foreign-key as primary-key contexts.
+ *
+ * @throws MappingException
+ */
+ public function getFieldForColumn(string $columnName): string
+ {
+ if (isset($this->fieldNames[$columnName])) {
+ return $this->fieldNames[$columnName];
+ }
+
+ foreach ($this->associationMappings as $assocName => $mapping) {
+ if (
+ $this->isAssociationWithSingleJoinColumn($assocName) &&
+ $this->associationMappings[$assocName]['joinColumns'][0]['name'] === $columnName
+ ) {
+ return $assocName;
+ }
+ }
+
+ throw MappingException::noFieldNameFoundForColumn($this->name, $columnName);
+ }
+
+ /**
+ * Sets the ID generator used to generate IDs for instances of this class.
+ */
+ public function setIdGenerator(AbstractIdGenerator $generator): void
+ {
+ $this->idGenerator = $generator;
+ }
+
+ /**
+ * Sets definition.
+ *
+ * @psalm-param array $definition
+ */
+ public function setCustomGeneratorDefinition(array $definition): void
+ {
+ $this->customGeneratorDefinition = $definition;
+ }
+
+ /**
+ * Sets the definition of the sequence ID generator for this class.
+ *
+ * The definition must have the following structure:
+ *
+ * array(
+ * 'sequenceName' => 'name',
+ * 'allocationSize' => 20,
+ * 'initialValue' => 1
+ * 'quoted' => 1
+ * )
+ *
+ *
+ * @psalm-param array{sequenceName?: string, allocationSize?: int|string, initialValue?: int|string, quoted?: mixed} $definition
+ *
+ * @throws MappingException
+ */
+ public function setSequenceGeneratorDefinition(array $definition): void
+ {
+ if (! isset($definition['sequenceName']) || trim($definition['sequenceName']) === '') {
+ throw MappingException::missingSequenceName($this->name);
+ }
+
+ if ($definition['sequenceName'][0] === '`') {
+ $definition['sequenceName'] = trim($definition['sequenceName'], '`');
+ $definition['quoted'] = true;
+ }
+
+ if (! isset($definition['allocationSize']) || trim((string) $definition['allocationSize']) === '') {
+ $definition['allocationSize'] = '1';
+ }
+
+ if (! isset($definition['initialValue']) || trim((string) $definition['initialValue']) === '') {
+ $definition['initialValue'] = '1';
+ }
+
+ $definition['allocationSize'] = (string) $definition['allocationSize'];
+ $definition['initialValue'] = (string) $definition['initialValue'];
+
+ $this->sequenceGeneratorDefinition = $definition;
+ }
+
+ /**
+ * Sets the version field mapping used for versioning. Sets the default
+ * value to use depending on the column type.
+ *
+ * @psalm-param array $mapping The version field mapping array.
+ *
+ * @throws MappingException
+ */
+ public function setVersionMapping(array &$mapping): void
+ {
+ $this->isVersioned = true;
+ $this->versionField = $mapping['fieldName'];
+ $this->requiresFetchAfterChange = true;
+
+ if (! isset($mapping['default'])) {
+ if (in_array($mapping['type'], ['integer', 'bigint', 'smallint'], true)) {
+ $mapping['default'] = 1;
+ } elseif ($mapping['type'] === 'datetime') {
+ $mapping['default'] = 'CURRENT_TIMESTAMP';
+ } else {
+ throw MappingException::unsupportedOptimisticLockingType($this->name, $mapping['fieldName'], $mapping['type']);
+ }
+ }
+ }
+
+ /**
+ * Sets whether this class is to be versioned for optimistic locking.
+ */
+ public function setVersioned(bool $bool): void
+ {
+ $this->isVersioned = $bool;
+
+ if ($bool) {
+ $this->requiresFetchAfterChange = true;
+ }
+ }
+
+ /**
+ * Sets the name of the field that is to be used for versioning if this class is
+ * versioned for optimistic locking.
+ */
+ public function setVersionField(string|null $versionField): void
+ {
+ $this->versionField = $versionField;
+ }
+
+ /**
+ * Marks this class as read only, no change tracking is applied to it.
+ */
+ public function markReadOnly(): void
+ {
+ $this->isReadOnly = true;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getFieldNames(): array
+ {
+ return array_keys($this->fieldMappings);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getAssociationNames(): array
+ {
+ return array_keys($this->associationMappings);
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @psalm-return class-string
+ *
+ * @throws InvalidArgumentException
+ */
+ public function getAssociationTargetClass(string $assocName): string
+ {
+ return $this->associationMappings[$assocName]['targetEntity']
+ ?? throw new InvalidArgumentException("Association name expected, '" . $assocName . "' is not an association.");
+ }
+
+ public function getName(): string
+ {
+ return $this->name;
+ }
+
+ public function isAssociationInverseSide(string $assocName): bool
+ {
+ return isset($this->associationMappings[$assocName])
+ && ! $this->associationMappings[$assocName]['isOwningSide'];
+ }
+
+ public function getAssociationMappedByTargetField(string $assocName): string
+ {
+ return $this->associationMappings[$assocName]['mappedBy'];
+ }
+
+ /**
+ * @return string|null null if the input value is null
+ * @psalm-return class-string|null
+ */
+ public function fullyQualifiedClassName(string|null $className): string|null
+ {
+ if (empty($className)) {
+ return $className;
+ }
+
+ if (! str_contains($className, '\\') && $this->namespace) {
+ return $this->namespace . '\\' . $className;
+ }
+
+ return $className;
+ }
+
+ public function getMetadataValue(string $name): mixed
+ {
+ if (isset($this->$name)) {
+ return $this->$name;
+ }
+
+ return null;
+ }
+
+ /**
+ * Map Embedded Class
+ *
+ * @psalm-param array $mapping
+ *
+ * @throws MappingException
+ */
+ public function mapEmbedded(array $mapping): void
+ {
+ $this->assertFieldNotMapped($mapping['fieldName']);
+
+ if (! isset($mapping['class']) && $this->isTypedProperty($mapping['fieldName'])) {
+ $type = $this->reflClass->getProperty($mapping['fieldName'])->getType();
+ if ($type instanceof ReflectionNamedType) {
+ $mapping['class'] = $type->getName();
+ }
+ }
+
+ $this->embeddedClasses[$mapping['fieldName']] = [
+ 'class' => $this->fullyQualifiedClassName($mapping['class']),
+ 'columnPrefix' => $mapping['columnPrefix'] ?? null,
+ 'declaredField' => $mapping['declaredField'] ?? null,
+ 'originalField' => $mapping['originalField'] ?? null,
+ ];
+ }
+
+ /**
+ * Inline the embeddable class
+ */
+ public function inlineEmbeddable(string $property, ClassMetadata $embeddable): void
+ {
+ foreach ($embeddable->fieldMappings as $fieldMapping) {
+ $fieldMapping['originalClass'] ??= $embeddable->name;
+ $fieldMapping['declaredField'] = isset($fieldMapping['declaredField'])
+ ? $property . '.' . $fieldMapping['declaredField']
+ : $property;
+ $fieldMapping['originalField'] ??= $fieldMapping['fieldName'];
+ $fieldMapping['fieldName'] = $property . '.' . $fieldMapping['fieldName'];
+
+ if (! empty($this->embeddedClasses[$property]['columnPrefix'])) {
+ $fieldMapping['columnName'] = $this->embeddedClasses[$property]['columnPrefix'] . $fieldMapping['columnName'];
+ } elseif ($this->embeddedClasses[$property]['columnPrefix'] !== false) {
+ assert($this->reflClass !== null);
+ assert($embeddable->reflClass !== null);
+ $fieldMapping['columnName'] = $this->namingStrategy
+ ->embeddedFieldToColumnName(
+ $property,
+ $fieldMapping['columnName'],
+ $this->reflClass->name,
+ $embeddable->reflClass->name,
+ );
+ }
+
+ $this->mapField($fieldMapping);
+ }
+ }
+
+ /** @throws MappingException */
+ private function assertFieldNotMapped(string $fieldName): void
+ {
+ if (
+ isset($this->fieldMappings[$fieldName]) ||
+ isset($this->associationMappings[$fieldName]) ||
+ isset($this->embeddedClasses[$fieldName])
+ ) {
+ throw MappingException::duplicateFieldMapping($this->name, $fieldName);
+ }
+ }
+
+ /**
+ * Gets the sequence name based on class metadata.
+ *
+ * @todo Sequence names should be computed in DBAL depending on the platform
+ */
+ public function getSequenceName(AbstractPlatform $platform): string
+ {
+ $sequencePrefix = $this->getSequencePrefix($platform);
+ $columnName = $this->getSingleIdentifierColumnName();
+
+ return $sequencePrefix . '_' . $columnName . '_seq';
+ }
+
+ /**
+ * Gets the sequence name prefix based on class metadata.
+ *
+ * @todo Sequence names should be computed in DBAL depending on the platform
+ */
+ public function getSequencePrefix(AbstractPlatform $platform): string
+ {
+ $tableName = $this->getTableName();
+ $sequencePrefix = $tableName;
+
+ // Prepend the schema name to the table name if there is one
+ $schemaName = $this->getSchemaName();
+ if ($schemaName) {
+ $sequencePrefix = $schemaName . '.' . $tableName;
+ }
+
+ return $sequencePrefix;
+ }
+
+ /** @psalm-param array $mapping */
+ private function assertMappingOrderBy(array $mapping): void
+ {
+ if (isset($mapping['orderBy']) && ! is_array($mapping['orderBy'])) {
+ throw new InvalidArgumentException("'orderBy' is expected to be an array, not " . gettype($mapping['orderBy']));
+ }
+ }
+
+ /** @psalm-param class-string $class */
+ private function getAccessibleProperty(ReflectionService $reflService, string $class, string $field): ReflectionProperty|null
+ {
+ $reflectionProperty = $reflService->getAccessibleProperty($class, $field);
+ if ($reflectionProperty?->isReadOnly()) {
+ $reflectionProperty = new ReflectionReadonlyProperty($reflectionProperty);
+ }
+
+ return $reflectionProperty;
}
}
diff --git a/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php b/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php
deleted file mode 100644
index ac776f223da..00000000000
--- a/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php
+++ /dev/null
@@ -1,3025 +0,0 @@
-ClassMetadata instance holds all the object-relational mapping metadata
- * of an entity and its associations.
- *
- * Once populated, ClassMetadata instances are usually cached in a serialized form.
- *
- * IMPORTANT NOTE:
- *
- * The fields of this class are only public for 2 reasons:
- * 1) To allow fast READ access.
- * 2) To drastically reduce the size of a serialized instance (private/protected members
- * get the whole class name, namespace inclusive, prepended to every property in
- * the serialized representation).
- *
- * @template-covariant T of object
- * @template-implements ClassMetadata
- * @psalm-type FieldMapping = array{
- * type: string,
- * fieldName: string,
- * columnName: string,
- * length?: int,
- * id?: bool,
- * nullable?: bool,
- * notInsertable?: bool,
- * notUpdatable?: bool,
- * generated?: int,
- * enumType?: class-string,
- * columnDefinition?: string,
- * precision?: int,
- * scale?: int,
- * unique?: string,
- * inherited?: class-string,
- * originalClass?: class-string,
- * originalField?: string,
- * quoted?: bool,
- * declared?: class-string,
- * declaredField?: string,
- * options?: array
- * }
- * @psalm-type JoinColumnData = array{
- * name: string,
- * referencedColumnName: string,
- * unique?: bool,
- * quoted?: bool,
- * fieldName?: string,
- * onDelete?: string,
- * columnDefinition?: string,
- * nullable?: bool,
- * }
- * @psalm-type AssociationMapping = array{
- * cache?: array,
- * cascade: array,
- * declared?: class-string,
- * fetch: mixed,
- * fieldName: string,
- * id?: bool,
- * inherited?: class-string,
- * indexBy?: string,
- * inversedBy: string|null,
- * isCascadeRemove: bool,
- * isCascadePersist: bool,
- * isCascadeRefresh: bool,
- * isCascadeMerge: bool,
- * isCascadeDetach: bool,
- * isOnDeleteCascade?: bool,
- * isOwningSide: bool,
- * joinColumns?: array,
- * joinColumnFieldNames?: array,
- * joinTable?: array,
- * joinTableColumns?: list,
- * mappedBy: string|null,
- * orderBy?: array,
- * originalClass?: class-string,
- * originalField?: string,
- * orphanRemoval?: bool,
- * relationToSourceKeyColumns?: array,
- * relationToTargetKeyColumns?: array,
- * sourceEntity: class-string,
- * sourceToTargetKeyColumns?: array,
- * targetEntity: class-string,
- * targetToSourceKeyColumns?: array,
- * type: int,
- * unique?: bool,
- * }
- * @psalm-type DiscriminatorColumnMapping = array{
- * name: string,
- * fieldName: string,
- * type: string,
- * length?: int,
- * columnDefinition?: string|null,
- * enumType?: class-string|null,
- * }
- */
-class ClassMetadataInfo implements ClassMetadata, Stringable
-{
- /* The inheritance mapping types */
- /**
- * NONE means the class does not participate in an inheritance hierarchy
- * and therefore does not need an inheritance mapping type.
- */
- public const INHERITANCE_TYPE_NONE = 1;
-
- /**
- * JOINED means the class will be persisted according to the rules of
- * Class Table Inheritance.
- */
- public const INHERITANCE_TYPE_JOINED = 2;
-
- /**
- * SINGLE_TABLE means the class will be persisted according to the rules of
- * Single Table Inheritance.
- */
- public const INHERITANCE_TYPE_SINGLE_TABLE = 3;
-
- /**
- * TABLE_PER_CLASS means the class will be persisted according to the rules
- * of Concrete Table Inheritance.
- */
- public const INHERITANCE_TYPE_TABLE_PER_CLASS = 4;
-
- /* The Id generator types. */
- /**
- * AUTO means the generator type will depend on what the used platform prefers.
- * Offers full portability.
- */
- public const GENERATOR_TYPE_AUTO = 1;
-
- /**
- * SEQUENCE means a separate sequence object will be used. Platforms that do
- * not have native sequence support may emulate it. Full portability is currently
- * not guaranteed.
- */
- public const GENERATOR_TYPE_SEQUENCE = 2;
-
- /**
- * IDENTITY means an identity column is used for id generation. The database
- * will fill in the id column on insertion. Platforms that do not support
- * native identity columns may emulate them. Full portability is currently
- * not guaranteed.
- */
- public const GENERATOR_TYPE_IDENTITY = 4;
-
- /**
- * NONE means the class does not have a generated id. That means the class
- * must have a natural, manually assigned id.
- */
- public const GENERATOR_TYPE_NONE = 5;
-
- /**
- * CUSTOM means that customer will use own ID generator that supposedly work
- */
- public const GENERATOR_TYPE_CUSTOM = 7;
-
- /**
- * DEFERRED_IMPLICIT means that changes of entities are calculated at commit-time
- * by doing a property-by-property comparison with the original data. This will
- * be done for all entities that are in MANAGED state at commit-time.
- *
- * This is the default change tracking policy.
- */
- public const CHANGETRACKING_DEFERRED_IMPLICIT = 1;
-
- /**
- * DEFERRED_EXPLICIT means that changes of entities are calculated at commit-time
- * by doing a property-by-property comparison with the original data. This will
- * be done only for entities that were explicitly saved (through persist() or a cascade).
- */
- public const CHANGETRACKING_DEFERRED_EXPLICIT = 2;
-
- /**
- * NOTIFY means that Doctrine relies on the entities sending out notifications
- * when their properties change. Such entity classes must implement
- * the NotifyPropertyChanged interface.
- */
- public const CHANGETRACKING_NOTIFY = 3;
-
- /**
- * Specifies that an association is to be fetched when it is first accessed.
- */
- public const FETCH_LAZY = 2;
-
- /**
- * Specifies that an association is to be fetched when the owner of the
- * association is fetched.
- */
- public const FETCH_EAGER = 3;
-
- /**
- * Specifies that an association is to be fetched lazy (on first access) and that
- * commands such as Collection#count, Collection#slice are issued directly against
- * the database if the collection is not yet initialized.
- */
- public const FETCH_EXTRA_LAZY = 4;
-
- /**
- * Identifies a one-to-one association.
- */
- public const ONE_TO_ONE = 1;
-
- /**
- * Identifies a many-to-one association.
- */
- public const MANY_TO_ONE = 2;
-
- /**
- * Identifies a one-to-many association.
- */
- public const ONE_TO_MANY = 4;
-
- /**
- * Identifies a many-to-many association.
- */
- public const MANY_TO_MANY = 8;
-
- /**
- * Combined bitmask for to-one (single-valued) associations.
- */
- public const TO_ONE = 3;
-
- /**
- * Combined bitmask for to-many (collection-valued) associations.
- */
- public const TO_MANY = 12;
-
- /**
- * ReadOnly cache can do reads, inserts and deletes, cannot perform updates or employ any locks,
- */
- public const CACHE_USAGE_READ_ONLY = 1;
-
- /**
- * Nonstrict Read Write Cache doesn’t employ any locks but can do inserts, update and deletes.
- */
- public const CACHE_USAGE_NONSTRICT_READ_WRITE = 2;
-
- /**
- * Read Write Attempts to lock the entity before update/delete.
- */
- public const CACHE_USAGE_READ_WRITE = 3;
-
- /**
- * The value of this column is never generated by the database.
- */
- public const GENERATED_NEVER = 0;
-
- /**
- * The value of this column is generated by the database on INSERT, but not on UPDATE.
- */
- public const GENERATED_INSERT = 1;
-
- /**
- * The value of this column is generated by the database on both INSERT and UDPATE statements.
- */
- public const GENERATED_ALWAYS = 2;
-
- /**
- * READ-ONLY: The namespace the entity class is contained in.
- *
- * @todo Not really needed. Usage could be localized.
- */
- public string|null $namespace = null;
-
- /**
- * READ-ONLY: The name of the entity class that is at the root of the mapped entity inheritance
- * hierarchy. If the entity is not part of a mapped inheritance hierarchy this is the same
- * as {@link $name}.
- *
- * @psalm-var class-string
- */
- public string $rootEntityName;
-
- /**
- * READ-ONLY: The definition of custom generator. Only used for CUSTOM
- * generator type
- *
- * The definition has the following structure:
- *
- * array(
- * 'class' => 'ClassName',
- * )
- *
- *
- * @todo Merge with tableGeneratorDefinition into generic generatorDefinition
- * @var array|null
- */
- public array|null $customGeneratorDefinition = null;
-
- /**
- * The name of the custom repository class used for the entity class.
- * (Optional).
- *
- * @psalm-var ?class-string
- */
- public string|null $customRepositoryClassName = null;
-
- /**
- * READ-ONLY: Whether this class describes the mapping of a mapped superclass.
- */
- public bool $isMappedSuperclass = false;
-
- /**
- * READ-ONLY: Whether this class describes the mapping of an embeddable class.
- */
- public bool $isEmbeddedClass = false;
-
- /**
- * READ-ONLY: The names of the parent classes (ancestors).
- *
- * @psalm-var list
- */
- public array $parentClasses = [];
-
- /**
- * READ-ONLY: The names of all subclasses (descendants).
- *
- * @psalm-var list
- */
- public array $subClasses = [];
-
- /**
- * READ-ONLY: The names of all embedded classes based on properties.
- *
- * @psalm-var array
- */
- public array $embeddedClasses = [];
-
- /**
- * READ-ONLY: The field names of all fields that are part of the identifier/primary key
- * of the mapped entity class.
- *
- * @psalm-var list
- */
- public array $identifier = [];
-
- /**
- * READ-ONLY: The inheritance mapping type used by the class.
- *
- * @psalm-var self::INHERITANCE_TYPE_*
- */
- public int $inheritanceType = self::INHERITANCE_TYPE_NONE;
-
- /**
- * READ-ONLY: The Id generator type used by the class.
- *
- * @psalm-var self::GENERATOR_TYPE_*
- */
- public int $generatorType = self::GENERATOR_TYPE_NONE;
-
- /**
- * READ-ONLY: The field mappings of the class.
- * Keys are field names and values are mapping definitions.
- *
- * The mapping definition array has the following values:
- *
- * - fieldName (string)
- * The name of the field in the Entity.
- *
- * - type (string)
- * The type name of the mapped field. Can be one of Doctrine's mapping types
- * or a custom mapping type.
- *
- * - columnName (string, optional)
- * The column name. Optional. Defaults to the field name.
- *
- * - length (integer, optional)
- * The database length of the column. Optional. Default value taken from
- * the type.
- *
- * - id (boolean, optional)
- * Marks the field as the primary key of the entity. Multiple fields of an
- * entity can have the id attribute, forming a composite key.
- *
- * - nullable (boolean, optional)
- * Whether the column is nullable. Defaults to FALSE.
- *
- * - 'notInsertable' (boolean, optional)
- * Whether the column is not insertable. Optional. Is only set if value is TRUE.
- *
- * - 'notUpdatable' (boolean, optional)
- * Whether the column is updatable. Optional. Is only set if value is TRUE.
- *
- * - columnDefinition (string, optional, schema-only)
- * The SQL fragment that is used when generating the DDL for the column.
- *
- * - precision (integer, optional, schema-only)
- * The precision of a decimal column. Only valid if the column type is decimal.
- *
- * - scale (integer, optional, schema-only)
- * The scale of a decimal column. Only valid if the column type is decimal.
- *
- * - 'unique' (string, optional, schema-only)
- * Whether a unique constraint should be generated for the column.
- *
- * @var mixed[]
- * @psalm-var array
- */
- public array $fieldMappings = [];
-
- /**
- * READ-ONLY: An array of field names. Used to look up field names from column names.
- * Keys are column names and values are field names.
- *
- * @psalm-var array
- */
- public array $fieldNames = [];
-
- /**
- * READ-ONLY: A map of field names to column names. Keys are field names and values column names.
- * Used to look up column names from field names.
- * This is the reverse lookup map of $_fieldNames.
- *
- * @deprecated 3.0 Remove this.
- *
- * @var mixed[]
- */
- public array $columnNames = [];
-
- /**
- * READ-ONLY: The discriminator value of this class.
- *
- * This does only apply to the JOINED and SINGLE_TABLE inheritance mapping strategies
- * where a discriminator column is used.
- *
- * @see discriminatorColumn
- */
- public mixed $discriminatorValue = null;
-
- /**
- * READ-ONLY: The discriminator map of all mapped classes in the hierarchy.
- *
- * This does only apply to the JOINED and SINGLE_TABLE inheritance mapping strategies
- * where a discriminator column is used.
- *
- * @see discriminatorColumn
- *
- * @var array
- *
- * @psalm-var array
- */
- public array $discriminatorMap = [];
-
- /**
- * READ-ONLY: The definition of the discriminator column used in JOINED and SINGLE_TABLE
- * inheritance mappings.
- *
- * @var array
- * @psalm-var DiscriminatorColumnMapping|null
- */
- public array|null $discriminatorColumn = null;
-
- /**
- * READ-ONLY: The primary table definition. The definition is an array with the
- * following entries:
- *
- * name =>
- * schema =>
- * indexes => array
- * uniqueConstraints => array
- *
- * @var mixed[]
- * @psalm-var array{
- * name: string,
- * schema?: string,
- * indexes?: array,
- * uniqueConstraints?: array,
- * options?: array,
- * quoted?: bool
- * }
- */
- public array $table;
-
- /**
- * READ-ONLY: The registered lifecycle callbacks for entities of this class.
- *
- * @psalm-var array>
- */
- public array $lifecycleCallbacks = [];
-
- /**
- * READ-ONLY: The registered entity listeners.
- *
- * @psalm-var array>
- */
- public array $entityListeners = [];
-
- /**
- * READ-ONLY: The association mappings of this class.
- *
- * The mapping definition array supports the following keys:
- *
- * - fieldName (string)
- * The name of the field in the entity the association is mapped to.
- *
- * - targetEntity (string)
- * The class name of the target entity. If it is fully-qualified it is used as is.
- * If it is a simple, unqualified class name the namespace is assumed to be the same
- * as the namespace of the source entity.
- *
- * - mappedBy (string, required for bidirectional associations)
- * The name of the field that completes the bidirectional association on the owning side.
- * This key must be specified on the inverse side of a bidirectional association.
- *
- * - inversedBy (string, required for bidirectional associations)
- * The name of the field that completes the bidirectional association on the inverse side.
- * This key must be specified on the owning side of a bidirectional association.
- *
- * - cascade (array, optional)
- * The names of persistence operations to cascade on the association. The set of possible
- * values are: "persist", "remove", "detach", "merge", "refresh", "all" (implies all others).
- *
- * - orderBy (array, one-to-many/many-to-many only)
- * A map of field names (of the target entity) to sorting directions (ASC/DESC).
- * Example: array('priority' => 'desc')
- *
- * - fetch (integer, optional)
- * The fetching strategy to use for the association, usually defaults to FETCH_LAZY.
- * Possible values are: ClassMetadata::FETCH_EAGER, ClassMetadata::FETCH_LAZY.
- *
- * - joinTable (array, optional, many-to-many only)
- * Specification of the join table and its join columns (foreign keys).
- * Only valid for many-to-many mappings. Note that one-to-many associations can be mapped
- * through a join table by simply mapping the association as many-to-many with a unique
- * constraint on the join table.
- *
- * - indexBy (string, optional, to-many only)
- * Specification of a field on target-entity that is used to index the collection by.
- * This field HAS to be either the primary key or a unique column. Otherwise the collection
- * does not contain all the entities that are actually related.
- *
- * A join table definition has the following structure:
- *
- * array(
- * 'name' => ,
- * 'joinColumns' => array(),
- * 'inverseJoinColumns' => array()
- * )
- *
- *
- * @psalm-var array
- */
- public array $associationMappings = [];
-
- /**
- * READ-ONLY: Flag indicating whether the identifier/primary key of the class is composite.
- */
- public bool $isIdentifierComposite = false;
-
- /**
- * READ-ONLY: Flag indicating whether the identifier/primary key contains at least one foreign key association.
- *
- * This flag is necessary because some code blocks require special treatment of this cases.
- */
- public bool $containsForeignIdentifier = false;
-
- /**
- * READ-ONLY: Flag indicating whether the identifier/primary key contains at least one ENUM type.
- *
- * This flag is necessary because some code blocks require special treatment of this cases.
- */
- public bool $containsEnumIdentifier = false;
-
- /**
- * READ-ONLY: The ID generator used for generating IDs for this class.
- *
- * @todo Remove!
- */
- public AbstractIdGenerator $idGenerator;
-
- /**
- * READ-ONLY: The definition of the sequence generator of this class. Only used for the
- * SEQUENCE generation strategy.
- *
- * The definition has the following structure:
- *
- * array(
- * 'sequenceName' => 'name',
- * 'allocationSize' => '20',
- * 'initialValue' => '1'
- * )
- *
- *
- * @var array|null
- * @psalm-var array{sequenceName: string, allocationSize: string, initialValue: string, quoted?: mixed}|null
- * @todo Merge with tableGeneratorDefinition into generic generatorDefinition
- */
- public array|null $sequenceGeneratorDefinition = null;
-
- /**
- * READ-ONLY: The policy used for change-tracking on entities of this class.
- */
- public int $changeTrackingPolicy = self::CHANGETRACKING_DEFERRED_IMPLICIT;
-
- /**
- * READ-ONLY: A Flag indicating whether one or more columns of this class
- * have to be reloaded after insert / update operations.
- */
- public bool $requiresFetchAfterChange = false;
-
- /**
- * READ-ONLY: A flag for whether or not instances of this class are to be versioned
- * with optimistic locking.
- */
- public bool $isVersioned = false;
-
- /**
- * READ-ONLY: The name of the field which is used for versioning in optimistic locking (if any).
- */
- public string|null $versionField = null;
-
- /** @var mixed[]|null */
- public array|null $cache = null;
-
- /**
- * The ReflectionClass instance of the mapped class.
- *
- * @var ReflectionClass|null
- */
- public ReflectionClass|null $reflClass = null;
-
- /**
- * Is this entity marked as "read-only"?
- *
- * That means it is never considered for change-tracking in the UnitOfWork. It is a very helpful performance
- * optimization for entities that are immutable, either in your domain or through the relation database
- * (coming from a view, or a history table for example).
- */
- public bool $isReadOnly = false;
-
- /**
- * NamingStrategy determining the default column and table names.
- */
- protected NamingStrategy $namingStrategy;
-
- /**
- * The ReflectionProperty instances of the mapped class.
- *
- * @var array
- */
- public array $reflFields = [];
-
- private InstantiatorInterface|null $instantiator = null;
-
- private TypedFieldMapper $typedFieldMapper;
-
- /**
- * Initializes a new ClassMetadata instance that will hold the object-relational mapping
- * metadata of the class with the given name.
- *
- * @param string $name The name of the entity class the new instance is used for.
- * @psalm-param class-string $name
- */
- public function __construct(public $name, NamingStrategy|null $namingStrategy = null, TypedFieldMapper|null $typedFieldMapper = null)
- {
- $this->rootEntityName = $name;
- $this->namingStrategy = $namingStrategy ?? new DefaultNamingStrategy();
- $this->instantiator = new Instantiator();
- $this->typedFieldMapper = $typedFieldMapper ?? new DefaultTypedFieldMapper();
- }
-
- /**
- * Gets the ReflectionProperties of the mapped class.
- *
- * @return ReflectionProperty[]|null[] An array of ReflectionProperty instances.
- * @psalm-return array
- */
- public function getReflectionProperties(): array
- {
- return $this->reflFields;
- }
-
- /**
- * Gets a ReflectionProperty for a specific field of the mapped class.
- */
- public function getReflectionProperty(string $name): ReflectionProperty|null
- {
- return $this->reflFields[$name];
- }
-
- /**
- * Gets the ReflectionProperty for the single identifier field.
- *
- * @throws BadMethodCallException If the class has a composite identifier.
- */
- public function getSingleIdReflectionProperty(): ReflectionProperty|null
- {
- if ($this->isIdentifierComposite) {
- throw new BadMethodCallException('Class ' . $this->name . ' has a composite identifier.');
- }
-
- return $this->reflFields[$this->identifier[0]];
- }
-
- /**
- * Extracts the identifier values of an entity of this class.
- *
- * For composite identifiers, the identifier values are returned as an array
- * with the same order as the field order in {@link identifier}.
- *
- * @param object $entity
- *
- * @return array
- */
- public function getIdentifierValues($entity): array
- {
- if ($this->isIdentifierComposite) {
- $id = [];
-
- foreach ($this->identifier as $idField) {
- $value = $this->reflFields[$idField]->getValue($entity);
-
- if ($value !== null) {
- $id[$idField] = $value;
- }
- }
-
- return $id;
- }
-
- $id = $this->identifier[0];
- $value = $this->reflFields[$id]->getValue($entity);
-
- if ($value === null) {
- return [];
- }
-
- return [$id => $value];
- }
-
- /**
- * Populates the entity identifier of an entity.
- *
- * @psalm-param array $id
- *
- * @todo Rename to assignIdentifier()
- */
- public function setIdentifierValues(object $entity, array $id): void
- {
- foreach ($id as $idField => $idValue) {
- $this->reflFields[$idField]->setValue($entity, $idValue);
- }
- }
-
- /**
- * Sets the specified field to the specified value on the given entity.
- */
- public function setFieldValue(object $entity, string $field, mixed $value): void
- {
- $this->reflFields[$field]->setValue($entity, $value);
- }
-
- /**
- * Gets the specified field's value off the given entity.
- */
- public function getFieldValue(object $entity, string $field): mixed
- {
- return $this->reflFields[$field]->getValue($entity);
- }
-
- /**
- * Creates a string representation of this instance.
- *
- * @return string The string representation of this instance.
- *
- * @todo Construct meaningful string representation.
- */
- public function __toString(): string
- {
- return self::class . '@' . spl_object_id($this);
- }
-
- /**
- * Determines which fields get serialized.
- *
- * It is only serialized what is necessary for best unserialization performance.
- * That means any metadata properties that are not set or empty or simply have
- * their default value are NOT serialized.
- *
- * Parts that are also NOT serialized because they can not be properly unserialized:
- * - reflClass (ReflectionClass)
- * - reflFields (ReflectionProperty array)
- *
- * @return string[] The names of all the fields that should be serialized.
- */
- public function __sleep()
- {
- // This metadata is always serialized/cached.
- $serialized = [
- 'associationMappings',
- 'columnNames', //TODO: 3.0 Remove this. Can use fieldMappings[$fieldName]['columnName']
- 'fieldMappings',
- 'fieldNames',
- 'embeddedClasses',
- 'identifier',
- 'isIdentifierComposite', // TODO: REMOVE
- 'name',
- 'namespace', // TODO: REMOVE
- 'table',
- 'rootEntityName',
- 'idGenerator', //TODO: Does not really need to be serialized. Could be moved to runtime.
- ];
-
- // The rest of the metadata is only serialized if necessary.
- if ($this->changeTrackingPolicy !== self::CHANGETRACKING_DEFERRED_IMPLICIT) {
- $serialized[] = 'changeTrackingPolicy';
- }
-
- if ($this->customRepositoryClassName) {
- $serialized[] = 'customRepositoryClassName';
- }
-
- if ($this->inheritanceType !== self::INHERITANCE_TYPE_NONE) {
- $serialized[] = 'inheritanceType';
- $serialized[] = 'discriminatorColumn';
- $serialized[] = 'discriminatorValue';
- $serialized[] = 'discriminatorMap';
- $serialized[] = 'parentClasses';
- $serialized[] = 'subClasses';
- }
-
- if ($this->generatorType !== self::GENERATOR_TYPE_NONE) {
- $serialized[] = 'generatorType';
- if ($this->generatorType === self::GENERATOR_TYPE_SEQUENCE) {
- $serialized[] = 'sequenceGeneratorDefinition';
- }
- }
-
- if ($this->isMappedSuperclass) {
- $serialized[] = 'isMappedSuperclass';
- }
-
- if ($this->isEmbeddedClass) {
- $serialized[] = 'isEmbeddedClass';
- }
-
- if ($this->containsForeignIdentifier) {
- $serialized[] = 'containsForeignIdentifier';
- }
-
- if ($this->containsEnumIdentifier) {
- $serialized[] = 'containsEnumIdentifier';
- }
-
- if ($this->isVersioned) {
- $serialized[] = 'isVersioned';
- $serialized[] = 'versionField';
- }
-
- if ($this->lifecycleCallbacks) {
- $serialized[] = 'lifecycleCallbacks';
- }
-
- if ($this->entityListeners) {
- $serialized[] = 'entityListeners';
- }
-
- if ($this->isReadOnly) {
- $serialized[] = 'isReadOnly';
- }
-
- if ($this->customGeneratorDefinition) {
- $serialized[] = 'customGeneratorDefinition';
- }
-
- if ($this->cache) {
- $serialized[] = 'cache';
- }
-
- if ($this->requiresFetchAfterChange) {
- $serialized[] = 'requiresFetchAfterChange';
- }
-
- return $serialized;
- }
-
- /**
- * Creates a new instance of the mapped class, without invoking the constructor.
- */
- public function newInstance(): object
- {
- return $this->instantiator->instantiate($this->name);
- }
-
- /**
- * Restores some state that can not be serialized/unserialized.
- */
- public function wakeupReflection(ReflectionService $reflService): void
- {
- // Restore ReflectionClass and properties
- $this->reflClass = $reflService->getClass($this->name);
- $this->instantiator = $this->instantiator ?: new Instantiator();
-
- $parentReflFields = [];
-
- foreach ($this->embeddedClasses as $property => $embeddedClass) {
- if (isset($embeddedClass['declaredField'])) {
- $childProperty = $this->getAccessibleProperty(
- $reflService,
- $this->embeddedClasses[$embeddedClass['declaredField']]['class'],
- $embeddedClass['originalField'],
- );
- assert($childProperty !== null);
- $parentReflFields[$property] = new ReflectionEmbeddedProperty(
- $parentReflFields[$embeddedClass['declaredField']],
- $childProperty,
- $this->embeddedClasses[$embeddedClass['declaredField']]['class'],
- );
-
- continue;
- }
-
- $fieldRefl = $this->getAccessibleProperty(
- $reflService,
- $embeddedClass['declared'] ?? $this->name,
- $property,
- );
-
- $parentReflFields[$property] = $fieldRefl;
- $this->reflFields[$property] = $fieldRefl;
- }
-
- foreach ($this->fieldMappings as $field => $mapping) {
- if (isset($mapping['declaredField']) && isset($parentReflFields[$mapping['declaredField']])) {
- $childProperty = $this->getAccessibleProperty($reflService, $mapping['originalClass'], $mapping['originalField']);
- assert($childProperty !== null);
-
- if (isset($mapping['enumType'])) {
- $childProperty = new ReflectionEnumProperty(
- $childProperty,
- $mapping['enumType'],
- );
- }
-
- $this->reflFields[$field] = new ReflectionEmbeddedProperty(
- $parentReflFields[$mapping['declaredField']],
- $childProperty,
- $mapping['originalClass'],
- );
- continue;
- }
-
- $this->reflFields[$field] = isset($mapping['declared'])
- ? $this->getAccessibleProperty($reflService, $mapping['declared'], $field)
- : $this->getAccessibleProperty($reflService, $this->name, $field);
-
- if (isset($mapping['enumType']) && $this->reflFields[$field] !== null) {
- $this->reflFields[$field] = new ReflectionEnumProperty(
- $this->reflFields[$field],
- $mapping['enumType'],
- );
- }
- }
-
- foreach ($this->associationMappings as $field => $mapping) {
- $this->reflFields[$field] = isset($mapping['declared'])
- ? $this->getAccessibleProperty($reflService, $mapping['declared'], $field)
- : $this->getAccessibleProperty($reflService, $this->name, $field);
- }
- }
-
- /**
- * Initializes a new ClassMetadata instance that will hold the object-relational mapping
- * metadata of the class with the given name.
- *
- * @param ReflectionService $reflService The reflection service.
- */
- public function initializeReflection(ReflectionService $reflService): void
- {
- $this->reflClass = $reflService->getClass($this->name);
- $this->namespace = $reflService->getClassNamespace($this->name);
-
- if ($this->reflClass) {
- $this->name = $this->rootEntityName = $this->reflClass->getName();
- }
-
- $this->table['name'] = $this->namingStrategy->classToTableName($this->name);
- }
-
- /**
- * Validates Identifier.
- *
- * @throws MappingException
- */
- public function validateIdentifier(): void
- {
- if ($this->isMappedSuperclass || $this->isEmbeddedClass) {
- return;
- }
-
- // Verify & complete identifier mapping
- if (! $this->identifier) {
- throw MappingException::identifierRequired($this->name);
- }
-
- if ($this->usesIdGenerator() && $this->isIdentifierComposite) {
- throw MappingException::compositeKeyAssignedIdGeneratorRequired($this->name);
- }
- }
-
- /**
- * Validates association targets actually exist.
- *
- * @throws MappingException
- */
- public function validateAssociations(): void
- {
- foreach ($this->associationMappings as $mapping) {
- if (
- ! class_exists($mapping['targetEntity'])
- && ! interface_exists($mapping['targetEntity'])
- && ! trait_exists($mapping['targetEntity'])
- ) {
- throw MappingException::invalidTargetEntityClass($mapping['targetEntity'], $this->name, $mapping['fieldName']);
- }
- }
- }
-
- /**
- * Validates lifecycle callbacks.
- *
- * @throws MappingException
- */
- public function validateLifecycleCallbacks(ReflectionService $reflService): void
- {
- foreach ($this->lifecycleCallbacks as $callbacks) {
- foreach ($callbacks as $callbackFuncName) {
- if (! $reflService->hasPublicMethod($this->name, $callbackFuncName)) {
- throw MappingException::lifecycleCallbackMethodNotFound($this->name, $callbackFuncName);
- }
- }
- }
- }
-
- /**
- * {@inheritDoc}
- *
- * Can return null when using static reflection, in violation of the LSP
- */
- public function getReflectionClass()
- {
- return $this->reflClass;
- }
-
- /** @psalm-param array{usage?: mixed, region?: mixed} $cache */
- public function enableCache(array $cache): void
- {
- if (! isset($cache['usage'])) {
- $cache['usage'] = self::CACHE_USAGE_READ_ONLY;
- }
-
- if (! isset($cache['region'])) {
- $cache['region'] = strtolower(str_replace('\\', '_', $this->rootEntityName));
- }
-
- $this->cache = $cache;
- }
-
- /** @psalm-param array{usage?: int, region?: string} $cache */
- public function enableAssociationCache(string $fieldName, array $cache): void
- {
- $this->associationMappings[$fieldName]['cache'] = $this->getAssociationCacheDefaults($fieldName, $cache);
- }
-
- /**
- * @psalm-param array{usage?: int, region?: string|null} $cache
- *
- * @return int[]|string[]
- * @psalm-return array{usage: int, region: string|null}
- */
- public function getAssociationCacheDefaults(string $fieldName, array $cache): array
- {
- if (! isset($cache['usage'])) {
- $cache['usage'] = $this->cache['usage'] ?? self::CACHE_USAGE_READ_ONLY;
- }
-
- if (! isset($cache['region'])) {
- $cache['region'] = strtolower(str_replace('\\', '_', $this->rootEntityName)) . '__' . $fieldName;
- }
-
- return $cache;
- }
-
- /**
- * Sets the change tracking policy used by this class.
- */
- public function setChangeTrackingPolicy(int $policy): void
- {
- $this->changeTrackingPolicy = $policy;
- }
-
- /**
- * Whether the change tracking policy of this class is "deferred explicit".
- */
- public function isChangeTrackingDeferredExplicit(): bool
- {
- return $this->changeTrackingPolicy === self::CHANGETRACKING_DEFERRED_EXPLICIT;
- }
-
- /**
- * Whether the change tracking policy of this class is "deferred implicit".
- */
- public function isChangeTrackingDeferredImplicit(): bool
- {
- return $this->changeTrackingPolicy === self::CHANGETRACKING_DEFERRED_IMPLICIT;
- }
-
- /**
- * Whether the change tracking policy of this class is "notify".
- */
- public function isChangeTrackingNotify(): bool
- {
- return $this->changeTrackingPolicy === self::CHANGETRACKING_NOTIFY;
- }
-
- /**
- * Checks whether a field is part of the identifier/primary key field(s).
- */
- public function isIdentifier(string $fieldName): bool
- {
- if (! $this->identifier) {
- return false;
- }
-
- if (! $this->isIdentifierComposite) {
- return $fieldName === $this->identifier[0];
- }
-
- return in_array($fieldName, $this->identifier, true);
- }
-
- public function isUniqueField(string $fieldName): bool
- {
- $mapping = $this->getFieldMapping($fieldName);
-
- return $mapping !== false && isset($mapping['unique']) && $mapping['unique'];
- }
-
- public function isNullable(string $fieldName): bool
- {
- $mapping = $this->getFieldMapping($fieldName);
-
- return $mapping !== false && isset($mapping['nullable']) && $mapping['nullable'];
- }
-
- /**
- * Gets a column name for a field name.
- * If the column name for the field cannot be found, the given field name
- * is returned.
- */
- public function getColumnName(string $fieldName): string
- {
- return $this->columnNames[$fieldName] ?? $fieldName;
- }
-
- /**
- * Gets the mapping of a (regular) field that holds some data but not a
- * reference to another object.
- *
- * @return mixed[] The field mapping.
- * @psalm-return FieldMapping
- *
- * @throws MappingException
- */
- public function getFieldMapping(string $fieldName): array
- {
- if (! isset($this->fieldMappings[$fieldName])) {
- throw MappingException::mappingNotFound($this->name, $fieldName);
- }
-
- return $this->fieldMappings[$fieldName];
- }
-
- /**
- * Gets the mapping of an association.
- *
- * @see ClassMetadataInfo::$associationMappings
- *
- * @param string $fieldName The field name that represents the association in
- * the object model.
- *
- * @return mixed[] The mapping.
- * @psalm-return AssociationMapping
- *
- * @throws MappingException
- */
- public function getAssociationMapping(string $fieldName): array
- {
- if (! isset($this->associationMappings[$fieldName])) {
- throw MappingException::mappingNotFound($this->name, $fieldName);
- }
-
- return $this->associationMappings[$fieldName];
- }
-
- /**
- * Gets all association mappings of the class.
- *
- * @psalm-return array
- */
- public function getAssociationMappings(): array
- {
- return $this->associationMappings;
- }
-
- /**
- * Gets the field name for a column name.
- * If no field name can be found the column name is returned.
- *
- * @return string The column alias.
- */
- public function getFieldName(string $columnName): string
- {
- return $this->fieldNames[$columnName] ?? $columnName;
- }
-
- /**
- * Checks whether given property has type
- */
- private function isTypedProperty(string $name): bool
- {
- return isset($this->reflClass)
- && $this->reflClass->hasProperty($name)
- && $this->reflClass->getProperty($name)->hasType();
- }
-
- /**
- * Validates & completes the given field mapping based on typed property.
- *
- * @param array{fieldName: string, type?: mixed} $mapping The field mapping to validate & complete.
- *
- * @return array{fieldName: string, enumType?: string, type?: mixed} The updated mapping.
- */
- private function validateAndCompleteTypedFieldMapping(array $mapping): array
- {
- $field = $this->reflClass->getProperty($mapping['fieldName']);
-
- $mapping = $this->typedFieldMapper->validateAndComplete($mapping, $field);
-
- return $mapping;
- }
-
- /**
- * Validates & completes the basic mapping information based on typed property.
- *
- * @param array{type: self::ONE_TO_ONE|self::MANY_TO_ONE|self::ONE_TO_MANY|self::MANY_TO_MANY, fieldName: string, targetEntity?: class-string} $mapping The mapping.
- *
- * @return mixed[] The updated mapping.
- */
- private function validateAndCompleteTypedAssociationMapping(array $mapping): array
- {
- $type = $this->reflClass->getProperty($mapping['fieldName'])->getType();
-
- if ($type === null || ($mapping['type'] & self::TO_ONE) === 0) {
- return $mapping;
- }
-
- if (! isset($mapping['targetEntity']) && $type instanceof ReflectionNamedType) {
- $mapping['targetEntity'] = $type->getName();
- }
-
- return $mapping;
- }
-
- /**
- * Validates & completes the given field mapping.
- *
- * @psalm-param array{
- * fieldName?: string,
- * columnName?: string,
- * id?: bool,
- * generated?: int,
- * enumType?: class-string,
- * } $mapping The field mapping to validate & complete.
- *
- * @return mixed[] The updated mapping.
- *
- * @throws MappingException
- */
- protected function validateAndCompleteFieldMapping(array $mapping): array
- {
- // Check mandatory fields
- if (! isset($mapping['fieldName']) || ! $mapping['fieldName']) {
- throw MappingException::missingFieldName($this->name);
- }
-
- if ($this->isTypedProperty($mapping['fieldName'])) {
- $mapping = $this->validateAndCompleteTypedFieldMapping($mapping);
- }
-
- if (! isset($mapping['type'])) {
- // Default to string
- $mapping['type'] = 'string';
- }
-
- // Complete fieldName and columnName mapping
- if (! isset($mapping['columnName'])) {
- $mapping['columnName'] = $this->namingStrategy->propertyToColumnName($mapping['fieldName'], $this->name);
- }
-
- if ($mapping['columnName'][0] === '`') {
- $mapping['columnName'] = trim($mapping['columnName'], '`');
- $mapping['quoted'] = true;
- }
-
- $this->columnNames[$mapping['fieldName']] = $mapping['columnName'];
-
- if (isset($this->fieldNames[$mapping['columnName']]) || ($this->discriminatorColumn && $this->discriminatorColumn['name'] === $mapping['columnName'])) {
- throw MappingException::duplicateColumnName($this->name, $mapping['columnName']);
- }
-
- $this->fieldNames[$mapping['columnName']] = $mapping['fieldName'];
-
- // Complete id mapping
- if (isset($mapping['id']) && $mapping['id'] === true) {
- if ($this->versionField === $mapping['fieldName']) {
- throw MappingException::cannotVersionIdField($this->name, $mapping['fieldName']);
- }
-
- if (! in_array($mapping['fieldName'], $this->identifier, true)) {
- $this->identifier[] = $mapping['fieldName'];
- }
-
- // Check for composite key
- if (! $this->isIdentifierComposite && count($this->identifier) > 1) {
- $this->isIdentifierComposite = true;
- }
- }
-
- if (isset($mapping['generated'])) {
- if (! in_array($mapping['generated'], [self::GENERATED_NEVER, self::GENERATED_INSERT, self::GENERATED_ALWAYS])) {
- throw MappingException::invalidGeneratedMode($mapping['generated']);
- }
-
- if ($mapping['generated'] === self::GENERATED_NEVER) {
- unset($mapping['generated']);
- }
- }
-
- if (isset($mapping['enumType'])) {
- if (! enum_exists($mapping['enumType'])) {
- throw MappingException::nonEnumTypeMapped($this->name, $mapping['fieldName'], $mapping['enumType']);
- }
-
- if (! empty($mapping['id'])) {
- $this->containsEnumIdentifier = true;
- }
- }
-
- return $mapping;
- }
-
- /**
- * Validates & completes the basic mapping information that is common to all
- * association mappings (one-to-one, many-ot-one, one-to-many, many-to-many).
- *
- * @psalm-param array $mapping The mapping.
- *
- * @return mixed[] The updated mapping.
- * @psalm-return array{
- * mappedBy: mixed|null,
- * inversedBy: mixed|null,
- * isOwningSide: bool,
- * sourceEntity: class-string,
- * targetEntity: string,
- * fieldName: mixed,
- * fetch: mixed,
- * cascade: array,
- * isCascadeRemove: bool,
- * isCascadePersist: bool,
- * isCascadeRefresh: bool,
- * isCascadeMerge: bool,
- * isCascadeDetach: bool,
- * type: int,
- * originalField: string,
- * originalClass: class-string,
- * ?orphanRemoval: bool
- * }
- *
- * @throws MappingException If something is wrong with the mapping.
- */
- protected function _validateAndCompleteAssociationMapping(array $mapping)
- {
- if (! isset($mapping['mappedBy'])) {
- $mapping['mappedBy'] = null;
- }
-
- if (! isset($mapping['inversedBy'])) {
- $mapping['inversedBy'] = null;
- }
-
- $mapping['isOwningSide'] = true; // assume owning side until we hit mappedBy
-
- if (empty($mapping['indexBy'])) {
- unset($mapping['indexBy']);
- }
-
- // If targetEntity is unqualified, assume it is in the same namespace as
- // the sourceEntity.
- $mapping['sourceEntity'] = $this->name;
-
- if ($this->isTypedProperty($mapping['fieldName'])) {
- $mapping = $this->validateAndCompleteTypedAssociationMapping($mapping);
- }
-
- if (isset($mapping['targetEntity'])) {
- $mapping['targetEntity'] = $this->fullyQualifiedClassName($mapping['targetEntity']);
- $mapping['targetEntity'] = ltrim($mapping['targetEntity'], '\\');
- }
-
- if (($mapping['type'] & self::MANY_TO_ONE) > 0 && isset($mapping['orphanRemoval']) && $mapping['orphanRemoval']) {
- throw MappingException::illegalOrphanRemoval($this->name, $mapping['fieldName']);
- }
-
- // Complete id mapping
- if (isset($mapping['id']) && $mapping['id'] === true) {
- if (isset($mapping['orphanRemoval']) && $mapping['orphanRemoval']) {
- throw MappingException::illegalOrphanRemovalOnIdentifierAssociation($this->name, $mapping['fieldName']);
- }
-
- if (! in_array($mapping['fieldName'], $this->identifier, true)) {
- if (isset($mapping['joinColumns']) && count($mapping['joinColumns']) >= 2) {
- throw MappingException::cannotMapCompositePrimaryKeyEntitiesAsForeignId(
- $mapping['targetEntity'],
- $this->name,
- $mapping['fieldName'],
- );
- }
-
- $this->identifier[] = $mapping['fieldName'];
- $this->containsForeignIdentifier = true;
- }
-
- // Check for composite key
- if (! $this->isIdentifierComposite && count($this->identifier) > 1) {
- $this->isIdentifierComposite = true;
- }
-
- if ($this->cache && ! isset($mapping['cache'])) {
- throw NonCacheableEntityAssociation::fromEntityAndField(
- $this->name,
- $mapping['fieldName'],
- );
- }
- }
-
- // Mandatory attributes for both sides
- // Mandatory: fieldName, targetEntity
- if (! isset($mapping['fieldName']) || ! $mapping['fieldName']) {
- throw MappingException::missingFieldName($this->name);
- }
-
- if (! isset($mapping['targetEntity'])) {
- throw MappingException::missingTargetEntity($mapping['fieldName']);
- }
-
- // Mandatory and optional attributes for either side
- if (! $mapping['mappedBy']) {
- if (isset($mapping['joinTable']) && $mapping['joinTable']) {
- if (isset($mapping['joinTable']['name']) && $mapping['joinTable']['name'][0] === '`') {
- $mapping['joinTable']['name'] = trim($mapping['joinTable']['name'], '`');
- $mapping['joinTable']['quoted'] = true;
- }
- }
- } else {
- $mapping['isOwningSide'] = false;
- }
-
- if (isset($mapping['id']) && $mapping['id'] === true && $mapping['type'] & self::TO_MANY) {
- throw MappingException::illegalToManyIdentifierAssociation($this->name, $mapping['fieldName']);
- }
-
- // Fetch mode. Default fetch mode to LAZY, if not set.
- if (! isset($mapping['fetch'])) {
- $mapping['fetch'] = self::FETCH_LAZY;
- }
-
- // Cascades
- $cascades = isset($mapping['cascade']) ? array_map('strtolower', $mapping['cascade']) : [];
-
- $allCascades = ['remove', 'persist', 'refresh', 'merge', 'detach'];
- if (in_array('all', $cascades, true)) {
- $cascades = $allCascades;
- } elseif (count($cascades) !== count(array_intersect($cascades, $allCascades))) {
- throw MappingException::invalidCascadeOption(
- array_diff($cascades, $allCascades),
- $this->name,
- $mapping['fieldName'],
- );
- }
-
- $mapping['cascade'] = $cascades;
- $mapping['isCascadeRemove'] = in_array('remove', $cascades, true);
- $mapping['isCascadePersist'] = in_array('persist', $cascades, true);
- $mapping['isCascadeRefresh'] = in_array('refresh', $cascades, true);
- $mapping['isCascadeMerge'] = in_array('merge', $cascades, true);
- $mapping['isCascadeDetach'] = in_array('detach', $cascades, true);
-
- return $mapping;
- }
-
- /**
- * Validates & completes a one-to-one association mapping.
- *
- * @psalm-param array $mapping The mapping to validate & complete.
- *
- * @return mixed[] The validated & completed mapping.
- * @psalm-return array{isOwningSide: mixed, orphanRemoval: bool, isCascadeRemove: bool}
- * @psalm-return array{
- * mappedBy: mixed|null,
- * inversedBy: mixed|null,
- * isOwningSide: bool,
- * sourceEntity: class-string,
- * targetEntity: string,
- * fieldName: mixed,
- * fetch: mixed,
- * cascade: array,
- * isCascadeRemove: bool,
- * isCascadePersist: bool,
- * isCascadeRefresh: bool,
- * isCascadeMerge: bool,
- * isCascadeDetach: bool,
- * type: int,
- * originalField: string,
- * originalClass: class-string,
- * joinColumns?: array{0: array{name: string, referencedColumnName: string}}|mixed,
- * id?: mixed,
- * sourceToTargetKeyColumns?: array,
- * joinColumnFieldNames?: array,
- * targetToSourceKeyColumns?: array,
- * orphanRemoval: bool
- * }
- *
- * @throws RuntimeException
- * @throws MappingException
- */
- protected function _validateAndCompleteOneToOneMapping(array $mapping)
- {
- $mapping = $this->_validateAndCompleteAssociationMapping($mapping);
-
- if (isset($mapping['joinColumns']) && $mapping['joinColumns']) {
- $mapping['isOwningSide'] = true;
- }
-
- if ($mapping['isOwningSide']) {
- if (empty($mapping['joinColumns'])) {
- // Apply default join column
- $mapping['joinColumns'] = [
- [
- 'name' => $this->namingStrategy->joinColumnName($mapping['fieldName'], $this->name),
- 'referencedColumnName' => $this->namingStrategy->referenceColumnName(),
- ],
- ];
- }
-
- $uniqueConstraintColumns = [];
-
- foreach ($mapping['joinColumns'] as &$joinColumn) {
- if ($mapping['type'] === self::ONE_TO_ONE && ! $this->isInheritanceTypeSingleTable()) {
- if (count($mapping['joinColumns']) === 1) {
- if (empty($mapping['id'])) {
- $joinColumn['unique'] = true;
- }
- } else {
- $uniqueConstraintColumns[] = $joinColumn['name'];
- }
- }
-
- if (empty($joinColumn['name'])) {
- $joinColumn['name'] = $this->namingStrategy->joinColumnName($mapping['fieldName'], $this->name);
- }
-
- if (empty($joinColumn['referencedColumnName'])) {
- $joinColumn['referencedColumnName'] = $this->namingStrategy->referenceColumnName();
- }
-
- if ($joinColumn['name'][0] === '`') {
- $joinColumn['name'] = trim($joinColumn['name'], '`');
- $joinColumn['quoted'] = true;
- }
-
- if ($joinColumn['referencedColumnName'][0] === '`') {
- $joinColumn['referencedColumnName'] = trim($joinColumn['referencedColumnName'], '`');
- $joinColumn['quoted'] = true;
- }
-
- $mapping['sourceToTargetKeyColumns'][$joinColumn['name']] = $joinColumn['referencedColumnName'];
- $mapping['joinColumnFieldNames'][$joinColumn['name']] = $joinColumn['fieldName'] ?? $joinColumn['name'];
- }
-
- if ($uniqueConstraintColumns) {
- if (! $this->table) {
- throw new RuntimeException('ClassMetadataInfo::setTable() has to be called before defining a one to one relationship.');
- }
-
- $this->table['uniqueConstraints'][$mapping['fieldName'] . '_uniq'] = ['columns' => $uniqueConstraintColumns];
- }
-
- $mapping['targetToSourceKeyColumns'] = array_flip($mapping['sourceToTargetKeyColumns']);
- }
-
- $mapping['orphanRemoval'] = isset($mapping['orphanRemoval']) && $mapping['orphanRemoval'];
- $mapping['isCascadeRemove'] = $mapping['orphanRemoval'] || $mapping['isCascadeRemove'];
-
- if ($mapping['orphanRemoval']) {
- unset($mapping['unique']);
- }
-
- if (isset($mapping['id']) && $mapping['id'] === true && ! $mapping['isOwningSide']) {
- throw MappingException::illegalInverseIdentifierAssociation($this->name, $mapping['fieldName']);
- }
-
- return $mapping;
- }
-
- /**
- * Validates & completes a one-to-many association mapping.
- *
- * @psalm-param array $mapping The mapping to validate and complete.
- *
- * @return mixed[] The validated and completed mapping.
- * @psalm-return array{
- * mappedBy: mixed,
- * inversedBy: mixed,
- * isOwningSide: bool,
- * sourceEntity: string,
- * targetEntity: string,
- * fieldName: mixed,
- * fetch: int|mixed,
- * cascade: array,
- * isCascadeRemove: bool,
- * isCascadePersist: bool,
- * isCascadeRefresh: bool,
- * isCascadeMerge: bool,
- * isCascadeDetach: bool,
- * orphanRemoval: bool
- * }
- *
- * @throws MappingException
- * @throws InvalidArgumentException
- */
- protected function _validateAndCompleteOneToManyMapping(array $mapping)
- {
- $mapping = $this->_validateAndCompleteAssociationMapping($mapping);
-
- // OneToMany-side MUST be inverse (must have mappedBy)
- if (! isset($mapping['mappedBy'])) {
- throw MappingException::oneToManyRequiresMappedBy($this->name, $mapping['fieldName']);
- }
-
- $mapping['orphanRemoval'] = isset($mapping['orphanRemoval']) && $mapping['orphanRemoval'];
- $mapping['isCascadeRemove'] = $mapping['orphanRemoval'] || $mapping['isCascadeRemove'];
-
- $this->assertMappingOrderBy($mapping);
-
- return $mapping;
- }
-
- /**
- * Validates & completes a many-to-many association mapping.
- *
- * @psalm-param array $mapping The mapping to validate & complete.
- * @psalm-param array $mapping The mapping to validate & complete.
- *
- * @return mixed[] The validated & completed mapping.
- * @psalm-return array{
- * mappedBy: mixed,
- * inversedBy: mixed,
- * isOwningSide: bool,
- * sourceEntity: class-string,
- * targetEntity: string,
- * fieldName: mixed,
- * fetch: mixed,
- * cascade: array,
- * isCascadeRemove: bool,
- * isCascadePersist: bool,
- * isCascadeRefresh: bool,
- * isCascadeMerge: bool,
- * isCascadeDetach: bool,
- * type: int,
- * originalField: string,
- * originalClass: class-string,
- * joinTable?: array{inverseJoinColumns: mixed}|mixed,
- * joinTableColumns?: list,
- * isOnDeleteCascade?: true,
- * relationToSourceKeyColumns?: array,
- * relationToTargetKeyColumns?: array,
- * orphanRemoval: bool
- * }
- *
- * @throws InvalidArgumentException
- */
- protected function _validateAndCompleteManyToManyMapping(array $mapping)
- {
- $mapping = $this->_validateAndCompleteAssociationMapping($mapping);
-
- if ($mapping['isOwningSide']) {
- // owning side MUST have a join table
- if (! isset($mapping['joinTable']['name'])) {
- $mapping['joinTable']['name'] = $this->namingStrategy->joinTableName($mapping['sourceEntity'], $mapping['targetEntity'], $mapping['fieldName']);
- }
-
- $selfReferencingEntityWithoutJoinColumns = $mapping['sourceEntity'] === $mapping['targetEntity']
- && (! (isset($mapping['joinTable']['joinColumns']) || isset($mapping['joinTable']['inverseJoinColumns'])));
-
- if (! isset($mapping['joinTable']['joinColumns'])) {
- $mapping['joinTable']['joinColumns'] = [
- [
- 'name' => $this->namingStrategy->joinKeyColumnName($mapping['sourceEntity'], $selfReferencingEntityWithoutJoinColumns ? 'source' : null),
- 'referencedColumnName' => $this->namingStrategy->referenceColumnName(),
- 'onDelete' => 'CASCADE',
- ],
- ];
- }
-
- if (! isset($mapping['joinTable']['inverseJoinColumns'])) {
- $mapping['joinTable']['inverseJoinColumns'] = [
- [
- 'name' => $this->namingStrategy->joinKeyColumnName($mapping['targetEntity'], $selfReferencingEntityWithoutJoinColumns ? 'target' : null),
- 'referencedColumnName' => $this->namingStrategy->referenceColumnName(),
- 'onDelete' => 'CASCADE',
- ],
- ];
- }
-
- $mapping['joinTableColumns'] = [];
-
- foreach ($mapping['joinTable']['joinColumns'] as &$joinColumn) {
- if (empty($joinColumn['name'])) {
- $joinColumn['name'] = $this->namingStrategy->joinKeyColumnName($mapping['sourceEntity'], $joinColumn['referencedColumnName']);
- }
-
- if (empty($joinColumn['referencedColumnName'])) {
- $joinColumn['referencedColumnName'] = $this->namingStrategy->referenceColumnName();
- }
-
- if ($joinColumn['name'][0] === '`') {
- $joinColumn['name'] = trim($joinColumn['name'], '`');
- $joinColumn['quoted'] = true;
- }
-
- if ($joinColumn['referencedColumnName'][0] === '`') {
- $joinColumn['referencedColumnName'] = trim($joinColumn['referencedColumnName'], '`');
- $joinColumn['quoted'] = true;
- }
-
- if (isset($joinColumn['onDelete']) && strtolower($joinColumn['onDelete']) === 'cascade') {
- $mapping['isOnDeleteCascade'] = true;
- }
-
- $mapping['relationToSourceKeyColumns'][$joinColumn['name']] = $joinColumn['referencedColumnName'];
- $mapping['joinTableColumns'][] = $joinColumn['name'];
- }
-
- foreach ($mapping['joinTable']['inverseJoinColumns'] as &$inverseJoinColumn) {
- if (empty($inverseJoinColumn['name'])) {
- $inverseJoinColumn['name'] = $this->namingStrategy->joinKeyColumnName($mapping['targetEntity'], $inverseJoinColumn['referencedColumnName']);
- }
-
- if (empty($inverseJoinColumn['referencedColumnName'])) {
- $inverseJoinColumn['referencedColumnName'] = $this->namingStrategy->referenceColumnName();
- }
-
- if ($inverseJoinColumn['name'][0] === '`') {
- $inverseJoinColumn['name'] = trim($inverseJoinColumn['name'], '`');
- $inverseJoinColumn['quoted'] = true;
- }
-
- if ($inverseJoinColumn['referencedColumnName'][0] === '`') {
- $inverseJoinColumn['referencedColumnName'] = trim($inverseJoinColumn['referencedColumnName'], '`');
- $inverseJoinColumn['quoted'] = true;
- }
-
- if (isset($inverseJoinColumn['onDelete']) && strtolower($inverseJoinColumn['onDelete']) === 'cascade') {
- $mapping['isOnDeleteCascade'] = true;
- }
-
- $mapping['relationToTargetKeyColumns'][$inverseJoinColumn['name']] = $inverseJoinColumn['referencedColumnName'];
- $mapping['joinTableColumns'][] = $inverseJoinColumn['name'];
- }
- }
-
- $mapping['orphanRemoval'] = isset($mapping['orphanRemoval']) && $mapping['orphanRemoval'];
-
- $this->assertMappingOrderBy($mapping);
-
- return $mapping;
- }
-
- /**
- * {@inheritDoc}
- */
- public function getIdentifierFieldNames(): array
- {
- return $this->identifier;
- }
-
- /**
- * Gets the name of the single id field. Note that this only works on
- * entity classes that have a single-field pk.
- *
- * @throws MappingException If the class doesn't have an identifier or it has a composite primary key.
- */
- public function getSingleIdentifierFieldName(): string
- {
- if ($this->isIdentifierComposite) {
- throw MappingException::singleIdNotAllowedOnCompositePrimaryKey($this->name);
- }
-
- if (! isset($this->identifier[0])) {
- throw MappingException::noIdDefined($this->name);
- }
-
- return $this->identifier[0];
- }
-
- /**
- * Gets the column name of the single id column. Note that this only works on
- * entity classes that have a single-field pk.
- *
- * @throws MappingException If the class doesn't have an identifier or it has a composite primary key.
- */
- public function getSingleIdentifierColumnName(): string
- {
- return $this->getColumnName($this->getSingleIdentifierFieldName());
- }
-
- /**
- * INTERNAL:
- * Sets the mapped identifier/primary key fields of this class.
- * Mainly used by the ClassMetadataFactory to assign inherited identifiers.
- *
- * @psalm-param list $identifier
- */
- public function setIdentifier(array $identifier): void
- {
- $this->identifier = $identifier;
- $this->isIdentifierComposite = (count($this->identifier) > 1);
- }
-
- /**
- * {@inheritDoc}
- */
- public function getIdentifier(): array
- {
- return $this->identifier;
- }
-
- public function hasField(string $fieldName): bool
- {
- return isset($this->fieldMappings[$fieldName]) || isset($this->embeddedClasses[$fieldName]);
- }
-
- /**
- * Gets an array containing all the column names.
- *
- * @psalm-param list|null $fieldNames
- *
- * @return mixed[]
- * @psalm-return list
- */
- public function getColumnNames(array|null $fieldNames = null): array
- {
- if ($fieldNames === null) {
- return array_keys($this->fieldNames);
- }
-
- return array_values(array_map([$this, 'getColumnName'], $fieldNames));
- }
-
- /**
- * Returns an array with all the identifier column names.
- *
- * @psalm-return list
- */
- public function getIdentifierColumnNames(): array
- {
- $columnNames = [];
-
- foreach ($this->identifier as $idProperty) {
- if (isset($this->fieldMappings[$idProperty])) {
- $columnNames[] = $this->fieldMappings[$idProperty]['columnName'];
-
- continue;
- }
-
- // Association defined as Id field
- $joinColumns = $this->associationMappings[$idProperty]['joinColumns'];
- $assocColumnNames = array_map(static fn ($joinColumn) => $joinColumn['name'], $joinColumns);
-
- $columnNames = array_merge($columnNames, $assocColumnNames);
- }
-
- return $columnNames;
- }
-
- /**
- * Sets the type of Id generator to use for the mapped class.
- *
- * @psalm-param self::GENERATOR_TYPE_* $generatorType
- */
- public function setIdGeneratorType(int $generatorType): void
- {
- $this->generatorType = $generatorType;
- }
-
- /**
- * Checks whether the mapped class uses an Id generator.
- */
- public function usesIdGenerator(): bool
- {
- return $this->generatorType !== self::GENERATOR_TYPE_NONE;
- }
-
- public function isInheritanceTypeNone(): bool
- {
- return $this->inheritanceType === self::INHERITANCE_TYPE_NONE;
- }
-
- /**
- * Checks whether the mapped class uses the JOINED inheritance mapping strategy.
- *
- * @return bool TRUE if the class participates in a JOINED inheritance mapping,
- * FALSE otherwise.
- */
- public function isInheritanceTypeJoined(): bool
- {
- return $this->inheritanceType === self::INHERITANCE_TYPE_JOINED;
- }
-
- /**
- * Checks whether the mapped class uses the SINGLE_TABLE inheritance mapping strategy.
- *
- * @return bool TRUE if the class participates in a SINGLE_TABLE inheritance mapping,
- * FALSE otherwise.
- */
- public function isInheritanceTypeSingleTable(): bool
- {
- return $this->inheritanceType === self::INHERITANCE_TYPE_SINGLE_TABLE;
- }
-
- /**
- * Checks whether the mapped class uses the TABLE_PER_CLASS inheritance mapping strategy.
- *
- * @return bool TRUE if the class participates in a TABLE_PER_CLASS inheritance mapping,
- * FALSE otherwise.
- */
- public function isInheritanceTypeTablePerClass(): bool
- {
- return $this->inheritanceType === self::INHERITANCE_TYPE_TABLE_PER_CLASS;
- }
-
- /**
- * Checks whether the class uses an identity column for the Id generation.
- */
- public function isIdGeneratorIdentity(): bool
- {
- return $this->generatorType === self::GENERATOR_TYPE_IDENTITY;
- }
-
- /**
- * Checks whether the class uses a sequence for id generation.
- *
- * @psalm-assert-if-true !null $this->sequenceGeneratorDefinition
- */
- public function isIdGeneratorSequence(): bool
- {
- return $this->generatorType === self::GENERATOR_TYPE_SEQUENCE;
- }
-
- /**
- * Checks whether the class has a natural identifier/pk (which means it does
- * not use any Id generator.
- */
- public function isIdentifierNatural(): bool
- {
- return $this->generatorType === self::GENERATOR_TYPE_NONE;
- }
-
- /**
- * Gets the type of a field.
- *
- * @todo 3.0 Remove this. PersisterHelper should fix it somehow
- */
- public function getTypeOfField(string $fieldName): string|null
- {
- return isset($this->fieldMappings[$fieldName])
- ? $this->fieldMappings[$fieldName]['type']
- : null;
- }
-
- /**
- * Gets the name of the primary table.
- */
- public function getTableName(): string
- {
- return $this->table['name'];
- }
-
- /**
- * Gets primary table's schema name.
- */
- public function getSchemaName(): string|null
- {
- return $this->table['schema'] ?? null;
- }
-
- /**
- * Gets the table name to use for temporary identifier tables of this class.
- */
- public function getTemporaryIdTableName(): string
- {
- // replace dots with underscores because PostgreSQL creates temporary tables in a special schema
- return str_replace('.', '_', $this->getTableName() . '_id_tmp');
- }
-
- /**
- * Sets the mapped subclasses of this class.
- *
- * @psalm-param list $subclasses The names of all mapped subclasses.
- */
- public function setSubclasses(array $subclasses): void
- {
- foreach ($subclasses as $subclass) {
- $this->subClasses[] = $this->fullyQualifiedClassName($subclass);
- }
- }
-
- /**
- * Sets the parent class names.
- * Assumes that the class names in the passed array are in the order:
- * directParent -> directParentParent -> directParentParentParent ... -> root.
- *
- * @psalm-param list $classNames
- */
- public function setParentClasses(array $classNames): void
- {
- $this->parentClasses = $classNames;
-
- if (count($classNames) > 0) {
- $this->rootEntityName = array_pop($classNames);
- }
- }
-
- /**
- * Sets the inheritance type used by the class and its subclasses.
- *
- * @psalm-param self::INHERITANCE_TYPE_* $type
- *
- * @throws MappingException
- */
- public function setInheritanceType(int $type): void
- {
- if (! $this->isInheritanceType($type)) {
- throw MappingException::invalidInheritanceType($this->name, $type);
- }
-
- $this->inheritanceType = $type;
- }
-
- /**
- * Sets the association to override association mapping of property for an entity relationship.
- *
- * @psalm-param array $overrideMapping
- *
- * @throws MappingException
- */
- public function setAssociationOverride(string $fieldName, array $overrideMapping): void
- {
- if (! isset($this->associationMappings[$fieldName])) {
- throw MappingException::invalidOverrideFieldName($this->name, $fieldName);
- }
-
- $mapping = $this->associationMappings[$fieldName];
-
- //if (isset($mapping['inherited']) && (count($overrideMapping) !== 1 || ! isset($overrideMapping['fetch']))) {
- // TODO: Deprecate overriding the fetch mode via association override for 3.0,
- // users should do this with a listener and a custom attribute/annotation
- // TODO: Enable this exception in 2.8
- //throw MappingException::illegalOverrideOfInheritedProperty($this->name, $fieldName);
- //}
-
- if (isset($overrideMapping['joinColumns'])) {
- $mapping['joinColumns'] = $overrideMapping['joinColumns'];
- }
-
- if (isset($overrideMapping['inversedBy'])) {
- $mapping['inversedBy'] = $overrideMapping['inversedBy'];
- }
-
- if (isset($overrideMapping['joinTable'])) {
- $mapping['joinTable'] = $overrideMapping['joinTable'];
- }
-
- if (isset($overrideMapping['fetch'])) {
- $mapping['fetch'] = $overrideMapping['fetch'];
- }
-
- $mapping['joinColumnFieldNames'] = null;
- $mapping['joinTableColumns'] = null;
- $mapping['sourceToTargetKeyColumns'] = null;
- $mapping['relationToSourceKeyColumns'] = null;
- $mapping['relationToTargetKeyColumns'] = null;
-
- switch ($mapping['type']) {
- case self::ONE_TO_ONE:
- $mapping = $this->_validateAndCompleteOneToOneMapping($mapping);
- break;
- case self::ONE_TO_MANY:
- $mapping = $this->_validateAndCompleteOneToManyMapping($mapping);
- break;
- case self::MANY_TO_ONE:
- $mapping = $this->_validateAndCompleteOneToOneMapping($mapping);
- break;
- case self::MANY_TO_MANY:
- $mapping = $this->_validateAndCompleteManyToManyMapping($mapping);
- break;
- }
-
- $this->associationMappings[$fieldName] = $mapping;
- }
-
- /**
- * Sets the override for a mapped field.
- *
- * @psalm-param array $overrideMapping
- *
- * @throws MappingException
- */
- public function setAttributeOverride(string $fieldName, array $overrideMapping): void
- {
- if (! isset($this->fieldMappings[$fieldName])) {
- throw MappingException::invalidOverrideFieldName($this->name, $fieldName);
- }
-
- $mapping = $this->fieldMappings[$fieldName];
-
- //if (isset($mapping['inherited'])) {
- // TODO: Enable this exception in 2.8
- //throw MappingException::illegalOverrideOfInheritedProperty($this->name, $fieldName);
- //}
-
- if (isset($mapping['id'])) {
- $overrideMapping['id'] = $mapping['id'];
- }
-
- if (! isset($overrideMapping['type'])) {
- $overrideMapping['type'] = $mapping['type'];
- }
-
- if (! isset($overrideMapping['fieldName'])) {
- $overrideMapping['fieldName'] = $mapping['fieldName'];
- }
-
- if ($overrideMapping['type'] !== $mapping['type']) {
- throw MappingException::invalidOverrideFieldType($this->name, $fieldName);
- }
-
- unset($this->fieldMappings[$fieldName]);
- unset($this->fieldNames[$mapping['columnName']]);
- unset($this->columnNames[$mapping['fieldName']]);
-
- $overrideMapping = $this->validateAndCompleteFieldMapping($overrideMapping);
-
- $this->fieldMappings[$fieldName] = $overrideMapping;
- }
-
- /**
- * Checks whether a mapped field is inherited from an entity superclass.
- */
- public function isInheritedField(string $fieldName): bool
- {
- return isset($this->fieldMappings[$fieldName]['inherited']);
- }
-
- /**
- * Checks if this entity is the root in any entity-inheritance-hierarchy.
- */
- public function isRootEntity(): bool
- {
- return $this->name === $this->rootEntityName;
- }
-
- /**
- * Checks whether a mapped association field is inherited from a superclass.
- */
- public function isInheritedAssociation(string $fieldName): bool
- {
- return isset($this->associationMappings[$fieldName]['inherited']);
- }
-
- public function isInheritedEmbeddedClass(string $fieldName): bool
- {
- return isset($this->embeddedClasses[$fieldName]['inherited']);
- }
-
- /**
- * Sets the name of the primary table the class is mapped to.
- *
- * @deprecated Use {@link setPrimaryTable}.
- */
- public function setTableName(string $tableName): void
- {
- $this->table['name'] = $tableName;
- }
-
- /**
- * Sets the primary table definition. The provided array supports the
- * following structure:
- *
- * name => (optional, defaults to class name)
- * indexes => array of indexes (optional)
- * uniqueConstraints => array of constraints (optional)
- *
- * If a key is omitted, the current value is kept.
- *
- * @psalm-param array $table The table description.
- */
- public function setPrimaryTable(array $table): void
- {
- if (isset($table['name'])) {
- // Split schema and table name from a table name like "myschema.mytable"
- if (str_contains($table['name'], '.')) {
- [$this->table['schema'], $table['name']] = explode('.', $table['name'], 2);
- }
-
- if ($table['name'][0] === '`') {
- $table['name'] = trim($table['name'], '`');
- $this->table['quoted'] = true;
- }
-
- $this->table['name'] = $table['name'];
- }
-
- if (isset($table['quoted'])) {
- $this->table['quoted'] = $table['quoted'];
- }
-
- if (isset($table['schema'])) {
- $this->table['schema'] = $table['schema'];
- }
-
- if (isset($table['indexes'])) {
- $this->table['indexes'] = $table['indexes'];
- }
-
- if (isset($table['uniqueConstraints'])) {
- $this->table['uniqueConstraints'] = $table['uniqueConstraints'];
- }
-
- if (isset($table['options'])) {
- $this->table['options'] = $table['options'];
- }
- }
-
- /**
- * Checks whether the given type identifies an inheritance type.
- */
- private function isInheritanceType(int $type): bool
- {
- return $type === self::INHERITANCE_TYPE_NONE ||
- $type === self::INHERITANCE_TYPE_SINGLE_TABLE ||
- $type === self::INHERITANCE_TYPE_JOINED ||
- $type === self::INHERITANCE_TYPE_TABLE_PER_CLASS;
- }
-
- /**
- * Adds a mapped field to the class.
- *
- * @psalm-param array $mapping The field mapping.
- *
- * @throws MappingException
- */
- public function mapField(array $mapping): void
- {
- $mapping = $this->validateAndCompleteFieldMapping($mapping);
- $this->assertFieldNotMapped($mapping['fieldName']);
-
- if (isset($mapping['generated'])) {
- $this->requiresFetchAfterChange = true;
- }
-
- $this->fieldMappings[$mapping['fieldName']] = $mapping;
- }
-
- /**
- * INTERNAL:
- * Adds an association mapping without completing/validating it.
- * This is mainly used to add inherited association mappings to derived classes.
- *
- * @psalm-param AssociationMapping $mapping
- *
- * @throws MappingException
- */
- public function addInheritedAssociationMapping(array $mapping/*, $owningClassName = null*/): void
- {
- if (isset($this->associationMappings[$mapping['fieldName']])) {
- throw MappingException::duplicateAssociationMapping($this->name, $mapping['fieldName']);
- }
-
- $this->associationMappings[$mapping['fieldName']] = $mapping;
- }
-
- /**
- * INTERNAL:
- * Adds a field mapping without completing/validating it.
- * This is mainly used to add inherited field mappings to derived classes.
- *
- * @psalm-param array $fieldMapping
- */
- public function addInheritedFieldMapping(array $fieldMapping): void
- {
- $this->fieldMappings[$fieldMapping['fieldName']] = $fieldMapping;
- $this->columnNames[$fieldMapping['fieldName']] = $fieldMapping['columnName'];
- $this->fieldNames[$fieldMapping['columnName']] = $fieldMapping['fieldName'];
- }
-
- /**
- * Adds a one-to-one mapping.
- *
- * @param array $mapping The mapping.
- */
- public function mapOneToOne(array $mapping): void
- {
- $mapping['type'] = self::ONE_TO_ONE;
-
- $mapping = $this->_validateAndCompleteOneToOneMapping($mapping);
-
- $this->_storeAssociationMapping($mapping);
- }
-
- /**
- * Adds a one-to-many mapping.
- *
- * @psalm-param array $mapping The mapping.
- */
- public function mapOneToMany(array $mapping): void
- {
- $mapping['type'] = self::ONE_TO_MANY;
-
- $mapping = $this->_validateAndCompleteOneToManyMapping($mapping);
-
- $this->_storeAssociationMapping($mapping);
- }
-
- /**
- * Adds a many-to-one mapping.
- *
- * @psalm-param array $mapping The mapping.
- */
- public function mapManyToOne(array $mapping): void
- {
- $mapping['type'] = self::MANY_TO_ONE;
-
- // A many-to-one mapping is essentially a one-one backreference
- $mapping = $this->_validateAndCompleteOneToOneMapping($mapping);
-
- $this->_storeAssociationMapping($mapping);
- }
-
- /**
- * Adds a many-to-many mapping.
- *
- * @psalm-param array $mapping The mapping.
- */
- public function mapManyToMany(array $mapping): void
- {
- $mapping['type'] = self::MANY_TO_MANY;
-
- $mapping = $this->_validateAndCompleteManyToManyMapping($mapping);
-
- $this->_storeAssociationMapping($mapping);
- }
-
- /**
- * Stores the association mapping.
- *
- * @psalm-param array $assocMapping
- *
- * @throws MappingException
- */
- protected function _storeAssociationMapping(array $assocMapping): void
- {
- $sourceFieldName = $assocMapping['fieldName'];
-
- $this->assertFieldNotMapped($sourceFieldName);
-
- $this->associationMappings[$sourceFieldName] = $assocMapping;
- }
-
- /**
- * Registers a custom repository class for the entity class.
- *
- * @param string|null $repositoryClassName The class name of the custom mapper.
- * @psalm-param class-string|null $repositoryClassName
- */
- public function setCustomRepositoryClass(string|null $repositoryClassName): void
- {
- $this->customRepositoryClassName = $this->fullyQualifiedClassName($repositoryClassName);
- }
-
- /**
- * Dispatches the lifecycle event of the given entity to the registered
- * lifecycle callbacks and lifecycle listeners.
- *
- * @deprecated Deprecated since version 2.4 in favor of \Doctrine\ORM\Event\ListenersInvoker
- *
- * @param string $lifecycleEvent The lifecycle event.
- */
- public function invokeLifecycleCallbacks(string $lifecycleEvent, object $entity): void
- {
- foreach ($this->lifecycleCallbacks[$lifecycleEvent] as $callback) {
- $entity->$callback();
- }
- }
-
- /**
- * Whether the class has any attached lifecycle listeners or callbacks for a lifecycle event.
- */
- public function hasLifecycleCallbacks(string $lifecycleEvent): bool
- {
- return isset($this->lifecycleCallbacks[$lifecycleEvent]);
- }
-
- /**
- * Gets the registered lifecycle callbacks for an event.
- *
- * @return string[]
- * @psalm-return list
- */
- public function getLifecycleCallbacks(string $event): array
- {
- return $this->lifecycleCallbacks[$event] ?? [];
- }
-
- /**
- * Adds a lifecycle callback for entities of this class.
- */
- public function addLifecycleCallback(string $callback, string $event): void
- {
- if ($this->isEmbeddedClass) {
- Deprecation::trigger(
- 'doctrine/orm',
- 'https://github.com/doctrine/orm/pull/8381',
- 'Registering lifecycle callback %s on Embedded class %s is not doing anything and will throw exception in 3.0',
- $event,
- $this->name,
- );
- }
-
- if (isset($this->lifecycleCallbacks[$event]) && in_array($callback, $this->lifecycleCallbacks[$event], true)) {
- return;
- }
-
- $this->lifecycleCallbacks[$event][] = $callback;
- }
-
- /**
- * Sets the lifecycle callbacks for entities of this class.
- * Any previously registered callbacks are overwritten.
- *
- * @psalm-param array> $callbacks
- */
- public function setLifecycleCallbacks(array $callbacks): void
- {
- $this->lifecycleCallbacks = $callbacks;
- }
-
- /**
- * Adds a entity listener for entities of this class.
- *
- * @param string $eventName The entity lifecycle event.
- * @param string $class The listener class.
- * @param string $method The listener callback method.
- *
- * @throws MappingException
- */
- public function addEntityListener(string $eventName, string $class, string $method): void
- {
- $class = $this->fullyQualifiedClassName($class);
-
- $listener = [
- 'class' => $class,
- 'method' => $method,
- ];
-
- if (! class_exists($class)) {
- throw MappingException::entityListenerClassNotFound($class, $this->name);
- }
-
- if (! method_exists($class, $method)) {
- throw MappingException::entityListenerMethodNotFound($class, $method, $this->name);
- }
-
- if (isset($this->entityListeners[$eventName]) && in_array($listener, $this->entityListeners[$eventName], true)) {
- throw MappingException::duplicateEntityListener($class, $method, $this->name);
- }
-
- $this->entityListeners[$eventName][] = $listener;
- }
-
- /**
- * Sets the discriminator column definition.
- *
- * @see getDiscriminatorColumn()
- *
- * @param mixed[]|null $columnDef
- * @psalm-param array{name: string|null, fieldName?: string, type?: string, length?: int, columnDefinition?: string|null, enumType?: class-string|null}|null $columnDef
- *
- * @throws MappingException
- */
- public function setDiscriminatorColumn(array|null $columnDef): void
- {
- if ($columnDef !== null) {
- if (! isset($columnDef['name'])) {
- throw MappingException::nameIsMandatoryForDiscriminatorColumns($this->name);
- }
-
- if (isset($this->fieldNames[$columnDef['name']])) {
- throw MappingException::duplicateColumnName($this->name, $columnDef['name']);
- }
-
- if (! isset($columnDef['fieldName'])) {
- $columnDef['fieldName'] = $columnDef['name'];
- }
-
- if (! isset($columnDef['type'])) {
- $columnDef['type'] = 'string';
- }
-
- if (in_array($columnDef['type'], ['boolean', 'array', 'object', 'datetime', 'time', 'date'], true)) {
- throw MappingException::invalidDiscriminatorColumnType($this->name, $columnDef['type']);
- }
-
- $this->discriminatorColumn = $columnDef;
- }
- }
-
- /**
- * @return array
- * @psalm-return DiscriminatorColumnMapping
- */
- final public function getDiscriminatorColumn(): array
- {
- if ($this->discriminatorColumn === null) {
- throw new LogicException('The discriminator column was not set.');
- }
-
- return $this->discriminatorColumn;
- }
-
- /**
- * Sets the discriminator values used by this class.
- * Used for JOINED and SINGLE_TABLE inheritance mapping strategies.
- *
- * @param array $map
- */
- public function setDiscriminatorMap(array $map): void
- {
- foreach ($map as $value => $className) {
- $this->addDiscriminatorMapClass($value, $className);
- }
- }
-
- /**
- * Adds one entry of the discriminator map with a new class and corresponding name.
- *
- * @throws MappingException
- */
- public function addDiscriminatorMapClass(int|string $name, string $className): void
- {
- $className = $this->fullyQualifiedClassName($className);
- $className = ltrim($className, '\\');
-
- $this->discriminatorMap[$name] = $className;
-
- if ($this->name === $className) {
- $this->discriminatorValue = $name;
-
- return;
- }
-
- if (! (class_exists($className) || interface_exists($className))) {
- throw MappingException::invalidClassInDiscriminatorMap($className, $this->name);
- }
-
- if (is_subclass_of($className, $this->name) && ! in_array($className, $this->subClasses, true)) {
- $this->subClasses[] = $className;
- }
- }
-
- public function hasAssociation(string $fieldName): bool
- {
- return isset($this->associationMappings[$fieldName]);
- }
-
- public function isSingleValuedAssociation(string $fieldName): bool
- {
- return isset($this->associationMappings[$fieldName])
- && ($this->associationMappings[$fieldName]['type'] & self::TO_ONE);
- }
-
- public function isCollectionValuedAssociation(string $fieldName): bool
- {
- return isset($this->associationMappings[$fieldName])
- && ! ($this->associationMappings[$fieldName]['type'] & self::TO_ONE);
- }
-
- /**
- * Is this an association that only has a single join column?
- */
- public function isAssociationWithSingleJoinColumn(string $fieldName): bool
- {
- return isset($this->associationMappings[$fieldName])
- && isset($this->associationMappings[$fieldName]['joinColumns'][0])
- && ! isset($this->associationMappings[$fieldName]['joinColumns'][1]);
- }
-
- /**
- * Returns the single association join column (if any).
- *
- * @throws MappingException
- */
- public function getSingleAssociationJoinColumnName(string $fieldName): string
- {
- if (! $this->isAssociationWithSingleJoinColumn($fieldName)) {
- throw MappingException::noSingleAssociationJoinColumnFound($this->name, $fieldName);
- }
-
- return $this->associationMappings[$fieldName]['joinColumns'][0]['name'];
- }
-
- /**
- * Returns the single association referenced join column name (if any).
- *
- * @throws MappingException
- */
- public function getSingleAssociationReferencedJoinColumnName(string $fieldName): string
- {
- if (! $this->isAssociationWithSingleJoinColumn($fieldName)) {
- throw MappingException::noSingleAssociationJoinColumnFound($this->name, $fieldName);
- }
-
- return $this->associationMappings[$fieldName]['joinColumns'][0]['referencedColumnName'];
- }
-
- /**
- * Used to retrieve a fieldname for either field or association from a given column.
- *
- * This method is used in foreign-key as primary-key contexts.
- *
- * @throws MappingException
- */
- public function getFieldForColumn(string $columnName): string
- {
- if (isset($this->fieldNames[$columnName])) {
- return $this->fieldNames[$columnName];
- }
-
- foreach ($this->associationMappings as $assocName => $mapping) {
- if (
- $this->isAssociationWithSingleJoinColumn($assocName) &&
- $this->associationMappings[$assocName]['joinColumns'][0]['name'] === $columnName
- ) {
- return $assocName;
- }
- }
-
- throw MappingException::noFieldNameFoundForColumn($this->name, $columnName);
- }
-
- /**
- * Sets the ID generator used to generate IDs for instances of this class.
- */
- public function setIdGenerator(AbstractIdGenerator $generator): void
- {
- $this->idGenerator = $generator;
- }
-
- /**
- * Sets definition.
- *
- * @psalm-param array $definition
- */
- public function setCustomGeneratorDefinition(array $definition): void
- {
- $this->customGeneratorDefinition = $definition;
- }
-
- /**
- * Sets the definition of the sequence ID generator for this class.
- *
- * The definition must have the following structure:
- *
- * array(
- * 'sequenceName' => 'name',
- * 'allocationSize' => 20,
- * 'initialValue' => 1
- * 'quoted' => 1
- * )
- *
- *
- * @psalm-param array{sequenceName?: string, allocationSize?: int|string, initialValue?: int|string, quoted?: mixed} $definition
- *
- * @throws MappingException
- */
- public function setSequenceGeneratorDefinition(array $definition): void
- {
- if (! isset($definition['sequenceName']) || trim($definition['sequenceName']) === '') {
- throw MappingException::missingSequenceName($this->name);
- }
-
- if ($definition['sequenceName'][0] === '`') {
- $definition['sequenceName'] = trim($definition['sequenceName'], '`');
- $definition['quoted'] = true;
- }
-
- if (! isset($definition['allocationSize']) || trim((string) $definition['allocationSize']) === '') {
- $definition['allocationSize'] = '1';
- }
-
- if (! isset($definition['initialValue']) || trim((string) $definition['initialValue']) === '') {
- $definition['initialValue'] = '1';
- }
-
- $definition['allocationSize'] = (string) $definition['allocationSize'];
- $definition['initialValue'] = (string) $definition['initialValue'];
-
- $this->sequenceGeneratorDefinition = $definition;
- }
-
- /**
- * Sets the version field mapping used for versioning. Sets the default
- * value to use depending on the column type.
- *
- * @psalm-param array $mapping The version field mapping array.
- *
- * @throws MappingException
- */
- public function setVersionMapping(array &$mapping): void
- {
- $this->isVersioned = true;
- $this->versionField = $mapping['fieldName'];
- $this->requiresFetchAfterChange = true;
-
- if (! isset($mapping['default'])) {
- if (in_array($mapping['type'], ['integer', 'bigint', 'smallint'], true)) {
- $mapping['default'] = 1;
- } elseif ($mapping['type'] === 'datetime') {
- $mapping['default'] = 'CURRENT_TIMESTAMP';
- } else {
- throw MappingException::unsupportedOptimisticLockingType($this->name, $mapping['fieldName'], $mapping['type']);
- }
- }
- }
-
- /**
- * Sets whether this class is to be versioned for optimistic locking.
- */
- public function setVersioned(bool $bool): void
- {
- $this->isVersioned = $bool;
-
- if ($bool) {
- $this->requiresFetchAfterChange = true;
- }
- }
-
- /**
- * Sets the name of the field that is to be used for versioning if this class is
- * versioned for optimistic locking.
- */
- public function setVersionField(string|null $versionField): void
- {
- $this->versionField = $versionField;
- }
-
- /**
- * Marks this class as read only, no change tracking is applied to it.
- */
- public function markReadOnly(): void
- {
- $this->isReadOnly = true;
- }
-
- /**
- * {@inheritDoc}
- */
- public function getFieldNames(): array
- {
- return array_keys($this->fieldMappings);
- }
-
- /**
- * {@inheritDoc}
- */
- public function getAssociationNames(): array
- {
- return array_keys($this->associationMappings);
- }
-
- /**
- * {@inheritDoc}
- *
- * @psalm-return class-string
- *
- * @throws InvalidArgumentException
- */
- public function getAssociationTargetClass(string $assocName): string
- {
- return $this->associationMappings[$assocName]['targetEntity']
- ?? throw new InvalidArgumentException("Association name expected, '" . $assocName . "' is not an association.");
- }
-
- public function getName(): string
- {
- return $this->name;
- }
-
- public function isAssociationInverseSide(string $assocName): bool
- {
- return isset($this->associationMappings[$assocName])
- && ! $this->associationMappings[$assocName]['isOwningSide'];
- }
-
- public function getAssociationMappedByTargetField(string $assocName): string
- {
- return $this->associationMappings[$assocName]['mappedBy'];
- }
-
- /**
- * @return string|null null if the input value is null
- * @psalm-return class-string|null
- */
- public function fullyQualifiedClassName(string|null $className): string|null
- {
- if (empty($className)) {
- return $className;
- }
-
- if (! str_contains($className, '\\') && $this->namespace) {
- return $this->namespace . '\\' . $className;
- }
-
- return $className;
- }
-
- public function getMetadataValue(string $name): mixed
- {
- if (isset($this->$name)) {
- return $this->$name;
- }
-
- return null;
- }
-
- /**
- * Map Embedded Class
- *
- * @psalm-param array $mapping
- *
- * @throws MappingException
- */
- public function mapEmbedded(array $mapping): void
- {
- $this->assertFieldNotMapped($mapping['fieldName']);
-
- if (! isset($mapping['class']) && $this->isTypedProperty($mapping['fieldName'])) {
- $type = $this->reflClass->getProperty($mapping['fieldName'])->getType();
- if ($type instanceof ReflectionNamedType) {
- $mapping['class'] = $type->getName();
- }
- }
-
- $this->embeddedClasses[$mapping['fieldName']] = [
- 'class' => $this->fullyQualifiedClassName($mapping['class']),
- 'columnPrefix' => $mapping['columnPrefix'] ?? null,
- 'declaredField' => $mapping['declaredField'] ?? null,
- 'originalField' => $mapping['originalField'] ?? null,
- ];
- }
-
- /**
- * Inline the embeddable class
- */
- public function inlineEmbeddable(string $property, ClassMetadataInfo $embeddable): void
- {
- foreach ($embeddable->fieldMappings as $fieldMapping) {
- $fieldMapping['originalClass'] ??= $embeddable->name;
- $fieldMapping['declaredField'] = isset($fieldMapping['declaredField'])
- ? $property . '.' . $fieldMapping['declaredField']
- : $property;
- $fieldMapping['originalField'] ??= $fieldMapping['fieldName'];
- $fieldMapping['fieldName'] = $property . '.' . $fieldMapping['fieldName'];
-
- if (! empty($this->embeddedClasses[$property]['columnPrefix'])) {
- $fieldMapping['columnName'] = $this->embeddedClasses[$property]['columnPrefix'] . $fieldMapping['columnName'];
- } elseif ($this->embeddedClasses[$property]['columnPrefix'] !== false) {
- assert($this->reflClass !== null);
- assert($embeddable->reflClass !== null);
- $fieldMapping['columnName'] = $this->namingStrategy
- ->embeddedFieldToColumnName(
- $property,
- $fieldMapping['columnName'],
- $this->reflClass->name,
- $embeddable->reflClass->name,
- );
- }
-
- $this->mapField($fieldMapping);
- }
- }
-
- /** @throws MappingException */
- private function assertFieldNotMapped(string $fieldName): void
- {
- if (
- isset($this->fieldMappings[$fieldName]) ||
- isset($this->associationMappings[$fieldName]) ||
- isset($this->embeddedClasses[$fieldName])
- ) {
- throw MappingException::duplicateFieldMapping($this->name, $fieldName);
- }
- }
-
- /**
- * Gets the sequence name based on class metadata.
- *
- * @todo Sequence names should be computed in DBAL depending on the platform
- */
- public function getSequenceName(AbstractPlatform $platform): string
- {
- $sequencePrefix = $this->getSequencePrefix($platform);
- $columnName = $this->getSingleIdentifierColumnName();
-
- return $sequencePrefix . '_' . $columnName . '_seq';
- }
-
- /**
- * Gets the sequence name prefix based on class metadata.
- *
- * @todo Sequence names should be computed in DBAL depending on the platform
- */
- public function getSequencePrefix(AbstractPlatform $platform): string
- {
- $tableName = $this->getTableName();
- $sequencePrefix = $tableName;
-
- // Prepend the schema name to the table name if there is one
- $schemaName = $this->getSchemaName();
- if ($schemaName) {
- $sequencePrefix = $schemaName . '.' . $tableName;
- }
-
- return $sequencePrefix;
- }
-
- /** @psalm-param array $mapping */
- private function assertMappingOrderBy(array $mapping): void
- {
- if (isset($mapping['orderBy']) && ! is_array($mapping['orderBy'])) {
- throw new InvalidArgumentException("'orderBy' is expected to be an array, not " . gettype($mapping['orderBy']));
- }
- }
-
- /** @psalm-param class-string $class */
- private function getAccessibleProperty(ReflectionService $reflService, string $class, string $field): ReflectionProperty|null
- {
- $reflectionProperty = $reflService->getAccessibleProperty($class, $field);
- if ($reflectionProperty?->isReadOnly()) {
- $reflectionProperty = new ReflectionReadonlyProperty($reflectionProperty);
- }
-
- return $reflectionProperty;
- }
-}
diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php
index 2ebb7775548..d793439d608 100644
--- a/lib/Doctrine/ORM/UnitOfWork.php
+++ b/lib/Doctrine/ORM/UnitOfWork.php
@@ -31,7 +31,6 @@
use Doctrine\ORM\Internal\CommitOrderCalculator;
use Doctrine\ORM\Internal\HydrationCompleteHandler;
use Doctrine\ORM\Mapping\ClassMetadata;
-use Doctrine\ORM\Mapping\ClassMetadataInfo;
use Doctrine\ORM\Mapping\MappingException;
use Doctrine\ORM\Persisters\Collection\CollectionPersister;
use Doctrine\ORM\Persisters\Collection\ManyToManyPersister;
@@ -79,7 +78,7 @@
*
* Internal note: This class contains highly performance-sensitive code.
*
- * @psalm-import-type AssociationMapping from ClassMetadataInfo
+ * @psalm-import-type AssociationMapping from ClassMetadata
*/
class UnitOfWork implements PropertyChangedListener
{
diff --git a/phpcs.xml.dist b/phpcs.xml.dist
index faf9017a16e..c55b0763731 100644
--- a/phpcs.xml.dist
+++ b/phpcs.xml.dist
@@ -189,7 +189,7 @@
lib/Doctrine/ORM/AbstractQuery.php
- lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php
+ lib/Doctrine/ORM/Mapping/ClassMetadata.php
lib/Doctrine/ORM/NativeQuery.php
lib/Doctrine/ORM/Query.php
lib/Doctrine/ORM/Query/TreeWalkerAdapter.php
diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon
index 2b58807bb0f..ef503bb60bf 100644
--- a/phpstan-baseline.neon
+++ b/phpstan-baseline.neon
@@ -131,14 +131,14 @@ parameters:
path: lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php
-
- message: "#^Method Doctrine\\\\ORM\\\\Mapping\\\\ClassMetadataInfo\\:\\:fullyQualifiedClassName\\(\\) should return class\\-string\\|null but returns string\\|null\\.$#"
+ message: "#^Method Doctrine\\\\ORM\\\\Mapping\\\\ClassMetadata\\:\\:fullyQualifiedClassName\\(\\) should return class\\-string\\|null but returns string\\|null\\.$#"
count: 1
- path: lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php
+ path: lib/Doctrine/ORM/Mapping/ClassMetadata.php
-
message: "#^Negated boolean expression is always false\\.$#"
count: 1
- path: lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php
+ path: lib/Doctrine/ORM/Mapping/ClassMetadata.php
-
message: "#^Expression on left side of \\?\\? is not nullable\\.$#"
diff --git a/psalm-baseline.xml b/psalm-baseline.xml
index 21d535dc458..a2faa413d3a 100644
--- a/psalm-baseline.xml
+++ b/psalm-baseline.xml
@@ -365,7 +365,7 @@
$parent->idGenerator
-
+
$this->columnNames
$this->columnNames
diff --git a/psalm.xml b/psalm.xml
index db04a6dd84c..f075d151188 100644
--- a/psalm.xml
+++ b/psalm.xml
@@ -82,7 +82,7 @@
-
+
@@ -152,7 +152,7 @@
-
+
diff --git a/tests/Doctrine/Tests/ORM/Mapping/MappingDriverTestCase.php b/tests/Doctrine/Tests/ORM/Mapping/MappingDriverTestCase.php
index ded61a42cde..f706c70b792 100644
--- a/tests/Doctrine/Tests/ORM/Mapping/MappingDriverTestCase.php
+++ b/tests/Doctrine/Tests/ORM/Mapping/MappingDriverTestCase.php
@@ -1585,7 +1585,7 @@ public static function loadMetadata(ClassMetadata $metadata): void
],
);
- $metadata->setIdGeneratorType(ORM\ClassMetadataInfo::GENERATOR_TYPE_NONE);
+ $metadata->setIdGeneratorType(ORM\ClassMetadata::GENERATOR_TYPE_NONE);
}
}