Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 2b62d35

Browse files
bobvandevijverdbannik
authored andcommittedJan 21, 2025··
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
1 parent c12fd2c commit 2b62d35

11 files changed

+232
-15
lines changed
 

‎src/Cache/CollectionCacheKey.php

+4-2
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,16 @@ class CollectionCacheKey extends CacheKey
4343
* @param string $association The field name that represents the association.
4444
* @param array<string, mixed> $ownerIdentifier The identifier of the owning entity.
4545
*/
46-
public function __construct($entityClass, $association, array $ownerIdentifier)
46+
public function __construct($entityClass, $association, array $ownerIdentifier, string $filterHash = '')
4747
{
4848
ksort($ownerIdentifier);
4949

5050
$this->ownerIdentifier = $ownerIdentifier;
5151
$this->entityClass = (string) $entityClass;
5252
$this->association = (string) $association;
5353

54-
parent::__construct(str_replace('\\', '.', strtolower($entityClass)) . '_' . implode(' ', $ownerIdentifier) . '__' . $association);
54+
$filterHash = $filterHash === '' ? '' : '_' . $filterHash;
55+
56+
parent::__construct(str_replace('\\', '.', strtolower($entityClass)) . '_' . implode(' ', $ownerIdentifier) . '__' . $association . $filterHash);
5557
}
5658
}

‎src/Cache/Persister/Collection/AbstractCollectionPersister.php

+8-2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use Doctrine\ORM\PersistentCollection;
2020
use Doctrine\ORM\Persisters\Collection\CollectionPersister;
2121
use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
22+
use Doctrine\ORM\Query\FilterCollection;
2223
use Doctrine\ORM\UnitOfWork;
2324

2425
use function array_values;
@@ -55,6 +56,9 @@ abstract class AbstractCollectionPersister implements CachedCollectionPersister
5556
/** @var string */
5657
protected $regionName;
5758

59+
/** @var FilterCollection */
60+
protected $filters;
61+
5862
/** @var CollectionHydrator */
5963
protected $hydrator;
6064

@@ -76,6 +80,7 @@ public function __construct(CollectionPersister $persister, Region $region, Enti
7680
$this->region = $region;
7781
$this->persister = $persister;
7882
$this->association = $association;
83+
$this->filters = $em->getFilters();
7984
$this->regionName = $region->getName();
8085
$this->uow = $em->getUnitOfWork();
8186
$this->metadataFactory = $em->getMetadataFactory();
@@ -189,7 +194,7 @@ public function containsKey(PersistentCollection $collection, $key)
189194
public function count(PersistentCollection $collection)
190195
{
191196
$ownerId = $this->uow->getEntityIdentifier($collection->getOwner());
192-
$key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association['fieldName'], $ownerId);
197+
$key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association['fieldName'], $ownerId, $this->filters->getHash());
193198
$entry = $this->region->get($key);
194199

195200
if ($entry !== null) {
@@ -241,7 +246,8 @@ protected function evictCollectionCache(PersistentCollection $collection)
241246
$key = new CollectionCacheKey(
242247
$this->sourceEntity->rootEntityName,
243248
$this->association['fieldName'],
244-
$this->uow->getEntityIdentifier($collection->getOwner())
249+
$this->uow->getEntityIdentifier($collection->getOwner()),
250+
$this->filters->getHash()
245251
);
246252

247253
$this->region->evict($key);

‎src/Cache/Persister/Collection/NonStrictReadWriteCachedCollectionPersister.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ public function afterTransactionRolledBack()
4545
public function delete(PersistentCollection $collection)
4646
{
4747
$ownerId = $this->uow->getEntityIdentifier($collection->getOwner());
48-
$key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association['fieldName'], $ownerId);
48+
$key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association['fieldName'], $ownerId, $this->filters->getHash());
4949

5050
$this->persister->delete($collection);
5151

@@ -65,7 +65,7 @@ public function update(PersistentCollection $collection)
6565
}
6666

6767
$ownerId = $this->uow->getEntityIdentifier($collection->getOwner());
68-
$key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association['fieldName'], $ownerId);
68+
$key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association['fieldName'], $ownerId, $this->filters->getHash());
6969

