From 4feaa470af4de8799a1e9e3db87191bac74b7a46 Mon Sep 17 00:00:00 2001 From: HypeMC Date: Wed, 18 Dec 2024 15:42:12 +0100 Subject: [PATCH 1/7] Fix fields of transient classes being considered duplicate with `reportFieldsWhereDeclared` --- src/Mapping/Driver/ReflectionBasedDriver.php | 5 +++ .../ORM/Functional/Ticket/GH10450Test.php | 44 +++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/src/Mapping/Driver/ReflectionBasedDriver.php b/src/Mapping/Driver/ReflectionBasedDriver.php index a2900d13e01..9becf228a31 100644 --- a/src/Mapping/Driver/ReflectionBasedDriver.php +++ b/src/Mapping/Driver/ReflectionBasedDriver.php @@ -32,8 +32,13 @@ private function isRepeatedPropertyDeclaration(ReflectionProperty $property, Cla || $metadata->isInheritedEmbeddedClass($property->name); } + /** @var class-string $declaringClass */ $declaringClass = $property->class; + if ($this->isTransient($declaringClass)) { + return isset($metadata->fieldMappings[$property->name]); + } + if ( isset($metadata->fieldMappings[$property->name]['declared']) && $metadata->fieldMappings[$property->name]['declared'] === $declaringClass diff --git a/tests/Tests/ORM/Functional/Ticket/GH10450Test.php b/tests/Tests/ORM/Functional/Ticket/GH10450Test.php index c522049f820..afe75d3bcdf 100644 --- a/tests/Tests/ORM/Functional/Ticket/GH10450Test.php +++ b/tests/Tests/ORM/Functional/Ticket/GH10450Test.php @@ -32,6 +32,15 @@ public function classesThatOverrideFieldNames(): Generator yield 'Entity class that redeclares a protected field inherited from a base entity' => [GH10450EntityChildProtected::class]; yield 'Entity class that redeclares a protected field inherited from a mapped superclass' => [GH10450MappedSuperclassChildProtected::class]; } + + public function testFieldsOfTransientClassesAreNotConsideredDuplicate(): void + { + $em = $this->getTestEntityManager(); + + $metadata = $em->getClassMetadata(GH10450Cat::class); + + self::assertArrayHasKey('id', $metadata->fieldMappings); + } } /** @@ -179,3 +188,38 @@ class GH10450MappedSuperclassChildProtected extends GH10450BaseMappedSuperclassP */ protected $field; } + +abstract class GH10450AbstractEntity +{ + /** + * @ORM\Column(type="integer") + * @ORM\Id + * @ORM\GeneratedValue + * + * @var int + */ + protected $id; +} + +/** + * @ORM\Entity + * @ORM\InheritanceType("SINGLE_TABLE") + * @ORM\DiscriminatorMap({ "cat": "GH10450Cat" }) + * @ORM\DiscriminatorColumn(name="type") + */ +abstract class GH10450Animal extends GH10450AbstractEntity +{ + /** + * @ORM\Column(type="text", name="base") + * + * @var string + */ + private $field; +} + +/** + * @ORM\Entity + */ +class GH10450Cat extends GH10450Animal +{ +} From 44d5d4a7799868a89a4bfdbc9b7bad2ea0291c8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Paris?= Date: Fri, 17 Jan 2025 08:27:11 +0100 Subject: [PATCH 2/7] Ignore deprecations from doctrine/common These new issues are caused by doctrine/common 3.5.0, released 2 weeks ago. --- phpstan-baseline.neon | 63 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index b5a3abe3551..252cad57c2a 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -3144,6 +3144,15 @@ parameters: count: 1 path: src/Persisters/SqlValueVisitor.php + - + message: ''' + #^Class Doctrine\\ORM\\Proxy\\Autoloader extends deprecated class Doctrine\\Common\\Proxy\\Autoloader\: + The Autoloader class is deprecated since doctrine/common 3\.5\.$# + ''' + identifier: class.extendsDeprecatedClass + count: 1 + path: src/Proxy/Autoloader.php + - message: '#^Method Doctrine\\ORM\\Proxy\\DefaultProxyClassNameResolver\:\:resolveClassName\(\) should return class\-string\ but returns class\-string\\>\|class\-string\\.$#' identifier: return.type @@ -3186,6 +3195,42 @@ parameters: count: 1 path: src/Proxy/ProxyFactory.php + - + message: ''' + #^Call to method __construct\(\) of deprecated class Doctrine\\Common\\Proxy\\AbstractProxyFactory\: + The AbstractProxyFactory class is deprecated since doctrine/common 3\.5\.$# + ''' + identifier: staticMethod.deprecatedClass + count: 1 + path: src/Proxy/ProxyFactory.php + + - + message: ''' + #^Call to method generateProxyClasses\(\) of deprecated class Doctrine\\Common\\Proxy\\AbstractProxyFactory\: + The AbstractProxyFactory class is deprecated since doctrine/common 3\.5\.$# + ''' + identifier: staticMethod.deprecatedClass + count: 1 + path: src/Proxy/ProxyFactory.php + + - + message: ''' + #^Call to method getProxy\(\) of deprecated class Doctrine\\Common\\Proxy\\AbstractProxyFactory\: + The AbstractProxyFactory class is deprecated since doctrine/common 3\.5\.$# + ''' + identifier: staticMethod.deprecatedClass + count: 1 + path: src/Proxy/ProxyFactory.php + + - + message: ''' + #^Class Doctrine\\ORM\\Proxy\\ProxyFactory extends deprecated class Doctrine\\Common\\Proxy\\AbstractProxyFactory\: + The AbstractProxyFactory class is deprecated since doctrine/common 3\.5\.$# + ''' + identifier: class.extendsDeprecatedClass + count: 1 + path: src/Proxy/ProxyFactory.php + - message: '#^Comparison operation "\<" between 0\|1\|2\|3\|4 and 0 is always false\.$#' identifier: smaller.alwaysFalse @@ -3198,6 +3243,15 @@ parameters: count: 1 path: src/Proxy/ProxyFactory.php + - + message: ''' + #^Instantiation of deprecated class Doctrine\\Common\\Proxy\\ProxyGenerator\: + The ProxyGenerator class is deprecated since doctrine/common 3\.5\.$# + ''' + identifier: new.deprecated + count: 1 + path: src/Proxy/ProxyFactory.php + - message: '#^Method Doctrine\\ORM\\Proxy\\ProxyFactory\:\:createCloner\(\) has Doctrine\\ORM\\EntityNotFoundException in PHPDoc @throws tag but it''s not thrown\.$#' identifier: throws.unusedType @@ -3336,6 +3390,15 @@ parameters: count: 1 path: src/Proxy/ProxyFactory.php + - + message: ''' + #^Return type of method Doctrine\\ORM\\Proxy\\ProxyFactory\:\:getProxy\(\) has typehint with deprecated interface Doctrine\\Common\\Proxy\\Proxy\: + The Proxy interface is deprecated since doctrine/common 3\.5\.$# + ''' + identifier: return.deprecatedInterface + count: 1 + path: src/Proxy/ProxyFactory.php + - message: '#^Call to function method_exists\(\) with ''Doctrine\\\\DBAL\\\\Cache\\\\QueryCacheProfile'' and ''getResultCache'' will always evaluate to true\.$#' identifier: function.alreadyNarrowedType From fff085b63f6b315e7aa1d08bf0df067986194db2 Mon Sep 17 00:00:00 2001 From: pawel-slowik <2052273+pawel-slowik@users.noreply.github.com> Date: Wed, 22 Jan 2025 00:12:00 +0100 Subject: [PATCH 3/7] Fix documentation for JoinColumn nullable (#11798) Nullability is not inherited from the PHP type. The change that enabled this feature was reversed in https://github.com/doctrine/orm/pull/8732. --- docs/en/reference/association-mapping.rst | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/docs/en/reference/association-mapping.rst b/docs/en/reference/association-mapping.rst index 55dfe57671e..3db6dfa457b 100644 --- a/docs/en/reference/association-mapping.rst +++ b/docs/en/reference/association-mapping.rst @@ -1368,8 +1368,7 @@ defaults to "id", just as in one-to-one or many-to-one mappings. Additionally, when using typed properties with Doctrine 2.9 or newer you can skip ``targetEntity`` in ``ManyToOne`` and ``OneToOne`` -associations as they will be set based on type. Also ``nullable`` -attribute on ``JoinColumn`` will be inherited from PHP type. So that: +associations as they will be set based on type. So that: .. configuration-block:: @@ -1409,7 +1408,7 @@ Is essentially the same as following: Date: Tue, 21 Jan 2025 10:25:36 +0100 Subject: [PATCH 4/7] Introduce testNotListedValueInEnumArray --- .../Hydration/SimpleObjectHydrator.php | 14 +++++++--- .../Hydration/SimpleObjectHydratorTest.php | 27 +++++++++++++++++++ 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/src/Internal/Hydration/SimpleObjectHydrator.php b/src/Internal/Hydration/SimpleObjectHydrator.php index 8c1788e7e52..16c0c25ccaa 100644 --- a/src/Internal/Hydration/SimpleObjectHydrator.php +++ b/src/Internal/Hydration/SimpleObjectHydrator.php @@ -16,6 +16,7 @@ use function array_search; use function count; use function in_array; +use function is_array; use function key; use function reset; use function sprintf; @@ -143,14 +144,21 @@ protected function hydrateRowData(array $row, array &$result) } if ($value !== null && isset($cacheKeyInfo['enumType'])) { - $originalValue = $value; + $originalValue = $currentValue = $value; try { - $value = $this->buildEnum($originalValue, $cacheKeyInfo['enumType']); + if (! is_array($originalValue)) { + $value = $this->buildEnum($originalValue, $cacheKeyInfo['enumType']); + } else { + $value = []; + foreach ($originalValue as $i => $currentValue) { + $value[$i] = $this->buildEnum($currentValue, $cacheKeyInfo['enumType']); + } + } } catch (ValueError $e) { throw MappingException::invalidEnumValue( $entityName, $cacheKeyInfo['fieldName'], - (string) $originalValue, + (string) $currentValue, $cacheKeyInfo['enumType'], $e ); diff --git a/tests/Tests/ORM/Hydration/SimpleObjectHydratorTest.php b/tests/Tests/ORM/Hydration/SimpleObjectHydratorTest.php index 01504102ad2..f4d3a9ac9fd 100644 --- a/tests/Tests/ORM/Hydration/SimpleObjectHydratorTest.php +++ b/tests/Tests/ORM/Hydration/SimpleObjectHydratorTest.php @@ -7,12 +7,15 @@ use Doctrine\DBAL\Types\Type as DBALType; use Doctrine\ORM\Internal\Hydration\HydrationException; use Doctrine\ORM\Internal\Hydration\SimpleObjectHydrator; +use Doctrine\ORM\Mapping\MappingException; use Doctrine\ORM\Query\ResultSetMapping; use Doctrine\Tests\DbalTypes\GH8565EmployeePayloadType; use Doctrine\Tests\DbalTypes\GH8565ManagerPayloadType; use Doctrine\Tests\Mocks\ArrayResultFactory; use Doctrine\Tests\Models\CMS\CmsAddress; use Doctrine\Tests\Models\Company\CompanyPerson; +use Doctrine\Tests\Models\Enums\Scale; +use Doctrine\Tests\Models\Enums\Unit; use Doctrine\Tests\Models\GH8565\GH8565Employee; use Doctrine\Tests\Models\GH8565\GH8565Manager; use Doctrine\Tests\Models\GH8565\GH8565Person; @@ -155,4 +158,28 @@ public function testWrongValuesShouldNotBeConvertedToPhpValue(): void $result = $hydrator->hydrateAll($stmt, $rsm); self::assertEquals($result[0], $expectedEntity); } + + /** + * @requires PHP 8.1 + */ + public function testNotListedValueInEnumArray(): void + { + $this->expectException(MappingException::class); + $this->expectExceptionMessage('Case "unknown_case" is not listed in enum "Doctrine\Tests\Models\Enums\Unit"'); + $rsm = new ResultSetMapping(); + $rsm->addEntityResult(Scale::class, 's'); + $rsm->addFieldResult('s', 's__id', 'id'); + $rsm->addFieldResult('s', 's__supported_units', 'supportedUnits'); + $rsm->addEnumResult('s__supported_units', Unit::class); + $resultSet = [ + [ + 's__id' => 1, + 's__supported_units' => 'g,m,unknown_case', + ], + ]; + + $stmt = ArrayResultFactory::createFromArray($resultSet); + $hydrator = new SimpleObjectHydrator($this->entityManager); + $hydrator->hydrateAll($stmt, $rsm); + } } From aa141bf001588ab43e126db4152ec9b14e282f49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Paris?= Date: Sat, 25 Jan 2025 12:17:06 +0100 Subject: [PATCH 5/7] Address quoteIdentifier() deprecation We should be using quoteSingleIdentifier(), assuming we only ever pass single identifiers here. See https://github.com/doctrine/dbal/pull/6590 --- src/Mapping/DefaultQuoteStrategy.php | 14 +++++++------- .../Tests/ORM/Functional/Ticket/DDC832Test.php | 10 +++++----- tests/Tests/OrmFunctionalTestCase.php | 18 +++++++++--------- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/Mapping/DefaultQuoteStrategy.php b/src/Mapping/DefaultQuoteStrategy.php index 6260336c027..b42de4e862b 100644 --- a/src/Mapping/DefaultQuoteStrategy.php +++ b/src/Mapping/DefaultQuoteStrategy.php @@ -24,7 +24,7 @@ class DefaultQuoteStrategy implements QuoteStrategy public function getColumnName(string $fieldName, ClassMetadata $class, AbstractPlatform $platform): string { return isset($class->fieldMappings[$fieldName]->quoted) - ? $platform->quoteIdentifier($class->fieldMappings[$fieldName]->columnName) + ? $platform->quoteSingleIdentifier($class->fieldMappings[$fieldName]->columnName) : $class->fieldMappings[$fieldName]->columnName; } @@ -42,7 +42,7 @@ public function getTableName(ClassMetadata $class, AbstractPlatform $platform): } return isset($class->table['quoted']) - ? $platform->quoteIdentifier($tableName) + ? $platform->quoteSingleIdentifier($tableName) : $tableName; } @@ -52,14 +52,14 @@ public function getTableName(ClassMetadata $class, AbstractPlatform $platform): public function getSequenceName(array $definition, ClassMetadata $class, AbstractPlatform $platform): string { return isset($definition['quoted']) - ? $platform->quoteIdentifier($definition['sequenceName']) + ? $platform->quoteSingleIdentifier($definition['sequenceName']) : $definition['sequenceName']; } public function getJoinColumnName(JoinColumnMapping $joinColumn, ClassMetadata $class, AbstractPlatform $platform): string { return isset($joinColumn->quoted) - ? $platform->quoteIdentifier($joinColumn->name) + ? $platform->quoteSingleIdentifier($joinColumn->name) : $joinColumn->name; } @@ -69,7 +69,7 @@ public function getReferencedJoinColumnName( AbstractPlatform $platform, ): string { return isset($joinColumn->quoted) - ? $platform->quoteIdentifier($joinColumn->referencedColumnName) + ? $platform->quoteSingleIdentifier($joinColumn->referencedColumnName) : $joinColumn->referencedColumnName; } @@ -87,7 +87,7 @@ public function getJoinTableName( $tableName = $association->joinTable->name; if (isset($association->joinTable->quoted)) { - $tableName = $platform->quoteIdentifier($tableName); + $tableName = $platform->quoteSingleIdentifier($tableName); } return $schema . $tableName; @@ -113,7 +113,7 @@ public function getIdentifierColumnNames(ClassMetadata $class, AbstractPlatform $joinColumns = $assoc->joinColumns; $assocQuotedColumnNames = array_map( static fn (JoinColumnMapping $joinColumn) => isset($joinColumn->quoted) - ? $platform->quoteIdentifier($joinColumn->name) + ? $platform->quoteSingleIdentifier($joinColumn->name) : $joinColumn->name, $joinColumns, ); diff --git a/tests/Tests/ORM/Functional/Ticket/DDC832Test.php b/tests/Tests/ORM/Functional/Ticket/DDC832Test.php index ca7f948518e..834c7a5cf5a 100644 --- a/tests/Tests/ORM/Functional/Ticket/DDC832Test.php +++ b/tests/Tests/ORM/Functional/Ticket/DDC832Test.php @@ -42,14 +42,14 @@ public function tearDown(): void $platform = $this->_em->getConnection()->getDatabasePlatform(); $sm = $this->createSchemaManager(); - $sm->dropTable($platform->quoteIdentifier('TREE_INDEX')); - $sm->dropTable($platform->quoteIdentifier('INDEX')); - $sm->dropTable($platform->quoteIdentifier('LIKE')); + $sm->dropTable($platform->quoteSingleIdentifier('TREE_INDEX')); + $sm->dropTable($platform->quoteSingleIdentifier('INDEX')); + $sm->dropTable($platform->quoteSingleIdentifier('LIKE')); // DBAL 3 if ($platform instanceof PostgreSQLPlatform && method_exists($platform, 'getIdentitySequenceName')) { - $sm->dropSequence($platform->quoteIdentifier('INDEX_id_seq')); - $sm->dropSequence($platform->quoteIdentifier('LIKE_id_seq')); + $sm->dropSequence($platform->quoteSingleIdentifier('INDEX_id_seq')); + $sm->dropSequence($platform->quoteSingleIdentifier('LIKE_id_seq')); } } diff --git a/tests/Tests/OrmFunctionalTestCase.php b/tests/Tests/OrmFunctionalTestCase.php index f2cf5f81f18..efd380dc1de 100644 --- a/tests/Tests/OrmFunctionalTestCase.php +++ b/tests/Tests/OrmFunctionalTestCase.php @@ -614,7 +614,7 @@ protected function tearDown(): void } if (isset($this->_usedModelSets['directorytree'])) { - $conn->executeStatement('DELETE FROM ' . $platform->quoteIdentifier('file')); + $conn->executeStatement('DELETE FROM ' . $platform->quoteSingleIdentifier('file')); // MySQL doesn't know deferred deletions therefore only executing the second query gives errors. $conn->executeStatement('DELETE FROM Directory WHERE parentDirectory_id IS NOT NULL'); $conn->executeStatement('DELETE FROM Directory'); @@ -707,17 +707,17 @@ protected function tearDown(): void $conn->executeStatement( sprintf( 'UPDATE %s SET %s = NULL', - $platform->quoteIdentifier('quote-address'), - $platform->quoteIdentifier('user-id'), + $platform->quoteSingleIdentifier('quote-address'), + $platform->quoteSingleIdentifier('user-id'), ), ); - $conn->executeStatement('DELETE FROM ' . $platform->quoteIdentifier('quote-users-groups')); - $conn->executeStatement('DELETE FROM ' . $platform->quoteIdentifier('quote-group')); - $conn->executeStatement('DELETE FROM ' . $platform->quoteIdentifier('quote-phone')); - $conn->executeStatement('DELETE FROM ' . $platform->quoteIdentifier('quote-user')); - $conn->executeStatement('DELETE FROM ' . $platform->quoteIdentifier('quote-address')); - $conn->executeStatement('DELETE FROM ' . $platform->quoteIdentifier('quote-city')); + $conn->executeStatement('DELETE FROM ' . $platform->quoteSingleIdentifier('quote-users-groups')); + $conn->executeStatement('DELETE FROM ' . $platform->quoteSingleIdentifier('quote-group')); + $conn->executeStatement('DELETE FROM ' . $platform->quoteSingleIdentifier('quote-phone')); + $conn->executeStatement('DELETE FROM ' . $platform->quoteSingleIdentifier('quote-user')); + $conn->executeStatement('DELETE FROM ' . $platform->quoteSingleIdentifier('quote-address')); + $conn->executeStatement('DELETE FROM ' . $platform->quoteSingleIdentifier('quote-city')); } if (isset($this->_usedModelSets['vct_onetoone'])) { From 6755bb0c7bdb1774152fddf7bb0ff20a4dd5a8a0 Mon Sep 17 00:00:00 2001 From: Bob van de Vijver Date: Wed, 8 Jan 2025 10:38:23 +0100 Subject: [PATCH 6/7] Fix Hydration when use ManyToMany[indexBy] The bug related (#11694) and fixed mapping of sql column alias to field in entity (#11783) and invalidate cache [cache/persisted/entity|cache/persisted/collection] when sql filter changes --- src/Cache/CollectionCacheKey.php | 6 +- .../AbstractCollectionPersister.php | 10 +++- ...rictReadWriteCachedCollectionPersister.php | 4 +- .../ReadWriteCachedCollectionPersister.php | 4 +- .../Entity/AbstractEntityPersister.php | 19 ++++-- .../Entity/BasicEntityPersister.php | 10 +++- src/Query/ResultSetMapping.php | 26 +++++++- .../Category.php | 43 +++++++++++++ .../CategoryTypeSQLFilter.php | 22 +++++++ .../ChangeFiltersTest.php | 60 +++++++++++++++++++ .../Company.php | 46 ++++++++++++++ 11 files changed, 235 insertions(+), 15 deletions(-) create mode 100644 tests/Tests/ORM/Functional/Ticket/SwitchContextWithFilterAndIndexedRelation/Category.php create mode 100644 tests/Tests/ORM/Functional/Ticket/SwitchContextWithFilterAndIndexedRelation/CategoryTypeSQLFilter.php create mode 100644 tests/Tests/ORM/Functional/Ticket/SwitchContextWithFilterAndIndexedRelation/ChangeFiltersTest.php create mode 100644 tests/Tests/ORM/Functional/Ticket/SwitchContextWithFilterAndIndexedRelation/Company.php diff --git a/src/Cache/CollectionCacheKey.php b/src/Cache/CollectionCacheKey.php index 56bf8df722c..b6c1ae803f8 100644 --- a/src/Cache/CollectionCacheKey.php +++ b/src/Cache/CollectionCacheKey.php @@ -43,7 +43,7 @@ class CollectionCacheKey extends CacheKey * @param string $association The field name that represents the association. * @param array $ownerIdentifier The identifier of the owning entity. */ - public function __construct($entityClass, $association, array $ownerIdentifier) + public function __construct($entityClass, $association, array $ownerIdentifier, string $filterHash = '') { ksort($ownerIdentifier); @@ -51,6 +51,8 @@ public function __construct($entityClass, $association, array $ownerIdentifier) $this->entityClass = (string) $entityClass; $this->association = (string) $association; - parent::__construct(str_replace('\\', '.', strtolower($entityClass)) . '_' . implode(' ', $ownerIdentifier) . '__' . $association); + $filterHash = $filterHash === '' ? '' : '_' . $filterHash; + + parent::__construct(str_replace('\\', '.', strtolower($entityClass)) . '_' . implode(' ', $ownerIdentifier) . '__' . $association . $filterHash); } } diff --git a/src/Cache/Persister/Collection/AbstractCollectionPersister.php b/src/Cache/Persister/Collection/AbstractCollectionPersister.php index 42d6ec8519c..18d4d7dab0e 100644 --- a/src/Cache/Persister/Collection/AbstractCollectionPersister.php +++ b/src/Cache/Persister/Collection/AbstractCollectionPersister.php @@ -19,6 +19,7 @@ use Doctrine\ORM\PersistentCollection; use Doctrine\ORM\Persisters\Collection\CollectionPersister; use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver; +use Doctrine\ORM\Query\FilterCollection; use Doctrine\ORM\UnitOfWork; use function array_values; @@ -55,6 +56,9 @@ abstract class AbstractCollectionPersister implements CachedCollectionPersister /** @var string */ protected $regionName; + /** @var FilterCollection */ + protected $filters; + /** @var CollectionHydrator */ protected $hydrator; @@ -76,6 +80,7 @@ public function __construct(CollectionPersister $persister, Region $region, Enti $this->region = $region; $this->persister = $persister; $this->association = $association; + $this->filters = $em->getFilters(); $this->regionName = $region->getName(); $this->uow = $em->getUnitOfWork(); $this->metadataFactory = $em->getMetadataFactory(); @@ -189,7 +194,7 @@ public function containsKey(PersistentCollection $collection, $key) public function count(PersistentCollection $collection) { $ownerId = $this->uow->getEntityIdentifier($collection->getOwner()); - $key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association['fieldName'], $ownerId); + $key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association['fieldName'], $ownerId, $this->filters->getHash()); $entry = $this->region->get($key); if ($entry !== null) { @@ -241,7 +246,8 @@ protected function evictCollectionCache(PersistentCollection $collection) $key = new CollectionCacheKey( $this->sourceEntity->rootEntityName, $this->association['fieldName'], - $this->uow->getEntityIdentifier($collection->getOwner()) + $this->uow->getEntityIdentifier($collection->getOwner()), + $this->filters->getHash() ); $this->region->evict($key); diff --git a/src/Cache/Persister/Collection/NonStrictReadWriteCachedCollectionPersister.php b/src/Cache/Persister/Collection/NonStrictReadWriteCachedCollectionPersister.php index 2441fc9960e..cfe62a2d143 100644 --- a/src/Cache/Persister/Collection/NonStrictReadWriteCachedCollectionPersister.php +++ b/src/Cache/Persister/Collection/NonStrictReadWriteCachedCollectionPersister.php @@ -45,7 +45,7 @@ public function afterTransactionRolledBack() public function delete(PersistentCollection $collection) { $ownerId = $this->uow->getEntityIdentifier($collection->getOwner()); - $key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association['fieldName'], $ownerId); + $key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association['fieldName'], $ownerId, $this->filters->getHash()); $this->persister->delete($collection); @@ -65,7 +65,7 @@ public function update(PersistentCollection $collection) } $ownerId = $this->uow->getEntityIdentifier($collection->getOwner()); - $key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association['fieldName'], $ownerId); + $key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association['fieldName'], $ownerId, $this->filters->getHash()); // Invalidate non initialized collections OR ordered collection if ($isDirty && ! $isInitialized || isset($this->association['orderBy'])) { diff --git a/src/Cache/Persister/Collection/ReadWriteCachedCollectionPersister.php b/src/Cache/Persister/Collection/ReadWriteCachedCollectionPersister.php index 0ec977695e2..35e7797b39b 100644 --- a/src/Cache/Persister/Collection/ReadWriteCachedCollectionPersister.php +++ b/src/Cache/Persister/Collection/ReadWriteCachedCollectionPersister.php @@ -68,7 +68,7 @@ public function afterTransactionRolledBack() public function delete(PersistentCollection $collection) { $ownerId = $this->uow->getEntityIdentifier($collection->getOwner()); - $key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association['fieldName'], $ownerId); + $key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association['fieldName'], $ownerId, $this->filters->getHash()); $lock = $this->region->lock($key); $this->persister->delete($collection); @@ -98,7 +98,7 @@ public function update(PersistentCollection $collection) $this->persister->update($collection); $ownerId = $this->uow->getEntityIdentifier($collection->getOwner()); - $key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association['fieldName'], $ownerId); + $key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association['fieldName'], $ownerId, $this->filters->getHash()); $lock = $this->region->lock($key); if ($lock === null) { diff --git a/src/Cache/Persister/Entity/AbstractEntityPersister.php b/src/Cache/Persister/Entity/AbstractEntityPersister.php index 3cdd27885a4..a9d447b96bf 100644 --- a/src/Cache/Persister/Entity/AbstractEntityPersister.php +++ b/src/Cache/Persister/Entity/AbstractEntityPersister.php @@ -22,9 +22,11 @@ use Doctrine\ORM\PersistentCollection; use Doctrine\ORM\Persisters\Entity\EntityPersister; use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver; +use Doctrine\ORM\Query\FilterCollection; use Doctrine\ORM\UnitOfWork; use function array_merge; +use function func_get_args; use function serialize; use function sha1; @@ -62,6 +64,9 @@ abstract class AbstractEntityPersister implements CachedEntityPersister /** @var Cache */ protected $cache; + /** @var FilterCollection */ + protected $filters; + /** @var CacheLogger|null */ protected $cacheLogger; @@ -91,6 +96,7 @@ public function __construct(EntityPersister $persister, Region $region, EntityMa $this->region = $region; $this->persister = $persister; $this->cache = $em->getCache(); + $this->filters = $em->getFilters(); $this->regionName = $region->getName(); $this->uow = $em->getUnitOfWork(); $this->metadataFactory = $em->getMetadataFactory(); @@ -261,7 +267,7 @@ protected function getHash($query, $criteria, ?array $orderBy = null, $limit = n ? $this->persister->expandCriteriaParameters($criteria) : $this->persister->expandParameters($criteria); - return sha1($query . serialize($params) . serialize($orderBy) . $limit . $offset); + return sha1($query . serialize($params) . serialize($orderBy) . $limit . $offset . $this->filters->getHash()); } /** @@ -524,7 +530,7 @@ public function loadManyToManyCollection(array $assoc, $sourceEntity, Persistent } $ownerId = $this->uow->getEntityIdentifier($collection->getOwner()); - $key = $this->buildCollectionCacheKey($assoc, $ownerId); + $key = $this->buildCollectionCacheKey($assoc, $ownerId, $this->filters->getHash()); $list = $persister->loadCollectionCache($collection, $key); if ($list !== null) { @@ -559,7 +565,7 @@ public function loadOneToManyCollection(array $assoc, $sourceEntity, PersistentC } $ownerId = $this->uow->getEntityIdentifier($collection->getOwner()); - $key = $this->buildCollectionCacheKey($assoc, $ownerId); + $key = $this->buildCollectionCacheKey($assoc, $ownerId, $this->filters->getHash()); $list = $persister->loadCollectionCache($collection, $key); if ($list !== null) { @@ -611,12 +617,15 @@ public function refresh(array $id, $entity, $lockMode = null) * * @return CollectionCacheKey */ - protected function buildCollectionCacheKey(array $association, $ownerId) + protected function buildCollectionCacheKey(array $association, $ownerId/*, string $filterHash */) { + $filterHash = (string) (func_get_args()[2] ?? ''); // todo: move to argument in next major release + return new CollectionCacheKey( $this->metadataFactory->getMetadataFor($association['sourceEntity'])->rootEntityName, $association['fieldName'], - $ownerId + $ownerId, + $filterHash ); } } diff --git a/src/Persisters/Entity/BasicEntityPersister.php b/src/Persisters/Entity/BasicEntityPersister.php index a9897f195fe..4073c606edc 100644 --- a/src/Persisters/Entity/BasicEntityPersister.php +++ b/src/Persisters/Entity/BasicEntityPersister.php @@ -1560,7 +1560,15 @@ protected function getSelectColumnSQL($field, ClassMetadata $class, $alias = 'r' $tableAlias = $this->getSQLTableAlias($class->name, $root); $fieldMapping = $class->fieldMappings[$field]; $sql = sprintf('%s.%s', $tableAlias, $this->quoteStrategy->getColumnName($field, $class, $this->platform)); - $columnAlias = $this->getSQLColumnAlias($fieldMapping['columnName']); + + $columnAlias = null; + if ($this->currentPersisterContext->rsm->hasColumnAliasByField($alias, $field)) { + $columnAlias = $this->currentPersisterContext->rsm->getColumnAliasByField($alias, $field); + } + + if ($columnAlias === null) { + $columnAlias = $this->getSQLColumnAlias($fieldMapping['columnName']); + } $this->currentPersisterContext->rsm->addFieldResult($alias, $columnAlias, $field); if (! empty($fieldMapping['enumType'])) { diff --git a/src/Query/ResultSetMapping.php b/src/Query/ResultSetMapping.php index b59bf8d0b99..1f3c1843f51 100644 --- a/src/Query/ResultSetMapping.php +++ b/src/Query/ResultSetMapping.php @@ -69,6 +69,13 @@ class ResultSetMapping */ public $fieldMappings = []; + /** + * Map field names for each class to alias + * + * @var array>> + */ + public $columnAliasMappings = []; + /** * Maps column names in the result set to the alias/field name to use in the mapped result. * @@ -335,7 +342,10 @@ public function addFieldResult($alias, $columnName, $fieldName, $declaringClass // column name => alias of owner $this->columnOwnerMap[$columnName] = $alias; // field name => class name of declaring class - $this->declaringClasses[$columnName] = $declaringClass ?: $this->aliasMap[$alias]; + $declaringClass = $declaringClass ?: $this->aliasMap[$alias]; + $this->declaringClasses[$columnName] = $declaringClass; + + $this->columnAliasMappings[$declaringClass][$alias][$fieldName] = $columnName; if (! $this->isMixed && $this->scalarMappings) { $this->isMixed = true; @@ -344,6 +354,20 @@ public function addFieldResult($alias, $columnName, $fieldName, $declaringClass return $this; } + public function hasColumnAliasByField(string $alias, string $fieldName): bool + { + $declaringClass = $this->aliasMap[$alias]; + + return isset($this->columnAliasMappings[$declaringClass][$alias][$fieldName]); + } + + public function getColumnAliasByField(string $alias, string $fieldName): string + { + $declaringClass = $this->aliasMap[$alias]; + + return $this->columnAliasMappings[$declaringClass][$alias][$fieldName]; + } + /** * Adds a joined entity result. * diff --git a/tests/Tests/ORM/Functional/Ticket/SwitchContextWithFilterAndIndexedRelation/Category.php b/tests/Tests/ORM/Functional/Ticket/SwitchContextWithFilterAndIndexedRelation/Category.php new file mode 100644 index 00000000000..f80d89bc98c --- /dev/null +++ b/tests/Tests/ORM/Functional/Ticket/SwitchContextWithFilterAndIndexedRelation/Category.php @@ -0,0 +1,43 @@ +name = $name; + $this->type = $type; + } +} diff --git a/tests/Tests/ORM/Functional/Ticket/SwitchContextWithFilterAndIndexedRelation/CategoryTypeSQLFilter.php b/tests/Tests/ORM/Functional/Ticket/SwitchContextWithFilterAndIndexedRelation/CategoryTypeSQLFilter.php new file mode 100644 index 00000000000..459e0c2f2f7 --- /dev/null +++ b/tests/Tests/ORM/Functional/Ticket/SwitchContextWithFilterAndIndexedRelation/CategoryTypeSQLFilter.php @@ -0,0 +1,22 @@ +getName() === Category::class) { + return sprintf('%s.%s = %s', $targetTableAlias, $targetEntity->fieldMappings['type']['fieldName'], $this->getParameter('type')); + } + + return ''; + } +} diff --git a/tests/Tests/ORM/Functional/Ticket/SwitchContextWithFilterAndIndexedRelation/ChangeFiltersTest.php b/tests/Tests/ORM/Functional/Ticket/SwitchContextWithFilterAndIndexedRelation/ChangeFiltersTest.php new file mode 100644 index 00000000000..c0bc57bdb10 --- /dev/null +++ b/tests/Tests/ORM/Functional/Ticket/SwitchContextWithFilterAndIndexedRelation/ChangeFiltersTest.php @@ -0,0 +1,60 @@ +setUpEntitySchema([ + Company::class, + Category::class, + ]); + } + + private function prepareData(): void + { + $cat1 = new Category('cat1', self::CAT_FOO); + $cat2 = new Category('cat2', self::CAT_BAR); + $companyA = new Company(self::COMPANY_A, [$cat1, $cat2]); + + $this->_em->persist($cat1); + $this->_em->persist($cat2); + $this->_em->persist($companyA); + $this->_em->flush(); + $this->_em->clear(); + } + + public function testIndexAliasUpdatedWithUpdatedFilter(): void + { + $this->prepareData(); + + $company = $this->_em->getRepository(Company::class)->findOneBy([]); + + self::assertCount(2, $company->categories); + self::assertEquals([self::CAT_FOO, self::CAT_BAR], $company->categories->map(static function (Category $c): string { + return $c->type; + })->getValues()); + + $this->_em->clear(); + $this->_em->getConfiguration()->addFilter(CategoryTypeSQLFilter::class, CategoryTypeSQLFilter::class); + $this->_em->getFilters()->enable(CategoryTypeSQLFilter::class)->setParameter('type', self::CAT_FOO); + + $company = $this->_em->getRepository(Company::class)->findOneBy([]); + + self::assertCount(1, $company->categories); + self::assertEquals([self::CAT_FOO], $company->categories->map(static function (Category $c): string { + return $c->type; + })->getValues()); + } +} diff --git a/tests/Tests/ORM/Functional/Ticket/SwitchContextWithFilterAndIndexedRelation/Company.php b/tests/Tests/ORM/Functional/Ticket/SwitchContextWithFilterAndIndexedRelation/Company.php new file mode 100644 index 00000000000..c0a54cfbeeb --- /dev/null +++ b/tests/Tests/ORM/Functional/Ticket/SwitchContextWithFilterAndIndexedRelation/Company.php @@ -0,0 +1,46 @@ + + */ + public $categories; + + /** @param Category[] $categories */ + public function __construct(string $name, array $categories) + { + $this->name = $name; + $this->categories = new ArrayCollection($categories); + } +} From 737cca5b78dafc3148c32d224ee8cc743c9a4604 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 4 Feb 2025 18:23:54 +0000 Subject: [PATCH 7/7] Bump doctrine/.github from 7.1.0 to 7.2.1 Bumps [doctrine/.github](https://github.com/doctrine/.github) from 7.1.0 to 7.2.1. - [Release notes](https://github.com/doctrine/.github/releases) - [Commits](https://github.com/doctrine/.github/compare/7.1.0...7.2.1) --- updated-dependencies: - dependency-name: doctrine/.github dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/coding-standards.yml | 2 +- .github/workflows/documentation.yml | 2 +- .github/workflows/release-on-milestone-closed.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/coding-standards.yml b/.github/workflows/coding-standards.yml index efc722b1353..755956430e8 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.1.0" + uses: "doctrine/.github/.github/workflows/coding-standards.yml@7.2.1" diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 58eb5f81459..deec2b9dee8 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.1.0" + uses: "doctrine/.github/.github/workflows/documentation.yml@7.2.1" diff --git a/.github/workflows/release-on-milestone-closed.yml b/.github/workflows/release-on-milestone-closed.yml index 8ed48106aca..d7ad5ab4e21 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.1.0" + uses: "doctrine/.github/.github/workflows/release-on-milestone-closed.yml@7.2.1" secrets: GIT_AUTHOR_EMAIL: ${{ secrets.GIT_AUTHOR_EMAIL }} GIT_AUTHOR_NAME: ${{ secrets.GIT_AUTHOR_NAME }}