Skip to content

Commit 1db4828

Browse files
authored
Merge pull request #2324 from franmomu/make_persist_collection_generic
Make `PersistentCollectionInterface` and `PersistentCollection` generic
2 parents d35b8d4 + 5e57d0e commit 1db4828

File tree

6 files changed

+106
-31
lines changed

6 files changed

+106
-31
lines changed

lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadata.php

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,8 @@
8585
* version?: bool,
8686
* lock?: bool,
8787
* inherited?: string,
88-
* declared?: class-string
88+
* declared?: class-string,
89+
* prime?: list<string>
8990
* }
9091
* @psalm-type FieldMapping = array{
9192
* type: string,
@@ -99,7 +100,6 @@
99100
* isOwningSide: bool,
100101
* isInverseSide: bool,
101102
* strategy?: string,
102-
* notSaved?: bool,
103103
* association?: int,
104104
* id?: bool,
105105
* collectionClass?: class-string,
@@ -124,7 +124,8 @@
124124
* lock?: bool,
125125
* notSaved?: bool,
126126
* inherited?: string,
127-
* declared?: class-string
127+
* declared?: class-string,
128+
* prime?: list<string>
128129
* }
129130
* @psalm-type AssociationFieldMapping = array{
130131
* type: string,
@@ -162,7 +163,8 @@
162163
* lock?: bool,
163164
* notSaved?: bool,
164165
* inherited?: string,
165-
* declared?: class-string
166+
* declared?: class-string,
167+
* prime?: list<string>
166168
* }
167169
* @psalm-type IndexKeys = array<string, mixed>
168170
* @psalm-type IndexOptions = array<string, mixed>

lib/Doctrine/ODM/MongoDB/PersistentCollection.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,18 @@
1010

