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