7070
// Invalidate non initialized collections OR ordered collection
7171
if ($isDirty && ! $isInitialized || isset($this->association['orderBy'])) {

‎src/Cache/Persister/Collection/ReadWriteCachedCollectionPersister.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ public function afterTransactionRolledBack()
6868
public function delete(PersistentCollection $collection)
6969
{
7070
$ownerId = $this->uow->getEntityIdentifier($collection->getOwner());
71-
$key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association['fieldName'], $ownerId);
71+
$key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association['fieldName'], $ownerId, $this->filters->getHash());
7272
$lock = $this->region->lock($key);
7373

7474
$this->persister->delete($collection);
@@ -98,7 +98,7 @@ public function update(PersistentCollection $collection)
9898
$this->persister->update($collection);
9999

100100
$ownerId = $this->uow->getEntityIdentifier($collection->getOwner());
101-
$key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association['fieldName'], $ownerId);
101+
$key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association['fieldName'], $ownerId, $this->filters->getHash());
102102
$lock = $this->region->lock($key);
103103

104104
if ($lock === null) {

‎src/Cache/Persister/Entity/AbstractEntityPersister.php

+11-5
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
use Doctrine\ORM\PersistentCollection;
2323
use Doctrine\ORM\Persisters\Entity\EntityPersister;
2424
use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
25+
use Doctrine\ORM\Query\FilterCollection;
2526
use Doctrine\ORM\UnitOfWork;
2627

2728
use function array_merge;
@@ -62,6 +63,9 @@ abstract class AbstractEntityPersister implements CachedEntityPersister
6263
/** @var Cache */
6364
protected $cache;
6465

66+
/** @var FilterCollection */
67+
protected $filters;
68+
6569
/** @var CacheLogger|null */
6670
protected $cacheLogger;
6771

@@ -91,6 +95,7 @@ public function __construct(EntityPersister $persister, Region $region, EntityMa
9195
$this->region = $region;
9296
$this->persister = $persister;
9397
$this->cache = $em->getCache();
98+
$this->filters = $em->getFilters();
9499
$this->regionName = $region->getName();
95100
$this->uow = $em->getUnitOfWork();
96101
$this->metadataFactory = $em->getMetadataFactory();
@@ -261,7 +266,7 @@ protected function getHash($query, $criteria, ?array $orderBy = null, $limit = n
261266
? $this->persister->expandCriteriaParameters($criteria)
262267
: $this->persister->expandParameters($criteria);
263268

264-
return sha1($query . serialize($params) . serialize($orderBy) . $limit . $offset);
269+
return sha1($query . serialize($params) . serialize($orderBy) . $limit . $offset . $this->filters->getHash());
265270
}
266271

267272
/**
@@ -524,7 +529,7 @@ public function loadManyToManyCollection(array $assoc, $sourceEntity, Persistent
524529
}
525530

526531
$ownerId = $this->uow->getEntityIdentifier($collection->getOwner());
527-
$key = $this->buildCollectionCacheKey($assoc, $ownerId);
532+
$key = $this->buildCollectionCacheKey($assoc, $ownerId, $this->filters->getHash());
528533
$list = $persister->loadCollectionCache($collection, $key);
529534

530535
if ($list !== null) {
@@ -559,7 +564,7 @@ public function loadOneToManyCollection(array $assoc, $sourceEntity, PersistentC
559564
}
560565

561566
$ownerId = $this->uow->getEntityIdentifier($collection->getOwner());
562-
$key = $this->buildCollectionCacheKey($assoc, $ownerId);
567+
$key = $this->buildCollectionCacheKey($assoc, $ownerId, $this->filters->getHash());
563568
$list = $persister->loadCollectionCache($collection, $key);
564569

565570
if ($list !== null) {
@@ -611,12 +616,13 @@ public function refresh(array $id, $entity, $lockMode = null)
611616
*
612617
* @return CollectionCacheKey
613618
*/
614-
protected function buildCollectionCacheKey(array $association, $ownerId)
619+
protected function buildCollectionCacheKey(array $association, $ownerId, string $filterHash)
615620
{
616621
return new CollectionCacheKey(
617622
$this->metadataFactory->getMetadataFor($association['sourceEntity'])->rootEntityName,
618623
$association['fieldName'],
619-
$ownerId
624+
$ownerId,
625+
$filterHash
620626
);
621627
}
622628
}

‎src/Persisters/Entity/BasicEntityPersister.php

+9-1
Original file line numberDiff line numberDiff line change
@@ -1560,7 +1560,15 @@ protected function getSelectColumnSQL($field, ClassMetadata $class, $alias = 'r'
15601560
$tableAlias = $this->getSQLTableAlias($class->name, $root);
15611561
$fieldMapping = $class->fieldMappings[$field];
15621562
$sql = sprintf('%s.%s', $tableAlias, $this->quoteStrategy->getColumnName($field, $class, $this->platform));
1563-
$columnAlias = $this->getSQLColumnAlias($fieldMapping['columnName']);
1563+
1564+
$columnAlias = null;
1565+
if ($this->currentPersisterContext->rsm->hasColumnAliasByField($alias, $field)) {
1566+
$columnAlias = $this->currentPersisterContext->rsm->getColumnAliasByField($alias, $field);
1567+
}
1568+
1569+
if ($columnAlias === null) {
1570+
$columnAlias = $this->getSQLColumnAlias($fieldMapping['columnName']);
1571+
}
15641572

15651573
$this->currentPersisterContext->rsm->addFieldResult($alias, $columnAlias, $field);
15661574
if (! empty($fieldMapping['enumType'])) {

‎src/Query/ResultSetMapping.php

+25-1
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,13 @@ class ResultSetMapping
6969
*/
7070
public $fieldMappings = [];
7171

72+
/**
73+
* Map field names for each class to alias
74+
*
75+
* @var array<class-string, array<string, array<string, string>>>
76+
*/
77+
public $columnAliasMappings = [];
78+
7279
/**
7380
* Maps column names in the result set to the alias/field name to use in the mapped result.
7481
*
@@ -335,7 +342,10 @@ public function addFieldResult($alias, $columnName, $fieldName, $declaringClass
335342
// column name => alias of owner
336343
$this->columnOwnerMap[$columnName] = $alias;
337344
// field name => class name of declaring class
338-
$this->declaringClasses[$columnName] = $declaringClass ?: $this->aliasMap[$alias];
345+
$declaringClass = $declaringClass ?: $this->aliasMap[$alias];
346+
$this->declaringClasses[$columnName] = $declaringClass;
347+
348+
$this->columnAliasMappings[$declaringClass][$alias][$fieldName] = $columnName;
339349

340350
if (! $this->isMixed && $this->scalarMappings) {
341351
$this->isMixed = true;
@@ -344,6 +354,20 @@ public function addFieldResult($alias, $columnName, $fieldName, $declaringClass
344354
return $this;
345355
}
346356

357+
public function hasColumnAliasByField(string $alias, string $fieldName): bool
358+
{
359+
$declaringClass = $this->aliasMap[$alias];
360+
361+
return isset($this->columnAliasMappings[$declaringClass][$alias][$fieldName]);
362+
}
363+
364+
public function getColumnAliasByField(string $alias, string $fieldName): string
365+
{
366+
$declaringClass = $this->aliasMap[$alias];
367+
368+
return $this->columnAliasMappings[$declaringClass][$alias][$fieldName];
369+
}
370+
347371
/**
348372
* Adds a joined entity result.
349373
*
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilterAndIndexedRelation;
6+
7+
use Doctrine\ORM\Mapping as ORM;
8+
9+
/**
10+
* @ORM\Entity
11+
* @ORM\Table(name="Category_Master")
12+
*/
13+
class Category
14+
{
15+
/**
16+
* @ORM\Id
17+
* @ORM\Column(type="integer")
18+
* @ORM\GeneratedValue(strategy="AUTO")
19+
*
20+
* @var int
21+
*/
22+
public $id;
23+
24+
/**
25+
* @ORM\Column(type="string")
26+
*
27+
* @var string
28+
*/
29+
public $name;
30+
31+
/**
32+
* @ORM\Column(type="string")
33+
*
34+
* @var string
35+
*/
36+
public $type;
37+
38+
public function __construct(string $name, string $type)
39+
{
40+
$this->name = $name;
41+
$this->type = $type;
42+
}
43+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilterAndIndexedRelation;
6+
7+
use Doctrine\ORM\Mapping\ClassMetadata;
8+
use Doctrine\ORM\Query\Filter\SQLFilter;
9+
10+
use function sprintf;
11+
12+
class CategoryTypeSQLFilter extends SQLFilter
13+
{
14+
public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias): string
15+
{
16+
if ($targetEntity->getName() === Category::class) {
17+
return sprintf('%s.%s = %s', $targetTableAlias, $targetEntity->fieldMappings['type']['fieldName'], $this->getParameter('type'));
18+
}
19+
20+
return '';
21+
}
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilterAndIndexedRelation;
6+
7+
use Doctrine\Tests\OrmFunctionalTestCase;
8+
9+
final class ChangeFiltersTest extends OrmFunctionalTestCase
10+
{
11+
private const COMPANY_A = 'A';
12+
private const CAT_BAR = 'bar';
13+
private const CAT_FOO = 'foo';
14+
15+
public function setUp(): void
16+
{
17+
parent::setUp();
18+
19+
$this->setUpEntitySchema([
20+
Company::class,
21+
Category::class,
22+
]);
23+
}
24+
25+
private function prepareData(): void
26+
{
27+
$cat1 = new Category('cat1', self::CAT_FOO);
28+
$cat2 = new Category('cat2', self::CAT_BAR);
29+
$companyA = new Company(self::COMPANY_A, [$cat1, $cat2]);
30+
31+
$this->_em->persist($cat1);
32+
$this->_em->persist($cat2);
33+
$this->_em->persist($companyA);
34+
$this->_em->flush();
35+
$this->_em->clear();
36+
}
37+
38+
public function testIndexAliasUpdatedWithUpdatedFilter(): void
39+
{
40+
$this->prepareData();
41+
42+
$company = $this->_em->getRepository(Company::class)->findOneBy([]);
43+
44+
self::assertCount(2, $company->categories);
45+
self::assertEquals([self::CAT_FOO, self::CAT_BAR], $company->categories->map(static function (Category $c): string {
46+
return $c->type;
47+
})->getValues());
48+
49+
$this->_em->clear();
50+
$this->_em->getConfiguration()->addFilter(CategoryTypeSQLFilter::class, CategoryTypeSQLFilter::class);
51+
$this->_em->getFilters()->enable(CategoryTypeSQLFilter::class)->setParameter('type', self::CAT_FOO);
52+
53+
$company = $this->_em->getRepository(Company::class)->findOneBy([]);
54+
55+
self::assertCount(1, $company->categories);
56+
self::assertEquals([self::CAT_FOO], $company->categories->map(static function (Category $c): string {
57+
return $c->type;
58+
})->getValues());
59+
}
60+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilterAndIndexedRelation;
6+
7+
use Doctrine\Common\Collections\ArrayCollection;
8+
use Doctrine\Common\Collections\Collection;
9+
use Doctrine\ORM\Mapping as ORM;
10+
11+
/**
12+
* @ORM\Entity
13+
* @ORM\Table(name="Company_Master")
14+
*/
15+
class Company
16+
{
17+
/**
18+
* @ORM\Id
19+
* @ORM\Column(type="integer")
20+
* @ORM\GeneratedValue(strategy="AUTO")
21+
*
22+
* @var int
23+
*/
24+
public $id;
25+
26+
/**
27+
* @ORM\Column(type="string")
28+
*
29+
* @var string
30+
*/
31+
public $name;
32+
33+
/**
34+
* @ORM\ManyToMany(targetEntity="Category", fetch="EAGER", indexBy="type")
35+
*
36+
* @var Collection<int, Category>
37+
*/
38+
public $categories;
39+
40+
/** @param Category[] $categories */
41+
public function __construct(string $name, array $categories)
42+
{
43+
$this->name = $name;
44+
$this->categories = new ArrayCollection($categories);
45+
}
46+
}

0 commit comments

Comments
 (0)
Please sign in to comment.