diff --git a/composer.json b/composer.json index cf7b249ae57..4821b195c53 100644 --- a/composer.json +++ b/composer.json @@ -36,7 +36,9 @@ "psr/cache": "^1 || ^2 || ^3", "symfony/console": "^3.0 || ^4.0 || ^5.0 || ^6.0", "symfony/polyfill-php72": "^1.23", - "symfony/polyfill-php80": "^1.16" + "symfony/polyfill-php80": "^1.16", + "symfony/var-dumper": "^6.1", + "symfony/var-exporter": "^6.2@dev" }, "require-dev": { "doctrine/annotations": "^1.13", diff --git a/docs/en/cookbook/accessing-private-properties-of-the-same-class-from-different-instance.rst b/docs/en/cookbook/accessing-private-properties-of-the-same-class-from-different-instance.rst deleted file mode 100644 index f6a1b423f5a..00000000000 --- a/docs/en/cookbook/accessing-private-properties-of-the-same-class-from-different-instance.rst +++ /dev/null @@ -1,74 +0,0 @@ -Accessing private/protected properties/methods of the same class from different instance -======================================================================================== - -.. sectionauthor:: Michael Olsavsky (olsavmic) - -As explained in the :doc:`restrictions for entity classes in the manual <../reference/architecture>`, -it is dangerous to access private/protected properties of different entity instance of the same class because of lazy loading. - -The proxy instance that's injected instead of the real entity may not be initialized yet -and therefore not contain expected data which may result in unexpected behavior. -That's a limitation of current proxy implementation - only public methods automatically initialize proxies. - -It is usually preferable to use a public interface to manipulate the object from outside the `$this` -context but it may not be convenient in some cases. The following example shows how to do it safely. - -Safely accessing private properties from different instance of the same class ------------------------------------------------------------------------------ - -To safely access private property of different instance of the same class, make sure to initialise -the proxy before use manually as follows: - -.. code-block:: php - - parent instanceof Proxy) { - $this->parent->__load(); - } - - // Accessing the `$this->parent->name` property without loading the proxy first - // may throw error in case the Proxy has not been initialized yet. - $this->parent->name; - } - - public function doSomethingWithAnotherInstance(self $instance) - { - // Always initializing the proxy before use - if ($instance instanceof Proxy) { - $instance->__load(); - } - - // Accessing the `$instance->name` property without loading the proxy first - // may throw error in case the Proxy has not been initialized yet. - $instance->name; - } - - // ... - } diff --git a/docs/en/cookbook/implementing-wakeup-or-clone.rst b/docs/en/cookbook/implementing-wakeup-or-clone.rst deleted file mode 100644 index c65a9a62216..00000000000 --- a/docs/en/cookbook/implementing-wakeup-or-clone.rst +++ /dev/null @@ -1,78 +0,0 @@ -Implementing Wakeup or Clone -============================ - -.. sectionauthor:: Roman Borschel (roman@code-factory.org) - -As explained in the :ref:`restrictions for entity classes in the manual -`, -it is usually not allowed for an entity to implement ``__wakeup`` -or ``__clone``, because Doctrine makes special use of them. -However, it is quite easy to make use of these methods in a safe -way by guarding the custom wakeup or clone code with an entity -identity check, as demonstrated in the following sections. - -Safely implementing __wakeup ----------------------------- - -To safely implement ``__wakeup``, simply enclose your -implementation code in an identity check as follows: - -.. code-block:: php - - id) { - // ... Your code here as normal ... - } - // otherwise do nothing, do NOT throw an exception! - } - - // ... - } - -Safely implementing __clone ---------------------------- - -Safely implementing ``__clone`` is pretty much the same: - -.. code-block:: php - - id) { - // ... Your code here as normal ... - } - // otherwise do nothing, do NOT throw an exception! - } - - // ... - } - -Summary -------- - -As you have seen, it is quite easy to safely make use of -``__wakeup`` and ``__clone`` in your entities without adding any -really Doctrine-specific or Doctrine-dependant code. - -These implementations are possible and safe because when Doctrine -invokes these methods, the entities never have an identity (yet). -Furthermore, it is possibly a good idea to check for the identity -in your code anyway, since it's rarely the case that you want to -unserialize or clone an entity with no identity. - - diff --git a/docs/en/index.rst b/docs/en/index.rst index d0e16d22b3b..effca58d9a9 100644 --- a/docs/en/index.rst +++ b/docs/en/index.rst @@ -112,7 +112,6 @@ Cookbook * **Implementation**: :doc:`Array Access ` | :doc:`Notify ChangeTracking Example ` | - :doc:`Using Wakeup Or Clone ` | :doc:`Working with DateTime ` | :doc:`Validation ` | :doc:`Entities in the Session ` | diff --git a/docs/en/reference/advanced-configuration.rst b/docs/en/reference/advanced-configuration.rst index 87790a2c58b..9852a9cbdad 100644 --- a/docs/en/reference/advanced-configuration.rst +++ b/docs/en/reference/advanced-configuration.rst @@ -324,13 +324,14 @@ identifier. You could simply do this: $item = $em->getReference('MyProject\Model\Item', $itemId); $cart->addItem($item); -Here, we added an Item to a Cart without loading the Item from the -database. If you invoke any method on the Item instance, it would -fully initialize its state transparently from the database. Here -$item is actually an instance of the proxy class that was generated -for the Item class but your code does not need to care. In fact it -**should not care**. Proxy objects should be transparent to your -code. +Here, we added an ``Item`` to a ``Cart`` without loading the Item from the +database. +If you access any persistent state that isn't yet available in the ``Item`` +instance, the proxying mechanism would fully initialize the object's state +transparently from the database. +Here ``$item`` is actually an instance of the proxy class that was generated +for the ``Item`` class but your code does not need to care. In fact it +**should not care**. Proxy objects should be transparent to your code. Association proxies ~~~~~~~~~~~~~~~~~~~ @@ -338,7 +339,7 @@ Association proxies The second most important situation where Doctrine uses proxy objects is when querying for objects. Whenever you query for an object that has a single-valued association to another object that -is configured LAZY, without joining that association in the same +is configured ``LAZY``, without joining that association in the same query, Doctrine puts proxy objects in place where normally the associated object would be. Just like other proxies it will transparently initialize itself on first access. diff --git a/docs/en/reference/architecture.rst b/docs/en/reference/architecture.rst index 74a5950434f..19e6c81f8c8 100644 --- a/docs/en/reference/architecture.rst +++ b/docs/en/reference/architecture.rst @@ -74,32 +74,13 @@ Entities An entity is a lightweight, persistent domain object. An entity can be any regular PHP class observing the following restrictions: - -- An entity class must not be final or contain final methods. -- All persistent properties/field of any entity class should - always be private or protected, otherwise lazy-loading might not - work as expected. In case you serialize entities (for example Session) - properties should be protected (See Serialize section below). -- An entity class must not implement ``__clone`` or - :doc:`do so safely <../cookbook/implementing-wakeup-or-clone>`. -- An entity class must not implement ``__wakeup`` or - :doc:`do so safely <../cookbook/implementing-wakeup-or-clone>`. - You can also consider implementing - `Serializable `_, - but be aware that it is deprecated since PHP 8.1. We do not recommend its usage. -- PHP 7.4 introduces :doc:`the new magic method ` - ``__unserialize``, which changes the execution priority between - ``__wakeup`` and itself when used. This can cause unexpected behaviour in - an Entity. +- An entity class must not be final nor readonly but it may contain + readonly properties or final methods. - Any two entity classes in a class hierarchy that inherit directly or indirectly from one another must not have a mapped property with the same name. That is, if B inherits from A then B must not have a mapped field with the same name as an already mapped field that is inherited from A. -- An entity cannot make use of func_get_args() to implement variable parameters. - Generated proxies do not support this for performance reasons and your code might - actually fail to work when violating this restriction. -- Entity cannot access private/protected properties/methods of another entity of the same class or :doc:`do so safely <../cookbook/accessing-private-properties-of-the-same-class-from-different-instance>`. Entities support inheritance, polymorphic associations, and polymorphic queries. Both abstract and concrete classes can be @@ -157,19 +138,17 @@ subsequent access must be through the interface type. Serializing entities ~~~~~~~~~~~~~~~~~~~~ -Serializing entities can be problematic and is not really -recommended, at least not as long as an entity instance still holds -references to proxy objects or is still managed by an -EntityManager. If you intend to serialize (and unserialize) entity -instances that still hold references to proxy objects you may run -into problems with private properties because of technical -limitations. Proxy objects implement ``__sleep`` and it is not -possible for ``__sleep`` to return names of private properties in -parent classes. On the other hand it is not a solution for proxy -objects to implement ``Serializable`` because Serializable does not -work well with any potential cyclic object references (at least we -did not find a way yet, if you did, please contact us). The -``Serializable`` interface is also deprecated beginning with PHP 8.1. +Serializing entities is generally to be avoided. + +If you intend to serialize (and unserialize) entities that still +hold references to proxy objects you may run into problems, because +all proxy properties will be initialized recursively, leading to +large serialized object graphs, especially for circular associations. + +If you really must serialize entities, regardless if proxies are +involved or not, then consider implementing the ``__serialize()`` +and ``__unserialize()`` magic methods and manually checking for +cyclic dependencies in your object graph. The EntityManager ~~~~~~~~~~~~~~~~~ diff --git a/docs/en/reference/dql-doctrine-query-language.rst b/docs/en/reference/dql-doctrine-query-language.rst index 2c045ba7706..31889f50ad3 100644 --- a/docs/en/reference/dql-doctrine-query-language.rst +++ b/docs/en/reference/dql-doctrine-query-language.rst @@ -180,10 +180,10 @@ not need to lazy load the association with another query. Doctrine allows you to walk all the associations between all the objects in your domain model. Objects that were not already - loaded from the database are replaced with lazy load proxy - instances. Non-loaded Collections are also replaced by lazy-load + loaded from the database are replaced with lazy-loading proxy + instances. Non-loaded Collections are also replaced by lazy-loading instances that fetch all the contained objects upon first access. - However relying on the lazy-load mechanism leads to many small + However relying on the lazy-loading mechanism leads to many small queries executed against the database, which can significantly affect the performance of your application. **Fetch Joins** are the solution to hydrate most or all of the entities that you need in a diff --git a/docs/en/reference/limitations-and-known-issues.rst b/docs/en/reference/limitations-and-known-issues.rst index 61c1e06bb8c..fa0f2be094f 100644 --- a/docs/en/reference/limitations-and-known-issues.rst +++ b/docs/en/reference/limitations-and-known-issues.rst @@ -177,27 +177,3 @@ MySQL with MyISAM tables Doctrine cannot provide atomic operations when calling ``EntityManager#flush()`` if one of the tables involved uses the storage engine MyISAM. You must use InnoDB or other storage engines that support transactions if you need integrity. - -Entities, Proxies and Reflection -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Using methods for Reflection on entities can be prone to error, when the entity -is actually a proxy the following methods will not work correctly: - -- ``new ReflectionClass`` -- ``new ReflectionObject`` -- ``get_class()`` -- ``get_parent_class()`` - -This is why ``Doctrine\Common\Util\ClassUtils`` class exists that has similar -methods, which resolve the proxy problem beforehand. - -.. code-block:: php - - getReference('Acme\Book'); - - $reflection = ClassUtils::newReflectionClass($bookProxy); - $class = ClassUtils::getClass($bookProxy)¸ diff --git a/docs/en/reference/working-with-objects.rst b/docs/en/reference/working-with-objects.rst index 2364530ec78..d397d845f76 100644 --- a/docs/en/reference/working-with-objects.rst +++ b/docs/en/reference/working-with-objects.rst @@ -161,28 +161,6 @@ your code. See the following code: echo "This will always be true!"; } -A slice of the generated proxy classes code looks like the -following piece of code. A real proxy class override ALL public -methods along the lines of the ``getName()`` method shown below: - -.. code-block:: php - - _load(); - return parent::getName(); - } - // .. other public methods of User - } - .. warning:: Traversing the object graph for parts that are lazy-loaded will @@ -413,14 +391,6 @@ Example: // $entity now refers to the fully managed copy returned by the merge operation. // The EntityManager $em now manages the persistence of $entity as usual. -.. note:: - - When you want to serialize/unserialize entities you - have to make all entity properties protected, never private. The - reason for this is, if you serialize a class that was a proxy - instance before, the private variables won't be serialized and a - PHP Notice is thrown. - The semantics of the merge operation, applied to an entity X, are as follows: diff --git a/docs/en/sidebar.rst b/docs/en/sidebar.rst index 26c3a702d31..0430bcc7504 100644 --- a/docs/en/sidebar.rst +++ b/docs/en/sidebar.rst @@ -73,7 +73,6 @@ cookbook/dql-user-defined-functions cookbook/implementing-arrayaccess-for-domain-objects cookbook/implementing-the-notify-changetracking-policy - cookbook/implementing-wakeup-or-clone cookbook/resolve-target-entity-listener cookbook/sql-table-prefixes cookbook/strategy-cookbook-introduction diff --git a/docs/en/toc.rst b/docs/en/toc.rst index f5e9330ad5b..fa92cf38021 100644 --- a/docs/en/toc.rst +++ b/docs/en/toc.rst @@ -75,7 +75,6 @@ Cookbook cookbook/dql-user-defined-functions cookbook/implementing-arrayaccess-for-domain-objects cookbook/implementing-the-notify-changetracking-policy - cookbook/implementing-wakeup-or-clone cookbook/resolve-target-entity-listener cookbook/sql-table-prefixes cookbook/strategy-cookbook-introduction diff --git a/docs/en/tutorials/getting-started.rst b/docs/en/tutorials/getting-started.rst index a1979ba98c9..6c8ec45f6fd 100644 --- a/docs/en/tutorials/getting-started.rst +++ b/docs/en/tutorials/getting-started.rst @@ -43,14 +43,15 @@ What are Entities? Entities are PHP Objects that can be identified over many requests by a unique identifier or primary key. These classes don't need to extend any -abstract base class or interface. An entity class must not be final -or contain final methods. Additionally it must not implement -**clone** nor **wakeup**, unless it :doc:`does so safely <../cookbook/implementing-wakeup-or-clone>`. +abstract base class or interface. An entity contains persistable properties. A persistable property is an instance variable of the entity that is saved into and retrieved from the database by Doctrine's data mapping capabilities. +An entity class must not be ``final`` nor ``readonly``, although it +can contain ``final`` methods or ``readonly`` properties. + An Example Model: Bug Tracker ----------------------------- @@ -884,18 +885,6 @@ domain model to match the requirements: understand the changes that have happened to the collection that are noteworthy for persistence. -.. warning:: - - Lazy load proxies always contain an instance of - Doctrine's EntityManager and all its dependencies. Therefore a - ``var_dump()`` will possibly dump a very large recursive structure - which is impossible to render and read. You have to use - ``Doctrine\Common\Util\Debug::dump()`` to restrict the dumping to a - human readable level. Additionally you should be aware that dumping - the EntityManager to a Browser may take several minutes, and the - ``Debug::dump()`` method just ignores any occurrences of it in Proxy - instances. - Because we only work with collections for the references we must be careful to implement a bidirectional reference in the domain model. The concept of owning or inverse side of a relation is central to @@ -1512,39 +1501,8 @@ The output of the engineer’s name is fetched from the database! What is happen Since we only retrieved the bug by primary key both the engineer and reporter are not immediately loaded from the database but are replaced by LazyLoading -proxies. These proxies will load behind the scenes, when the first method -is called on them. - -Sample code of this proxy generated code can be found in the specified Proxy -Directory, it looks like: - -.. code-block:: php - - _load(); - return parent::addReportedBug($bug); - } - - public function assignedToBug($bug) - { - $this->_load(); - return parent::assignedToBug($bug); - } - } - -See how upon each method call the proxy is lazily loaded from the -database? +proxies. These proxies will load behind the scenes, when attempting to access +any of their un-initialized state. The call prints: diff --git a/lib/Doctrine/ORM/Internal/Hydration/SimpleObjectHydrator.php b/lib/Doctrine/ORM/Internal/Hydration/SimpleObjectHydrator.php index 454330290eb..353c65d3bd7 100644 --- a/lib/Doctrine/ORM/Internal/Hydration/SimpleObjectHydrator.php +++ b/lib/Doctrine/ORM/Internal/Hydration/SimpleObjectHydrator.php @@ -148,10 +148,6 @@ protected function hydrateRowData(array $row, array &$result) } } - if (isset($this->_hints[Query::HINT_REFRESH_ENTITY])) { - $this->registerManaged($this->class, $this->_hints[Query::HINT_REFRESH_ENTITY], $data); - } - $uow = $this->_em->getUnitOfWork(); $entity = $uow->createEntity($entityName, $data, $this->_hints); diff --git a/lib/Doctrine/ORM/Proxy/ProxyFactory.php b/lib/Doctrine/ORM/Proxy/ProxyFactory.php index ffa87ccbb35..4a14f583ce2 100644 --- a/lib/Doctrine/ORM/Proxy/ProxyFactory.php +++ b/lib/Doctrine/ORM/Proxy/ProxyFactory.php @@ -6,16 +6,26 @@ use Closure; use Doctrine\Common\Proxy\AbstractProxyFactory; -use Doctrine\Common\Proxy\Proxy as BaseProxy; +use Doctrine\Common\Proxy\Proxy as CommonProxy; use Doctrine\Common\Proxy\ProxyDefinition; use Doctrine\Common\Proxy\ProxyGenerator; use Doctrine\Common\Util\ClassUtils; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityNotFoundException; use Doctrine\ORM\Persisters\Entity\EntityPersister; +use Doctrine\ORM\Proxy\Proxy as LegacyProxy; +use Doctrine\Persistence\Proxy; use Doctrine\ORM\UnitOfWork; use Doctrine\ORM\Utility\IdentifierFlattener; use Doctrine\Persistence\Mapping\ClassMetadata; +use Symfony\Component\VarExporter\Hydrator; +use Symfony\Component\VarExporter\LazyGhostTrait; +use Symfony\Component\VarExporter\ProxyHelper; +use Symfony\Component\VarExporter\VarExporter; + +use function array_flip; + +use const PHP_VERSION_ID; /** * This factory is used to create proxy objects for entities at runtime. @@ -24,6 +34,21 @@ */ class ProxyFactory extends AbstractProxyFactory { + private const PROXY_CLASS_TEMPLATE = <<<'EOPHP' +; + +/** + * DO NOT EDIT THIS FILE - IT WAS CREATED BY DOCTRINE\'S PROXY GENERATOR + */ +class extends \ implements \ +{ + +} + +EOPHP; + /** @var EntityManagerInterface The EntityManager this factory is bound to. */ private $em; @@ -55,7 +80,15 @@ public function __construct(EntityManagerInterface $em, $proxyDir, $proxyNs, $au { $proxyGenerator = new ProxyGenerator($proxyDir, $proxyNs); - $proxyGenerator->setPlaceholder('baseProxyInterface', Proxy::class); + if (trait_exists(LazyGhostTrait::class)) { + $proxyGenerator->setPlaceholder('baseProxyInterface', Proxy::class); + $proxyGenerator->setPlaceholder('proxyBody', \Closure::fromCallable([$this, 'generateProxyBody'])); + $proxyGenerator->setPlaceholder('proxyModifiers', \Closure::fromCallable([$this, 'generateProxyModifiers'])); + $proxyGenerator->setProxyClassTemplate(self::PROXY_CLASS_TEMPLATE); + } else { + $proxyGenerator->setPlaceholder('baseProxyInterface', LegacyProxy::class); + } + parent::__construct($proxyGenerator, $em->getMetadataFactory(), $autoGenerate); $this->em = $em; @@ -82,19 +115,27 @@ protected function createProxyDefinition($className) $classMetadata = $this->em->getClassMetadata($className); $entityPersister = $this->uow->getEntityPersister($className); + if (trait_exists(LazyGhostTrait::class, false)) { + $initializer = $this->createLazyInitializer($classMetadata, $entityPersister); + $cloner = null; + } else { + $initializer = $this->createInitializer($classMetadata, $entityPersister); + $cloner = $this->createCloner($classMetadata, $entityPersister); + } + return new ProxyDefinition( ClassUtils::generateProxyClassName($className, $this->proxyNs), $classMetadata->getIdentifierFieldNames(), $classMetadata->getReflectionProperties(), - $this->createInitializer($classMetadata, $entityPersister), - $this->createCloner($classMetadata, $entityPersister) + $initializer, + $cloner ); } /** * Creates a closure capable of initializing a proxy * - * @psalm-return Closure(BaseProxy):void + * @psalm-return Closure(CommonProxy):void * * @throws EntityNotFoundException */ @@ -102,7 +143,7 @@ private function createInitializer(ClassMetadata $classMetadata, EntityPersister { $wakeupProxy = $classMetadata->getReflectionClass()->hasMethod('__wakeup'); - return function (BaseProxy $proxy) use ($entityPersister, $classMetadata, $wakeupProxy): void { + return function (CommonProxy $proxy) use ($entityPersister, $classMetadata, $wakeupProxy): void { $initializer = $proxy->__getInitializer(); $cloner = $proxy->__getCloner(); @@ -142,16 +183,44 @@ private function createInitializer(ClassMetadata $classMetadata, EntityPersister }; } + /** + * Creates a closure capable of initializing a proxy + * + * @psalm-return Closure(Proxy):void + * + * @throws EntityNotFoundException + */ + private function createLazyInitializer(ClassMetadata $classMetadata, EntityPersister $entityPersister): Closure + { + return function (Proxy $proxy) use ($entityPersister, $classMetadata): void { + $identifier = $classMetadata->getIdentifierValues($proxy); + $entity = $entityPersister->loadById($identifier, $proxy); + + if ($entity === $proxy) { + return; + } + + if ($entity === null) { + throw EntityNotFoundException::fromClassNameAndIdentifier( + $classMetadata->getName(), + $this->identifierFlattener->flattenIdentifier($classMetadata, $identifier) + ); + } + + Hydrator::hydrate($proxy, (array) $entity); + }; + } + /** * Creates a closure capable of finalizing state a cloned proxy * - * @psalm-return Closure(BaseProxy):void + * @psalm-return Closure(CommonProxy):void * * @throws EntityNotFoundException */ private function createCloner(ClassMetadata $classMetadata, EntityPersister $entityPersister): Closure { - return function (BaseProxy $proxy) use ($entityPersister, $classMetadata): void { + return function (CommonProxy $proxy) use ($entityPersister, $classMetadata): void { if ($proxy->__isInitialized()) { return; } @@ -180,4 +249,57 @@ private function createCloner(ClassMetadata $classMetadata, EntityPersister $ent } }; } + + private function generateProxyModifiers(ClassMetadata $class): string + { + return PHP_VERSION_ID >= 80200 && $class->getReflectionClass()->isReadOnly() ? 'readonly ' : ''; + } + + private function generateProxyBody(ClassMetadata $class): string + { + $body = ProxyHelper::generateLazyGhost($class->getReflectionClass()); + $body = substr($body, 7 + strpos($body, "\n{")); + $body = substr($body, 0, strpos($body, "\n}")); + $body = str_replace('LazyGhostTrait;', str_replace("\n ", "\n", 'LazyGhostTrait { + isLazyObjectInitialized as __isInitialized; + initializeLazyObject as __load; + createLazyGhost as private; + resetLazyObject as private; + }'), $body); + + $body .= ' + + public function __construct(?\Closure $initializer = null) + { + self::createLazyGhost($initializer, ' . $this->exportSkippedProperties($class) . ', $this); + }'; + + return $body; + } + + private function exportSkippedProperties(ClassMetadata $class): string + { + $skippedProperties = []; + $identifiers = array_flip($class->getIdentifierFieldNames()); + + foreach ($class->getReflectionClass()->getProperties() as $property) { + $name = $property->getName(); + + if ($property->isStatic() || (($class->hasField($name) || $class->hasAssociation($name)) && ! isset($identifiers[$name]))) { + continue; + } + + $prefix = $property->isPrivate() ? "\0" . $property->getDeclaringClass()->getName() . "\0" : ($property->isProtected() ? "\0*\0" : ''); + + $skippedProperties[$prefix . $name] = true; + } + + uksort($skippedProperties, 'strnatcmp'); + + $export = VarExporter::export($skippedProperties); + $export = str_replace(VarExporter::export($class->getName()), 'parent::class', $export); + $export = str_replace("\n", "\n ", $export); + + return $export; + } } diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index 951867b8cdc..da596bf48f7 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -694,7 +694,7 @@ public function computeChangeSet(ClassMetadata $class, $entity) $value->setOwner($entity, $assoc); $value->setDirty(! $value->isEmpty()); - $class->reflFields[$name]->setValue($entity, $value); + $refProp->setValue($entity, $value); $actualData[$name] = $value; @@ -2689,15 +2689,9 @@ public function createEntity($className, array $data, &$hints = []) && $unmanagedProxy instanceof Proxy && $this->isIdentifierEquals($unmanagedProxy, $entity) ) { - // DDC-1238 - we have a managed instance, but it isn't the provided one. - // Therefore we clear its identifier. Also, we must re-fetch metadata since the - // refreshed object may be anything - - foreach ($class->identifier as $fieldName) { - $class->reflFields[$fieldName]->setValue($unmanagedProxy, null); - } - - return $unmanagedProxy; + // We will hydrate the given un-managed proxy anyway: + // continue work, but consider it the entity from now on + $entity = $unmanagedProxy; } } @@ -2870,23 +2864,25 @@ public function createEntity($className, array $data, &$hints = []) break; default: + $normalizedAssociatedId = $this->normalizeIdentifier($targetClass, $associatedId); + switch (true) { // We are negating the condition here. Other cases will assume it is valid! case $hints['fetchMode'][$class->name][$field] !== ClassMetadata::FETCH_EAGER: - $newValue = $this->em->getProxyFactory()->getProxy($assoc['targetEntity'], $associatedId); + $newValue = $this->em->getProxyFactory()->getProxy($assoc['targetEntity'], $normalizedAssociatedId); break; // Deferred eager load only works for single identifier classes case isset($hints[self::HINT_DEFEREAGERLOAD]) && ! $targetClass->isIdentifierComposite: // TODO: Is there a faster approach? - $this->eagerLoadingEntities[$targetClass->rootEntityName][$relatedIdHash] = current($associatedId); + $this->eagerLoadingEntities[$targetClass->rootEntityName][$relatedIdHash] = current($normalizedAssociatedId); - $newValue = $this->em->getProxyFactory()->getProxy($assoc['targetEntity'], $associatedId); + $newValue = $this->em->getProxyFactory()->getProxy($assoc['targetEntity'], $normalizedAssociatedId); break; default: // TODO: This is very imperformant, ignore it? - $newValue = $this->em->find($assoc['targetEntity'], $associatedId); + $newValue = $this->em->find($assoc['targetEntity'], $normalizedAssociatedId); break; } @@ -3682,4 +3678,38 @@ private function convertSingleFieldIdentifierToPHPValue(ClassMetadata $class, $i $class->getTypeOfField($class->getSingleIdentifierFieldName()) ); } + + /** + * Given a flat identifier, this method will produce another flat identifier, but with all + * association fields that are mapped as identifiers replaced by entity references, recursively. + */ + private function normalizeIdentifier(ClassMetadata $targetClass, array $flatIdentifier): array + { + $normalizedAssociatedId = []; + + foreach ($targetClass->getIdentifierFieldNames() as $name) { + if (! array_key_exists($name, $flatIdentifier)) { + continue; + } + + if (! $targetClass->isSingleValuedAssociation($name)) { + $normalizedAssociatedId[$name] = $flatIdentifier[$name]; + continue; + } + + $targetIdMetadata = $this->em->getClassMetadata($targetClass->getAssociationTargetClass($name)); + + // Note: the ORM prevents using an entity with a composite identifier as an identifier association + // therefore, reset($targetIdMetadata->identifier) is always correct + $normalizedAssociatedId[$name] = $this->em->getReference( + $targetIdMetadata->getName(), + $this->normalizeIdentifier( + $targetIdMetadata, + [reset($targetIdMetadata->identifier) => $flatIdentifier[$name]] + ) + ); + } + + return $normalizedAssociatedId; + } } diff --git a/tests/Doctrine/Tests/ORM/Functional/DetachedEntityTest.php b/tests/Doctrine/Tests/ORM/Functional/DetachedEntityTest.php index 17badfecabb..a2d907b70e2 100644 --- a/tests/Doctrine/Tests/ORM/Functional/DetachedEntityTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/DetachedEntityTest.php @@ -4,6 +4,7 @@ namespace Doctrine\Tests\ORM\Functional; +use Doctrine\Common\Proxy\Proxy as CommonProxy; use Doctrine\DBAL\Exception\UniqueConstraintViolationException; use Doctrine\ORM\OptimisticLockException; use Doctrine\Persistence\Proxy; @@ -153,7 +154,12 @@ public function testUninitializedLazyAssociationsAreIgnoredOnMerge(): void self::assertFalse($address2->user->__isInitialized()); $detachedAddress2 = unserialize(serialize($address2)); self::assertInstanceOf(Proxy::class, $detachedAddress2->user); - self::assertFalse($detachedAddress2->user->__isInitialized()); + + if ($detachedAddress2->user instanceof Proxy && ! $detachedAddress2->user instanceof CommonProxy) { + self::assertTrue($detachedAddress2->user->__isInitialized()); + } else { + self::assertFalse($detachedAddress2->user->__isInitialized()); + } $managedAddress2 = $this->_em->merge($detachedAddress2); self::assertInstanceOf(Proxy::class, $managedAddress2->user); diff --git a/tests/Doctrine/Tests/ORM/Functional/MergeProxiesTest.php b/tests/Doctrine/Tests/ORM/Functional/MergeProxiesTest.php index 2acf9843c38..6256a6f2c12 100644 --- a/tests/Doctrine/Tests/ORM/Functional/MergeProxiesTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/MergeProxiesTest.php @@ -67,7 +67,7 @@ public function testMergeUnserializedUnInitializedProxy(): void self::assertSame( $managed, - $this->_em->merge(unserialize(serialize($this->_em->merge($detachedUninitialized)))) + $this->_em->merge($this->_em->merge($detachedUninitialized)) ); self::assertFalse($managed->__isInitialized()); diff --git a/tests/Doctrine/Tests/ORM/Functional/ProxiesLikeEntitiesTest.php b/tests/Doctrine/Tests/ORM/Functional/ProxiesLikeEntitiesTest.php index 74ea162fbce..61443dd17b1 100644 --- a/tests/Doctrine/Tests/ORM/Functional/ProxiesLikeEntitiesTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/ProxiesLikeEntitiesTest.php @@ -58,7 +58,6 @@ public function testPersistUpdate(): void { // Considering case (a) $proxy = $this->_em->getProxyFactory()->getProxy(CmsUser::class, ['id' => 123]); - $proxy->__isInitialized__ = true; $proxy->id = null; $proxy->username = 'ocra'; $proxy->name = 'Marco'; diff --git a/tests/Doctrine/Tests/ORM/Functional/ReferenceProxyTest.php b/tests/Doctrine/Tests/ORM/Functional/ReferenceProxyTest.php index 10ef8561f2a..76b13c2e5a6 100644 --- a/tests/Doctrine/Tests/ORM/Functional/ReferenceProxyTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/ReferenceProxyTest.php @@ -4,6 +4,7 @@ namespace Doctrine\Tests\ORM\Functional; +use Doctrine\Common\Proxy\Proxy as CommonProxy; use Doctrine\Common\Util\ClassUtils; use Doctrine\Persistence\Proxy; use Doctrine\Tests\Models\Company\CompanyAuction; @@ -151,7 +152,11 @@ public function testWakeupCalledOnProxy(): void $entity->setName('Doctrine 2 Cookbook'); - self::assertTrue($entity->wakeUp, 'Loading the proxy should call __wakeup().'); + if ($entity instanceof CommonProxy) { + self::assertTrue($entity->wakeUp, 'Loading the proxy should call __wakeup().'); + } else { + self::assertFalse($entity->wakeUp, 'Loading the proxy should not call __wakeup().'); + } } public function testDoNotInitializeProxyOnGettingTheIdentifier(): void diff --git a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheManyToOneTest.php b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheManyToOneTest.php index cd2e2527891..538edd9d575 100644 --- a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheManyToOneTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheManyToOneTest.php @@ -251,11 +251,11 @@ public function testPutAndLoadNonCacheableCompositeManyToOne(): void self::assertInstanceOf(Action::class, $entity->getComplexAction()->getAction1()); self::assertInstanceOf(Action::class, $entity->getComplexAction()->getAction2()); - $this->assertQueryCount(1); + $this->assertQueryCount(0); self::assertEquals('login', $entity->getComplexAction()->getAction1()->name); - $this->assertQueryCount(1); + $this->assertQueryCount(0); self::assertEquals('rememberme', $entity->getComplexAction()->getAction2()->name); - $this->assertQueryCount(1); + $this->assertQueryCount(0); } } diff --git a/tests/Doctrine/Tests/ORM/Proxy/ProxyFactoryTest.php b/tests/Doctrine/Tests/ORM/Proxy/ProxyFactoryTest.php index 18b47d334d4..81df1aad346 100644 --- a/tests/Doctrine/Tests/ORM/Proxy/ProxyFactoryTest.php +++ b/tests/Doctrine/Tests/ORM/Proxy/ProxyFactoryTest.php @@ -143,8 +143,6 @@ public function testFailedProxyLoadingDoesNotMarkTheProxyAsInitialized(): void } self::assertFalse($proxy->__isInitialized()); - self::assertInstanceOf(Closure::class, $proxy->__getInitializer(), 'The initializer wasn\'t removed'); - self::assertInstanceOf(Closure::class, $proxy->__getCloner(), 'The cloner wasn\'t removed'); } /** @group DDC-2432 */ @@ -171,8 +169,6 @@ public function testFailedProxyCloningDoesNotMarkTheProxyAsInitialized(): void } self::assertFalse($proxy->__isInitialized()); - self::assertInstanceOf(Closure::class, $proxy->__getInitializer(), 'The initializer wasn\'t removed'); - self::assertInstanceOf(Closure::class, $proxy->__getCloner(), 'The cloner wasn\'t removed'); } public function testProxyClonesParentFields(): void