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 }} diff --git a/docs/en/reference/association-mapping.rst b/docs/en/reference/association-mapping.rst index fcb2c8a42df..cbdb3087f6f 100644 --- a/docs/en/reference/association-mapping.rst +++ b/docs/en/reference/association-mapping.rst @@ -903,8 +903,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:: @@ -931,7 +930,7 @@ Is essentially the same as following: ownerIdentifier = $ownerIdentifier; - 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 8c087a8b2fb..25b335d09cf 100644 --- a/src/Cache/Persister/Collection/AbstractCollectionPersister.php +++ b/src/Cache/Persister/Collection/AbstractCollectionPersister.php @@ -18,6 +18,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; @@ -35,6 +36,7 @@ abstract class AbstractCollectionPersister implements CachedCollectionPersister protected array $queuedCache = []; protected string $regionName; + protected FilterCollection $filters; protected CollectionHydrator $hydrator; protected CacheLogger|null $cacheLogger; @@ -48,6 +50,10 @@ public function __construct( $cacheConfig = $configuration->getSecondLevelCacheConfiguration(); $cacheFactory = $cacheConfig->getCacheFactory(); + $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(); @@ -135,7 +141,7 @@ public function containsKey(PersistentCollection $collection, mixed $key): bool public function count(PersistentCollection $collection): int { $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) { diff --git a/src/Cache/Persister/Collection/NonStrictReadWriteCachedCollectionPersister.php b/src/Cache/Persister/Collection/NonStrictReadWriteCachedCollectionPersister.php index ac861f44031..41b3648a366 100644 --- a/src/Cache/Persister/Collection/NonStrictReadWriteCachedCollectionPersister.php +++ b/src/Cache/Persister/Collection/NonStrictReadWriteCachedCollectionPersister.php @@ -36,7 +36,7 @@ public function afterTransactionRolledBack(): void public function delete(PersistentCollection $collection): void { $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); @@ -53,7 +53,7 @@ public function update(PersistentCollection $collection): void } $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 || $this->association->isOrdered()) { diff --git a/src/Cache/Persister/Collection/ReadWriteCachedCollectionPersister.php b/src/Cache/Persister/Collection/ReadWriteCachedCollectionPersister.php index 347a065f2ce..6ecf34b3eed 100644 --- a/src/Cache/Persister/Collection/ReadWriteCachedCollectionPersister.php +++ b/src/Cache/Persister/Collection/ReadWriteCachedCollectionPersister.php @@ -61,7 +61,7 @@ public function afterTransactionRolledBack(): void public function delete(PersistentCollection $collection): void { $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); @@ -88,7 +88,7 @@ public function update(PersistentCollection $collection): void $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 3fcc0fc8e98..945ad5b348b 100644 --- a/src/Cache/Persister/Entity/AbstractEntityPersister.php +++ b/src/Cache/Persister/Entity/AbstractEntityPersister.php @@ -24,10 +24,12 @@ use Doctrine\ORM\PersistentCollection; use Doctrine\ORM\Persisters\Entity\EntityPersister; use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver; +use Doctrine\ORM\Query\FilterCollection; use Doctrine\ORM\Query\ResultSetMapping; use Doctrine\ORM\UnitOfWork; use function array_merge; +use function func_get_args; use function serialize; use function sha1; @@ -43,6 +45,7 @@ abstract class AbstractEntityPersister implements CachedEntityPersister protected TimestampCacheKey $timestampKey; protected EntityHydrator $hydrator; protected Cache $cache; + protected FilterCollection $filters; protected CacheLogger|null $cacheLogger = null; protected string $regionName; @@ -64,6 +67,7 @@ public function __construct( $cacheFactory = $cacheConfig->getCacheFactory(); $this->cache = $em->getCache(); + $this->filters = $em->getFilters(); $this->regionName = $region->getName(); $this->uow = $em->getUnitOfWork(); $this->metadataFactory = $em->getMetadataFactory(); @@ -215,7 +219,7 @@ protected function getHash( ? $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()); } /** @@ -472,7 +476,7 @@ public function loadManyToManyCollection( } $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) { @@ -503,7 +507,7 @@ public function loadOneToManyCollection( } $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) { @@ -546,12 +550,15 @@ public function refresh(array $id, object $entity, LockMode|int|null $lockMode = } /** @param array $ownerId */ - protected function buildCollectionCacheKey(AssociationMapping $association, array $ownerId): CollectionCacheKey + protected function buildCollectionCacheKey(AssociationMapping $association, array $ownerId, /* string $filterHash */): CollectionCacheKey { + $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, + $filterHash, ); } } diff --git a/src/Internal/Hydration/SimpleObjectHydrator.php b/src/Internal/Hydration/SimpleObjectHydrator.php index eab7b9bae18..6f808f82fb1 100644 --- a/src/Internal/Hydration/SimpleObjectHydrator.php +++ b/src/Internal/Hydration/SimpleObjectHydrator.php @@ -17,6 +17,7 @@ use function assert; use function count; use function in_array; +use function is_array; use function key; use function reset; use function sprintf; @@ -138,14 +139,21 @@ protected function hydrateRowData(array $row, array &$result): void } 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/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/src/Mapping/Driver/ReflectionBasedDriver.php b/src/Mapping/Driver/ReflectionBasedDriver.php index 7d8547150ac..2425d6151e2 100644 --- a/src/Mapping/Driver/ReflectionBasedDriver.php +++ b/src/Mapping/Driver/ReflectionBasedDriver.php @@ -22,8 +22,13 @@ trait ReflectionBasedDriver */ private function isRepeatedPropertyDeclaration(ReflectionProperty $property, ClassMetadata $metadata): bool { + /** @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/src/Persisters/Entity/BasicEntityPersister.php b/src/Persisters/Entity/BasicEntityPersister.php index 91b2eaa8832..fd5e9250960 100644 --- a/src/Persisters/Entity/BasicEntityPersister.php +++ b/src/Persisters/Entity/BasicEntityPersister.php @@ -1500,7 +1500,15 @@ protected function getSelectColumnSQL(string $field, ClassMetadata $class, strin $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 430db10fec8..c0ccc127fd5 100644 --- a/src/Query/ResultSetMapping.php +++ b/src/Query/ResultSetMapping.php @@ -66,6 +66,13 @@ class ResultSetMapping */ public array $fieldMappings = []; + /** + * Map field names for each class to alias + * + * @var array>> + */ + public array $columnAliasMappings = []; + /** * Maps column names in the result set to the alias/field name to use in the mapped result. * @@ -328,7 +335,10 @@ public function addFieldResult(string $alias, string $columnName, string $fieldN // 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; @@ -337,6 +347,20 @@ public function addFieldResult(string $alias, string $columnName, string $fieldN 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/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/ORM/Functional/Ticket/GH10450Test.php b/tests/Tests/ORM/Functional/Ticket/GH10450Test.php index 777bc9d118e..79001b6dfe6 100644 --- a/tests/Tests/ORM/Functional/Ticket/GH10450Test.php +++ b/tests/Tests/ORM/Functional/Ticket/GH10450Test.php @@ -30,6 +30,15 @@ public static 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); + } } #[ORM\Entity] @@ -113,3 +122,26 @@ class GH10450MappedSuperclassChildProtected extends GH10450BaseMappedSuperclassP #[ORM\Column(type: 'text', name: 'child')] protected string $field; } + +abstract class GH10450AbstractEntity +{ + #[ORM\Column(type: 'integer')] + #[ORM\Id] + #[ORM\GeneratedValue] + protected int $id; +} + +#[ORM\Entity] +#[ORM\InheritanceType('SINGLE_TABLE')] +#[ORM\DiscriminatorMap(['cat' => GH10450Cat::class])] +#[ORM\DiscriminatorColumn(name: 'type')] +abstract class GH10450Animal extends GH10450AbstractEntity +{ + #[ORM\Column(type: 'text', name: 'base')] + private string $field; +} + +#[ORM\Entity] +class GH10450Cat extends GH10450Animal +{ +} 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..fb9a2eb6e22 --- /dev/null +++ b/tests/Tests/ORM/Functional/Ticket/SwitchContextWithFilterAndIndexedRelation/Category.php @@ -0,0 +1,29 @@ +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..d9a96470a38 --- /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..0c85555d4fb --- /dev/null +++ b/tests/Tests/ORM/Functional/Ticket/SwitchContextWithFilterAndIndexedRelation/Company.php @@ -0,0 +1,33 @@ + */ + #[ORM\ManyToMany(targetEntity: Category::class, fetch: 'EAGER', indexBy: 'type')] + public Collection $categories; + + /** @param Category[] $categories */ + public function __construct(string $name, array $categories) + { + $this->name = $name; + $this->categories = new ArrayCollection($categories); + } +} diff --git a/tests/Tests/ORM/Hydration/SimpleObjectHydratorTest.php b/tests/Tests/ORM/Hydration/SimpleObjectHydratorTest.php index 59a667ab07e..bf2f6586aa0 100644 --- a/tests/Tests/ORM/Hydration/SimpleObjectHydratorTest.php +++ b/tests/Tests/ORM/Hydration/SimpleObjectHydratorTest.php @@ -7,11 +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 +159,25 @@ public function testWrongValuesShouldNotBeConvertedToPhpValue(): void $result = $hydrator->hydrateAll($stmt, $rsm); self::assertEquals($result[0], $expectedEntity); } + + 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::createWrapperResultFromArray($resultSet); + $hydrator = new SimpleObjectHydrator($this->entityManager); + $hydrator->hydrateAll($stmt, $rsm); + } } 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'])) {