From c2713adebc23555fab98bfcf863ba256ecbd1904 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Thu, 10 Oct 2024 14:53:34 +0200 Subject: [PATCH 01/26] property hooks. --- .../ObjectCastPropertyAccessor.php | 41 +++++++++++++++++++ .../PropertyAccessors/PropertyAccessor.php | 10 +++++ 2 files changed, 51 insertions(+) create mode 100644 src/Mapping/PropertyAccessors/ObjectCastPropertyAccessor.php create mode 100644 src/Mapping/PropertyAccessors/PropertyAccessor.php diff --git a/src/Mapping/PropertyAccessors/ObjectCastPropertyAccessor.php b/src/Mapping/PropertyAccessors/ObjectCastPropertyAccessor.php new file mode 100644 index 00000000000..6e966a817ff --- /dev/null +++ b/src/Mapping/PropertyAccessors/ObjectCastPropertyAccessor.php @@ -0,0 +1,41 @@ +isPrivate() ? "\0" . ltrim($class, '\\') . "\0" . $name : ($reflectionProperty->isProtected() ? "\0*\0" . $name : $name); + + return new self($reflectionProperty, $key); + } + + public function fromReflectionProperty(ReflectionProperty $reflectionProperty): self + { + $name = $reflectionProperty->getName(); + $key = $reflectionProperty->isPrivate() ? "\0" . ltrim($reflectionProperty->getDeclaringClass()->getName(), '\\') . "\0" . $name : ($reflectionProperty->isProtected() ? "\0*\0" . $name : $name); + + return new self($reflectionProperty, $key); + } + + private function __construct(private ReflectionProperty $reflectionProperty, private string $key) + { + } + + public function setValue(object $object, mixed $value): void + { + + } + + public function getValue(object $object): mixed + { + return ((array) $object)[$this->key] ?? null; + } +} diff --git a/src/Mapping/PropertyAccessors/PropertyAccessor.php b/src/Mapping/PropertyAccessors/PropertyAccessor.php new file mode 100644 index 00000000000..503b5bbe249 --- /dev/null +++ b/src/Mapping/PropertyAccessors/PropertyAccessor.php @@ -0,0 +1,10 @@ + Date: Thu, 10 Oct 2024 22:21:19 +0200 Subject: [PATCH 02/26] Add all necessary accessors, adapting doctrine/persistence and ORM internal reflection properties. no tests. --- .../PropertyAccessors/AccessorFactory.php | 24 +++++++ .../EmbeddablePropertyAccessor.php | 43 ++++++++++++ .../EnumPropertyAccessor.php | 66 +++++++++++++++++++ .../ObjectCastPropertyAccessor.php | 15 ++++- .../PropertyAccessors/ReadonlyAccessor.php | 45 +++++++++++++ .../TypedNoDefaultPropertyAccessor.php | 61 +++++++++++++++++ 6 files changed, 252 insertions(+), 2 deletions(-) create mode 100644 src/Mapping/PropertyAccessors/AccessorFactory.php create mode 100644 src/Mapping/PropertyAccessors/EmbeddablePropertyAccessor.php create mode 100644 src/Mapping/PropertyAccessors/EnumPropertyAccessor.php create mode 100644 src/Mapping/PropertyAccessors/ReadonlyAccessor.php create mode 100644 src/Mapping/PropertyAccessors/TypedNoDefaultPropertyAccessor.php diff --git a/src/Mapping/PropertyAccessors/AccessorFactory.php b/src/Mapping/PropertyAccessors/AccessorFactory.php new file mode 100644 index 00000000000..e5ea61127aa --- /dev/null +++ b/src/Mapping/PropertyAccessors/AccessorFactory.php @@ -0,0 +1,24 @@ +getProperty($propertyName); + $accessor = ObjectCastPropertyAccessor::fromReflectionProperty($reflectionProperty); + + if ($reflectionProperty->hasType() && ! $reflectionProperty->getType()->allowsNull()) { + $accessor = new TypedNoDefaultPropertyAccessor($accessor, $reflectionProperty); + } + + if ($reflectionProperty->isReadOnly()) { + $accessor = new ReadonlyAccessor($accessor, $reflectionProperty); + } + + return $accessor; + } +} diff --git a/src/Mapping/PropertyAccessors/EmbeddablePropertyAccessor.php b/src/Mapping/PropertyAccessors/EmbeddablePropertyAccessor.php new file mode 100644 index 00000000000..87f7aa8f416 --- /dev/null +++ b/src/Mapping/PropertyAccessors/EmbeddablePropertyAccessor.php @@ -0,0 +1,43 @@ +parent->getValue($object); + + if ($embeddedObject === null) { + self::$instantiator ??= new Instantiator(); + + $embeddedObject = self::$instantiator->instantiate($this->embeddedClass); + + $this->parent->setValue($object, $embeddedObject); + } + + $this->child->setValue($embeddedObject, $value); + } + + public function getValue(object $object): mixed + { + $embeddedObject = $this->parent->getValue($object); + + if ($embeddedObject === null) { + return null; + } + + return $this->child->getValue($embeddedObject); + } +} diff --git a/src/Mapping/PropertyAccessors/EnumPropertyAccessor.php b/src/Mapping/PropertyAccessors/EnumPropertyAccessor.php new file mode 100644 index 00000000000..04286062ace --- /dev/null +++ b/src/Mapping/PropertyAccessors/EnumPropertyAccessor.php @@ -0,0 +1,66 @@ +toEnum($value); + } + + $this->parent->setValue($object, $value); + } + + public function getValue(object $object): mixed + { + $enum = $this->parent->getValue($object); + + if ($enum === null) { + return null; + } + + return $this->fromEnum($enum); + } + + private function fromEnum($enum) + { + if (is_array($enum)) { + return array_map(static function (BackedEnum $enum) { + return $enum->value; + }, $enum); + } + + return $enum->value; + } + + /** + * @param int|string|int[]|string[]|BackedEnum|BackedEnum[] $value + * + * @return ($value is int|string|BackedEnum ? BackedEnum : BackedEnum[]) + */ + private function toEnum($value) + { + if ($value instanceof BackedEnum) { + return $value; + } + + if (is_array($value)) { + $v = reset($value); + if ($v instanceof BackedEnum) { + return $value; + } + + return array_map([$this->enumType, 'from'], $value); + } + + return $this->enumType::from($value); + } +} diff --git a/src/Mapping/PropertyAccessors/ObjectCastPropertyAccessor.php b/src/Mapping/PropertyAccessors/ObjectCastPropertyAccessor.php index 6e966a817ff..508164ff04a 100644 --- a/src/Mapping/PropertyAccessors/ObjectCastPropertyAccessor.php +++ b/src/Mapping/PropertyAccessors/ObjectCastPropertyAccessor.php @@ -2,13 +2,14 @@ namespace Doctrine\ORM\Mapping\PropertyAccessors; +use Doctrine\ORM\Proxy\InternalProxy; use ReflectionProperty; use function ltrim; class ObjectCastPropertyAccessor implements PropertyAccessor { - public function fromNames(string $class, string $name) + public static function fromNames(string $class, string $name): self { $reflectionProperty = new ReflectionProperty($class, $name); @@ -17,7 +18,7 @@ public function fromNames(string $class, string $name) return new self($reflectionProperty, $key); } - public function fromReflectionProperty(ReflectionProperty $reflectionProperty): self + public static function fromReflectionProperty(ReflectionProperty $reflectionProperty): self { $name = $reflectionProperty->getName(); $key = $reflectionProperty->isPrivate() ? "\0" . ltrim($reflectionProperty->getDeclaringClass()->getName(), '\\') . "\0" . $name : ($reflectionProperty->isProtected() ? "\0*\0" . $name : $name); @@ -31,7 +32,17 @@ private function __construct(private ReflectionProperty $reflectionProperty, pri public function setValue(object $object, mixed $value): void { + if (! ($object instanceof InternalProxy && ! $object->__isInitialized())) { + $this->reflectionProperty->setValue($object, $value); + return; + } + + $object->__setInitialized(true); + + $this->reflectionProperty->setValue($object, $value); + + $object->__setInitialized(false); } public function getValue(object $object): mixed diff --git a/src/Mapping/PropertyAccessors/ReadonlyAccessor.php b/src/Mapping/PropertyAccessors/ReadonlyAccessor.php new file mode 100644 index 00000000000..944fa6330e6 --- /dev/null +++ b/src/Mapping/PropertyAccessors/ReadonlyAccessor.php @@ -0,0 +1,45 @@ +reflectionProperty->isReadOnly()) { + throw new InvalidArgumentException(sprintf( + '%s::$%s must be readonly property', + $this->reflectionProperty->getDeclaringClass()->getName(), + $this->reflectionProperty->getName(), + )); + } + } + + public function setValue(object $object, mixed $value): void + { + if (! $this->reflectionProperty->isInitialized($object)) { + $this->parent->setValue($object, $value); + + return; + } + + if ($this->parent->getValue($object) !== $value) { + throw new LogicException(sprintf( + 'Attempting to change readonly property %s::$%s.', + $this->reflectionProperty->getDeclaringClass()->getName(), + $this->reflectionProperty->getName(), + )); + } + } + + public function getValue(object $object): mixed + { + return $this->parent->getValue($object); + } +} diff --git a/src/Mapping/PropertyAccessors/TypedNoDefaultPropertyAccessor.php b/src/Mapping/PropertyAccessors/TypedNoDefaultPropertyAccessor.php new file mode 100644 index 00000000000..b3a4ed4ce0b --- /dev/null +++ b/src/Mapping/PropertyAccessors/TypedNoDefaultPropertyAccessor.php @@ -0,0 +1,61 @@ +reflectionProperty->hasType()) { + throw new InvalidArgumentException(sprintf( + '%s::$%s must have a type when used with TypedNoDefaultPropertyAccessor', + $this->reflectionProperty->getDeclaringClass()->getName(), + $this->reflectionProperty->getName(), + )); + } + + if (! $this->reflectionProperty->getType()->allowsNull()) { + throw new InvalidArgumentException(sprintf( + '%s::$%s must not be nullable when used with TypedNoDefaultPropertyAccessor', + $this->reflectionProperty->getDeclaringClass()->getName(), + $this->reflectionProperty->getName(), + )); + } + } + + public function setValue(object $object, mixed $value): void + { + if ($value === null) { + if ($this->unsetter === null) { + $propertyName = $this->reflectionProperty->getName(); + $this->unsetter = function () use ($propertyName): void { + unset($this->$propertyName); + }; + } + + $unsetter = $this->unsetter->bindTo($object, $this->reflectionProperty->getDeclaringClass()->getName()); + + assert($unsetter instanceof Closure); + + $unsetter(); + + return; + } + + $this->parent->setValue($object, $value); + } + + public function getValue(object $object): mixed + { + return $this->reflectionProperty->isInitialized($object) ? $this->parent->getValue($object) : null; + } +} From b3cffe2d1268d0f8b4eee6be674aa64536581921 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Thu, 10 Oct 2024 23:31:48 +0200 Subject: [PATCH 03/26] Introduce LegacyReflectionFields abstraction, deriving from propertyAccessors at runtime. --- src/Mapping/ClassMetadata.php | 93 ++++++++------- src/Mapping/LegacyReflectionFields.php | 106 ++++++++++++++++++ .../PropertyAccessors/AccessorFactory.php | 24 ---- .../TypedNoDefaultPropertyAccessor.php | 2 +- src/Proxy/ProxyFactory.php | 6 +- 5 files changed, 162 insertions(+), 69 deletions(-) create mode 100644 src/Mapping/LegacyReflectionFields.php delete mode 100644 src/Mapping/PropertyAccessors/AccessorFactory.php diff --git a/src/Mapping/ClassMetadata.php b/src/Mapping/ClassMetadata.php index 7351d09bce0..85dcd69900b 100644 --- a/src/Mapping/ClassMetadata.php +++ b/src/Mapping/ClassMetadata.php @@ -14,6 +14,13 @@ use Doctrine\ORM\Cache\Exception\NonCacheableEntityAssociation; use Doctrine\ORM\EntityRepository; use Doctrine\ORM\Id\AbstractIdGenerator; +use Doctrine\ORM\Mapping\PropertyAccessors\AccessorFactory; +use Doctrine\ORM\Mapping\PropertyAccessors\EmbeddablePropertyAccessor; +use Doctrine\ORM\Mapping\PropertyAccessors\EnumPropertyAccessor; +use Doctrine\ORM\Mapping\PropertyAccessors\ObjectCastPropertyAccessor; +use Doctrine\ORM\Mapping\PropertyAccessors\PropertyAccessor; +use Doctrine\ORM\Mapping\PropertyAccessors\ReadonlyAccessor; +use Doctrine\ORM\Mapping\PropertyAccessors\TypedNoDefaultPropertyAccessor; use Doctrine\Persistence\Mapping\ClassMetadata as PersistenceClassMetadata; use Doctrine\Persistence\Mapping\ReflectionService; use Doctrine\Persistence\Reflection\EnumReflectionProperty; @@ -541,9 +548,12 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable /** * The ReflectionProperty instances of the mapped class. * - * @var array + * @var LegacyReflectionFields|array */ - public array $reflFields = []; + public LegacyReflectionFields|array $reflFields = []; + + /** @var array */ + public array $propertyAccessors = []; private InstantiatorInterface|null $instantiator = null; @@ -570,7 +580,7 @@ public function __construct(public string $name, NamingStrategy|null $namingStra * @return ReflectionProperty[]|null[] An array of ReflectionProperty instances. * @phpstan-return array */ - public function getReflectionProperties(): array + public function getReflectionProperties(): array|LegacyReflectionFields { return $this->reflFields; } @@ -792,76 +802,74 @@ public function wakeupReflection(ReflectionService $reflService): void { // Restore ReflectionClass and properties $this->reflClass = $reflService->getClass($this->name); + $this->reflFields = new LegacyReflectionFields($this, $reflService); $this->instantiator = $this->instantiator ?: new Instantiator(); - $parentReflFields = []; + $parentAccessors = []; foreach ($this->embeddedClasses as $property => $embeddedClass) { if (isset($embeddedClass->declaredField)) { assert($embeddedClass->originalField !== null); - $childProperty = $this->getAccessibleProperty( - $reflService, + $childAccessor = $this->createPropertyAccessor( $this->embeddedClasses[$embeddedClass->declaredField]->class, $embeddedClass->originalField, ); - assert($childProperty !== null); - $parentReflFields[$property] = new ReflectionEmbeddedProperty( - $parentReflFields[$embeddedClass->declaredField], - $childProperty, + + $parentAccessors[$property] = new EmbeddablePropertyAccessor( + $parentAccessors[$embeddedClass->declaredField], + $childAccessor, $this->embeddedClasses[$embeddedClass->declaredField]->class, ); continue; } - $fieldRefl = $this->getAccessibleProperty( - $reflService, + $accessor = $this->createPropertyAccessor( $embeddedClass->declared ?? $this->name, $property, ); - $parentReflFields[$property] = $fieldRefl; - $this->reflFields[$property] = $fieldRefl; + $parentAccessors[$property] = $accessor; + $this->propertyAccessors[$property] = $accessor; } foreach ($this->fieldMappings as $field => $mapping) { - if (isset($mapping->declaredField) && isset($parentReflFields[$mapping->declaredField])) { + if (isset($mapping->declaredField) && isset($parentAccessors[$mapping->declaredField])) { assert($mapping->originalField !== null); assert($mapping->originalClass !== null); - $childProperty = $this->getAccessibleProperty($reflService, $mapping->originalClass, $mapping->originalField); - assert($childProperty !== null); + $accessor = $this->createPropertyAccessor($mapping->originalClass, $mapping->originalField); - if (isset($mapping->enumType)) { - $childProperty = new EnumReflectionProperty( - $childProperty, + if ($mapping->enumType !== null) { + $accessor = new EnumPropertyAccessor( + $accessor, $mapping->enumType, ); } - $this->reflFields[$field] = new ReflectionEmbeddedProperty( - $parentReflFields[$mapping->declaredField], - $childProperty, + $this->propertyAccessors[$field] = new EmbeddablePropertyAccessor( + $parentAccessors[$mapping->declaredField], + $accessor, $mapping->originalClass, ); continue; } - $this->reflFields[$field] = isset($mapping->declared) - ? $this->getAccessibleProperty($reflService, $mapping->declared, $field) - : $this->getAccessibleProperty($reflService, $this->name, $field); + $this->propertyAccessors[$field] = isset($mapping->declared) + ? $this->createPropertyAccessor($mapping->declared, $field) + : $this->createPropertyAccessor($this->name, $field); - if (isset($mapping->enumType) && $this->reflFields[$field] !== null) { - $this->reflFields[$field] = new EnumReflectionProperty( - $this->reflFields[$field], + if ($mapping->enumType !== null) { + $this->propertyAccessors[$field] = new EnumPropertyAccessor( + $this->propertyAccessors[$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); + $this->propertyAccessors[$field] = isset($mapping->declared) + ? $this->createPropertyAccessor($mapping->declared, $field) + : $this->createPropertyAccessor($this->name, $field); } } @@ -2659,20 +2667,19 @@ public function getSequencePrefix(AbstractPlatform $platform): string } /** @phpstan-param class-string $class */ - private function getAccessibleProperty(ReflectionService $reflService, string $class, string $field): ReflectionProperty|null + private function createPropertyAccessor(string $className, string $propertyName): PropertyAccessor { - $reflectionProperty = $reflService->getAccessibleProperty($class, $field); - if ($reflectionProperty?->isReadOnly()) { - $declaringClass = $reflectionProperty->class; - if ($declaringClass !== $class) { - $reflectionProperty = $reflService->getAccessibleProperty($declaringClass, $field); - } + $reflectionProperty = new ReflectionProperty($className, $propertyName); + $accessor = ObjectCastPropertyAccessor::fromReflectionProperty($reflectionProperty); - if ($reflectionProperty !== null) { - $reflectionProperty = new ReflectionReadonlyProperty($reflectionProperty); - } + if ($reflectionProperty->hasType() && ! $reflectionProperty->getType()->allowsNull()) { + $accessor = new TypedNoDefaultPropertyAccessor($accessor, $reflectionProperty); + } + + if ($reflectionProperty->isReadOnly()) { + $accessor = new ReadonlyAccessor($accessor, $reflectionProperty); } - return $reflectionProperty; + return $accessor; } } diff --git a/src/Mapping/LegacyReflectionFields.php b/src/Mapping/LegacyReflectionFields.php new file mode 100644 index 00000000000..f81f7bd06fd --- /dev/null +++ b/src/Mapping/LegacyReflectionFields.php @@ -0,0 +1,106 @@ +classMetadata->propertyAccessors[$offset]); + } + + public function offsetGet($field): mixed + { + if (isset($this->reflFields[$field])) { + return $this->reflFields[$field]; + } + + if (isset($this->classMetadata->propertyAccessors[$field])) { + $fieldName = str_contains('.', $field) ? $this->classMetadata->fieldMappings[$field]->originalField : $field; + $className = $this->classMetadata->name; + + if (isset($this->classMetadata->fieldMappings[$field]) && $this->classMetadata->fieldMappings[$field]->originalClass !== null) { + $className = $this->classMetadata->fieldMappings[$field]->originalClass; + } elseif (isset($this->classMetadata->fieldMappings[$field]) && $this->classMetadata->fieldMappings[$field]->declared !== null) { + $className = $this->classMetadata->fieldMappings[$field]->declared; + } elseif (isset($this->classMetadata->associationMappings[$field]) && $this->classMetadata->associationMappings[$field]->declared !== null) { + $className = $this->classMetadata->associationMappings[$field]->declared; + } elseif (isset($this->classMetadata->embeddedClasses[$field]) && $this->classMetadata->embeddedClasses[$field]->declared !== null) { + $className = $this->classMetadata->embeddedClasses[$field]->declared; + } + + $this->reflFields[$field] = $this->getAccessibleProperty($className, $fieldName); + + if (isset($this->classMetadata->fieldMappings[$field])) { + if ($this->classMetadata->fieldMappings[$field]->enumType !== null) { + $this->reflFields[$field] = new EnumReflectionProperty( + $this->reflFields[$field], + $this->classMetadata->fieldMappings[$field]->enumType, + ); + } + + if ($this->classMetadata->fieldMappings[$field]->originalField !== null) { + $this->reflFields[$field] = new ReflectionEmbeddedProperty( + $this->reflFields[$field], + $this->getAccessibleProperty($this->classMetadata->fieldMappings[$field]->originalClass, $this->classMetadata->fieldMappings[$field]->originalField), + $this->classMetadata->embeddedClasses[$fieldName]->class, + ); + } + } + + return $this->reflFields[$field]; + } + + throw new \OutOfBoundsException('Unknown field: ' . $this->classMetadata->name .' ::$' . $field); + } + + public function offsetSet($offset, $value): void + { + $this->reflFields[$offset] = $value; + } + + public function offsetUnset($offset): void + { + unset($this->reflFields[$offset]); + } + + /** @psalm-param class-string $class */ + private function getAccessibleProperty(string $class, string $field): ReflectionProperty|null + { + $reflectionProperty = $this->reflectionService->getAccessibleProperty($class, $field); + if ($reflectionProperty?->isReadOnly()) { + $declaringClass = $reflectionProperty->class; + if ($declaringClass !== $class) { + $reflectionProperty = $this->reflectionService->getAccessibleProperty($declaringClass, $field); + } + + if ($reflectionProperty !== null) { + $reflectionProperty = new ReflectionReadonlyProperty($reflectionProperty); + } + } + + return $reflectionProperty; + } + + public function getIterator(): Traversable + { + $keys = array_keys($this->classMetadata->propertyAccessors); + + foreach ($keys as $key) { + yield $key => $this->offsetGet($key); + } + } +} diff --git a/src/Mapping/PropertyAccessors/AccessorFactory.php b/src/Mapping/PropertyAccessors/AccessorFactory.php deleted file mode 100644 index e5ea61127aa..00000000000 --- a/src/Mapping/PropertyAccessors/AccessorFactory.php +++ /dev/null @@ -1,24 +0,0 @@ -getProperty($propertyName); - $accessor = ObjectCastPropertyAccessor::fromReflectionProperty($reflectionProperty); - - if ($reflectionProperty->hasType() && ! $reflectionProperty->getType()->allowsNull()) { - $accessor = new TypedNoDefaultPropertyAccessor($accessor, $reflectionProperty); - } - - if ($reflectionProperty->isReadOnly()) { - $accessor = new ReadonlyAccessor($accessor, $reflectionProperty); - } - - return $accessor; - } -} diff --git a/src/Mapping/PropertyAccessors/TypedNoDefaultPropertyAccessor.php b/src/Mapping/PropertyAccessors/TypedNoDefaultPropertyAccessor.php index b3a4ed4ce0b..351cd975b00 100644 --- a/src/Mapping/PropertyAccessors/TypedNoDefaultPropertyAccessor.php +++ b/src/Mapping/PropertyAccessors/TypedNoDefaultPropertyAccessor.php @@ -23,7 +23,7 @@ public function __construct(private PropertyAccessor $parent, private Reflection )); } - if (! $this->reflectionProperty->getType()->allowsNull()) { + if ($this->reflectionProperty->getType()->allowsNull()) { throw new InvalidArgumentException(sprintf( '%s::$%s must not be nullable when used with TypedNoDefaultPropertyAccessor', $this->reflectionProperty->getDeclaringClass()->getName(), diff --git a/src/Proxy/ProxyFactory.php b/src/Proxy/ProxyFactory.php index b2d114a6698..40fe4814975 100644 --- a/src/Proxy/ProxyFactory.php +++ b/src/Proxy/ProxyFactory.php @@ -279,7 +279,11 @@ private function getProxyFactory(string $className): Closure $entityPersister = $this->uow->getEntityPersister($className); $initializer = $this->createLazyInitializer($class, $entityPersister, $this->identifierFlattener); $proxyClassName = $this->loadProxyClass($class); - $identifierFields = array_intersect_key($class->getReflectionProperties(), $identifiers); + $identifierFields = []; + + foreach ($identifiers as $identifier => $_) { + $identifierFields[$identifier] = $class->getReflectionProperty($identifier); + } $proxyFactory = Closure::bind(static function (array $identifier) use ($initializer, $skippedProperties, $identifierFields, $className): InternalProxy { $proxy = self::createLazyGhost(static function (InternalProxy $object) use ($initializer, $identifier): void { From 7d61a1e73fe30d45182581c7e5f1813c6df37c3d Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Fri, 11 Oct 2024 00:18:33 +0200 Subject: [PATCH 04/26] Fixes in LegacyReflectionFields. --- src/Mapping/LegacyReflectionFields.php | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/Mapping/LegacyReflectionFields.php b/src/Mapping/LegacyReflectionFields.php index f81f7bd06fd..1442168e2d8 100644 --- a/src/Mapping/LegacyReflectionFields.php +++ b/src/Mapping/LegacyReflectionFields.php @@ -29,7 +29,7 @@ public function offsetGet($field): mixed } if (isset($this->classMetadata->propertyAccessors[$field])) { - $fieldName = str_contains('.', $field) ? $this->classMetadata->fieldMappings[$field]->originalField : $field; + $fieldName = str_contains($field, '.') ? $this->classMetadata->fieldMappings[$field]->originalField : $field; $className = $this->classMetadata->name; if (isset($this->classMetadata->fieldMappings[$field]) && $this->classMetadata->fieldMappings[$field]->originalClass !== null) { @@ -53,10 +53,18 @@ public function offsetGet($field): mixed } if ($this->classMetadata->fieldMappings[$field]->originalField !== null) { + $parentField = str_replace('.' . $fieldName, '', $field); + + if (!str_contains($parentField, '.')) { + $parentClass = $this->classMetadata->name; + } else { + $parentClass = $this->classMetadata->fieldMappings[$parentField]->originalClass; + } + $this->reflFields[$field] = new ReflectionEmbeddedProperty( + $this->getAccessibleProperty($parentClass, $parentField), $this->reflFields[$field], - $this->getAccessibleProperty($this->classMetadata->fieldMappings[$field]->originalClass, $this->classMetadata->fieldMappings[$field]->originalField), - $this->classMetadata->embeddedClasses[$fieldName]->class, + $this->classMetadata->fieldMappings[$field]->originalClass, ); } } From fcc53b260f4394a9fff0054bfec5f28b3fa21ede Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Fri, 11 Oct 2024 00:39:19 +0200 Subject: [PATCH 05/26] Use ClassMetadata::$propertyAccessors in all places. --- src/Internal/Hydration/ObjectHydrator.php | 10 ++-- src/Mapping/ClassMetadata.php | 20 ++++---- src/Mapping/ClassMetadataFactory.php | 4 +- .../EnumPropertyAccessor.php | 2 +- src/PersistentCollection.php | 4 +- .../Entity/BasicEntityPersister.php | 16 +++---- .../Entity/JoinedSubclassPersister.php | 2 +- src/Proxy/ProxyFactory.php | 6 +-- src/UnitOfWork.php | 46 +++++++++---------- tests/Tests/ORM/Functional/EnumTest.php | 2 +- .../ORM/Functional/Ticket/DDC168Test.php | 2 +- .../Tests/ORM/Functional/ValueObjectsTest.php | 24 ---------- .../Mapping/ClassMetadataLoadEventTest.php | 5 +- tests/Tests/ORM/Mapping/ClassMetadataTest.php | 6 +-- 14 files changed, 63 insertions(+), 86 deletions(-) diff --git a/src/Internal/Hydration/ObjectHydrator.php b/src/Internal/Hydration/ObjectHydrator.php index 21383e8c16e..9b0abb87ed9 100644 --- a/src/Internal/Hydration/ObjectHydrator.php +++ b/src/Internal/Hydration/ObjectHydrator.php @@ -171,7 +171,7 @@ private function initRelatedCollection( ): PersistentCollection { $oid = spl_object_id($entity); $relation = $class->associationMappings[$fieldName]; - $value = $class->reflFields[$fieldName]->getValue($entity); + $value = $class->propertyAccessors[$fieldName]->getValue($entity); if ($value === null || is_array($value)) { $value = new ArrayCollection((array) $value); @@ -186,7 +186,7 @@ private function initRelatedCollection( ); $value->setOwner($entity, $relation); - $class->reflFields[$fieldName]->setValue($entity, $value); + $class->propertyAccessors[$fieldName]->setValue($entity, $value); $this->uow->setOriginalEntityProperty($oid, $fieldName, $value); $this->initializedCollections[$oid . $fieldName] = $value; @@ -346,7 +346,7 @@ protected function hydrateRowData(array $row, array &$result): void $parentClass = $this->metadataCache[$this->resultSetMapping()->aliasMap[$parentAlias]]; $relationField = $this->resultSetMapping()->relationMap[$dqlAlias]; $relation = $parentClass->associationMappings[$relationField]; - $reflField = $parentClass->reflFields[$relationField]; + $reflField = $parentClass->propertyAccessors[$relationField]; // Get a reference to the parent object to which the joined element belongs. if ($this->resultSetMapping()->isMixed && isset($this->rootAliases[$parentAlias])) { @@ -446,13 +446,13 @@ protected function hydrateRowData(array $row, array &$result): void if ($relation->inversedBy !== null) { $inverseAssoc = $targetClass->associationMappings[$relation->inversedBy]; if ($inverseAssoc->isToOne()) { - $targetClass->reflFields[$inverseAssoc->fieldName]->setValue($element, $parentObject); + $targetClass->propertyAccessors[$inverseAssoc->fieldName]->setValue($element, $parentObject); $this->uow->setOriginalEntityProperty(spl_object_id($element), $inverseAssoc->fieldName, $parentObject); } } } else { // For sure bidirectional, as there is no inverse side in unidirectional mappings - $targetClass->reflFields[$relation->mappedBy]->setValue($element, $parentObject); + $targetClass->propertyAccessors[$relation->mappedBy]->setValue($element, $parentObject); $this->uow->setOriginalEntityProperty(spl_object_id($element), $relation->mappedBy, $parentObject); } diff --git a/src/Mapping/ClassMetadata.php b/src/Mapping/ClassMetadata.php index 85dcd69900b..d14e964457d 100644 --- a/src/Mapping/ClassMetadata.php +++ b/src/Mapping/ClassMetadata.php @@ -580,17 +580,17 @@ public function __construct(public string $name, NamingStrategy|null $namingStra * @return ReflectionProperty[]|null[] An array of ReflectionProperty instances. * @phpstan-return array */ - public function getReflectionProperties(): array|LegacyReflectionFields + public function getPropertyAccessors(): array { - return $this->reflFields; + return $this->propertyAccessors; } /** * Gets a ReflectionProperty for a specific field of the mapped class. */ - public function getReflectionProperty(string $name): ReflectionProperty|null + public function getPropertyAccessor(string $name): PropertyAccessor|null { - return $this->reflFields[$name]; + return $this->propertyAccessors[$name]; } /** @@ -604,7 +604,7 @@ public function getSingleIdReflectionProperty(): ReflectionProperty|null throw new BadMethodCallException('Class ' . $this->name . ' has a composite identifier.'); } - return $this->reflFields[$this->identifier[0]]; + return $this->propertyAccessors[$this->identifier[0]]; } /** @@ -621,7 +621,7 @@ public function getIdentifierValues(object $entity): array $id = []; foreach ($this->identifier as $idField) { - $value = $this->reflFields[$idField]->getValue($entity); + $value = $this->propertyAccessors[$idField]->getValue($entity); if ($value !== null) { $id[$idField] = $value; @@ -632,7 +632,7 @@ public function getIdentifierValues(object $entity): array } $id = $this->identifier[0]; - $value = $this->reflFields[$id]->getValue($entity); + $value = $this->propertyAccessors[$id]->getValue($entity); if ($value === null) { return []; @@ -651,7 +651,7 @@ public function getIdentifierValues(object $entity): array public function setIdentifierValues(object $entity, array $id): void { foreach ($id as $idField => $idValue) { - $this->reflFields[$idField]->setValue($entity, $idValue); + $this->propertyAccessors[$idField]->setValue($entity, $idValue); } } @@ -660,7 +660,7 @@ public function setIdentifierValues(object $entity, array $id): void */ public function setFieldValue(object $entity, string $field, mixed $value): void { - $this->reflFields[$field]->setValue($entity, $value); + $this->propertyAccessors[$field]->setValue($entity, $value); } /** @@ -668,7 +668,7 @@ public function setFieldValue(object $entity, string $field, mixed $value): void */ public function getFieldValue(object $entity, string $field): mixed { - return $this->reflFields[$field]->getValue($entity); + return $this->propertyAccessors[$field]->getValue($entity); } /** diff --git a/src/Mapping/ClassMetadataFactory.php b/src/Mapping/ClassMetadataFactory.php index b29f20c67b1..f66b53ff2ef 100644 --- a/src/Mapping/ClassMetadataFactory.php +++ b/src/Mapping/ClassMetadataFactory.php @@ -440,8 +440,8 @@ private function addInheritedFields(ClassMetadata $subClass, ClassMetadata $pare $subClass->addInheritedFieldMapping($subClassMapping); } - foreach ($parentClass->reflFields as $name => $field) { - $subClass->reflFields[$name] = $field; + foreach ($parentClass->propertyAccessors as $name => $field) { + $subClass->propertyAccessors[$name] = $field; } } diff --git a/src/Mapping/PropertyAccessors/EnumPropertyAccessor.php b/src/Mapping/PropertyAccessors/EnumPropertyAccessor.php index 04286062ace..9682b6069f5 100644 --- a/src/Mapping/PropertyAccessors/EnumPropertyAccessor.php +++ b/src/Mapping/PropertyAccessors/EnumPropertyAccessor.php @@ -2,7 +2,7 @@ namespace Doctrine\ORM\Mapping\PropertyAccessors; -use ReflectionProperty; +use BackedEnum; class EnumPropertyAccessor implements PropertyAccessor { diff --git a/src/PersistentCollection.php b/src/PersistentCollection.php index e83e246d7de..0a0a23750ab 100644 --- a/src/PersistentCollection.php +++ b/src/PersistentCollection.php @@ -140,7 +140,7 @@ public function hydrateAdd(mixed $element): void if ($this->backRefFieldName && $this->getMapping()->isOneToMany()) { assert($this->typeClass !== null); // Set back reference to owner - $this->typeClass->reflFields[$this->backRefFieldName]->setValue( + $this->typeClass->propertyAccessors[$this->backRefFieldName]->setValue( $element, $this->owner, ); @@ -166,7 +166,7 @@ public function hydrateSet(mixed $key, mixed $element): void if ($this->backRefFieldName && $this->getMapping()->isOneToMany()) { assert($this->typeClass !== null); // Set back reference to owner - $this->typeClass->reflFields[$this->backRefFieldName]->setValue( + $this->typeClass->propertyAccessors[$this->backRefFieldName]->setValue( $element, $this->owner, ); diff --git a/src/Persisters/Entity/BasicEntityPersister.php b/src/Persisters/Entity/BasicEntityPersister.php index 91b2eaa8832..5ac67d5e566 100644 --- a/src/Persisters/Entity/BasicEntityPersister.php +++ b/src/Persisters/Entity/BasicEntityPersister.php @@ -470,7 +470,7 @@ final protected function updateTable( $where[] = $versionColumn; $types[] = $this->class->fieldMappings[$versionField]->type; - $params[] = $this->class->reflFields[$versionField]->getValue($entity); + $params[] = $this->class->propertyAccessors[$versionField]->getValue($entity); switch ($versionFieldType) { case Types::SMALLINT: @@ -781,7 +781,7 @@ public function loadOneToOneEntity(AssociationMapping $assoc, object $sourceEnti // Complete bidirectional association, if necessary if ($targetEntity !== null && $isInverseSingleValued) { - $targetClass->reflFields[$assoc->inversedBy]->setValue($targetEntity, $sourceEntity); + $targetClass->propertyAccessors[$assoc->inversedBy]->setValue($targetEntity, $sourceEntity); } return $targetEntity; @@ -828,7 +828,7 @@ public function loadOneToOneEntity(AssociationMapping $assoc, object $sourceEnti } } else { $computedIdentifier[$targetClass->getFieldForColumn($targetKeyColumn)] = - $sourceClass->reflFields[$sourceClass->fieldNames[$sourceKeyColumn]]->getValue($sourceEntity); + $sourceClass->propertyAccessors[$sourceClass->fieldNames[$sourceKeyColumn]]->getValue($sourceEntity); } } @@ -1045,7 +1045,7 @@ private function getManyToManyStatement( switch (true) { case $sourceClass->containsForeignIdentifier: $field = $sourceClass->getFieldForColumn($sourceKeyColumn); - $value = $sourceClass->reflFields[$field]->getValue($sourceEntity); + $value = $sourceClass->propertyAccessors[$field]->getValue($sourceEntity); if (isset($sourceClass->associationMappings[$field])) { $value = $this->em->getUnitOfWork()->getEntityIdentifier($value); @@ -1056,7 +1056,7 @@ private function getManyToManyStatement( case isset($sourceClass->fieldNames[$sourceKeyColumn]): $field = $sourceClass->fieldNames[$sourceKeyColumn]; - $value = $sourceClass->reflFields[$field]->getValue($sourceEntity); + $value = $sourceClass->propertyAccessors[$field]->getValue($sourceEntity); break; @@ -1454,7 +1454,7 @@ protected function getInsertColumnList(): array { $columns = []; - foreach ($this->class->reflFields as $name => $field) { + foreach ($this->class->propertyAccessors as $name => $field) { if ($this->class->isVersioned && $this->class->versionField === $name) { continue; } @@ -1800,7 +1800,7 @@ private function getOneToManyStatement( foreach ($owningAssoc->targetToSourceKeyColumns as $sourceKeyColumn => $targetKeyColumn) { if ($sourceClass->containsForeignIdentifier) { $field = $sourceClass->getFieldForColumn($sourceKeyColumn); - $value = $sourceClass->reflFields[$field]->getValue($sourceEntity); + $value = $sourceClass->propertyAccessors[$field]->getValue($sourceEntity); if (isset($sourceClass->associationMappings[$field])) { $value = $this->em->getUnitOfWork()->getEntityIdentifier($value); @@ -1818,7 +1818,7 @@ private function getOneToManyStatement( } $field = $sourceClass->fieldNames[$sourceKeyColumn]; - $value = $sourceClass->reflFields[$field]->getValue($sourceEntity); + $value = $sourceClass->propertyAccessors[$field]->getValue($sourceEntity); $criteria[$tableAlias . '.' . $targetKeyColumn] = $value; $parameters[] = [ diff --git a/src/Persisters/Entity/JoinedSubclassPersister.php b/src/Persisters/Entity/JoinedSubclassPersister.php index 67c277be811..b7511c20cb8 100644 --- a/src/Persisters/Entity/JoinedSubclassPersister.php +++ b/src/Persisters/Entity/JoinedSubclassPersister.php @@ -459,7 +459,7 @@ protected function getInsertColumnList(): array ? $this->class->getIdentifierColumnNames() : []; - foreach ($this->class->reflFields as $name => $field) { + foreach ($this->class->propertyAccessors as $name => $field) { if ( isset($this->class->fieldMappings[$name]->inherited) && ! isset($this->class->fieldMappings[$name]->id) diff --git a/src/Proxy/ProxyFactory.php b/src/Proxy/ProxyFactory.php index 40fe4814975..e460217667b 100644 --- a/src/Proxy/ProxyFactory.php +++ b/src/Proxy/ProxyFactory.php @@ -232,8 +232,8 @@ private function createLazyInitializer(ClassMetadata $classMetadata, EntityPersi $class = $entityPersister->getClassMetadata(); - foreach ($class->getReflectionProperties() as $property) { - if (! $property || isset($identifier[$property->getName()]) || ! $class->hasField($property->getName()) && ! $class->hasAssociation($property->getName())) { + foreach ($class->getPropertyAccessors() as $name => $property) { + if (! $property || isset($identifier[$name]) || ! $class->hasField($name) && ! $class->hasAssociation($name)) { continue; } @@ -282,7 +282,7 @@ private function getProxyFactory(string $className): Closure $identifierFields = []; foreach ($identifiers as $identifier => $_) { - $identifierFields[$identifier] = $class->getReflectionProperty($identifier); + $identifierFields[$identifier] = $class->getPropertyAccessor($identifier); } $proxyFactory = Closure::bind(static function (array $identifier) use ($initializer, $skippedProperties, $identifierFields, $className): InternalProxy { diff --git a/src/UnitOfWork.php b/src/UnitOfWork.php index 87b434994c6..ff67f30c599 100644 --- a/src/UnitOfWork.php +++ b/src/UnitOfWork.php @@ -585,7 +585,7 @@ public function computeChangeSet(ClassMetadata $class, object $entity): void $actualData = []; - foreach ($class->reflFields as $name => $refProp) { + foreach ($class->propertyAccessors as $name => $refProp) { $value = $refProp->getValue($entity); if ($class->isCollectionValuedAssociation($name) && $value !== null) { @@ -705,7 +705,7 @@ public function computeChangeSet(ClassMetadata $class, object $entity): void $newValue = clone $actualValue; $newValue->setOwner($entity, $assoc); - $class->reflFields[$propName]->setValue($entity, $newValue); + $class->propertyAccessors[$propName]->setValue($entity, $newValue); } } @@ -744,7 +744,7 @@ public function computeChangeSet(ClassMetadata $class, object $entity): void // Look for changes in associations of the entity foreach ($class->associationMappings as $field => $assoc) { - $val = $class->reflFields[$field]->getValue($entity); + $val = $class->propertyAccessors[$field]->getValue($entity); if ($val === null) { continue; } @@ -980,7 +980,7 @@ public function recomputeSingleEntityChangeSet(ClassMetadata $class, object $ent $actualData = []; - foreach ($class->reflFields as $name => $refProp) { + foreach ($class->propertyAccessors as $name => $refProp) { if ( ( ! $class->isIdentifier($name) || ! $class->isIdGeneratorIdentity()) && ($name !== $class->versionField) @@ -1166,7 +1166,7 @@ private function executeDeletions(): void // is obtained by a new entity because the old one went out of scope. //$this->entityStates[$oid] = self::STATE_NEW; if (! $class->isIdentifierNatural()) { - $class->reflFields[$class->identifier[0]]->setValue($entity, null); + $class->propertyAccessors[$class->identifier[0]]->setValue($entity, null); } if ($invoke !== ListenersInvoker::INVOKE_NONE) { @@ -2028,7 +2028,7 @@ private function cascadeRefresh(object $entity, array &$visited, LockMode|int|nu ); foreach ($associationMappings as $assoc) { - $relatedEntities = $class->reflFields[$assoc->fieldName]->getValue($entity); + $relatedEntities = $class->propertyAccessors[$assoc->fieldName]->getValue($entity); switch (true) { case $relatedEntities instanceof PersistentCollection: @@ -2069,7 +2069,7 @@ private function cascadeDetach(object $entity, array &$visited): void ); foreach ($associationMappings as $assoc) { - $relatedEntities = $class->reflFields[$assoc->fieldName]->getValue($entity); + $relatedEntities = $class->propertyAccessors[$assoc->fieldName]->getValue($entity); switch (true) { case $relatedEntities instanceof PersistentCollection: @@ -2115,7 +2115,7 @@ private function cascadePersist(object $entity, array &$visited): void ); foreach ($associationMappings as $assoc) { - $relatedEntities = $class->reflFields[$assoc->fieldName]->getValue($entity); + $relatedEntities = $class->propertyAccessors[$assoc->fieldName]->getValue($entity); switch (true) { case $relatedEntities instanceof PersistentCollection: @@ -2178,7 +2178,7 @@ private function cascadeRemove(object $entity, array &$visited): void $entitiesToCascade = []; foreach ($associationMappings as $assoc) { - $relatedEntities = $class->reflFields[$assoc->fieldName]->getValue($entity); + $relatedEntities = $class->propertyAccessors[$assoc->fieldName]->getValue($entity); switch (true) { case $relatedEntities instanceof Collection: @@ -2234,7 +2234,7 @@ public function lock(object $entity, LockMode|int $lockMode, DateTimeInterface|i $this->initializeObject($entity); assert($class->versionField !== null); - $entityVersion = $class->reflFields[$class->versionField]->getValue($entity); + $entityVersion = $class->propertyAccessors[$class->versionField]->getValue($entity); // phpcs:ignore SlevomatCodingStandard.Operators.DisallowEqualOperators.DisallowedNotEqualOperator if ($entityVersion != $lockVersion) { @@ -2401,7 +2401,7 @@ public function createEntity(string $className, array $data, array &$hints = []) foreach ($data as $field => $value) { if (isset($class->fieldMappings[$field])) { - $class->reflFields[$field]->setValue($entity, $value); + $class->propertyAccessors[$field]->setValue($entity, $value); } } @@ -2431,21 +2431,21 @@ public function createEntity(string $className, array $data, array &$hints = []) if (isset($data[$field]) && is_object($data[$field]) && isset($this->entityStates[spl_object_id($data[$field])])) { $this->originalEntityData[$oid][$field] = $data[$field]; - $class->reflFields[$field]->setValue($entity, $data[$field]); - $targetClass->reflFields[$assoc->mappedBy]->setValue($data[$field], $entity); + $class->propertyAccessors[$field]->setValue($entity, $data[$field]); + $targetClass->propertyAccessors[$assoc->mappedBy]->setValue($data[$field], $entity); continue 2; } // Inverse side of x-to-one can never be lazy - $class->reflFields[$field]->setValue($entity, $this->getEntityPersister($assoc->targetEntity)->loadOneToOneEntity($assoc, $entity)); + $class->propertyAccessors[$field]->setValue($entity, $this->getEntityPersister($assoc->targetEntity)->loadOneToOneEntity($assoc, $entity)); continue 2; } // use the entity association if (isset($data[$field]) && is_object($data[$field]) && isset($this->entityStates[spl_object_id($data[$field])])) { - $class->reflFields[$field]->setValue($entity, $data[$field]); + $class->propertyAccessors[$field]->setValue($entity, $data[$field]); $this->originalEntityData[$oid][$field] = $data[$field]; break; @@ -2477,7 +2477,7 @@ public function createEntity(string $className, array $data, array &$hints = []) if (! $associatedId) { // Foreign key is NULL - $class->reflFields[$field]->setValue($entity, null); + $class->propertyAccessors[$field]->setValue($entity, null); $this->originalEntityData[$oid][$field] = null; break; @@ -2543,11 +2543,11 @@ public function createEntity(string $className, array $data, array &$hints = []) } $this->originalEntityData[$oid][$field] = $newValue; - $class->reflFields[$field]->setValue($entity, $newValue); + $class->propertyAccessors[$field]->setValue($entity, $newValue); if ($assoc->inversedBy !== null && $assoc->isOneToOne() && $newValue !== null) { $inverseAssoc = $targetClass->associationMappings[$assoc->inversedBy]; - $targetClass->reflFields[$inverseAssoc->fieldName]->setValue($newValue, $entity); + $targetClass->propertyAccessors[$inverseAssoc->fieldName]->setValue($newValue, $entity); } break; @@ -2563,7 +2563,7 @@ public function createEntity(string $className, array $data, array &$hints = []) if (isset($data[$field]) && $data[$field] instanceof PersistentCollection) { $data[$field]->setOwner($entity, $assoc); - $class->reflFields[$field]->setValue($entity, $data[$field]); + $class->propertyAccessors[$field]->setValue($entity, $data[$field]); $this->originalEntityData[$oid][$field] = $data[$field]; break; @@ -2574,7 +2574,7 @@ public function createEntity(string $className, array $data, array &$hints = []) $pColl->setOwner($entity, $assoc); $pColl->setInitialized(false); - $reflField = $class->reflFields[$field]; + $reflField = $class->propertyAccessors[$field]; $reflField->setValue($entity, $pColl); if ($hints['fetchMode'][$class->name][$field] === ClassMetadata::FETCH_EAGER) { @@ -2654,7 +2654,7 @@ private function eagerLoadCollections(array $collections, ToManyInverseSideMappi $found = $this->getEntityPersister($targetEntity)->loadAll([$mappedBy => $entities], $mapping->orderBy); $targetClass = $this->em->getClassMetadata($targetEntity); - $targetProperty = $targetClass->getReflectionProperty($mappedBy); + $targetProperty = $targetClass->getPropertyAccessor($mappedBy); assert($targetProperty !== null); foreach ($found as $targetValue) { @@ -2676,7 +2676,7 @@ private function eagerLoadCollections(array $collections, ToManyInverseSideMappi $idHash = implode(' ', $id); if ($mapping->indexBy !== null) { - $indexByProperty = $targetClass->getReflectionProperty($mapping->indexBy); + $indexByProperty = $targetClass->getPropertyAccessor($mapping->indexBy); assert($indexByProperty !== null); $collectionBatch[$idHash]->hydrateSet($indexByProperty->getValue($targetValue), $targetValue); } else { @@ -3241,7 +3241,7 @@ final public function assignPostInsertId(object $entity, mixed $generatedId): vo $idValue = $this->convertSingleFieldIdentifierToPHPValue($class, $generatedId); $oid = spl_object_id($entity); - $class->reflFields[$idField]->setValue($entity, $idValue); + $class->propertyAccessors[$idField]->setValue($entity, $idValue); $this->entityIdentifiers[$oid] = [$idField => $idValue]; $this->entityStates[$oid] = self::STATE_MANAGED; diff --git a/tests/Tests/ORM/Functional/EnumTest.php b/tests/Tests/ORM/Functional/EnumTest.php index ba84a921240..c4439be9672 100644 --- a/tests/Tests/ORM/Functional/EnumTest.php +++ b/tests/Tests/ORM/Functional/EnumTest.php @@ -470,7 +470,7 @@ public static function provideCardClasses(): iterable public function testItAllowsReadingAttributes(): void { $metadata = $this->_em->getClassMetadata(Card::class); - $property = $metadata->getReflectionProperty('suit'); + $property = $metadata->reflFields['suit']; $attributes = $property->getAttributes(); diff --git a/tests/Tests/ORM/Functional/Ticket/DDC168Test.php b/tests/Tests/ORM/Functional/Ticket/DDC168Test.php index 54147ba39c4..7e83cfa47bd 100644 --- a/tests/Tests/ORM/Functional/Ticket/DDC168Test.php +++ b/tests/Tests/ORM/Functional/Ticket/DDC168Test.php @@ -25,7 +25,7 @@ protected function setUp(): void $this->oldMetadata = $this->_em->getClassMetadata(CompanyEmployee::class); $metadata = clone $this->oldMetadata; - ksort($metadata->reflFields); + ksort($metadata->propertyAccessors); $this->_em->getMetadataFactory()->setMetadataFor(CompanyEmployee::class, $metadata); } diff --git a/tests/Tests/ORM/Functional/ValueObjectsTest.php b/tests/Tests/ORM/Functional/ValueObjectsTest.php index ad77a29b29f..1ed080d145a 100644 --- a/tests/Tests/ORM/Functional/ValueObjectsTest.php +++ b/tests/Tests/ORM/Functional/ValueObjectsTest.php @@ -45,30 +45,6 @@ protected function setUp(): void ); } - public function testMetadataHasReflectionEmbeddablesAccessible(): void - { - $classMetadata = $this->_em->getClassMetadata(DDC93Person::class); - - if (class_exists(CommonRuntimePublicReflectionProperty::class)) { - self::assertInstanceOf( - CommonRuntimePublicReflectionProperty::class, - $classMetadata->getReflectionProperty('address'), - ); - } elseif (class_exists(RuntimeReflectionProperty::class)) { - self::assertInstanceOf( - RuntimeReflectionProperty::class, - $classMetadata->getReflectionProperty('address'), - ); - } else { - self::assertInstanceOf( - ReflectionProperty::class, - $classMetadata->getReflectionProperty('address'), - ); - } - - self::assertInstanceOf(ReflectionEmbeddedProperty::class, $classMetadata->getReflectionProperty('address.street')); - } - public function testCRUD(): void { $person = new DDC93Person(); diff --git a/tests/Tests/ORM/Mapping/ClassMetadataLoadEventTest.php b/tests/Tests/ORM/Mapping/ClassMetadataLoadEventTest.php index 294ac3d53b7..2a1cdcd0b07 100644 --- a/tests/Tests/ORM/Mapping/ClassMetadataLoadEventTest.php +++ b/tests/Tests/ORM/Mapping/ClassMetadataLoadEventTest.php @@ -10,6 +10,7 @@ use Doctrine\ORM\Mapping\Entity; use Doctrine\ORM\Mapping\GeneratedValue; use Doctrine\ORM\Mapping\Id; +use Doctrine\ORM\Mapping\PropertyAccessors\PropertyAccessor; use Doctrine\ORM\Mapping\Table; use Doctrine\Tests\OrmTestCase; use PHPUnit\Framework\Attributes\Group; @@ -26,8 +27,8 @@ public function testEvent(): void $evm->addEventListener(Events::loadClassMetadata, $this); $classMetadata = $metadataFactory->getMetadataFor(LoadEventTestEntity::class); self::assertTrue($classMetadata->hasField('about')); - self::assertArrayHasKey('about', $classMetadata->reflFields); - self::assertInstanceOf(ReflectionProperty::class, $classMetadata->reflFields['about']); + self::assertArrayHasKey('about', $classMetadata->propertyAccessors); + self::assertInstanceOf(PropertyAccessor::class, $classMetadata->propertyAccessors['about']); } public function loadClassMetadata(LoadClassMetadataEventArgs $eventArgs): void diff --git a/tests/Tests/ORM/Mapping/ClassMetadataTest.php b/tests/Tests/ORM/Mapping/ClassMetadataTest.php index 296c655d743..d99d618e418 100644 --- a/tests/Tests/ORM/Mapping/ClassMetadataTest.php +++ b/tests/Tests/ORM/Mapping/ClassMetadataTest.php @@ -79,7 +79,7 @@ public function testClassMetadataInstanceSerialization(): void $cm->initializeReflection(new RuntimeReflectionService()); // Test initial state - self::assertTrue(count($cm->getReflectionProperties()) === 0); + self::assertTrue(count($cm->getPropertyAccessors()) === 0); self::assertInstanceOf(ReflectionClass::class, $cm->reflClass); self::assertEquals(CmsUser::class, $cm->name); self::assertEquals(CmsUser::class, $cm->rootEntityName); @@ -105,7 +105,7 @@ public function testClassMetadataInstanceSerialization(): void $cm->wakeupReflection(new RuntimeReflectionService()); // Check state - self::assertTrue(count($cm->getReflectionProperties()) > 0); + self::assertTrue(count($cm->getPropertyAccessors()) > 0); self::assertEquals('Doctrine\Tests\Models\CMS', $cm->namespace); self::assertInstanceOf(ReflectionClass::class, $cm->reflClass); self::assertEquals(CmsUser::class, $cm->name); @@ -1015,7 +1015,7 @@ public function testWakeupReflectionWithEmbeddableAndStaticReflectionService(): $classMetadata->mapField($field); $classMetadata->wakeupReflection(new StaticReflectionService()); - self::assertEquals(['test' => null, 'test.embeddedProperty' => null], $classMetadata->getReflectionProperties()); + self::assertEquals(['test' => null, 'test.embeddedProperty' => null], $classMetadata->getPropertyAccessors()); } public function testGetColumnNamesWithGivenFieldNames(): void From bd292481bdee546153b31766dec4487e9958fa32 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Fri, 11 Oct 2024 00:48:22 +0200 Subject: [PATCH 06/26] Adjust test. --- tests/Tests/ORM/Mapping/ClassMetadataTest.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/Tests/ORM/Mapping/ClassMetadataTest.php b/tests/Tests/ORM/Mapping/ClassMetadataTest.php index d99d618e418..1194b4096b9 100644 --- a/tests/Tests/ORM/Mapping/ClassMetadataTest.php +++ b/tests/Tests/ORM/Mapping/ClassMetadataTest.php @@ -988,7 +988,7 @@ public function testCanInstantiateInternalPhpClassSubclassFromUnserializedMetada self::assertInstanceOf(MyArrayObjectEntity::class, $classMetadata->newInstance()); } - public function testWakeupReflectionWithEmbeddableAndStaticReflectionService(): void + public function testWakeupReflectionWithEmbeddable(): void { if (! class_exists(StaticReflectionService::class)) { self::markTestSkipped('This test is not supported by the current installed doctrine/persistence version'); @@ -998,7 +998,7 @@ public function testWakeupReflectionWithEmbeddableAndStaticReflectionService(): $classMetadata->mapEmbedded( [ - 'fieldName' => 'test', + 'fieldName' => 'embedded', 'class' => TestEntity1::class, 'columnPrefix' => false, ], @@ -1008,14 +1008,14 @@ public function testWakeupReflectionWithEmbeddableAndStaticReflectionService(): 'fieldName' => 'test.embeddedProperty', 'type' => 'string', 'originalClass' => TestEntity1::class, - 'declaredField' => 'test', - 'originalField' => 'embeddedProperty', + 'declaredField' => 'embedded', + 'originalField' => 'name', ]; $classMetadata->mapField($field); $classMetadata->wakeupReflection(new StaticReflectionService()); - self::assertEquals(['test' => null, 'test.embeddedProperty' => null], $classMetadata->getPropertyAccessors()); + self::assertEquals(['embedded', 'test.embeddedProperty'], array_keys($classMetadata->getPropertyAccessors())); } public function testGetColumnNamesWithGivenFieldNames(): void From eba01f8d0e35e314d72ad96244648f476d05cfa1 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Fri, 11 Oct 2024 00:50:53 +0200 Subject: [PATCH 07/26] Style, missing getReflectionProperties()Property() that were renamed. --- src/Mapping/ClassMetadata.php | 19 ++++++++++++++++--- src/Mapping/LegacyReflectionFields.php | 17 ++++++++++++----- .../EmbeddablePropertyAccessor.php | 2 ++ .../EnumPropertyAccessor.php | 8 +++++++- .../ObjectCastPropertyAccessor.php | 2 ++ .../PropertyAccessors/PropertyAccessor.php | 2 ++ .../PropertyAccessors/ReadonlyAccessor.php | 2 ++ .../TypedNoDefaultPropertyAccessor.php | 2 ++ src/Proxy/ProxyFactory.php | 1 - .../Tests/ORM/Functional/ValueObjectsTest.php | 5 ----- .../Mapping/ClassMetadataLoadEventTest.php | 1 - tests/Tests/ORM/Mapping/ClassMetadataTest.php | 1 + 12 files changed, 46 insertions(+), 16 deletions(-) diff --git a/src/Mapping/ClassMetadata.php b/src/Mapping/ClassMetadata.php index d14e964457d..4a5a8a056df 100644 --- a/src/Mapping/ClassMetadata.php +++ b/src/Mapping/ClassMetadata.php @@ -14,7 +14,6 @@ use Doctrine\ORM\Cache\Exception\NonCacheableEntityAssociation; use Doctrine\ORM\EntityRepository; use Doctrine\ORM\Id\AbstractIdGenerator; -use Doctrine\ORM\Mapping\PropertyAccessors\AccessorFactory; use Doctrine\ORM\Mapping\PropertyAccessors\EmbeddablePropertyAccessor; use Doctrine\ORM\Mapping\PropertyAccessors\EnumPropertyAccessor; use Doctrine\ORM\Mapping\PropertyAccessors\ObjectCastPropertyAccessor; @@ -23,7 +22,6 @@ use Doctrine\ORM\Mapping\PropertyAccessors\TypedNoDefaultPropertyAccessor; use Doctrine\Persistence\Mapping\ClassMetadata as PersistenceClassMetadata; use Doctrine\Persistence\Mapping\ReflectionService; -use Doctrine\Persistence\Reflection\EnumReflectionProperty; use InvalidArgumentException; use LogicException; use ReflectionClass; @@ -580,6 +578,16 @@ public function __construct(public string $name, NamingStrategy|null $namingStra * @return ReflectionProperty[]|null[] An array of ReflectionProperty instances. * @phpstan-return array */ + public function getReflectionProperties(): array + { + return $this->reflFields; + } + + /** + * Gets the ReflectionProperties of the mapped class. + * + * @return PropertyAccessor[] An array of PropertyAccessor instances. + */ public function getPropertyAccessors(): array { return $this->propertyAccessors; @@ -588,9 +596,14 @@ public function getPropertyAccessors(): array /** * Gets a ReflectionProperty for a specific field of the mapped class. */ + public function getReflectionProperty(string $name): ReflectionProperty|null + { + return $this->reflFields[$name]; + } + public function getPropertyAccessor(string $name): PropertyAccessor|null { - return $this->propertyAccessors[$name]; + return $this->propertyAccessors[$name] ?? null; } /** diff --git a/src/Mapping/LegacyReflectionFields.php b/src/Mapping/LegacyReflectionFields.php index 1442168e2d8..41e4b72ea91 100644 --- a/src/Mapping/LegacyReflectionFields.php +++ b/src/Mapping/LegacyReflectionFields.php @@ -1,15 +1,22 @@ classMetadata->fieldMappings[$field]->originalField !== null) { $parentField = str_replace('.' . $fieldName, '', $field); - if (!str_contains($parentField, '.')) { + if (! str_contains($parentField, '.')) { $parentClass = $this->classMetadata->name; } else { $parentClass = $this->classMetadata->fieldMappings[$parentField]->originalClass; @@ -72,7 +79,7 @@ public function offsetGet($field): mixed return $this->reflFields[$field]; } - throw new \OutOfBoundsException('Unknown field: ' . $this->classMetadata->name .' ::$' . $field); + throw new OutOfBoundsException('Unknown field: ' . $this->classMetadata->name . ' ::$' . $field); } public function offsetSet($offset, $value): void diff --git a/src/Mapping/PropertyAccessors/EmbeddablePropertyAccessor.php b/src/Mapping/PropertyAccessors/EmbeddablePropertyAccessor.php index 87f7aa8f416..39e6fc3590a 100644 --- a/src/Mapping/PropertyAccessors/EmbeddablePropertyAccessor.php +++ b/src/Mapping/PropertyAccessors/EmbeddablePropertyAccessor.php @@ -1,5 +1,7 @@ Date: Fri, 11 Oct 2024 01:02:42 +0200 Subject: [PATCH 08/26] Deprecate access to ClassMetadata::$reflFields. --- src/Mapping/LegacyReflectionFields.php | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/Mapping/LegacyReflectionFields.php b/src/Mapping/LegacyReflectionFields.php index 41e4b72ea91..224624b5bae 100644 --- a/src/Mapping/LegacyReflectionFields.php +++ b/src/Mapping/LegacyReflectionFields.php @@ -5,6 +5,7 @@ namespace Doctrine\ORM\Mapping; use ArrayAccess; +use Doctrine\Deprecations\Deprecation; use Doctrine\Persistence\Mapping\ReflectionService; use Doctrine\Persistence\Reflection\EnumReflectionProperty; use IteratorAggregate; @@ -26,6 +27,12 @@ public function __construct(private ClassMetadata $classMetadata, private Reflec public function offsetExists($offset): bool { + Deprecation::trigger( + 'doctrine/orm', + 'https://github.com/doctrine/orm/pull/11659', + 'Access to ClassMetadata::$reflFields is deprecated and will be removed in Doctrine ORM 4.0.', + ); + return isset($this->classMetadata->propertyAccessors[$offset]); } @@ -35,6 +42,12 @@ public function offsetGet($field): mixed return $this->reflFields[$field]; } + Deprecation::trigger( + 'doctrine/orm', + 'https://github.com/doctrine/orm/pull/11659', + 'Access to ClassMetadata::$reflFields is deprecated and will be removed in Doctrine ORM 4.0.', + ); + if (isset($this->classMetadata->propertyAccessors[$field])) { $fieldName = str_contains($field, '.') ? $this->classMetadata->fieldMappings[$field]->originalField : $field; $className = $this->classMetadata->name; @@ -112,6 +125,12 @@ private function getAccessibleProperty(string $class, string $field): Reflection public function getIterator(): Traversable { + Deprecation::trigger( + 'doctrine/orm', + 'https://github.com/doctrine/orm/pull/11659', + 'Access to ClassMetadata::$reflFields is deprecated and will be removed in Doctrine ORM 4.0.', + ); + $keys = array_keys($this->classMetadata->propertyAccessors); foreach ($keys as $key) { From 0c1cf853fc07ed42310a082d2049c2fc9a03c09f Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sat, 12 Oct 2024 03:18:18 +0200 Subject: [PATCH 09/26] Address PHPStan issues. --- src/Mapping/ClassMetadata.php | 14 ++++++++++++-- src/Mapping/LegacyReflectionFields.php | 10 ++++++++++ .../PropertyAccessors/EnumPropertyAccessor.php | 2 +- src/Proxy/ProxyFactory.php | 5 +++-- 4 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/Mapping/ClassMetadata.php b/src/Mapping/ClassMetadata.php index 4a5a8a056df..0365bfeaa2e 100644 --- a/src/Mapping/ClassMetadata.php +++ b/src/Mapping/ClassMetadata.php @@ -607,8 +607,6 @@ public function getPropertyAccessor(string $name): PropertyAccessor|null } /** - * Gets the ReflectionProperty for the single identifier field. - * * @throws BadMethodCallException If the class has a composite identifier. */ public function getSingleIdReflectionProperty(): ReflectionProperty|null @@ -617,6 +615,18 @@ public function getSingleIdReflectionProperty(): ReflectionProperty|null throw new BadMethodCallException('Class ' . $this->name . ' has a composite identifier.'); } + return $this->reflFields[$this->identifier[0]]; + } + + /** + * @throws BadMethodCallException If the class has a composite identifier. + */ + public function getSingleIdPropertyAccessor(): PropertyAccessor|null + { + if ($this->isIdentifierComposite) { + throw new BadMethodCallException('Class ' . $this->name . ' has a composite identifier.'); + } + return $this->propertyAccessors[$this->identifier[0]]; } diff --git a/src/Mapping/LegacyReflectionFields.php b/src/Mapping/LegacyReflectionFields.php index 224624b5bae..cac33d7fbac 100644 --- a/src/Mapping/LegacyReflectionFields.php +++ b/src/Mapping/LegacyReflectionFields.php @@ -8,6 +8,7 @@ use Doctrine\Deprecations\Deprecation; use Doctrine\Persistence\Mapping\ReflectionService; use Doctrine\Persistence\Reflection\EnumReflectionProperty; +use Generator; use IteratorAggregate; use OutOfBoundsException; use ReflectionProperty; @@ -19,12 +20,14 @@ class LegacyReflectionFields implements ArrayAccess, IteratorAggregate { + /** @var array */ private array $reflFields = []; public function __construct(private ClassMetadata $classMetadata, private ReflectionService $reflectionService) { } + /** @param string $offset */ public function offsetExists($offset): bool { Deprecation::trigger( @@ -36,6 +39,7 @@ public function offsetExists($offset): bool return isset($this->classMetadata->propertyAccessors[$offset]); } + /** @param string $field */ public function offsetGet($field): mixed { if (isset($this->reflFields[$field])) { @@ -95,11 +99,16 @@ public function offsetGet($field): mixed throw new OutOfBoundsException('Unknown field: ' . $this->classMetadata->name . ' ::$' . $field); } + /** + * @param string $offset + * @param ReflectionProperty $value + */ public function offsetSet($offset, $value): void { $this->reflFields[$offset] = $value; } + /** @param string $offset */ public function offsetUnset($offset): void { unset($this->reflFields[$offset]); @@ -123,6 +132,7 @@ private function getAccessibleProperty(string $class, string $field): Reflection return $reflectionProperty; } + /** @return Generator */ public function getIterator(): Traversable { Deprecation::trigger( diff --git a/src/Mapping/PropertyAccessors/EnumPropertyAccessor.php b/src/Mapping/PropertyAccessors/EnumPropertyAccessor.php index 1afecd417cc..1cdcbf08ad0 100644 --- a/src/Mapping/PropertyAccessors/EnumPropertyAccessor.php +++ b/src/Mapping/PropertyAccessors/EnumPropertyAccessor.php @@ -36,7 +36,7 @@ public function getValue(object $object): mixed return $this->fromEnum($enum); } - private function fromEnum($enum) + private function fromEnum(array|BackedEnum $enum): int|string|array { if (is_array($enum)) { return array_map(static function (BackedEnum $enum) { diff --git a/src/Proxy/ProxyFactory.php b/src/Proxy/ProxyFactory.php index 830be983dff..31d57769065 100644 --- a/src/Proxy/ProxyFactory.php +++ b/src/Proxy/ProxyFactory.php @@ -18,6 +18,7 @@ use function array_combine; use function array_flip; +use function array_keys; use function assert; use function bin2hex; use function chmod; @@ -232,7 +233,7 @@ private function createLazyInitializer(ClassMetadata $classMetadata, EntityPersi $class = $entityPersister->getClassMetadata(); foreach ($class->getPropertyAccessors() as $name => $property) { - if (! $property || isset($identifier[$name]) || ! $class->hasField($name) && ! $class->hasAssociation($name)) { + if (isset($identifier[$name]) || ! $class->hasField($name) && ! $class->hasAssociation($name)) { continue; } @@ -280,7 +281,7 @@ private function getProxyFactory(string $className): Closure $proxyClassName = $this->loadProxyClass($class); $identifierFields = []; - foreach ($identifiers as $identifier => $_) { + foreach (array_keys($identifiers) as $identifier) { $identifierFields[$identifier] = $class->getPropertyAccessor($identifier); } From 622ba2dcc71f270229cd5e4fcff6671f24baa1e6 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sat, 12 Oct 2024 03:19:52 +0200 Subject: [PATCH 10/26] Mark all PropertyAccessor classes @internal. --- src/Mapping/PropertyAccessors/EmbeddablePropertyAccessor.php | 1 + src/Mapping/PropertyAccessors/EnumPropertyAccessor.php | 1 + src/Mapping/PropertyAccessors/ObjectCastPropertyAccessor.php | 1 + src/Mapping/PropertyAccessors/PropertyAccessor.php | 1 + src/Mapping/PropertyAccessors/ReadonlyAccessor.php | 1 + src/Mapping/PropertyAccessors/TypedNoDefaultPropertyAccessor.php | 1 + 6 files changed, 6 insertions(+) diff --git a/src/Mapping/PropertyAccessors/EmbeddablePropertyAccessor.php b/src/Mapping/PropertyAccessors/EmbeddablePropertyAccessor.php index 39e6fc3590a..1cc3a50b475 100644 --- a/src/Mapping/PropertyAccessors/EmbeddablePropertyAccessor.php +++ b/src/Mapping/PropertyAccessors/EmbeddablePropertyAccessor.php @@ -6,6 +6,7 @@ use Doctrine\Instantiator\Instantiator; +/** @internal */ class EmbeddablePropertyAccessor implements PropertyAccessor { private static Instantiator|null $instantiator = null; diff --git a/src/Mapping/PropertyAccessors/EnumPropertyAccessor.php b/src/Mapping/PropertyAccessors/EnumPropertyAccessor.php index 1cdcbf08ad0..8c9033af60a 100644 --- a/src/Mapping/PropertyAccessors/EnumPropertyAccessor.php +++ b/src/Mapping/PropertyAccessors/EnumPropertyAccessor.php @@ -10,6 +10,7 @@ use function is_array; use function reset; +/** @internal */ class EnumPropertyAccessor implements PropertyAccessor { public function __construct(private PropertyAccessor $parent, private string $enumType) diff --git a/src/Mapping/PropertyAccessors/ObjectCastPropertyAccessor.php b/src/Mapping/PropertyAccessors/ObjectCastPropertyAccessor.php index 7babd0144ac..38810f993a4 100644 --- a/src/Mapping/PropertyAccessors/ObjectCastPropertyAccessor.php +++ b/src/Mapping/PropertyAccessors/ObjectCastPropertyAccessor.php @@ -9,6 +9,7 @@ use function ltrim; +/** @internal */ class ObjectCastPropertyAccessor implements PropertyAccessor { public static function fromNames(string $class, string $name): self diff --git a/src/Mapping/PropertyAccessors/PropertyAccessor.php b/src/Mapping/PropertyAccessors/PropertyAccessor.php index c340e334318..7316d8a348f 100644 --- a/src/Mapping/PropertyAccessors/PropertyAccessor.php +++ b/src/Mapping/PropertyAccessors/PropertyAccessor.php @@ -4,6 +4,7 @@ namespace Doctrine\ORM\Mapping\PropertyAccessors; +/** @internal */ interface PropertyAccessor { public function setValue(object $object, mixed $value): void; diff --git a/src/Mapping/PropertyAccessors/ReadonlyAccessor.php b/src/Mapping/PropertyAccessors/ReadonlyAccessor.php index 56e60d00514..62faf6873df 100644 --- a/src/Mapping/PropertyAccessors/ReadonlyAccessor.php +++ b/src/Mapping/PropertyAccessors/ReadonlyAccessor.php @@ -10,6 +10,7 @@ use function sprintf; +/** @internal */ class ReadonlyAccessor implements PropertyAccessor { public function __construct(private PropertyAccessor $parent, private ReflectionProperty $reflectionProperty) diff --git a/src/Mapping/PropertyAccessors/TypedNoDefaultPropertyAccessor.php b/src/Mapping/PropertyAccessors/TypedNoDefaultPropertyAccessor.php index a35ae5f14c7..87850ad4b4e 100644 --- a/src/Mapping/PropertyAccessors/TypedNoDefaultPropertyAccessor.php +++ b/src/Mapping/PropertyAccessors/TypedNoDefaultPropertyAccessor.php @@ -11,6 +11,7 @@ use function assert; use function sprintf; +/** @internal */ class TypedNoDefaultPropertyAccessor implements PropertyAccessor { private Closure|null $unsetter = null; From 23c31aec51e473cb935280f0be3420aab6478d0e Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sun, 3 Nov 2024 22:11:32 +0100 Subject: [PATCH 11/26] Static analysis. --- src/Mapping/ClassMetadata.php | 6 +++--- src/Mapping/LegacyReflectionFields.php | 6 +++++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Mapping/ClassMetadata.php b/src/Mapping/ClassMetadata.php index 0365bfeaa2e..5aa76811738 100644 --- a/src/Mapping/ClassMetadata.php +++ b/src/Mapping/ClassMetadata.php @@ -546,7 +546,7 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable /** * The ReflectionProperty instances of the mapped class. * - * @var LegacyReflectionFields|array + * @var LegacyReflectionFields|array */ public LegacyReflectionFields|array $reflFields = []; @@ -575,8 +575,8 @@ public function __construct(public string $name, NamingStrategy|null $namingStra /** * Gets the ReflectionProperties of the mapped class. * - * @return ReflectionProperty[]|null[] An array of ReflectionProperty instances. - * @phpstan-return array + * @return LegacyReflectionFields|ReflectionProperty[] An array of ReflectionProperty instances. + * @phpstan-return LegacyReflectionFields|array */ public function getReflectionProperties(): array { diff --git a/src/Mapping/LegacyReflectionFields.php b/src/Mapping/LegacyReflectionFields.php index cac33d7fbac..d9533611a2a 100644 --- a/src/Mapping/LegacyReflectionFields.php +++ b/src/Mapping/LegacyReflectionFields.php @@ -18,9 +18,13 @@ use function str_contains; use function str_replace; +/** + * @template-implements ArrayAccess + * @template-implements IteratorAggregate + */ class LegacyReflectionFields implements ArrayAccess, IteratorAggregate { - /** @var array */ + /** @var array */ private array $reflFields = []; public function __construct(private ClassMetadata $classMetadata, private ReflectionService $reflectionService) From e82690d2565b7aeee3e8b768cf7b1e8a605c2714 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sun, 3 Nov 2024 22:51:53 +0100 Subject: [PATCH 12/26] More psalm to fix the errors. --- src/Mapping/ClassMetadata.php | 10 ++--- src/Mapping/LegacyReflectionFields.php | 40 +++++++++++++------ .../EmbeddablePropertyAccessor.php | 1 + .../EnumPropertyAccessor.php | 23 +++++------ .../ObjectCastPropertyAccessor.php | 1 + 5 files changed, 42 insertions(+), 33 deletions(-) diff --git a/src/Mapping/ClassMetadata.php b/src/Mapping/ClassMetadata.php index 5aa76811738..c810a674719 100644 --- a/src/Mapping/ClassMetadata.php +++ b/src/Mapping/ClassMetadata.php @@ -576,7 +576,7 @@ public function __construct(public string $name, NamingStrategy|null $namingStra * Gets the ReflectionProperties of the mapped class. * * @return LegacyReflectionFields|ReflectionProperty[] An array of ReflectionProperty instances. - * @phpstan-return LegacyReflectionFields|array + * @phpstan-return LegacyReflectionFields|array */ public function getReflectionProperties(): array { @@ -606,9 +606,7 @@ public function getPropertyAccessor(string $name): PropertyAccessor|null return $this->propertyAccessors[$name] ?? null; } - /** - * @throws BadMethodCallException If the class has a composite identifier. - */ + /** @throws BadMethodCallException If the class has a composite identifier. */ public function getSingleIdReflectionProperty(): ReflectionProperty|null { if ($this->isIdentifierComposite) { @@ -618,9 +616,7 @@ public function getSingleIdReflectionProperty(): ReflectionProperty|null return $this->reflFields[$this->identifier[0]]; } - /** - * @throws BadMethodCallException If the class has a composite identifier. - */ + /** @throws BadMethodCallException If the class has a composite identifier. */ public function getSingleIdPropertyAccessor(): PropertyAccessor|null { if ($this->isIdentifierComposite) { diff --git a/src/Mapping/LegacyReflectionFields.php b/src/Mapping/LegacyReflectionFields.php index d9533611a2a..579ff311539 100644 --- a/src/Mapping/LegacyReflectionFields.php +++ b/src/Mapping/LegacyReflectionFields.php @@ -32,7 +32,7 @@ public function __construct(private ClassMetadata $classMetadata, private Reflec } /** @param string $offset */ - public function offsetExists($offset): bool + public function offsetExists($offset): bool // phpcs:ignore { Deprecation::trigger( 'doctrine/orm', @@ -43,8 +43,12 @@ public function offsetExists($offset): bool return isset($this->classMetadata->propertyAccessors[$offset]); } - /** @param string $field */ - public function offsetGet($field): mixed + /** + * @param string $field + * + * @psalm-suppress LessSpecificImplementedReturnType + */ + public function offsetGet($field): mixed // phpcs:ignore { if (isset($this->reflFields[$field])) { return $this->reflFields[$field]; @@ -60,6 +64,8 @@ public function offsetGet($field): mixed $fieldName = str_contains($field, '.') ? $this->classMetadata->fieldMappings[$field]->originalField : $field; $className = $this->classMetadata->name; + assert(is_string($fieldName)); + if (isset($this->classMetadata->fieldMappings[$field]) && $this->classMetadata->fieldMappings[$field]->originalClass !== null) { $className = $this->classMetadata->fieldMappings[$field]->originalClass; } elseif (isset($this->classMetadata->fieldMappings[$field]) && $this->classMetadata->fieldMappings[$field]->declared !== null) { @@ -70,6 +76,7 @@ public function offsetGet($field): mixed $className = $this->classMetadata->embeddedClasses[$field]->declared; } + /** @psalm-suppress ArgumentTypeCoercion */ $this->reflFields[$field] = $this->getAccessibleProperty($className, $fieldName); if (isset($this->classMetadata->fieldMappings[$field])) { @@ -81,7 +88,8 @@ public function offsetGet($field): mixed } if ($this->classMetadata->fieldMappings[$field]->originalField !== null) { - $parentField = str_replace('.' . $fieldName, '', $field); + $parentField = str_replace('.' . $fieldName, '', $field); + $originalClass = $this->classMetadata->fieldMappings[$field]->originalClass; if (! str_contains($parentField, '.')) { $parentClass = $this->classMetadata->name; @@ -89,10 +97,13 @@ public function offsetGet($field): mixed $parentClass = $this->classMetadata->fieldMappings[$parentField]->originalClass; } + /** @psalm-var class-string $parentClass */ + /** @psalm-var class-string $originalClass */ + $this->reflFields[$field] = new ReflectionEmbeddedProperty( $this->getAccessibleProperty($parentClass, $parentField), $this->reflFields[$field], - $this->classMetadata->fieldMappings[$field]->originalClass, + $originalClass, ); } } @@ -104,33 +115,36 @@ public function offsetGet($field): mixed } /** - * @param string $offset + * @param string $offset * @param ReflectionProperty $value */ - public function offsetSet($offset, $value): void + public function offsetSet($offset, $value): void // phpcs:ignore { $this->reflFields[$offset] = $value; } /** @param string $offset */ - public function offsetUnset($offset): void + public function offsetUnset($offset): void // phpcs:ignore { unset($this->reflFields[$offset]); } /** @psalm-param class-string $class */ - private function getAccessibleProperty(string $class, string $field): ReflectionProperty|null + private function getAccessibleProperty(string $class, string $field): ReflectionProperty { $reflectionProperty = $this->reflectionService->getAccessibleProperty($class, $field); - if ($reflectionProperty?->isReadOnly()) { + + assert($reflectionProperty !== null); + + if ($reflectionProperty->isReadOnly()) { $declaringClass = $reflectionProperty->class; if ($declaringClass !== $class) { $reflectionProperty = $this->reflectionService->getAccessibleProperty($declaringClass, $field); - } - if ($reflectionProperty !== null) { - $reflectionProperty = new ReflectionReadonlyProperty($reflectionProperty); + assert($reflectionProperty !== null); } + + $reflectionProperty = new ReflectionReadonlyProperty($reflectionProperty); } return $reflectionProperty; diff --git a/src/Mapping/PropertyAccessors/EmbeddablePropertyAccessor.php b/src/Mapping/PropertyAccessors/EmbeddablePropertyAccessor.php index 1cc3a50b475..c446e0c819e 100644 --- a/src/Mapping/PropertyAccessors/EmbeddablePropertyAccessor.php +++ b/src/Mapping/PropertyAccessors/EmbeddablePropertyAccessor.php @@ -14,6 +14,7 @@ class EmbeddablePropertyAccessor implements PropertyAccessor public function __construct( private PropertyAccessor $parent, private PropertyAccessor $child, + /** @var class-string */ private string $embeddedClass, ) { } diff --git a/src/Mapping/PropertyAccessors/EnumPropertyAccessor.php b/src/Mapping/PropertyAccessors/EnumPropertyAccessor.php index 8c9033af60a..54024eb470e 100644 --- a/src/Mapping/PropertyAccessors/EnumPropertyAccessor.php +++ b/src/Mapping/PropertyAccessors/EnumPropertyAccessor.php @@ -13,6 +13,7 @@ /** @internal */ class EnumPropertyAccessor implements PropertyAccessor { + /** @param class-string $enumType */ public function __construct(private PropertyAccessor $parent, private string $enumType) { } @@ -37,7 +38,12 @@ public function getValue(object $object): mixed return $this->fromEnum($enum); } - private function fromEnum(array|BackedEnum $enum): int|string|array + /** + * @param BackedEnum|BackedEnum[] $enum + * + * @return ($enum is BackedEnum ? (string|int) : (string[]|int[])) + */ + private function fromEnum($enum) { if (is_array($enum)) { return array_map(static function (BackedEnum $enum) { @@ -49,22 +55,13 @@ private function fromEnum(array|BackedEnum $enum): int|string|array } /** - * @param int|string|int[]|string[]|BackedEnum|BackedEnum[] $value + * @param int|string|int[]|string[] $value * - * @return ($value is int|string|BackedEnum ? BackedEnum : BackedEnum[]) + * @return ($value is int|string ? BackedEnum : BackedEnum[]) */ - private function toEnum(int|string|array|BackedEnum $value) + private function toEnum($value) { - if ($value instanceof BackedEnum) { - return $value; - } - if (is_array($value)) { - $v = reset($value); - if ($v instanceof BackedEnum) { - return $value; - } - return array_map([$this->enumType, 'from'], $value); } diff --git a/src/Mapping/PropertyAccessors/ObjectCastPropertyAccessor.php b/src/Mapping/PropertyAccessors/ObjectCastPropertyAccessor.php index 38810f993a4..2015acda289 100644 --- a/src/Mapping/PropertyAccessors/ObjectCastPropertyAccessor.php +++ b/src/Mapping/PropertyAccessors/ObjectCastPropertyAccessor.php @@ -12,6 +12,7 @@ /** @internal */ class ObjectCastPropertyAccessor implements PropertyAccessor { + /** @param class-string $class */ public static function fromNames(string $class, string $name): self { $reflectionProperty = new ReflectionProperty($class, $name); From 073809cf5c91457856b56e167b6df9d5b585cc08 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sun, 3 Nov 2024 23:46:55 +0100 Subject: [PATCH 13/26] Fixup EnumPropertyAccessor::toEnum --- .../PropertyAccessors/EnumPropertyAccessor.php | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/Mapping/PropertyAccessors/EnumPropertyAccessor.php b/src/Mapping/PropertyAccessors/EnumPropertyAccessor.php index 54024eb470e..29ed7dc689f 100644 --- a/src/Mapping/PropertyAccessors/EnumPropertyAccessor.php +++ b/src/Mapping/PropertyAccessors/EnumPropertyAccessor.php @@ -55,13 +55,25 @@ private function fromEnum($enum) } /** - * @param int|string|int[]|string[] $value + * @param BackedEnum|BackedEnum[]|int|string|int[]|string[] $value * - * @return ($value is int|string ? BackedEnum : BackedEnum[]) + * @return ($value is int|string|BackedEnum ? BackedEnum : BackedEnum[]) + * + * @psalm-suppress InvalidReturnStatement + * @psalm-suppress PossiblyInvalidArgument */ private function toEnum($value) { + if ($value instanceof BackedEnum) { + return $value; + } + if (is_array($value)) { + $v = reset($value); + if ($v instanceof BackedEnum) { + return $value; + } + return array_map([$this->enumType, 'from'], $value); } From 2f98e11562598d689e702a3c3d4626d97598c997 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Mon, 4 Nov 2024 00:17:23 +0100 Subject: [PATCH 14/26] Remove last use of reflFields in core. --- src/Mapping/LegacyReflectionFields.php | 2 ++ src/Tools/SchemaValidator.php | 7 +++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Mapping/LegacyReflectionFields.php b/src/Mapping/LegacyReflectionFields.php index 579ff311539..1d988607489 100644 --- a/src/Mapping/LegacyReflectionFields.php +++ b/src/Mapping/LegacyReflectionFields.php @@ -15,6 +15,8 @@ use Traversable; use function array_keys; +use function assert; +use function is_string; use function str_contains; use function str_replace; diff --git a/src/Tools/SchemaValidator.php b/src/Tools/SchemaValidator.php index f602f69b4d4..456cbd1f7b1 100644 --- a/src/Tools/SchemaValidator.php +++ b/src/Tools/SchemaValidator.php @@ -23,6 +23,7 @@ use Doctrine\ORM\Mapping\FieldMapping; use ReflectionEnum; use ReflectionNamedType; +use ReflectionProperty; use function array_diff; use function array_filter; @@ -31,7 +32,6 @@ use function array_push; use function array_search; use function array_values; -use function assert; use function class_exists; use function class_parents; use function count; @@ -329,9 +329,8 @@ private function validatePropertiesTypes(ClassMetadata $class): array array_filter( array_map( function (FieldMapping $fieldMapping) use ($class): string|null { - $fieldName = $fieldMapping->fieldName; - assert(isset($class->reflFields[$fieldName])); - $propertyType = $class->reflFields[$fieldName]->getType(); + $fieldName = $fieldMapping->fieldName; + $propertyType = (new ReflectionProperty($fieldMapping->declared ?: $class->name, $fieldName))->getType(); // If the field type is not a built-in type, we cannot check it if (! Type::hasType($fieldMapping->type)) { From c2a2386df968b561dbb628db31e65ae7439179ef Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Mon, 4 Nov 2024 00:35:53 +0100 Subject: [PATCH 15/26] suppress phpcs that cant be done --- src/Mapping/PropertyAccessors/EnumPropertyAccessor.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Mapping/PropertyAccessors/EnumPropertyAccessor.php b/src/Mapping/PropertyAccessors/EnumPropertyAccessor.php index 29ed7dc689f..759717e0d99 100644 --- a/src/Mapping/PropertyAccessors/EnumPropertyAccessor.php +++ b/src/Mapping/PropertyAccessors/EnumPropertyAccessor.php @@ -43,7 +43,7 @@ public function getValue(object $object): mixed * * @return ($enum is BackedEnum ? (string|int) : (string[]|int[])) */ - private function fromEnum($enum) + private function fromEnum($enum) // phpcs:ignore { if (is_array($enum)) { return array_map(static function (BackedEnum $enum) { @@ -62,7 +62,7 @@ private function fromEnum($enum) * @psalm-suppress InvalidReturnStatement * @psalm-suppress PossiblyInvalidArgument */ - private function toEnum($value) + private function toEnum($value) // phpcs:ignore { if ($value instanceof BackedEnum) { return $value; From 8c9bfca25584d1d98bb848658804938a7d2e747f Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Mon, 4 Nov 2024 00:37:45 +0100 Subject: [PATCH 16/26] Fix wrong type, phpstan failure. --- src/Mapping/ClassMetadata.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Mapping/ClassMetadata.php b/src/Mapping/ClassMetadata.php index c810a674719..e9f01e62b69 100644 --- a/src/Mapping/ClassMetadata.php +++ b/src/Mapping/ClassMetadata.php @@ -578,7 +578,7 @@ public function __construct(public string $name, NamingStrategy|null $namingStra * @return LegacyReflectionFields|ReflectionProperty[] An array of ReflectionProperty instances. * @phpstan-return LegacyReflectionFields|array */ - public function getReflectionProperties(): array + public function getReflectionProperties(): array|LegacyReflectionFields { return $this->reflFields; } From 6ff2b130d39d9810978a1c98ac147318e01b0030 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sat, 7 Dec 2024 00:07:50 +0100 Subject: [PATCH 17/26] Add comment to PropertyAccessor interface --- src/Mapping/PropertyAccessors/PropertyAccessor.php | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Mapping/PropertyAccessors/PropertyAccessor.php b/src/Mapping/PropertyAccessors/PropertyAccessor.php index 7316d8a348f..4603ac9c605 100644 --- a/src/Mapping/PropertyAccessors/PropertyAccessor.php +++ b/src/Mapping/PropertyAccessors/PropertyAccessor.php @@ -4,7 +4,17 @@ namespace Doctrine\ORM\Mapping\PropertyAccessors; -/** @internal */ +/** + * A property accessor is a class that allows to read and write properties on objects regardless of visibility. + * + * We use them while creating objects from database rows in {@link UnitOfWork::createEntity()} or when + * computing changesets from objects that are about the written back to the database in {@link UnitOfWork::computeChangeSet()}. + * + * This abstraction over ReflectionProperty is necessary, because for several features of either Doctrine or PHP, we + * need to handle edge cases in reflection at a central location in the code. + * + * @internal + */ interface PropertyAccessor { public function setValue(object $object, mixed $value): void; From 238fb740284c22142019c59703717efff00778ef Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sat, 7 Dec 2024 00:37:59 +0100 Subject: [PATCH 18/26] Add RawValuePropertyAccessor to see how it will look in 8.4, pre support for lazy objects. --- src/Mapping/ClassMetadata.php | 8 ++- .../RawValuePropertyAccessor.php | 52 +++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 src/Mapping/PropertyAccessors/RawValuePropertyAccessor.php diff --git a/src/Mapping/ClassMetadata.php b/src/Mapping/ClassMetadata.php index e9f01e62b69..aff76fe8c05 100644 --- a/src/Mapping/ClassMetadata.php +++ b/src/Mapping/ClassMetadata.php @@ -18,6 +18,7 @@ use Doctrine\ORM\Mapping\PropertyAccessors\EnumPropertyAccessor; use Doctrine\ORM\Mapping\PropertyAccessors\ObjectCastPropertyAccessor; use Doctrine\ORM\Mapping\PropertyAccessors\PropertyAccessor; +use Doctrine\ORM\Mapping\PropertyAccessors\RawValuePropertyAccessor; use Doctrine\ORM\Mapping\PropertyAccessors\ReadonlyAccessor; use Doctrine\ORM\Mapping\PropertyAccessors\TypedNoDefaultPropertyAccessor; use Doctrine\Persistence\Mapping\ClassMetadata as PersistenceClassMetadata; @@ -62,6 +63,8 @@ use function trait_exists; use function trim; +use const PHP_VERSION_ID; + /** * A ClassMetadata instance holds all the object-relational mapping metadata * of an entity and its associations. @@ -2689,7 +2692,10 @@ public function getSequencePrefix(AbstractPlatform $platform): string private function createPropertyAccessor(string $className, string $propertyName): PropertyAccessor { $reflectionProperty = new ReflectionProperty($className, $propertyName); - $accessor = ObjectCastPropertyAccessor::fromReflectionProperty($reflectionProperty); + + $accessor = PHP_VERSION_ID >= 80400 + ? RawValuePropertyAccessor::fromReflectionProperty($reflectionProperty) + : ObjectCastPropertyAccessor::fromReflectionProperty($reflectionProperty); if ($reflectionProperty->hasType() && ! $reflectionProperty->getType()->allowsNull()) { $accessor = new TypedNoDefaultPropertyAccessor($accessor, $reflectionProperty); diff --git a/src/Mapping/PropertyAccessors/RawValuePropertyAccessor.php b/src/Mapping/PropertyAccessors/RawValuePropertyAccessor.php new file mode 100644 index 00000000000..27d3cf6ab87 --- /dev/null +++ b/src/Mapping/PropertyAccessors/RawValuePropertyAccessor.php @@ -0,0 +1,52 @@ +getName(); + $key = $reflectionProperty->isPrivate() ? "\0" . ltrim($reflectionProperty->getDeclaringClass()->getName(), '\\') . "\0" . $name : ($reflectionProperty->isProtected() ? "\0*\0" . $name : $name); + + return new self($reflectionProperty, $key); + } + + private function __construct(private ReflectionProperty $reflectionProperty, private string $key) + { + } + + public function setValue(object $object, mixed $value): void + { + if (! ($object instanceof InternalProxy && ! $object->__isInitialized())) { + $this->reflectionProperty->setRawValue($object, $value); + + return; + } + + $object->__setInitialized(true); + + $this->reflectionProperty->setRawValue($object, $value); + + $object->__setInitialized(false); + } + + public function getValue(object $object): mixed + { + return ((array) $object)[$this->key] ?? null; + } +} From 5a220078e937814edaf23586999c5b1f9871dd62 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sun, 8 Dec 2024 21:11:41 +0100 Subject: [PATCH 19/26] Update PR with PHP Stan by fixing some and baselining other violations. --- phpstan-baseline.neon | 42 ++++++++++++++++++++--------------- src/Mapping/ClassMetadata.php | 2 +- src/Mapping/FieldMapping.php | 3 ++- 3 files changed, 27 insertions(+), 20 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 44cb4153e21..f57c40d880a 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -984,12 +984,6 @@ parameters: count: 1 path: src/Mapping/ClassMetadata.php - - - message: '#^Parameter \#1 \$class of method Doctrine\\Persistence\\Mapping\\ReflectionService\:\:getAccessibleProperty\(\) expects class\-string, string given\.$#' - identifier: argument.type - count: 1 - path: src/Mapping/ClassMetadata.php - - message: '#^Parameter \#1 \$mapping of method Doctrine\\ORM\\Mapping\\ClassMetadata\\:\:validateAndCompleteTypedAssociationMapping\(\) expects array\{type\: 1\|2\|4\|8, fieldName\: string, targetEntity\?\: class\-string\}, non\-empty\-array\ given\.$#' identifier: argument.type @@ -1032,18 +1026,6 @@ parameters: count: 2 path: src/Mapping/ClassMetadata.php - - - message: '#^Parameter \#2 \$class of method Doctrine\\ORM\\Mapping\\ClassMetadata\\:\:getAccessibleProperty\(\) expects class\-string, string given\.$#' - identifier: argument.type - count: 1 - path: src/Mapping/ClassMetadata.php - - - - message: '#^Parameter \#3 \$embeddedClass of class Doctrine\\ORM\\Mapping\\ReflectionEmbeddedProperty constructor expects class\-string, string given\.$#' - identifier: argument.type - count: 1 - path: src/Mapping/ClassMetadata.php - - message: '#^Property Doctrine\\ORM\\Mapping\\ClassMetadata\:\:\$customRepositoryClassName with generic class Doctrine\\ORM\\EntityRepository does not specify its types\: T$#' identifier: missingType.generics @@ -1500,12 +1482,36 @@ parameters: count: 1 path: src/Mapping/JoinTableMapping.php + - + message: '#^Method Doctrine\\ORM\\Mapping\\LegacyReflectionFields\:\:__construct\(\) has parameter \$classMetadata with generic class Doctrine\\ORM\\Mapping\\ClassMetadata but does not specify its types\: T$#' + identifier: missingType.generics + count: 1 + path: src/Mapping/LegacyReflectionFields.php + + - + message: '#^Parameter \#1 \$class of method Doctrine\\Persistence\\Mapping\\ReflectionService\:\:getAccessibleProperty\(\) expects class\-string, string given\.$#' + identifier: argument.type + count: 1 + path: src/Mapping/LegacyReflectionFields.php + - message: '#^Method Doctrine\\ORM\\Mapping\\MappedSuperclass\:\:__construct\(\) has parameter \$repositoryClass with generic class Doctrine\\ORM\\EntityRepository but does not specify its types\: T$#' identifier: missingType.generics count: 1 path: src/Mapping/MappedSuperclass.php + - + message: '#^Method Doctrine\\ORM\\Mapping\\PropertyAccessors\\EnumPropertyAccessor\:\:toEnum\(\) should return array\\|BackedEnum but returns array\\.$#' + identifier: return.type + count: 1 + path: src/Mapping/PropertyAccessors/EnumPropertyAccessor.php + + - + message: '#^Parameter \#1 \$callback of function array_map expects \(callable\(BackedEnum\|int\|string\)\: mixed\)\|null, array\{class\-string\, ''from''\} given\.$#' + identifier: argument.type + count: 1 + path: src/Mapping/PropertyAccessors/EnumPropertyAccessor.php + - message: '#^Method Doctrine\\ORM\\Mapping\\QuoteStrategy\:\:getColumnAlias\(\) has parameter \$class with generic class Doctrine\\ORM\\Mapping\\ClassMetadata but does not specify its types\: T$#' identifier: missingType.generics diff --git a/src/Mapping/ClassMetadata.php b/src/Mapping/ClassMetadata.php index aff76fe8c05..f99fe4c4570 100644 --- a/src/Mapping/ClassMetadata.php +++ b/src/Mapping/ClassMetadata.php @@ -2688,7 +2688,7 @@ public function getSequencePrefix(AbstractPlatform $platform): string return $sequencePrefix; } - /** @phpstan-param class-string $class */ + /** @phpstan-param class-string $className */ private function createPropertyAccessor(string $className, string $propertyName): PropertyAccessor { $reflectionProperty = new ReflectionProperty($className, $propertyName); diff --git a/src/Mapping/FieldMapping.php b/src/Mapping/FieldMapping.php index 928497f776c..37b89aebabe 100644 --- a/src/Mapping/FieldMapping.php +++ b/src/Mapping/FieldMapping.php @@ -54,6 +54,7 @@ final class FieldMapping implements ArrayAccess */ public string|null $inherited = null; + /** @var class-string|null */ public string|null $originalClass = null; public string|null $originalField = null; public bool|null $quoted = null; @@ -101,7 +102,7 @@ public function __construct( * scale?: int|null, * unique?: bool|null, * inherited?: string|null, - * originalClass?: string|null, + * originalClass?: class-string|null, * originalField?: string|null, * quoted?: bool|null, * declared?: string|null, From 82cf29407c5800548722db61be975643de27bb0c Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sat, 15 Feb 2025 00:01:22 +0100 Subject: [PATCH 20/26] Update src/Mapping/PropertyAccessors/PropertyAccessor.php MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Grégoire Paris --- src/Mapping/PropertyAccessors/PropertyAccessor.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Mapping/PropertyAccessors/PropertyAccessor.php b/src/Mapping/PropertyAccessors/PropertyAccessor.php index 4603ac9c605..be3254a0676 100644 --- a/src/Mapping/PropertyAccessors/PropertyAccessor.php +++ b/src/Mapping/PropertyAccessors/PropertyAccessor.php @@ -8,7 +8,7 @@ * A property accessor is a class that allows to read and write properties on objects regardless of visibility. * * We use them while creating objects from database rows in {@link UnitOfWork::createEntity()} or when - * computing changesets from objects that are about the written back to the database in {@link UnitOfWork::computeChangeSet()}. + * computing changesets from objects that are about to be written back to the database in {@link UnitOfWork::computeChangeSet()}. * * This abstraction over ReflectionProperty is necessary, because for several features of either Doctrine or PHP, we * need to handle edge cases in reflection at a central location in the code. From 1cae0534a09b25280ac08cd29df4f78c102d7d1c Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sat, 15 Feb 2025 21:38:09 +0100 Subject: [PATCH 21/26] Extract PropertyAccessorFactory, tests for enum and typednodefault accessors. --- src/Mapping/ClassMetadata.php | 41 +++--------- .../PropertyAccessorFactory.php | 32 ++++++++++ .../EnumPropertyAccessorTest.php | 64 +++++++++++++++++++ .../TypedNoDefaultPropertyAccessorTest.php | 43 +++++++++++++ 4 files changed, 147 insertions(+), 33 deletions(-) create mode 100644 src/Mapping/PropertyAccessors/PropertyAccessorFactory.php create mode 100644 tests/Tests/ORM/Mapping/PropertyAccessors/EnumPropertyAccessorTest.php create mode 100644 tests/Tests/ORM/Mapping/PropertyAccessors/TypedNoDefaultPropertyAccessorTest.php diff --git a/src/Mapping/ClassMetadata.php b/src/Mapping/ClassMetadata.php index f99fe4c4570..0b60ad2bac6 100644 --- a/src/Mapping/ClassMetadata.php +++ b/src/Mapping/ClassMetadata.php @@ -16,11 +16,8 @@ use Doctrine\ORM\Id\AbstractIdGenerator; use Doctrine\ORM\Mapping\PropertyAccessors\EmbeddablePropertyAccessor; use Doctrine\ORM\Mapping\PropertyAccessors\EnumPropertyAccessor; -use Doctrine\ORM\Mapping\PropertyAccessors\ObjectCastPropertyAccessor; use Doctrine\ORM\Mapping\PropertyAccessors\PropertyAccessor; -use Doctrine\ORM\Mapping\PropertyAccessors\RawValuePropertyAccessor; -use Doctrine\ORM\Mapping\PropertyAccessors\ReadonlyAccessor; -use Doctrine\ORM\Mapping\PropertyAccessors\TypedNoDefaultPropertyAccessor; +use Doctrine\ORM\Mapping\PropertyAccessors\PropertyAccessorFactory; use Doctrine\Persistence\Mapping\ClassMetadata as PersistenceClassMetadata; use Doctrine\Persistence\Mapping\ReflectionService; use InvalidArgumentException; @@ -63,8 +60,6 @@ use function trait_exists; use function trim; -use const PHP_VERSION_ID; - /** * A ClassMetadata instance holds all the object-relational mapping metadata * of an entity and its associations. @@ -832,7 +827,7 @@ public function wakeupReflection(ReflectionService $reflService): void foreach ($this->embeddedClasses as $property => $embeddedClass) { if (isset($embeddedClass->declaredField)) { assert($embeddedClass->originalField !== null); - $childAccessor = $this->createPropertyAccessor( + $childAccessor = PropertyAccessorFactory::createPropertyAccessor( $this->embeddedClasses[$embeddedClass->declaredField]->class, $embeddedClass->originalField, ); @@ -846,7 +841,7 @@ public function wakeupReflection(ReflectionService $reflService): void continue; } - $accessor = $this->createPropertyAccessor( + $accessor = PropertyAccessorFactory::createPropertyAccessor( $embeddedClass->declared ?? $this->name, $property, ); @@ -859,7 +854,7 @@ public function wakeupReflection(ReflectionService $reflService): void if (isset($mapping->declaredField) && isset($parentAccessors[$mapping->declaredField])) { assert($mapping->originalField !== null); assert($mapping->originalClass !== null); - $accessor = $this->createPropertyAccessor($mapping->originalClass, $mapping->originalField); + $accessor = PropertyAccessorFactory::createPropertyAccessor($mapping->originalClass, $mapping->originalField); if ($mapping->enumType !== null) { $accessor = new EnumPropertyAccessor( @@ -877,8 +872,8 @@ public function wakeupReflection(ReflectionService $reflService): void } $this->propertyAccessors[$field] = isset($mapping->declared) - ? $this->createPropertyAccessor($mapping->declared, $field) - : $this->createPropertyAccessor($this->name, $field); + ? PropertyAccessorFactory::createPropertyAccessor($mapping->declared, $field) + : PropertyAccessorFactory::createPropertyAccessor($this->name, $field); if ($mapping->enumType !== null) { $this->propertyAccessors[$field] = new EnumPropertyAccessor( @@ -890,8 +885,8 @@ public function wakeupReflection(ReflectionService $reflService): void foreach ($this->associationMappings as $field => $mapping) { $this->propertyAccessors[$field] = isset($mapping->declared) - ? $this->createPropertyAccessor($mapping->declared, $field) - : $this->createPropertyAccessor($this->name, $field); + ? PropertyAccessorFactory::createPropertyAccessor($mapping->declared, $field) + : PropertyAccessorFactory::createPropertyAccessor($this->name, $field); } } @@ -2687,24 +2682,4 @@ public function getSequencePrefix(AbstractPlatform $platform): string return $sequencePrefix; } - - /** @phpstan-param class-string $className */ - private function createPropertyAccessor(string $className, string $propertyName): PropertyAccessor - { - $reflectionProperty = new ReflectionProperty($className, $propertyName); - - $accessor = PHP_VERSION_ID >= 80400 - ? RawValuePropertyAccessor::fromReflectionProperty($reflectionProperty) - : ObjectCastPropertyAccessor::fromReflectionProperty($reflectionProperty); - - if ($reflectionProperty->hasType() && ! $reflectionProperty->getType()->allowsNull()) { - $accessor = new TypedNoDefaultPropertyAccessor($accessor, $reflectionProperty); - } - - if ($reflectionProperty->isReadOnly()) { - $accessor = new ReadonlyAccessor($accessor, $reflectionProperty); - } - - return $accessor; - } } diff --git a/src/Mapping/PropertyAccessors/PropertyAccessorFactory.php b/src/Mapping/PropertyAccessors/PropertyAccessorFactory.php new file mode 100644 index 00000000000..46d5409ad58 --- /dev/null +++ b/src/Mapping/PropertyAccessors/PropertyAccessorFactory.php @@ -0,0 +1,32 @@ += 80400 + ? RawValuePropertyAccessor::fromReflectionProperty($reflectionProperty) + : ObjectCastPropertyAccessor::fromReflectionProperty($reflectionProperty); + + if ($reflectionProperty->hasType() && ! $reflectionProperty->getType()->allowsNull()) { + $accessor = new TypedNoDefaultPropertyAccessor($accessor, $reflectionProperty); + } + + if ($reflectionProperty->isReadOnly()) { + $accessor = new ReadonlyAccessor($accessor, $reflectionProperty); + } + + return $accessor; + } +} diff --git a/tests/Tests/ORM/Mapping/PropertyAccessors/EnumPropertyAccessorTest.php b/tests/Tests/ORM/Mapping/PropertyAccessors/EnumPropertyAccessorTest.php new file mode 100644 index 00000000000..a92df1fde27 --- /dev/null +++ b/tests/Tests/ORM/Mapping/PropertyAccessors/EnumPropertyAccessorTest.php @@ -0,0 +1,64 @@ +setValue($object, EnumType::A); + + $this->assertEquals($object->enum, EnumType::A); + $this->assertEquals(EnumType::A->value, $accessor->getValue($object)); + } + + public function testEnumSetDatabaseGetValue(): void + { + $object = new EnumClass(); + $accessor = PropertyAccessorFactory::createPropertyAccessor(EnumClass::class, 'enum'); + + $accessor = new EnumPropertyAccessor($accessor, EnumType::class); + + $accessor->setValue($object, EnumType::A->value); + + $this->assertEquals($object->enum, EnumType::A); + $this->assertEquals(EnumType::A->value, $accessor->getValue($object)); + } + + public function testEnumSetDatabaseArrayGetValue(): void + { + $object = new EnumClass(); + $accessor = PropertyAccessorFactory::createPropertyAccessor(EnumClass::class, 'enumList'); + + $accessor = new EnumPropertyAccessor($accessor, EnumType::class); + + $accessor->setValue($object, $values = [EnumType::A->value, EnumType::B->value, EnumType::C->value]); + + $this->assertEquals($object->enumList, [EnumType::A, EnumType::B, EnumType::C]); + $this->assertEquals($values, $accessor->getValue($object)); + } +} + +class EnumClass +{ + public EnumType $enum; + public array $enumList; +} + +enum EnumType: string +{ + case A = 'a'; + case B = 'b'; + case C = 'c'; +} diff --git a/tests/Tests/ORM/Mapping/PropertyAccessors/TypedNoDefaultPropertyAccessorTest.php b/tests/Tests/ORM/Mapping/PropertyAccessors/TypedNoDefaultPropertyAccessorTest.php new file mode 100644 index 00000000000..64b8be7a60c --- /dev/null +++ b/tests/Tests/ORM/Mapping/PropertyAccessors/TypedNoDefaultPropertyAccessorTest.php @@ -0,0 +1,43 @@ +assertInstanceOf(TypedNoDefaultPropertyAccessor::class, $accessor); + + $object = new TypedClass(); + $accessor->setValue($object, 42); + $this->assertEquals(42, $accessor->getValue($object)); + } + + public function testSetNullWithoutDefault(): void + { + $accessor = PropertyAccessorFactory::createPropertyAccessor(TypedClass::class, 'property'); + + $object = new TypedClass(); + $accessor->setValue($object, null); + $this->assertNull($accessor->getValue($object)); + + $accessor->setValue($object, 42); + $this->assertEquals(42, $accessor->getValue($object)); + + $accessor->setValue($object, null); + $this->assertNull($accessor->getValue($object)); + } +} + +class TypedClass +{ + public int $property; +} From 673cf0d4d8f5ba00c6ece096f1c80d991fd9811c Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sat, 15 Feb 2025 21:45:01 +0100 Subject: [PATCH 22/26] Add test for ObjectCastPropertyAccessor. --- .../ObjectCastPropertyAccessorTest.php | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 tests/Tests/ORM/Mapping/PropertyAccessors/ObjectCastPropertyAccessorTest.php diff --git a/tests/Tests/ORM/Mapping/PropertyAccessors/ObjectCastPropertyAccessorTest.php b/tests/Tests/ORM/Mapping/PropertyAccessors/ObjectCastPropertyAccessorTest.php new file mode 100644 index 00000000000..0f9c2e3c841 --- /dev/null +++ b/tests/Tests/ORM/Mapping/PropertyAccessors/ObjectCastPropertyAccessorTest.php @@ -0,0 +1,79 @@ +setValue($object, 'value'); + + $this->assertEquals($object->property, 'value'); + $this->assertEquals('value', $accessor->getValue($object)); + } + + public function testSetGetPrivatePropertyValue(): void + { + $object = new ObjectClass(); + $accessor = ObjectCastPropertyAccessor::fromNames(ObjectClass::class, 'property2'); + + $accessor->setValue($object, 'value'); + + $this->assertEquals($object->getProperty2(), 'value'); + $this->assertEquals('value', $accessor->getValue($object)); + } + + public function testSetGetInternalProxyValue(): void + { + $object = new ObjectClassInternalProxy(); + $accessor = ObjectCastPropertyAccessor::fromNames(ObjectClassInternalProxy::class, 'property'); + + $accessor->setValue($object, 'value'); + + $this->assertEquals($object->property, 'value'); + $this->assertEquals('value', $accessor->getValue($object)); + $this->assertFalse($object->isInitialized); + $this->assertEquals(2, $object->counter); + } +} + +class ObjectClass +{ + public $property; + private $property2; + + public function getProperty2() + { + return $this->property2; + } +} + +class ObjectClassInternalProxy implements InternalProxy +{ + public $property; + public $isInitialized = false; + public $counter = 0; + + public function __setInitialized(bool $initialized): void + { + $this->isInitialized = $initialized; + $this->counter++; + } + + public function __load(): void + { + } + + /** Returns whether this proxy is initialized or not. */ + public function __isInitialized(): bool + { + return $this->isInitialized; + } +} From 72ce662e45e3a911079d1aaa4277af5c6b2d1ae5 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sat, 15 Feb 2025 22:09:36 +0100 Subject: [PATCH 23/26] Tests for ObjectCastPropertyAccessor and RawValuePropertyAccessor. --- tests/Tests/Models/PropertyHooks/User.php | 46 ++++++++++++++++ .../ObjectCastPropertyAccessorTest.php | 11 ++-- .../RawValuePropertyAccessorTest.php | 54 +++++++++++++++++++ 3 files changed, 108 insertions(+), 3 deletions(-) create mode 100644 tests/Tests/Models/PropertyHooks/User.php create mode 100644 tests/Tests/ORM/Mapping/PropertyAccessors/RawValuePropertyAccessorTest.php diff --git a/tests/Tests/Models/PropertyHooks/User.php b/tests/Tests/Models/PropertyHooks/User.php new file mode 100644 index 00000000000..101e41b5b3e --- /dev/null +++ b/tests/Tests/Models/PropertyHooks/User.php @@ -0,0 +1,46 @@ +first = $value; + } + } + + public string $last { + set { + if (strlen($value) === 0) { + throw new ValueError("Name must be non-empty"); + } + $this->last = $value; + } + } + + public string $fullName { + // Override the "read" action with arbitrary logic. + get => $this->first . " " . $this->last; + + // Override the "write" action with arbitrary logic. + set { + [$this->first, $this->last] = explode(' ', $value, 2); + } + } + + public string $language = 'de' { + // Override the "read" action with arbitrary logic. + get => strtoupper($this->language); + + // Override the "write" action with arbitrary logic. + set { + $this->language = strtolower($value); + } + } +} diff --git a/tests/Tests/ORM/Mapping/PropertyAccessors/ObjectCastPropertyAccessorTest.php b/tests/Tests/ORM/Mapping/PropertyAccessors/ObjectCastPropertyAccessorTest.php index 0f9c2e3c841..bc8b5d2ebd4 100644 --- a/tests/Tests/ORM/Mapping/PropertyAccessors/ObjectCastPropertyAccessorTest.php +++ b/tests/Tests/ORM/Mapping/PropertyAccessors/ObjectCastPropertyAccessorTest.php @@ -1,5 +1,7 @@ property2; } @@ -57,9 +61,10 @@ public function getProperty2() class ObjectClassInternalProxy implements InternalProxy { + /** @var string */ public $property; - public $isInitialized = false; - public $counter = 0; + public bool $isInitialized = false; + public int $counter = 0; public function __setInitialized(bool $initialized): void { diff --git a/tests/Tests/ORM/Mapping/PropertyAccessors/RawValuePropertyAccessorTest.php b/tests/Tests/ORM/Mapping/PropertyAccessors/RawValuePropertyAccessorTest.php new file mode 100644 index 00000000000..9ef1e15bcb5 --- /dev/null +++ b/tests/Tests/ORM/Mapping/PropertyAccessors/RawValuePropertyAccessorTest.php @@ -0,0 +1,54 @@ += 8.4.0')] +class RawValuePropertyAccessorTest extends OrmTestCase +{ + public function testSetGetValue(): void + { + $object = new User(); + $reflection = new ReflectionObject($object); + $accessorFirst = RawValuePropertyAccessor::fromReflectionProperty($reflection->getProperty('first')); + $accessorLast = RawValuePropertyAccessor::fromReflectionProperty($reflection->getProperty('last')); + + $accessorFirst->setValue($object, 'Benjamin'); + $accessorLast->setValue($object, 'Eberlei'); + + self::assertEquals('Benjamin Eberlei', $object->fullName); + self::assertEquals('Benjamin', $accessorFirst->getValue($object)); + self::assertEquals('Eberlei', $accessorLast->getValue($object)); + + $accessorFirst->setValue($object, ''); + $accessorLast->setValue($object, ''); + + self::assertEquals('', trim($object->fullName)); + } + + public function testSetGetValueWithLanguage(): void + { + $object = new User(); + $reflection = new ReflectionObject($object); + $accessor = RawValuePropertyAccessor::fromReflectionProperty($reflection->getProperty('language')); + + $accessor->setValue($object, 'en'); + + self::assertEquals('EN', $object->language); + self::assertEquals('en', $accessor->getValue($object)); + + $accessor->setValue($object, 'EN'); + + self::assertEquals('EN', $object->language); + self::assertEquals('EN', $accessor->getValue($object)); + } +} From e7db1b005f382eb2f03df9b5470199c675048fde Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sat, 15 Feb 2025 22:17:29 +0100 Subject: [PATCH 24/26] Add ReadOnlyAccessorTest --- .../ReadOnlyAccessorTest.php | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 tests/Tests/ORM/Mapping/PropertyAccessors/ReadOnlyAccessorTest.php diff --git a/tests/Tests/ORM/Mapping/PropertyAccessors/ReadOnlyAccessorTest.php b/tests/Tests/ORM/Mapping/PropertyAccessors/ReadOnlyAccessorTest.php new file mode 100644 index 00000000000..262c0f3a60d --- /dev/null +++ b/tests/Tests/ORM/Mapping/PropertyAccessors/ReadOnlyAccessorTest.php @@ -0,0 +1,42 @@ +assertInstanceOf(ReadonlyAccessor::class, $accessor); + + $accessor->setValue($object, 1); + + $this->assertEquals($object->property, 1); + $this->assertEquals(1, $accessor->getValue($object)); + } + + public function testReadOnlyPropertyOnlyOnce(): void + { + $object = new ReadOnlyClass(); + $accessor = PropertyAccessorFactory::createPropertyAccessor(ReadOnlyClass::class, 'property'); + + $this->assertInstanceOf(ReadonlyAccessor::class, $accessor); + + $this->expectException(LogicException::class); + + $accessor->setValue($object, 1); + $accessor->setValue($object, 2); + } +} + +class ReadOnlyClass +{ + public readonly int $property; +} From 8e1a27b8cc813b060a495992291d610ea4ebb302 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sat, 15 Feb 2025 22:32:16 +0100 Subject: [PATCH 25/26] Explain deprecation in UPGRADE.md --- UPGRADE.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/UPGRADE.md b/UPGRADE.md index 73ae9d9a3cd..c4a06347365 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -1,8 +1,21 @@ # Upgrade to 3.4 +## Discriminator Map class duplicates + Using the same class several times in a discriminator map is deprecated. In 4.0, this will be an error. +## `Doctrine\ORM\Mapping\ClassMetadata::$reflFields` deprecated + +To better support property hooks and lazy proxies in the future, `$reflFields` had to +be deprecated because we cannot use the PHP internal reflection API directly anymore. + +The property was changed from an array to an object of type `LegacyReflectionFields` +that implements `ArrayAccess`. + +Use the new `Doctrine\ORM\Mapping\PropertyAccessors\PropertyAccessor` API and access +through `Doctrine\ORM\Mapping\ClassMetadata::$propertyAccessors` instead. + # Upgrade to 3.3 ## Deprecate `DatabaseDriver` From 5077ae41e52152228238f53a04e5bb2f48b49834 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sat, 15 Feb 2025 23:25:34 +0100 Subject: [PATCH 26/26] Housekeeping --- .../ORM/Mapping/PropertyAccessors/ReadOnlyAccessorTest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/Tests/ORM/Mapping/PropertyAccessors/ReadOnlyAccessorTest.php b/tests/Tests/ORM/Mapping/PropertyAccessors/ReadOnlyAccessorTest.php index 262c0f3a60d..56df2aaad44 100644 --- a/tests/Tests/ORM/Mapping/PropertyAccessors/ReadOnlyAccessorTest.php +++ b/tests/Tests/ORM/Mapping/PropertyAccessors/ReadOnlyAccessorTest.php @@ -1,5 +1,7 @@