1111
/**
1212
* A PersistentCollection represents a collection of elements that have persistent state.
13+
*
14+
* @template TKey of array-key
15+
* @template T of object
16+
* @template-implements PersistentCollectionInterface<TKey,T>
1317
*/
1418
final class PersistentCollection implements PersistentCollectionInterface
1519
{
20+
/** @use PersistentCollectionTrait<TKey, T> */
1621
use PersistentCollectionTrait;
1722

1823
/**
19-
* @param BaseCollection $coll
24+
* @param BaseCollection<TKey, T> $coll
2025
*/
2126
public function __construct(BaseCollection $coll, DocumentManager $dm, UnitOfWork $uow)
2227
{

lib/Doctrine/ODM/MongoDB/PersistentCollection/PersistentCollectionInterface.php

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,28 @@
1313
* Interface for persistent collection classes.
1414
*
1515
* @internal
16+
*
17+
* @psalm-import-type FieldMapping from \Doctrine\ODM\MongoDB\Mapping\ClassMetadata
18+
*
19+
* @template TKey of array-key
20+
* @template T of object
21+
* @template-extends Collection<TKey, T>
1622
*/
1723
interface PersistentCollectionInterface extends Collection
1824
{
1925
/**
2026
* Sets the document manager and unit of work (used during merge operations).
27+
*
28+
* @return void
2129
*/
2230
public function setDocumentManager(DocumentManager $dm);
2331

2432
/**
2533
* Sets the array of raw mongo data that will be used to initialize this collection.
2634
*
2735
* @param array $mongoData
36+
*
37+
* @return void
2838
*/
2939
public function setMongoData(array $mongoData);
3040

@@ -39,6 +49,8 @@ public function getMongoData();
3949
* Set hints to account for during reconstitution/lookup of the documents.
4050
*
4151
* @param array $hints
52+
*
53+
* @return void
4254
*/
4355
public function setHints(array $hints);
4456

@@ -52,6 +64,8 @@ public function getHints();
5264
/**
5365
* Initializes the collection by loading its contents from the database
5466
* if the collection is not yet initialized.
67+
*
68+
* @return void
5569
*/
5670
public function initialize();
5771

@@ -67,12 +81,18 @@ public function isDirty();
6781
* Sets a boolean flag, indicating whether this collection is dirty.
6882
*
6983
* @param bool $dirty Whether the collection should be marked dirty or not.
84+
*
85+
* @return void
7086
*/
7187
public function setDirty($dirty);
7288

7389
/**
74-
* Sets the collection's owning entity together with the AssociationMapping that
90+
* Sets the collection's owning document together with the AssociationMapping that
7591
* describes the association between the owner and the elements of the collection.
92+
*
93+
* @psalm-param FieldMapping $mapping
94+
*
95+
* @return void
7696
*/
7797
public function setOwner(object $document, array $mapping);
7898

@@ -81,12 +101,16 @@ public function setOwner(object $document, array $mapping);
81101
* itself numerically if using save strategy that is enforcing BSON array.
82102
* Reindexing is safe as snapshot is taken only after synchronizing collection
83103
* with database or clearing it.
104+
*
105+
* @return void
84106
*/
85107
public function takeSnapshot();
86108

87109
/**
88110
* Clears the internal snapshot information and sets isDirty to true if the collection
89111
* has elements.
112+
*
113+
* @return void
90114
*/
91115
public function clearSnapshot();
92116

@@ -128,11 +152,13 @@ public function getOwner(): ?object;
128152

129153
/**
130154
* @return array
155+
* @psalm-return FieldMapping
131156
*/
132157
public function getMapping();
133158

134159
/**
135160
* @return ClassMetadata
161+
* @psalm-return ClassMetadata<T>
136162
*
137163
* @throws MongoDBException
138164
*/
@@ -142,6 +168,8 @@ public function getTypeClass();
142168
* Sets the initialized flag of the collection, forcing it into that state.
143169
*
144170
* @param bool $bool
171+
*
172+
* @return void
145173
*/
146174
public function setInitialized($bool);
147175

@@ -155,7 +183,7 @@ public function isInitialized();
155183
/**
156184
* Returns the wrapped Collection instance.
157185
*
158-
* @return Collection
186+
* @return Collection<TKey, T>
159187
*/
160188
public function unwrap();
161189
}

lib/Doctrine/ODM/MongoDB/PersistentCollection/PersistentCollectionTrait.php

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Closure;
88
use Doctrine\Common\Collections\Collection as BaseCollection;
99
use Doctrine\ODM\MongoDB\DocumentManager;
10+
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
1011
use Doctrine\ODM\MongoDB\MongoDBException;
1112
use Doctrine\ODM\MongoDB\UnitOfWork;
1213
use Doctrine\ODM\MongoDB\Utility\CollectionHelper;
@@ -21,25 +22,32 @@
2122

2223
/**
2324
* Trait with methods needed to implement PersistentCollectionInterface.
25+
*
26+
* @psalm-import-type FieldMapping from ClassMetadata
27+
* @template TKey of array-key
28+
* @template T of object
2429
*/
2530
trait PersistentCollectionTrait
2631
{
2732
/**
2833
* A snapshot of the collection at the moment it was fetched from the database.
2934
* This is used to create a diff of the collection at commit time.
3035
*
31-
* @var array
36+
* @var array<TKey, T>
3237
*/
3338
private $snapshot = [];
3439

3540
/**
36-
* Collection's owning entity
41+
* Collection's owning document
3742
*
3843
* @var object|null
3944
*/
4045
private $owner;
4146

42-
/** @var array|null */
47+
/**
48+
* @var array|null
49+
* @psalm-var FieldMapping|null
50+
*/
4351
private $mapping;
4452

4553
/**
@@ -60,7 +68,7 @@ trait PersistentCollectionTrait
6068
/**
6169
* The wrapped Collection instance.
6270
*
63-
* @var BaseCollection
71+
* @var BaseCollection<TKey, T>
6472
*/
6573
private $coll;
6674

@@ -130,6 +138,7 @@ public function initialize()
130138
return;
131139
}
132140

141+
/** @psalm-var array<TKey, T> $newObjects */
133142
$newObjects = [];
134143

135144
if ($this->isDirty) {
@@ -164,7 +173,7 @@ public function initialize()
164173
/**
165174
* Marks this collection as changed/dirty.
166175
*/
167-
private function changed()
176+
private function changed(): void
168177
{
169178
if ($this->isDirty) {
170179
return;
@@ -681,7 +690,7 @@ public function unwrap()
681690
* 2. New collection is not dirty, if reused on other document nothing
682691
* changes.
683692
* 3. Snapshot leads to invalid diffs being generated.
684-
* 4. Lazy loading grabs entities from old owner object.
693+
* 4. Lazy loading grabs documents from old owner object.
685694
* 5. New collection is connected to old owner and leads to duplicate keys.
686695
*/
687696
public function __clone()
@@ -730,11 +739,10 @@ private function doAdd($value, $arrayAccess)
730739
* Actual logic for removing element by its key.
731740
*
732741
* @param mixed $offset
733-
* @param bool $arrayAccess
734742
*
735-
* @return bool
743+
* @return bool|T|null
736744
*/
737-
private function doRemove($offset, $arrayAccess)
745+
private function doRemove($offset, bool $arrayAccess)
738746
{
739747
$this->initialize();
740748
if ($arrayAccess) {
@@ -758,9 +766,8 @@ private function doRemove($offset, $arrayAccess)
758766
*
759767
* @param mixed $offset
760768
* @param mixed $value
761-
* @param bool $arrayAccess
762769
*/
763-
private function doSet($offset, $value, $arrayAccess)
770+
private function doSet($offset, $value, bool $arrayAccess): void
764771
{
765772
$arrayAccess ? $this->coll->offsetSet($offset, $value) : $this->coll->set($offset, $value);
766773

@@ -777,10 +784,8 @@ private function doSet($offset, $value, $arrayAccess)
777784
*
778785
* Embedded documents are automatically considered as "orphan removal enabled" because they might have references
779786
* that require to trigger cascade remove operations.
780-
*
781-
* @return bool
782787
*/
783-
private function isOrphanRemovalEnabled()
788+
private function isOrphanRemovalEnabled(): bool
784789
{
785790
if ($this->mapping === null) {
786791
return false;
@@ -795,10 +800,8 @@ private function isOrphanRemovalEnabled()
795800

796801
/**
797802
* Checks whether collection owner needs to be scheduled for dirty change in case the collection is modified.
798-
*
799-
* @return bool
800803
*/
801-
private function needsSchedulingForSynchronization()
804+
private function needsSchedulingForSynchronization(): bool
802805
{
803806
return $this->owner && $this->dm && ! empty($this->mapping['isOwningSide'])
804807
&& $this->dm->getClassMetadata(get_class($this->owner))->isChangeTrackingNotify();

phpstan-baseline.neon

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,3 +228,27 @@ parameters:
228228
message: "#^Method Doctrine\\\\ODM\\\\MongoDB\\\\Tests\\\\SchemaManagerTest\\:\\:writeOptions\\(\\) should return PHPUnit\\\\Framework\\\\Constraint\\\\Constraint but returns PHPUnit\\\\Framework\\\\Constraint\\\\ArraySubset\\.$#"
229229
count: 1
230230
path: tests/Doctrine/ODM/MongoDB/Tests/SchemaManagerTest.php
231+
232+
# import type in PHPStan does not work, see https://github.com/phpstan/phpstan/issues/5091
233+
-
234+
message: "#^Access to offset '.+' on an unknown class Doctrine\\\\ODM\\\\MongoDB\\\\PersistentCollection\\\\FieldMapping\\.$#"
235+
count: 12
236+
path: lib/Doctrine/ODM/MongoDB/PersistentCollection.php
237+
238+
# import type in PHPStan does not work, see https://github.com/phpstan/phpstan/issues/5091
239+
-
240+
message: "#^Method Doctrine\\\\ODM\\\\MongoDB\\\\PersistentCollection\\:\\:getMapping\\(\\) should return array\\('type' \\=\\> string, 'fieldName' \\=\\> string, 'name' \\=\\> string, 'isCascadeRemove' \\=\\> bool, 'isCascadePersist' \\=\\> bool, 'isCascadeRefresh' \\=\\> bool, 'isCascadeMerge' \\=\\> bool, 'isCascadeDetach' \\=\\> bool, \\.\\.\\.\\) but returns Doctrine\\\\ODM\\\\MongoDB\\\\PersistentCollection\\\\FieldMapping\\|null\\.$#"
241+
count: 1
242+
path: lib/Doctrine/ODM/MongoDB/PersistentCollection.php
243+
244+
# import type in PHPStan does not work, see https://github.com/phpstan/phpstan/issues/5091
245+
-
246+
message: "#^Property Doctrine\\\\ODM\\\\MongoDB\\\\PersistentCollection\\:\\:\\$mapping has unknown class Doctrine\\\\ODM\\\\MongoDB\\\\PersistentCollection\\\\FieldMapping as its type\\.$#"
247+
count: 1
248+
path: lib/Doctrine/ODM/MongoDB/PersistentCollection.php
249+
250+
# import type in PHPStan does not work, see https://github.com/phpstan/phpstan/issues/5091
251+
-
252+
message: "#^Property Doctrine\\\\ODM\\\\MongoDB\\\\PersistentCollection\\<TKey of \\(int\\|string\\),T of object\\>\\:\\:\\$mapping \\(Doctrine\\\\ODM\\\\MongoDB\\\\PersistentCollection\\\\FieldMapping\\|null\\) does not accept array\\<string, array\\<int\\|string, mixed\\>\\|bool\\|int\\|string\\|null\\>\\.$#"
253+
count: 1
254+
path: lib/Doctrine/ODM/MongoDB/PersistentCollection.php

tests/Doctrine/ODM/MongoDB/Tests/PersistentCollectionTest.php

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,19 @@ public function testExceptionForGetTypeClassWithoutDocumentManager()
4242
$unserialized = unserialize($serialized);
4343
assert($unserialized instanceof PersistentCollection);
4444

45-
$unserialized->setOwner($owner, ['targetDocument' => stdClass::class]);
45+
$unserialized->setOwner($owner, [
46+
'type' => ClassMetadata::ONE,
47+
'name' => 'name',
48+
'fieldName' => 'fieldName',
49+
'isCascadeRemove' => false,
50+
'isCascadePersist' => false,
51+
'isCascadeRefresh' => false,
52+
'isCascadeMerge' => false,
53+
'isCascadeDetach' => false,
54+
'isOwningSide' => false,
55+
'isInverseSide' => false,
56+
'targetDocument' => stdClass::class,
57+
]);
4658
$this->expectException(MongoDBException::class);
4759
$this->expectExceptionMessage(
4860
'No DocumentManager is associated with this PersistentCollection, ' .
@@ -274,9 +286,10 @@ public function testOffsetExistsIsForwarded()
274286
public function testOffsetGetIsForwarded()
275287
{
276288
$collection = $this->getMockCollection();
277-
$collection->expects($this->once())->method('offsetGet')->willReturn(2);
289+
$object = new stdClass();
290+
$collection->expects($this->once())->method('offsetGet')->willReturn($object);
278291
$pcoll = new PersistentCollection($collection, $this->dm, $this->uow);
279-
$this->assertSame(2, $pcoll[0]);
292+
$this->assertSame($object, $pcoll[0]);
280293
}
281294

282295
public function testOffsetUnsetIsForwarded()
@@ -302,12 +315,12 @@ public function testOffsetSetIsForwarded()
302315
$collection = $this->getMockCollection();
303316
$collection->expects($this->exactly(2))->method('offsetSet');
304317
$pcoll = new PersistentCollection($collection, $this->dm, $this->uow);
305-
$pcoll[] = 1;
306-
$pcoll[1] = 2;
318+
$pcoll[] = new stdClass();
319+
$pcoll[1] = new stdClass();
307320
$collection->expects($this->once())->method('add');
308-
$pcoll->add(3);
321+
$pcoll->add(new stdClass());
309322
$collection->expects($this->once())->method('set');
310-
$pcoll->set(3, 4);
323+
$pcoll->set(3, new stdClass());
311324
}
312325

313326
public function testIsEmptyIsForwardedWhenCollectionIsInitialized()

0 commit comments

Comments
 (0)