*/ abstract class AssociationMapping implements ArrayAccess { /** * The names of persistence operations to cascade on the association. * * @var list<'persist'|'remove'|'detach'|'refresh'|'all'> */ public array $cascade = []; /** * The fetching strategy to use for the association, usually defaults to FETCH_LAZY. * * @var ClassMetadata::FETCH_*|null */ public int|null $fetch = null; /** * This is set when the association is inherited by this class from another * (inheritance) parent entity class. The value is the FQCN of the * topmost entity class that contains this association. (If there are * transient classes in the class hierarchy, these are ignored, so the * class property may in fact come from a class further up in the PHP class * hierarchy.) To-many associations initially declared in mapped * superclasses are not considered 'inherited' in the nearest * entity subclasses. * * @var class-string|null */ public string|null $inherited = null; /** * This is set when the association does not appear in the current class * for the first time, but is initially declared in another parent * entity or mapped superclass. The value is the FQCN of the * topmost non-transient class that contains association information for * this relationship. * * @var class-string|null */ public string|null $declared = null; public array|null $cache = null; public bool|null $id = null; public bool|null $isOnDeleteCascade = null; /** @var class-string|null */ public string|null $originalClass = null; public string|null $originalField = null; public bool $orphanRemoval = false; public bool|null $unique = null; /** * @param string $fieldName The name of the field in the entity * the association is mapped to. * @param class-string $sourceEntity The class name of the source entity. * In the case of to-many associations * initially present in mapped * superclasses, the nearest * entity subclasses will be * considered the respective source * entities. * @param class-string $targetEntity 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. */ final public function __construct( public readonly string $fieldName, public string $sourceEntity, public readonly string $targetEntity, ) { } /** * @param mixed[] $mappingArray * @psalm-param array{ * fieldName: string, * sourceEntity: class-string, * targetEntity: class-string, * cascade?: list<'persist'|'remove'|'detach'|'refresh'|'all'>, * fetch?: ClassMetadata::FETCH_*|null, * inherited?: class-string|null, * declared?: class-string|null, * cache?: array|null, * id?: bool|null, * isOnDeleteCascade?: bool|null, * originalClass?: class-string|null, * originalField?: string|null, * orphanRemoval?: bool, * unique?: bool|null, * joinTable?: mixed[]|null, * type?: int, * isOwningSide: bool, * } $mappingArray */ public static function fromMappingArray(array $mappingArray): static { unset($mappingArray['isOwningSide'], $mappingArray['type']); $mapping = new static( $mappingArray['fieldName'], $mappingArray['sourceEntity'], $mappingArray['targetEntity'], ); unset($mappingArray['fieldName'], $mappingArray['sourceEntity'], $mappingArray['targetEntity']); foreach ($mappingArray as $key => $value) { if ($key === 'joinTable') { assert($mapping instanceof ManyToManyAssociationMapping); if ($value === [] || $value === null) { continue; } assert($mapping instanceof ManyToManyOwningSideMapping); $mapping->joinTable = JoinTableMapping::fromMappingArray($value); continue; } if (property_exists($mapping, $key)) { $mapping->$key = $value; } else { throw new OutOfRangeException('Unknown property ' . $key . ' on class ' . static::class); } } return $mapping; } /** * @psalm-assert-if-true OwningSideMapping $this * @psalm-assert-if-false InverseSideMapping $this */ final public function isOwningSide(): bool { return $this instanceof OwningSideMapping; } /** @psalm-assert-if-true ToOneAssociationMapping $this */ final public function isToOne(): bool { return $this instanceof ToOneAssociationMapping; } /** @psalm-assert-if-true ToManyAssociationMapping $this */ final public function isToMany(): bool { return $this instanceof ToManyAssociationMapping; } /** @psalm-assert-if-true OneToOneOwningSideMapping $this */ final public function isOneToOneOwningSide(): bool { return $this->isOneToOne() && $this->isOwningSide(); } /** @psalm-assert-if-true OneToOneOwningSideMapping|ManyToOneAssociationMapping $this */ final public function isToOneOwningSide(): bool { return $this->isToOne() && $this->isOwningSide(); } /** @psalm-assert-if-true ManyToManyOwningSideMapping $this */ final public function isManyToManyOwningSide(): bool { return $this instanceof ManyToManyOwningSideMapping; } /** @psalm-assert-if-true OneToOneAssociationMapping $this */ final public function isOneToOne(): bool { return $this instanceof OneToOneAssociationMapping; } /** @psalm-assert-if-true OneToManyAssociationMapping $this */ final public function isOneToMany(): bool { return $this instanceof OneToManyAssociationMapping; } /** @psalm-assert-if-true ManyToOneAssociationMapping $this */ final public function isManyToOne(): bool { return $this instanceof ManyToOneAssociationMapping; } /** @psalm-assert-if-true ManyToManyAssociationMapping $this */ final public function isManyToMany(): bool { return $this instanceof ManyToManyAssociationMapping; } /** @psalm-assert-if-true ToManyAssociationMapping $this */ final public function isOrdered(): bool { return $this->isToMany() && $this->orderBy() !== []; } /** @psalm-assert-if-true ToManyAssociationMapping $this */ public function isIndexed(): bool { return false; } final public function type(): int { return match (true) { $this instanceof OneToOneAssociationMapping => ClassMetadata::ONE_TO_ONE, $this instanceof OneToManyAssociationMapping => ClassMetadata::ONE_TO_MANY, $this instanceof ManyToOneAssociationMapping => ClassMetadata::MANY_TO_ONE, $this instanceof ManyToManyAssociationMapping => ClassMetadata::MANY_TO_MANY, default => throw new Exception('Cannot determine type for ' . static::class), }; } /** @param string $offset */ public function offsetExists(mixed $offset): bool { return isset($this->$offset) || in_array($offset, ['isOwningSide', 'type'], true); } final public function offsetGet(mixed $offset): mixed { return match ($offset) { 'isOwningSide' => $this->isOwningSide(), 'type' => $this->type(), 'isCascadeRemove' => $this->isCascadeRemove(), 'isCascadePersist' => $this->isCascadePersist(), 'isCascadeRefresh' => $this->isCascadeRefresh(), 'isCascadeDetach' => $this->isCascadeDetach(), default => property_exists($this, $offset) ? $this->$offset : throw new OutOfRangeException(sprintf( 'Unknown property "%s" on class %s', $offset, static::class, )), }; } public function offsetSet(mixed $offset, mixed $value): void { assert($offset !== null); if (! property_exists($this, $offset)) { throw new OutOfRangeException(sprintf( 'Unknown property "%s" on class %s', $offset, static::class, )); } if ($offset === 'joinTable') { $value = JoinTableMapping::fromMappingArray($value); } $this->$offset = $value; } /** @param string $offset */ public function offsetUnset(mixed $offset): void { if (! property_exists($this, $offset)) { throw new OutOfRangeException(sprintf( 'Unknown property "%s" on class %s', $offset, static::class, )); } $this->$offset = null; } final public function isCascadeRemove(): bool { return in_array('remove', $this->cascade, true); } final public function isCascadePersist(): bool { return in_array('persist', $this->cascade, true); } final public function isCascadeRefresh(): bool { return in_array('refresh', $this->cascade, true); } final public function isCascadeDetach(): bool { return in_array('detach', $this->cascade, true); } /** @return array */ public function toArray(): array { $array = (array) $this; $array['isOwningSide'] = $this->isOwningSide(); $array['type'] = $this->type(); return $array; } /** @return list */ public function __sleep(): array { $serialized = ['fieldName', 'sourceEntity', 'targetEntity']; if (count($this->cascade) > 0) { $serialized[] = 'cascade'; } foreach ( [ 'fetch', 'inherited', 'declared', 'cache', 'originalClass', 'originalField', ] as $stringOrArrayProperty ) { if ($this->$stringOrArrayProperty !== null) { $serialized[] = $stringOrArrayProperty; } } foreach (['id', 'orphanRemoval', 'isOnDeleteCascade', 'unique'] as $boolProperty) { if ($this->$boolProperty) { $serialized[] = $boolProperty; } } return $serialized; } }