diff --git a/.github/workflows/coding-standards.yml b/.github/workflows/coding-standards.yml index 755956430e8..d9ff9aaac0f 100644 --- a/.github/workflows/coding-standards.yml +++ b/.github/workflows/coding-standards.yml @@ -24,4 +24,4 @@ on: jobs: coding-standards: - uses: "doctrine/.github/.github/workflows/coding-standards.yml@7.2.1" + uses: "doctrine/.github/.github/workflows/coding-standards.yml@7.2.2" diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index deec2b9dee8..c32cd1c6d2f 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -17,4 +17,4 @@ on: jobs: documentation: name: "Documentation" - uses: "doctrine/.github/.github/workflows/documentation.yml@7.2.1" + uses: "doctrine/.github/.github/workflows/documentation.yml@7.2.2" diff --git a/.github/workflows/release-on-milestone-closed.yml b/.github/workflows/release-on-milestone-closed.yml index d7ad5ab4e21..bcbfa8deb3c 100644 --- a/.github/workflows/release-on-milestone-closed.yml +++ b/.github/workflows/release-on-milestone-closed.yml @@ -7,7 +7,7 @@ on: jobs: release: - uses: "doctrine/.github/.github/workflows/release-on-milestone-closed.yml@7.2.1" + uses: "doctrine/.github/.github/workflows/release-on-milestone-closed.yml@7.2.2" secrets: GIT_AUTHOR_EMAIL: ${{ secrets.GIT_AUTHOR_EMAIL }} GIT_AUTHOR_NAME: ${{ secrets.GIT_AUTHOR_NAME }} diff --git a/composer.json b/composer.json index c4de9690eaf..860cc846a4b 100644 --- a/composer.json +++ b/composer.json @@ -41,14 +41,14 @@ }, "require-dev": { "doctrine/annotations": "^1.13 || ^2", - "doctrine/coding-standard": "^9.0.2 || ^12.0", + "doctrine/coding-standard": "^9.0.2 || ^13.0", "phpbench/phpbench": "^0.16.10 || ^1.0", "phpstan/extension-installer": "~1.1.0 || ^1.4", "phpstan/phpstan": "~1.4.10 || 2.0.3", "phpstan/phpstan-deprecation-rules": "^1 || ^2", "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6", "psr/log": "^1 || ^2 || ^3", - "squizlabs/php_codesniffer": "3.7.2", + "squizlabs/php_codesniffer": "3.12.0", "symfony/cache": "^4.4 || ^5.4 || ^6.4 || ^7.0", "symfony/var-exporter": "^4.4 || ^5.4 || ^6.2 || ^7.0", "symfony/yaml": "^3.4 || ^4.0 || ^5.0 || ^6.0 || ^7.0" diff --git a/docs/en/reference/advanced-configuration.rst b/docs/en/reference/advanced-configuration.rst index 42a0833aca1..769153050f9 100644 --- a/docs/en/reference/advanced-configuration.rst +++ b/docs/en/reference/advanced-configuration.rst @@ -71,8 +71,8 @@ Configuration Options The following sections describe all the configuration options available on a ``Doctrine\ORM\Configuration`` instance. -Proxy Directory (***REQUIRED***) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Proxy Directory (**REQUIRED**) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: php @@ -85,8 +85,8 @@ classes. For a detailed explanation on proxy classes and how they are used in Doctrine, refer to the "Proxy Objects" section further down. -Proxy Namespace (***REQUIRED***) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Proxy Namespace (**REQUIRED**) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: php @@ -98,8 +98,8 @@ Gets or sets the namespace to use for generated proxy classes. For a detailed explanation on proxy classes and how they are used in Doctrine, refer to the "Proxy Objects" section further down. -Metadata Driver (***REQUIRED***) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Metadata Driver (**REQUIRED**) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: php @@ -144,8 +144,8 @@ accept either a single directory as a string or an array of directories. With this feature a single driver can support multiple directories of Entities. -Metadata Cache (***RECOMMENDED***) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Metadata Cache (**RECOMMENDED**) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: php @@ -166,8 +166,8 @@ For development you should use an array cache like ``Symfony\Component\Cache\Adapter\ArrayAdapter`` which only caches data on a per-request basis. -Query Cache (***RECOMMENDED***) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Query Cache (**RECOMMENDED**) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: php @@ -189,8 +189,8 @@ For development you should use an array cache like ``Symfony\Component\Cache\Adapter\ArrayAdapter`` which only caches data on a per-request basis. -SQL Logger (***Optional***) -~~~~~~~~~~~~~~~~~~~~~~~~~~~ +SQL Logger (**Optional**) +~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: php @@ -202,8 +202,8 @@ Gets or sets the logger to use for logging all SQL statements executed by Doctrine. The logger class must implement the deprecated ``Doctrine\DBAL\Logging\SQLLogger`` interface. -Auto-generating Proxy Classes (***OPTIONAL***) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Auto-generating Proxy Classes (**OPTIONAL**) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Proxy classes can either be generated manually through the Doctrine Console or automatically at runtime by Doctrine. The configuration @@ -446,7 +446,7 @@ correctly if sub-namespaces use different metadata driver implementations. -Default Repository (***OPTIONAL***) +Default Repository (**OPTIONAL**) ----------------------------------- Specifies the FQCN of a subclass of the EntityRepository. @@ -461,7 +461,7 @@ That will be available for all entities without a custom repository class. The default value is ``Doctrine\ORM\EntityRepository``. Any repository class must be a subclass of EntityRepository otherwise you got an ORMException -Ignoring entities (***OPTIONAL***) +Ignoring entities (**OPTIONAL**) ----------------------------------- Specifies the Entity FQCNs to ignore. diff --git a/docs/en/reference/association-mapping.rst b/docs/en/reference/association-mapping.rst index 3db6dfa457b..4ee569abc79 100644 --- a/docs/en/reference/association-mapping.rst +++ b/docs/en/reference/association-mapping.rst @@ -1426,7 +1426,7 @@ Is essentially the same as following: - + diff --git a/docs/en/reference/events.rst b/docs/en/reference/events.rst index dbde6d19df2..b3cd8a67138 100644 --- a/docs/en/reference/events.rst +++ b/docs/en/reference/events.rst @@ -299,7 +299,7 @@ specific to a particular entity class's lifecycle. diff --git a/docs/en/reference/second-level-cache.rst b/docs/en/reference/second-level-cache.rst index cf5cb37d4fa..34c82d20bdf 100644 --- a/docs/en/reference/second-level-cache.rst +++ b/docs/en/reference/second-level-cache.rst @@ -323,7 +323,7 @@ level cache region. @@ -431,7 +431,7 @@ It caches the primary keys of association and cache each element will be cached diff --git a/docs/en/reference/working-with-associations.rst b/docs/en/reference/working-with-associations.rst index 2c265679fa1..b9f99f36ee5 100644 --- a/docs/en/reference/working-with-associations.rst +++ b/docs/en/reference/working-with-associations.rst @@ -736,6 +736,35 @@ methods: .. note:: - There is a limitation on the compatibility of Criteria comparisons. - You have to use scalar values only as the value in a comparison or - the behaviour between different backends is not the same. + Depending on whether the collection has already been loaded from the + database or not, criteria matching may happen at the database/SQL level + or on objects in memory. This may lead to different results and come + surprising, for example when a code change in one place leads to a collection + becoming initialized and, as a side effect, returning a different result + or even breaking a ``matching()`` call somewhere else. Also, collection + initialization state in practical use cases may differ from the one covered + in unit tests. + + Database level comparisons are based on scalar representations of the values + stored in entity properties. The field names passed to expressions correspond + to property names. Comparison and sorting may be affected by + database-specific behavior. For example, MySQL enum types sort by index position, + not lexicographically by value. + + In-memory handling is based on the ``Selectable`` API of `Doctrine Collections `. + In this case, field names passed to expressions are being used to derive accessor + method names. Strict type comparisons are used for equal and not-equal checks, + and generally PHP language rules are being used for other comparison operators + or sorting. + + As a general guidance, for consistent results use the Criteria API with scalar + values only. Note that ``DateTime`` and ``DateTimeImmutable`` are two predominant + examples of value objects that are *not* scalars. + + Refrain from using special database-level column types or custom Doctrine Types + that may lead to database-specific comparison or sorting rules being applied, or + to database-level values being different from object field values. + + Provide accessor methods for all entity fields used in criteria expressions, + and implement those methods in a way that their return value is the + same as the database-level value. diff --git a/docs/en/reference/xml-mapping.rst b/docs/en/reference/xml-mapping.rst index c8c1abe51d4..76268b5c797 100644 --- a/docs/en/reference/xml-mapping.rst +++ b/docs/en/reference/xml-mapping.rst @@ -17,7 +17,7 @@ setup for the latest code in trunk. .. code-block:: xml @@ -103,7 +103,7 @@ of several common elements: // Doctrine.Tests.ORM.Mapping.User.dcm.xml @@ -770,7 +770,7 @@ entity relationship. You can define this in XML with the "association-key" attri .. code-block:: xml diff --git a/docs/en/tutorials/composite-primary-keys.rst b/docs/en/tutorials/composite-primary-keys.rst index 386f8f140c0..9e28184389b 100644 --- a/docs/en/tutorials/composite-primary-keys.rst +++ b/docs/en/tutorials/composite-primary-keys.rst @@ -86,7 +86,7 @@ and year of production as primary keys: @@ -127,11 +127,12 @@ And for querying you can use arrays to both DQL and EntityRepositories: namespace VehicleCatalogue\Model; // $em is the EntityManager - $audi = $em->find("VehicleCatalogue\Model\Car", array("name" => "Audi A8", "year" => 2010)); + $audi = $em->find("VehicleCatalogue\Model\Car", ["name" => "Audi A8", "year" => 2010]); - $dql = "SELECT c FROM VehicleCatalogue\Model\Car c WHERE c.id = ?1"; + $dql = "SELECT c FROM VehicleCatalogue\Model\Car c WHERE c.name = ?1 AND c.year = ?2"; $audi = $em->createQuery($dql) - ->setParameter(1, ["name" => "Audi A8", "year" => 2010]) + ->setParameter(1, "Audi A8") + ->setParameter(2, 2010) ->getSingleResult(); You can also use this entity in associations. Doctrine will then generate two foreign keys one for ``name`` @@ -268,7 +269,7 @@ We keep up the example of an Article with arbitrary attributes, the mapping look .. code-block:: xml diff --git a/docs/en/tutorials/extra-lazy-associations.rst b/docs/en/tutorials/extra-lazy-associations.rst index fbf1f00abd6..660043872ab 100644 --- a/docs/en/tutorials/extra-lazy-associations.rst +++ b/docs/en/tutorials/extra-lazy-associations.rst @@ -87,7 +87,7 @@ switch to extra lazy as shown in these examples: diff --git a/docs/en/tutorials/getting-started.rst b/docs/en/tutorials/getting-started.rst index 1fa463dd797..43cad63e343 100644 --- a/docs/en/tutorials/getting-started.rst +++ b/docs/en/tutorials/getting-started.rst @@ -558,7 +558,7 @@ methods, but you only need to choose one. @@ -1139,7 +1139,7 @@ the ``Product`` before: @@ -1294,7 +1294,7 @@ Finally, we'll add metadata mappings for the ``User`` entity. @@ -1818,7 +1818,7 @@ we have to adjust the metadata slightly. .. code-block:: xml diff --git a/docs/en/tutorials/working-with-indexed-associations.rst b/docs/en/tutorials/working-with-indexed-associations.rst index e6822de3952..43c63c1b3f1 100644 --- a/docs/en/tutorials/working-with-indexed-associations.rst +++ b/docs/en/tutorials/working-with-indexed-associations.rst @@ -128,7 +128,7 @@ here are the code and mappings for it: diff --git a/docs/en/tutorials/working-with-indexed-associations/market.xml b/docs/en/tutorials/working-with-indexed-associations/market.xml index 3fc9fa2a857..726d1d60884 100644 --- a/docs/en/tutorials/working-with-indexed-associations/market.xml +++ b/docs/en/tutorials/working-with-indexed-associations/market.xml @@ -1,6 +1,6 @@ diff --git a/src/Mapping/ClassMetadataInfo.php b/src/Mapping/ClassMetadataInfo.php index 335d9e9ca2e..4cf28c25963 100644 --- a/src/Mapping/ClassMetadataInfo.php +++ b/src/Mapping/ClassMetadataInfo.php @@ -1278,7 +1278,6 @@ public function enableAssociationCache($fieldName, array $cache) /** * @param string $fieldName - * @param array $cache * @phpstan-param array{usage?: int|null, region?: string|null} $cache * * @return int[]|string[] diff --git a/src/ORMInvalidArgumentException.php b/src/ORMInvalidArgumentException.php index 2181d207c28..b2db07ff43b 100644 --- a/src/ORMInvalidArgumentException.php +++ b/src/ORMInvalidArgumentException.php @@ -309,7 +309,7 @@ private static function newEntityFoundThroughRelationshipMessage(array $associat . ' configured to cascade persist operations for entity: ' . self::objToStr($entity) . '.' . ' To solve this issue: Either explicitly call EntityManager#persist()' . ' on this unknown entity or configure cascade persist' - . ' this association in the mapping for example @ManyToOne(..,cascade={"persist"}).' + . ' this association in the mapping for example #[ORM\ManyToOne(..., cascade: [\'persist\'])].' . (method_exists($entity, '__toString') ? '' : ' If you cannot find out which entity causes the problem implement \'' diff --git a/src/Persisters/Entity/BasicEntityPersister.php b/src/Persisters/Entity/BasicEntityPersister.php index 4073c606edc..2e905bca7ae 100644 --- a/src/Persisters/Entity/BasicEntityPersister.php +++ b/src/Persisters/Entity/BasicEntityPersister.php @@ -1374,6 +1374,12 @@ protected function getSelectColumnsSQL() $joinCondition[] = $this->getSQLTableAlias($association['sourceEntity'], $assocAlias) . '.' . $sourceCol . ' = ' . $this->getSQLTableAlias($association['targetEntity']) . '.' . $targetCol; } + + // Add filter SQL + $filterSql = $this->generateFilterConditionSQL($eagerEntity, $joinTableAlias); + if ($filterSql) { + $joinCondition[] = $filterSql; + } } $this->currentPersisterContext->selectJoinSql .= ' ' . $joinTableName . ' ' . $joinTableAlias . ' ON '; diff --git a/src/Proxy/ProxyFactory.php b/src/Proxy/ProxyFactory.php index 9abe921d381..d91b4feebce 100644 --- a/src/Proxy/ProxyFactory.php +++ b/src/Proxy/ProxyFactory.php @@ -378,7 +378,7 @@ private function createLazyInitializer(ClassMetadata $classMetadata, EntityPersi $class = $entityPersister->getClassMetadata(); foreach ($class->getReflectionProperties() as $property) { - if (isset($identifier[$property->name]) || ! $class->hasField($property->name) && ! $class->hasAssociation($property->name)) { + if (isset($identifier[$property->name])) { continue; } @@ -448,7 +448,7 @@ private function getProxyFactory(string $className): Closure foreach ($reflector->getProperties($filter) as $property) { $name = $property->name; - if ($property->isStatic() || (($class->hasField($name) || $class->hasAssociation($name)) && ! isset($identifiers[$name]))) { + if ($property->isStatic() || ! isset($identifiers[$name])) { continue; } diff --git a/src/Tools/Pagination/LimitSubqueryOutputWalker.php b/src/Tools/Pagination/LimitSubqueryOutputWalker.php index 95b9066db2b..ea7ec0f630d 100644 --- a/src/Tools/Pagination/LimitSubqueryOutputWalker.php +++ b/src/Tools/Pagination/LimitSubqueryOutputWalker.php @@ -105,17 +105,24 @@ public function __construct($query, $parserResult, array $queryComponents) $this->platform = $query->getEntityManager()->getConnection()->getDatabasePlatform(); $this->rsm = $parserResult->getResultSetMapping(); - $query = clone $query; + $cloneQuery = clone $query; + + $cloneQuery->setParameters(clone $query->getParameters()); + $cloneQuery->setCacheable(false); + + foreach ($query->getHints() as $name => $value) { + $cloneQuery->setHint($name, $value); + } // Reset limit and offset - $this->firstResult = $query->getFirstResult(); - $this->maxResults = $query->getMaxResults(); - $query->setFirstResult(0)->setMaxResults(null); + $this->firstResult = $cloneQuery->getFirstResult(); + $this->maxResults = $cloneQuery->getMaxResults(); + $cloneQuery->setFirstResult(0)->setMaxResults(null); - $this->em = $query->getEntityManager(); + $this->em = $cloneQuery->getEntityManager(); $this->quoteStrategy = $this->em->getConfiguration()->getQuoteStrategy(); - parent::__construct($query, $parserResult, $queryComponents); + parent::__construct($cloneQuery, $parserResult, $queryComponents); } /** diff --git a/src/UnitOfWork.php b/src/UnitOfWork.php index ff30f14f6d8..e10e4148b4d 100644 --- a/src/UnitOfWork.php +++ b/src/UnitOfWork.php @@ -49,6 +49,7 @@ use Exception; use InvalidArgumentException; use RuntimeException; +use Symfony\Component\VarExporter\Hydrator; use UnexpectedValueException; use function array_chunk; @@ -2944,6 +2945,11 @@ public function createEntity($className, array $data, &$hints = []) if ($this->isUninitializedObject($entity)) { $entity->__setInitialized(true); + + if ($this->em->getConfiguration()->isLazyGhostObjectEnabled()) { + // Initialize properties that have default values to their default value (similar to what + Hydrator::hydrate($entity, (array) $class->reflClass->newInstanceWithoutConstructor()); + } } else { if ( ! isset($hints[Query::HINT_REFRESH]) diff --git a/tests/Tests/Mocks/ConnectionMock.php b/tests/Tests/Mocks/ConnectionMock.php index e25c8e0d5dc..41035c3e182 100644 --- a/tests/Tests/Mocks/ConnectionMock.php +++ b/tests/Tests/Mocks/ConnectionMock.php @@ -118,13 +118,11 @@ public function setDatabasePlatform(AbstractPlatform $platform): void $this->_platformMock = $platform; } - /** @return array */ public function getExecuteStatements(): array { return $this->_executeStatements; } - /** @return array */ public function getDeletes(): array { return $this->_deletes; diff --git a/tests/Tests/Models/GH11524/GH11524Entity.php b/tests/Tests/Models/GH11524/GH11524Entity.php new file mode 100644 index 00000000000..175c8e77a3b --- /dev/null +++ b/tests/Tests/Models/GH11524/GH11524Entity.php @@ -0,0 +1,31 @@ +getObject(); + + if (! $object instanceof GH11524Relation) { + return; + } + + $object->setCurrentLocale('en'); + } +} diff --git a/tests/Tests/Models/GH11524/GH11524Relation.php b/tests/Tests/Models/GH11524/GH11524Relation.php new file mode 100644 index 00000000000..ae028a5f7cf --- /dev/null +++ b/tests/Tests/Models/GH11524/GH11524Relation.php @@ -0,0 +1,50 @@ +currentLocale = $locale; + } + + public function getTranslation(): string + { + if ($this->currentLocale === null) { + throw new LogicException('The current locale must be set to retrieve translation.'); + } + + return 'fake'; + } +} diff --git a/tests/Tests/ORM/Functional/EagerFetchCollectionTest.php b/tests/Tests/ORM/Functional/EagerFetchCollectionTest.php index 88397c6a12f..3a21a93831c 100644 --- a/tests/Tests/ORM/Functional/EagerFetchCollectionTest.php +++ b/tests/Tests/ORM/Functional/EagerFetchCollectionTest.php @@ -7,9 +7,11 @@ use Doctrine\Common\Collections\ArrayCollection; use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Query\QueryException; +use Doctrine\ORM\Tools\Pagination\Paginator; use Doctrine\Tests\OrmFunctionalTestCase; use function count; +use function iterator_to_array; class EagerFetchCollectionTest extends OrmFunctionalTestCase { @@ -96,6 +98,16 @@ public function testSubselectFetchJoinWithAllowedWhenOverriddenNotEager(): void $this->assertIsString($query->getSql()); } + public function testSubselectFetchJoinWithAllowedWhenOverriddenNotEagerPaginator(): void + { + $query = $this->_em->createQuery('SELECT o, c FROM ' . EagerFetchOwner::class . ' o JOIN o.children c WITH c.id = 1'); + $query->setMaxResults(1); + $query->setFetchMode(EagerFetchChild::class, 'owner', ORM\ClassMetadata::FETCH_LAZY); + + $paginator = new Paginator($query, true); + $this->assertIsArray(iterator_to_array($paginator)); + } + public function testEagerFetchWithIterable(): void { $this->createOwnerWithChildren(2); diff --git a/tests/Tests/ORM/Functional/LifecycleCallbackTest.php b/tests/Tests/ORM/Functional/LifecycleCallbackTest.php index bff77353b58..c50918c2ffe 100644 --- a/tests/Tests/ORM/Functional/LifecycleCallbackTest.php +++ b/tests/Tests/ORM/Functional/LifecycleCallbackTest.php @@ -66,10 +66,11 @@ public function testPreSavePostSaveCallbacksAreInvoked(): void self::assertTrue($entity->postPersistCallbackInvoked); $this->_em->clear(); + LifecycleCallbackTestEntity::$postLoadCallbackInvoked = false; // Reset the tracking of the postLoad invocation $query = $this->_em->createQuery('select e from Doctrine\Tests\ORM\Functional\LifecycleCallbackTestEntity e'); $result = $query->getResult(); - self::assertTrue($result[0]->postLoadCallbackInvoked); + self::assertTrue($result[0]::$postLoadCallbackInvoked); $result[0]->value = 'hello again'; @@ -130,12 +131,14 @@ public function testGetReferenceWithPostLoadEventIsDelayedUntilProxyTrigger(): v $id = $entity->getId(); $this->_em->clear(); + LifecycleCallbackTestEntity::$postLoadCallbackInvoked = false; // Reset the tracking of the postLoad invocation $reference = $this->_em->getReference(LifecycleCallbackTestEntity::class, $id); - self::assertFalse($reference->postLoadCallbackInvoked); + self::assertFalse($reference::$postLoadCallbackInvoked); + $this->assertTrue($this->isUninitializedObject($reference)); $reference->getValue(); // trigger proxy load - self::assertTrue($reference->postLoadCallbackInvoked); + self::assertTrue($reference::$postLoadCallbackInvoked); } /** @group DDC-958 */ @@ -148,13 +151,14 @@ public function testPostLoadTriggeredOnRefresh(): void $id = $entity->getId(); $this->_em->clear(); + LifecycleCallbackTestEntity::$postLoadCallbackInvoked = false; // Reset the tracking of the postLoad invocation $reference = $this->_em->find(LifecycleCallbackTestEntity::class, $id); - self::assertTrue($reference->postLoadCallbackInvoked); - $reference->postLoadCallbackInvoked = false; + self::assertTrue($reference::$postLoadCallbackInvoked); + $reference::$postLoadCallbackInvoked = false; $this->_em->refresh($reference); - self::assertTrue($reference->postLoadCallbackInvoked, 'postLoad should be invoked when refresh() is called.'); + self::assertTrue($reference::$postLoadCallbackInvoked, 'postLoad should be invoked when refresh() is called.'); } /** @group DDC-113 */ @@ -197,6 +201,7 @@ public function testCascadedEntitiesLoadedInPostLoad(): void $this->_em->flush(); $this->_em->clear(); + LifecycleCallbackTestEntity::$postLoadCallbackInvoked = false; // Reset the tracking of the postLoad invocation $dql = <<<'DQL' SELECT @@ -214,9 +219,9 @@ public function testCascadedEntitiesLoadedInPostLoad(): void ->createQuery(sprintf($dql, $e1->getId(), $e2->getId())) ->getResult(); - self::assertTrue(current($entities)->postLoadCallbackInvoked); + self::assertTrue(current($entities)::$postLoadCallbackInvoked); self::assertTrue(current($entities)->postLoadCascaderNotNull); - self::assertTrue(current($entities)->cascader->postLoadCallbackInvoked); + self::assertTrue(current($entities)->cascader::$postLoadCallbackInvoked); self::assertEquals(current($entities)->cascader->postLoadEntitiesCount, 2); } @@ -239,6 +244,8 @@ public function testCascadedEntitiesNotLoadedInPostLoadDuringIteration(): void $this->_em->flush(); $this->_em->clear(); + LifecycleCallbackTestEntity::$postLoadCallbackInvoked = false; // Reset the tracking of the postLoad invocation + LifecycleCallbackCascader::$postLoadCallbackInvoked = false; $dql = <<<'DQL' SELECT @@ -256,7 +263,7 @@ public function testCascadedEntitiesNotLoadedInPostLoadDuringIteration(): void $result = iterator_to_array($query->iterate()); foreach ($result as $entity) { - self::assertTrue($entity[0]->postLoadCallbackInvoked); + self::assertTrue($entity[0]::$postLoadCallbackInvoked); self::assertFalse($entity[0]->postLoadCascaderNotNull); break; @@ -265,7 +272,7 @@ public function testCascadedEntitiesNotLoadedInPostLoadDuringIteration(): void $iterableResult = iterator_to_array($query->toIterable()); foreach ($iterableResult as $entity) { - self::assertTrue($entity->postLoadCallbackInvoked); + self::assertTrue($entity::$postLoadCallbackInvoked); self::assertFalse($entity->postLoadCascaderNotNull); break; @@ -283,6 +290,7 @@ public function testCascadedEntitiesNotLoadedInPostLoadDuringIterationWithSimple $this->_em->flush(); $this->_em->clear(); + LifecycleCallbackTestEntity::$postLoadCallbackInvoked = false; // Reset the tracking of the postLoad invocation $query = $this->_em->createQuery( 'SELECT e FROM Doctrine\Tests\ORM\Functional\LifecycleCallbackTestEntity AS e' @@ -291,7 +299,7 @@ public function testCascadedEntitiesNotLoadedInPostLoadDuringIterationWithSimple $result = iterator_to_array($query->iterate(null, Query::HYDRATE_SIMPLEOBJECT)); foreach ($result as $entity) { - self::assertTrue($entity[0]->postLoadCallbackInvoked); + self::assertTrue($entity[0]::$postLoadCallbackInvoked); self::assertFalse($entity[0]->postLoadCascaderNotNull); break; @@ -300,7 +308,7 @@ public function testCascadedEntitiesNotLoadedInPostLoadDuringIterationWithSimple $result = iterator_to_array($query->toIterable([], Query::HYDRATE_SIMPLEOBJECT)); foreach ($result as $entity) { - self::assertTrue($entity->postLoadCallbackInvoked); + self::assertTrue($entity::$postLoadCallbackInvoked); self::assertFalse($entity->postLoadCascaderNotNull); break; @@ -325,6 +333,8 @@ public function testPostLoadIsInvokedOnFetchJoinedEntities(): void $this->_em->flush(); $this->_em->clear(); + LifecycleCallbackTestEntity::$postLoadCallbackInvoked = false; // Reset the tracking of the postLoad invocation + LifecycleCallbackCascader::$postLoadCallbackInvoked = false; $dql = <<<'DQL' SELECT @@ -342,9 +352,9 @@ public function testPostLoadIsInvokedOnFetchJoinedEntities(): void ->createQuery($dql)->setParameter('entA_id', $entA->getId()) ->getOneOrNullResult(); - self::assertTrue($fetchedA->postLoadCallbackInvoked); + self::assertTrue($fetchedA::$postLoadCallbackInvoked); foreach ($fetchedA->entities as $fetchJoinedEntB) { - self::assertTrue($fetchJoinedEntB->postLoadCallbackInvoked); + self::assertTrue($fetchJoinedEntB::$postLoadCallbackInvoked); } } @@ -492,7 +502,7 @@ class LifecycleCallbackTestEntity public $postPersistCallbackInvoked = false; /** @var bool */ - public $postLoadCallbackInvoked = false; + public static $postLoadCallbackInvoked = false; /** @var bool */ public $postLoadCascaderNotNull = false; @@ -546,7 +556,7 @@ public function doStuffOnPostPersist(): void /** @PostLoad */ public function doStuffOnPostLoad(): void { - $this->postLoadCallbackInvoked = true; + self::$postLoadCallbackInvoked = true; $this->postLoadCascaderNotNull = isset($this->cascader); } @@ -572,7 +582,7 @@ class LifecycleCallbackCascader { /* test stuff */ /** @var bool */ - public $postLoadCallbackInvoked = false; + public static $postLoadCallbackInvoked = false; /** @var int */ public $postLoadEntitiesCount = 0; @@ -599,7 +609,7 @@ public function __construct() /** @PostLoad */ public function doStuffOnPostLoad(): void { - $this->postLoadCallbackInvoked = true; + self::$postLoadCallbackInvoked = true; $this->postLoadEntitiesCount = count($this->entities); } diff --git a/tests/Tests/ORM/Functional/QueryTest.php b/tests/Tests/ORM/Functional/QueryTest.php index 791c5c31719..99ec34f0bdb 100644 --- a/tests/Tests/ORM/Functional/QueryTest.php +++ b/tests/Tests/ORM/Functional/QueryTest.php @@ -558,7 +558,9 @@ public function testModifiedLimitQuery(): void $this->_em->flush(); $this->_em->clear(); - $data = $this->_em->createQuery('SELECT u FROM ' . CmsUser::class . ' u') + $query = 'SELECT u FROM ' . CmsUser::class . ' u ORDER BY u.username'; + + $data = $this->_em->createQuery($query) ->setFirstResult(1) ->setMaxResults(2) ->getResult(); @@ -567,7 +569,7 @@ public function testModifiedLimitQuery(): void self::assertEquals('gblanco1', $data[0]->username); self::assertEquals('gblanco2', $data[1]->username); - $data = $this->_em->createQuery('SELECT u FROM ' . CmsUser::class . ' u') + $data = $this->_em->createQuery($query) ->setFirstResult(3) ->setMaxResults(2) ->getResult(); @@ -576,7 +578,7 @@ public function testModifiedLimitQuery(): void self::assertEquals('gblanco3', $data[0]->username); self::assertEquals('gblanco4', $data[1]->username); - $data = $this->_em->createQuery('SELECT u FROM ' . CmsUser::class . ' u') + $data = $this->_em->createQuery($query) ->setFirstResult(3) ->setMaxResults(2) ->getScalarResult(); diff --git a/tests/Tests/ORM/Functional/SQLFilterTest.php b/tests/Tests/ORM/Functional/SQLFilterTest.php index f6b966cdd88..6e8ee365a9b 100644 --- a/tests/Tests/ORM/Functional/SQLFilterTest.php +++ b/tests/Tests/ORM/Functional/SQLFilterTest.php @@ -537,6 +537,21 @@ public function testToOneFilter(): void self::assertEquals(2, count($query->getResult())); } + public function testOneToOneInverseSideWithFilter(): void + { + $this->loadFixtureData(); + + $conf = $this->_em->getConfiguration(); + $conf->addFilter('country', '\Doctrine\Tests\ORM\Functional\CMSCountryFilter'); + $this->_em->getFilters()->enable('country')->setParameterList('country', ['Germany'], Types::STRING); + + $user = $this->_em->find(CmsUser::class, $this->userId); + self::assertNotEmpty($user->address); + + $user2 = $this->_em->find(CmsUser::class, $this->userId2); + self::assertEmpty($user2->address); + } + public function testManyToManyFilter(): void { $this->loadFixtureData(); diff --git a/tests/Tests/ORM/Functional/Ticket/DDC1690Test.php b/tests/Tests/ORM/Functional/Ticket/DDC1690Test.php index 9dd806334d1..9179631b058 100644 --- a/tests/Tests/ORM/Functional/Ticket/DDC1690Test.php +++ b/tests/Tests/ORM/Functional/Ticket/DDC1690Test.php @@ -51,15 +51,20 @@ public function testChangeTracking(): void $parentId = $parent->getId(); $childId = $child->getId(); unset($parent, $child); + DDC1690Parent::$addPropertyChangedListenerInvoked = false; + DDC1690Child::$addPropertyChangedListenerInvoked = false; $parent = $this->_em->find(DDC1690Parent::class, $parentId); $child = $this->_em->find(DDC1690Child::class, $childId); + self::assertTrue($parent::$addPropertyChangedListenerInvoked); self::assertEquals(1, count($parent->listeners)); - self::assertCount(0, $child->listeners); + $this->assertTrue($this->isUninitializedObject($child)); + self::assertFalse($child::$addPropertyChangedListenerInvoked); $this->_em->getUnitOfWork()->initializeObject($child); + self::assertTrue($child::$addPropertyChangedListenerInvoked); self::assertCount(1, $child->listeners); unset($parent, $child); @@ -106,6 +111,11 @@ protected function onPropertyChanged($propName, $oldValue, $newValue): void */ class DDC1690Parent extends NotifyBaseEntity { + /** + * @var bool + */ + public static $addPropertyChangedListenerInvoked = false; + /** * @var int * @Id @@ -151,11 +161,23 @@ public function getChild(): DDC1690Child { return $this->child; } + + public function addPropertyChangedListener(PropertyChangedListener $listener): void + { + self::$addPropertyChangedListenerInvoked = true; + + parent::addPropertyChangedListener($listener); + } } /** @Entity */ class DDC1690Child extends NotifyBaseEntity { + /** + * @var bool + */ + public static $addPropertyChangedListenerInvoked = false; + /** * @var int * @Id @@ -201,4 +223,11 @@ public function getParent(): DDC1690Parent { return $this->parent; } + + public function addPropertyChangedListener(PropertyChangedListener $listener): void + { + self::$addPropertyChangedListenerInvoked = true; + + parent::addPropertyChangedListener($listener); + } } diff --git a/tests/Tests/ORM/Functional/Ticket/DDC2230Test.php b/tests/Tests/ORM/Functional/Ticket/DDC2230Test.php index 8557858ac92..2ac7fca6765 100644 --- a/tests/Tests/ORM/Functional/Ticket/DDC2230Test.php +++ b/tests/Tests/ORM/Functional/Ticket/DDC2230Test.php @@ -57,16 +57,17 @@ public function testNotifyTrackingCalledOnProxyInitialization(): void $this->_em->persist($insertedAddress); $this->_em->flush(); $this->_em->clear(); + DDC2230Address::$listener = null; // Reset the tracking state $addressProxy = $this->_em->getReference(DDC2230Address::class, $insertedAddress->id); assert($addressProxy instanceof DDC2230Address); self::assertTrue($this->isUninitializedObject($addressProxy)); - self::assertNull($addressProxy->listener); + self::assertNull($addressProxy::$listener); $this->_em->getUnitOfWork()->initializeObject($addressProxy); - self::assertSame($this->_em->getUnitOfWork(), $addressProxy->listener); + self::assertSame($this->_em->getUnitOfWork(), $addressProxy::$listener); } } @@ -102,12 +103,12 @@ class DDC2230Address implements NotifyPropertyChanged */ public $id; - /** @var \Doctrine\Common\PropertyChangedListener */ - public $listener; + /** @var \Doctrine\Common\PropertyChangedListener|null */ + public static $listener; /** {@inheritDoc} */ public function addPropertyChangedListener(PropertyChangedListener $listener) { - $this->listener = $listener; + self::$listener = $listener; } } diff --git a/tests/Tests/ORM/Functional/Ticket/GH11524Test.php b/tests/Tests/ORM/Functional/Ticket/GH11524Test.php new file mode 100644 index 00000000000..d0f44109de5 --- /dev/null +++ b/tests/Tests/ORM/Functional/Ticket/GH11524Test.php @@ -0,0 +1,49 @@ +createSchemaForModels( + GH11524Entity::class, + GH11524Relation::class + ); + + $this->_em->getEventManager()->addEventListener(Events::postLoad, new GH11524Listener()); + } + + public function testPostLoadCalledOnProxy(): void + { + $relation = new GH11524Relation(); + $relation->name = 'test'; + $this->_em->persist($relation); + + $entity = new GH11524Entity(); + $entity->relation = $relation; + + $this->_em->persist($entity); + $this->_em->flush(); + + $this->_em->clear(); + + $reloadedEntity = $this->_em->find(GH11524Entity::class, $entity->id); + + $reloadedRelation = $reloadedEntity->relation; + + $this->assertTrue($this->isUninitializedObject($reloadedRelation)); + + $this->assertSame('fake', $reloadedRelation->getTranslation(), 'The property set by the postLoad listener must get initialized on usage.'); + } +} diff --git a/tests/Tests/ORM/Mapping/Reflection/ReflectionPropertiesGetterTest.php b/tests/Tests/ORM/Mapping/Reflection/ReflectionPropertiesGetterTest.php index 9d5271add83..f550d9b34be 100644 --- a/tests/Tests/ORM/Mapping/Reflection/ReflectionPropertiesGetterTest.php +++ b/tests/Tests/ORM/Mapping/Reflection/ReflectionPropertiesGetterTest.php @@ -84,7 +84,7 @@ public function testPropertiesAreAccessible(): void public function testPropertyGetterIsIdempotent(): void { - $getter = (new ReflectionPropertiesGetter(new RuntimeReflectionService())); + $getter = new ReflectionPropertiesGetter(new RuntimeReflectionService()); self::assertSame( $getter->getProperties(ClassWithMixedProperties::class), @@ -110,7 +110,7 @@ public function testPropertyGetterWillSkipPropertiesNotRetrievedByTheRuntimeRefl ->expects(self::atLeastOnce()) ->method('getAccessibleProperty'); - $getter = (new ReflectionPropertiesGetter($reflectionService)); + $getter = new ReflectionPropertiesGetter($reflectionService); self::assertEmpty($getter->getProperties(ClassWithMixedProperties::class)); } @@ -127,7 +127,7 @@ public function testPropertyGetterWillSkipClassesNotRetrievedByTheRuntimeReflect $reflectionService->expects(self::never())->method('getAccessibleProperty'); - $getter = (new ReflectionPropertiesGetter($reflectionService)); + $getter = new ReflectionPropertiesGetter($reflectionService); self::assertEmpty($getter->getProperties(ClassWithMixedProperties::class)); } diff --git a/tests/Tests/ORM/ORMInvalidArgumentExceptionTest.php b/tests/Tests/ORM/ORMInvalidArgumentExceptionTest.php index e878bc067b6..6af40bd0d74 100644 --- a/tests/Tests/ORM/ORMInvalidArgumentExceptionTest.php +++ b/tests/Tests/ORM/ORMInvalidArgumentExceptionTest.php @@ -85,7 +85,7 @@ public function __toString(): string . 'persist operations for entity: stdClass@' . spl_object_id($entity1) . '. To solve this issue: Either explicitly call EntityManager#persist() on this unknown entity ' . 'or configure cascade persist this association in the mapping for example ' - . '@ManyToOne(..,cascade={"persist"}). If you cannot find out which entity causes the problem ' + . '#[ORM\ManyToOne(..., cascade: [\'persist\'])]. If you cannot find out which entity causes the problem ' . 'implement \'baz1#__toString()\' to get a clue.', ], 'two entities found' => [ @@ -104,13 +104,13 @@ public function __toString(): string . 'cascade persist operations for entity: stdClass@' . spl_object_id($entity1) . '. ' . 'To solve this issue: Either explicitly call EntityManager#persist() on this unknown entity ' . 'or configure cascade persist this association in the mapping for example ' - . '@ManyToOne(..,cascade={"persist"}). If you cannot find out which entity causes the problem ' + . '#[ORM\ManyToOne(..., cascade: [\'persist\'])]. If you cannot find out which entity causes the problem ' . 'implement \'baz1#__toString()\' to get a clue.' . "\n" . ' * A new entity was found through the relationship \'foo2#bar2\' that was not configured to ' . 'cascade persist operations for entity: stdClass@' . spl_object_id($entity2) . '. To solve ' . 'this issue: Either explicitly call EntityManager#persist() on this unknown entity or ' . 'configure cascade persist this association in the mapping for example ' - . '@ManyToOne(..,cascade={"persist"}). If you cannot find out which entity causes the problem ' + . '#[ORM\ManyToOne(..., cascade: [\'persist\'])]. If you cannot find out which entity causes the problem ' . 'implement \'baz2#__toString()\' to get a clue.', ], 'two entities found, one is stringable' => [ @@ -124,7 +124,7 @@ public function __toString(): string . 'persist operations for entity: ThisIsAStringRepresentationOfEntity3' . '. To solve this issue: Either explicitly call EntityManager#persist() on this unknown entity ' . 'or configure cascade persist this association in the mapping for example ' - . '@ManyToOne(..,cascade={"persist"}).', + . '#[ORM\ManyToOne(..., cascade: [\'persist\'])].', ], ]; } diff --git a/tests/Tests/ORM/Tools/Pagination/LimitSubqueryOutputWalkerTest.php b/tests/Tests/ORM/Tools/Pagination/LimitSubqueryOutputWalkerTest.php index 95d2047d593..c11eccc8994 100644 --- a/tests/Tests/ORM/Tools/Pagination/LimitSubqueryOutputWalkerTest.php +++ b/tests/Tests/ORM/Tools/Pagination/LimitSubqueryOutputWalkerTest.php @@ -46,6 +46,24 @@ private function replaceDatabasePlatform(AbstractPlatform $platform): void $this->entityManager->getConnection()->setDatabasePlatform($platform); } + public function testSubqueryClonedCompletely(): void + { + $query = $this->createQuery('SELECT p FROM Doctrine\Tests\ORM\Tools\Pagination\MyBlogPost p'); + $query->setParameter('dummy-param', 123); + $query->setHint('dummy-hint', 'dummy-value'); + $query->setCacheable(true); + + $walker = new LimitSubqueryOutputWalker($query, new Query\ParserResult(), []); + + self::assertNotSame($query, $walker->getQuery()); + self::assertTrue($walker->getQuery()->hasHint('dummy-hint')); + self::assertSame('dummy-value', $walker->getQuery()->getHint('dummy-hint')); + self::assertNotSame($query->getParameters(), $walker->getQuery()->getParameters()); + self::assertInstanceOf(Query\Parameter::class, $param = $walker->getQuery()->getParameter('dummy-param')); + self::assertSame(123, $param->getValue()); + self::assertFalse($walker->getQuery()->isCacheable()); + } + public function testLimitSubquery(): void { $query = $this->createQuery('SELECT p, c, a FROM Doctrine\Tests\ORM\Tools\Pagination\MyBlogPost p JOIN p.category c JOIN p.author a'); diff --git a/tests/Tests/OrmFunctionalTestCase.php b/tests/Tests/OrmFunctionalTestCase.php index 73c812acbe1..7bc387f68d1 100644 --- a/tests/Tests/OrmFunctionalTestCase.php +++ b/tests/Tests/OrmFunctionalTestCase.php @@ -679,8 +679,6 @@ protected function tearDown(): void } /** - * @param array $classNames - * * @throws RuntimeException */ protected function setUpEntitySchema(array $classNames): void