Completed
Pull Request — master (#1803)
by Maciej
20:22
created

PersistentCollectionTrait::getHints()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
crap 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\ODM\MongoDB\PersistentCollection;
6
7
use Closure;
8
use Doctrine\Common\Collections\Collection as BaseCollection;
9
use Doctrine\ODM\MongoDB\DocumentManager;
10
use Doctrine\ODM\MongoDB\MongoDBException;
11
use Doctrine\ODM\MongoDB\UnitOfWork;
12
use Doctrine\ODM\MongoDB\Utility\CollectionHelper;
13
use function array_udiff;
14
use function array_udiff_assoc;
15
use function array_values;
16
use function count;
17
use function get_class;
18
use function is_object;
19
use function spl_object_hash;
20
21
/**
22
 * Trait with methods needed to implement PersistentCollectionInterface.
23
 */
24
trait PersistentCollectionTrait
25
{
26
    /**
27
     * A snapshot of the collection at the moment it was fetched from the database.
28
     * This is used to create a diff of the collection at commit time.
29
     *
30
     * @var array
31
     */
32
    private $snapshot = [];
33
34
    /**
35
     * Collection's owning entity
36
     *
37
     * @var object
38
     */
39
    private $owner;
40
41
    /** @var array */
42
    private $mapping;
43
44
    /**
45
     * Whether the collection is dirty and needs to be synchronized with the database
46
     * when the UnitOfWork that manages its persistent state commits.
47
     *
48
     * @var bool
49
     */
50
    private $isDirty = false;
51
52
    /**
53
     * Whether the collection has already been initialized.
54
     *
55
     * @var bool
56
     */
57
    private $initialized = true;
58
59
    /**
60
     * The wrapped Collection instance.
61
     *
62
     * @var BaseCollection
63
     */
64
    private $coll;
65
66
    /**
67
     * The DocumentManager that manages the persistence of the collection.
68
     *
69
     * @var DocumentManager
70
     */
71
    private $dm;
72
73
    /**
74
     * The UnitOfWork that manages the persistence of the collection.
75
     *
76
     * @var UnitOfWork
77
     */
78
    private $uow;
79
80
    /**
81
     * The raw mongo data that will be used to initialize this collection.
82
     *
83
     * @var array
84
     */
85
    private $mongoData = [];
86
87
    /**
88
     * Any hints to account for during reconstitution/lookup of the documents.
89
     *
90
     * @var array
91
     */
92
    private $hints = [];
93
94
    /** {@inheritdoc} */
95 2
    public function setDocumentManager(DocumentManager $dm)
96
    {
97 2
        $this->dm  = $dm;
98 2
        $this->uow = $dm->getUnitOfWork();
99 2
    }
100
101
    /** {@inheritdoc} */
102 171
    public function setMongoData(array $mongoData)
103
    {
104 171
        $this->mongoData = $mongoData;
105 171
    }
106
107
    /** {@inheritdoc} */
108 169
    public function getMongoData()
109
    {
110 169
        return $this->mongoData;
111
    }
112
113
    /** {@inheritdoc} */
114 259
    public function setHints(array $hints)
115
    {
116 259
        $this->hints = $hints;
117 259
    }
118
119
    /** {@inheritdoc} */
120 170
    public function getHints()
121
    {
122 170
        return $this->hints;
123
    }
124
125
    /** {@inheritdoc} */
126 399
    public function initialize()
127
    {
128 399
        if ($this->initialized || ! $this->mapping) {
129 391
            return;
130
        }
131
132 184
        $newObjects = [];
133
134 184
        if ($this->isDirty) {
135
            // Remember any NEW objects added through add()
136 17
            $newObjects = $this->coll->toArray();
137
        }
138
139 184
        $this->initialized = true;
140
141 184
        $this->coll->clear();
142 184
        $this->uow->loadCollection($this);
143 184
        $this->takeSnapshot();
144
145 184
        $this->mongoData = [];
146
147
        // Reattach any NEW objects added through add()
148 184
        if (! $newObjects) {
149 180
            return;
150
        }
151
152 17
        foreach ($newObjects as $key => $obj) {
153 17
            if (CollectionHelper::isHash($this->mapping['strategy'])) {
154
                $this->coll->set($key, $obj);
155
            } else {
156 17
                $this->coll->add($obj);
157
            }
158
        }
159
160 17
        $this->isDirty = true;
161 17
    }
162
163
    /**
164
     * Marks this collection as changed/dirty.
165
     */
166 210
    private function changed()
167
    {
168 210
        if ($this->isDirty) {
169 137
            return;
170
        }
171
172 210
        $this->isDirty = true;
173
174 210
        if (! $this->needsSchedulingForDirtyCheck()) {
175 208
            return;
176
        }
177
178 2
        $this->uow->scheduleForDirtyCheck($this->owner);
179 2
    }
180
181
    /** {@inheritdoc} */
182 404
    public function isDirty()
183
    {
184 404
        if ($this->isDirty) {
185 269
            return true;
186
        }
187 354
        if (! $this->initialized && count($this->coll)) {
188
            // not initialized collection with added elements
189
            return true;
190
        }
191 354
        if ($this->initialized) {
192
            // if initialized let's check with last known snapshot
193 348
            return $this->coll->toArray() !== $this->snapshot;
194
        }
195 94
        return false;
196
    }
197
198
    /** {@inheritdoc} */
199 393
    public function setDirty($dirty)
200
    {
201 393
        $this->isDirty = $dirty;
202 393
    }
203
204
    /** {@inheritdoc} */
205 414
    public function setOwner($document, array $mapping)
206
    {
207 414
        $this->owner   = $document;
208 414
        $this->mapping = $mapping;
209 414
    }
210
211
    /** {@inheritdoc} */
212 293
    public function takeSnapshot()
213
    {
214 293
        if (CollectionHelper::isList($this->mapping['strategy'])) {
215 280
            $array = $this->coll->toArray();
216 280
            $this->coll->clear();
217 280
            foreach ($array as $document) {
218 255
                $this->coll->add($document);
219
            }
220
        }
221 293
        $this->snapshot = $this->coll->toArray();
222 293
        $this->isDirty  = false;
223 293
    }
224
225
    /** {@inheritdoc} */
226 24
    public function clearSnapshot()
227
    {
228 24
        $this->snapshot = [];
229 24
        $this->isDirty  = $this->coll->count() ? true : false;
230 24
    }
231
232
    /** {@inheritdoc} */
233
    public function getSnapshot()
234
    {
235
        return $this->snapshot;
236
    }
237
238
    /** {@inheritdoc} */
239 91 View Code Duplication
    public function getDeleteDiff()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
240
    {
241 91
        return array_udiff_assoc(
242 91
            $this->snapshot,
243 91
            $this->coll->toArray(),
244
            static function ($a, $b) {
245 46
                return $a === $b ? 0 : 1;
246 91
            }
247
        );
248
    }
249
250
    /** {@inheritdoc} */
251 150 View Code Duplication
    public function getDeletedDocuments()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
252
    {
253
        $compare = static function ($a, $b) {
254 102
            $compareA = is_object($a) ? spl_object_hash($a) : $a;
255 102
            $compareb = is_object($b) ? spl_object_hash($b) : $b;
256 102
            return $compareA === $compareb ? 0 : ($compareA > $compareb ? 1 : -1);
257 150
        };
258 150
        return array_values(array_udiff(
259 150
            $this->snapshot,
260 150
            $this->coll->toArray(),
261 150
            $compare
262
        ));
263
    }
264
265
    /** {@inheritdoc} */
266 91 View Code Duplication
    public function getInsertDiff()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
267
    {
268 91
        return array_udiff_assoc(
269 91
            $this->coll->toArray(),
270 91
            $this->snapshot,
271
            static function ($a, $b) {
272 46
                return $a === $b ? 0 : 1;
273 91
            }
274
        );
275
    }
276
277
    /** {@inheritdoc} */
278 4 View Code Duplication
    public function getInsertedDocuments()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
279
    {
280
        $compare = static function ($a, $b) {
281 4
            $compareA = is_object($a) ? spl_object_hash($a) : $a;
282 4
            $compareb = is_object($b) ? spl_object_hash($b) : $b;
283 4
            return $compareA === $compareb ? 0 : ($compareA > $compareb ? 1 : -1);
284 4
        };
285 4
        return array_values(array_udiff(
286 4
            $this->coll->toArray(),
287 4
            $this->snapshot,
288 4
            $compare
289
        ));
290
    }
291
292
    /** {@inheritdoc} */
293 403
    public function getOwner()
294
    {
295 403
        return $this->owner;
296
    }
297
298
    /** {@inheritdoc} */
299 284
    public function getMapping()
300
    {
301 284
        return $this->mapping;
302
    }
303
304
    /** {@inheritdoc} */
305 5
    public function getTypeClass()
306
    {
307 5
        switch (true) {
308 5
            case $this->dm === null:
309 1
                throw new MongoDBException('No DocumentManager is associated with this PersistentCollection, please set one using setDocumentManager method.');
310 4
            case empty($this->mapping):
311 1
                throw new MongoDBException('No mapping is associated with this PersistentCollection, please set one using setOwner method.');
312 3
            case empty($this->mapping['targetDocument']):
313 1
                throw new MongoDBException('Specifying targetDocument is required for the ClassMetadata to be obtained.');
314
            default:
315 2
                return $this->dm->getClassMetadata($this->mapping['targetDocument']);
316
        }
317
    }
318
319
    /** {@inheritdoc} */
320 262
    public function setInitialized($bool)
321
    {
322 262
        $this->initialized = $bool;
323 262
    }
324
325
    /** {@inheritdoc} */
326 18
    public function isInitialized()
327
    {
328 18
        return $this->initialized;
329
    }
330
331
    /** {@inheritdoc} */
332 16
    public function first()
333
    {
334 16
        $this->initialize();
335 16
        return $this->coll->first();
336
    }
337
338
    /** {@inheritdoc} */
339 2
    public function last()
340
    {
341 2
        $this->initialize();
342 2
        return $this->coll->last();
343
    }
344
345
    /**
346
     * {@inheritdoc}
347
     */
348 4
    public function remove($key)
349
    {
350 4
        return $this->doRemove($key, false);
351
    }
352
353
    /**
354
     * {@inheritdoc}
355
     */
356 17
    public function removeElement($element)
357
    {
358 17
        $this->initialize();
359 17
        $removed = $this->coll->removeElement($element);
360
361 17
        if (! $removed) {
362
            return $removed;
363
        }
364
365 17
        $this->changed();
366
367 17
        return $removed;
368
    }
369
370
    /**
371
     * {@inheritdoc}
372
     */
373
    public function containsKey($key)
374
    {
375
        $this->initialize();
376
        return $this->coll->containsKey($key);
377
    }
378
379
    /**
380
     * {@inheritdoc}
381
     */
382 3
    public function contains($element)
383
    {
384 3
        $this->initialize();
385 3
        return $this->coll->contains($element);
386
    }
387
388
    /**
389
     * {@inheritdoc}
390
     */
391
    public function exists(Closure $p)
392
    {
393
        $this->initialize();
394
        return $this->coll->exists($p);
395
    }
396
397
    /**
398
     * {@inheritdoc}
399
     */
400 2
    public function indexOf($element)
401
    {
402 2
        $this->initialize();
403 2
        return $this->coll->indexOf($element);
404
    }
405
406
    /**
407
     * {@inheritdoc}
408
     */
409 19
    public function get($key)
410
    {
411 19
        $this->initialize();
412 19
        return $this->coll->get($key);
413
    }
414
415
    /**
416
     * {@inheritdoc}
417
     */
418
    public function getKeys()
419
    {
420
        $this->initialize();
421
        return $this->coll->getKeys();
422
    }
423
424
    /**
425
     * {@inheritdoc}
426
     */
427
    public function getValues()
428
    {
429
        $this->initialize();
430
        return $this->coll->getValues();
431
    }
432
433
    /**
434
     * {@inheritdoc}
435
     */
436 122
    public function count()
437
    {
438
        // Workaround around not being able to directly count inverse collections anymore
439 122
        $this->initialize();
440
441 122
        return $this->coll->count();
442
    }
443
444
    /**
445
     * {@inheritdoc}
446
     */
447 35
    public function set($key, $value)
448
    {
449 35
        return $this->doSet($key, $value, false);
450
    }
451
452
    /**
453
     * {@inheritdoc}
454
     */
455 169
    public function add($value)
456
    {
457 169
        return $this->doAdd($value, false);
458
    }
459
460
    /**
461
     * {@inheritdoc}
462
     */
463 375
    public function isEmpty()
464
    {
465 375
        return $this->initialized ? $this->coll->isEmpty() : $this->count() === 0;
466
    }
467
468
    /**
469
     * {@inheritdoc}
470
     */
471 328
    public function getIterator()
472
    {
473 328
        $this->initialize();
474 328
        return $this->coll->getIterator();
475
    }
476
477
    /**
478
     * {@inheritdoc}
479
     */
480 225
    public function map(Closure $func)
481
    {
482 225
        $this->initialize();
483 225
        return $this->coll->map($func);
484
    }
485
486
    /**
487
     * {@inheritdoc}
488
     */
489
    public function filter(Closure $p)
490
    {
491
        $this->initialize();
492
        return $this->coll->filter($p);
493
    }
494
495
    /**
496
     * {@inheritdoc}
497
     */
498
    public function forAll(Closure $p)
499
    {
500
        $this->initialize();
501
        return $this->coll->forAll($p);
502
    }
503
504
    /**
505
     * {@inheritdoc}
506
     */
507
    public function partition(Closure $p)
508
    {
509
        $this->initialize();
510
        return $this->coll->partition($p);
511
    }
512
513
    /**
514
     * {@inheritdoc}
515
     */
516 23
    public function toArray()
517
    {
518 23
        $this->initialize();
519 23
        return $this->coll->toArray();
520
    }
521
522
    /**
523
     * {@inheritdoc}
524
     */
525 29
    public function clear()
526
    {
527 29
        if ($this->initialized && $this->isEmpty()) {
528
            return;
529
        }
530
531 29
        if ($this->isOrphanRemovalEnabled()) {
532 28
            $this->initialize();
533 28
            foreach ($this->coll as $element) {
534 28
                $this->uow->scheduleOrphanRemoval($element);
535
            }
536
        }
537
538 29
        $this->mongoData = [];
539 29
        $this->coll->clear();
540
541
        // Nothing to do for inverse-side collections
542 29
        if (! $this->mapping['isOwningSide']) {
543
            return;
544
        }
545
546
        // Nothing to do if the collection was initialized but contained no data
547 29
        if ($this->initialized && empty($this->snapshot)) {
548 2
            return;
549
        }
550
551 27
        $this->changed();
552 27
        $this->uow->scheduleCollectionDeletion($this);
553 27
        $this->takeSnapshot();
554 27
    }
555
556
    /**
557
     * {@inheritdoc}
558
     */
559 1
    public function slice($offset, $length = null)
560
    {
561 1
        $this->initialize();
562 1
        return $this->coll->slice($offset, $length);
563
    }
564
565
    /**
566
     * Called by PHP when this collection is serialized. Ensures that only the
567
     * elements are properly serialized.
568
     *
569
     * @internal Tried to implement Serializable first but that did not work well
570
     *           with circular references. This solution seems simpler and works well.
571
     */
572 4
    public function __sleep()
573
    {
574 4
        return ['coll', 'initialized'];
575
    }
576
577
    /* ArrayAccess implementation */
578
579
    /**
580
     * @see containsKey()
581
     */
582 2
    public function offsetExists($offset)
583
    {
584 2
        $this->initialize();
585 2
        return $this->coll->offsetExists($offset);
586
    }
587
588
    /**
589
     * @see get()
590
     */
591 82
    public function offsetGet($offset)
592
    {
593 82
        $this->initialize();
594 82
        return $this->coll->offsetGet($offset);
595
    }
596
597
    /**
598
     * @see add()
599
     * @see set()
600
     */
601 42
    public function offsetSet($offset, $value)
602
    {
603 42
        if (! isset($offset)) {
604 41
            return $this->doAdd($value, true);
605
        }
606
607 3
        $this->doSet($offset, $value, true);
608 3
    }
609
610
    /**
611
     * @see remove()
612
     */
613 19
    public function offsetUnset($offset)
614
    {
615 19
        $this->doRemove($offset, true);
616 19
    }
617
618
    public function key()
619
    {
620
        return $this->coll->key();
621
    }
622
623
    /**
624
     * Gets the element of the collection at the current iterator position.
625
     */
626 1
    public function current()
627
    {
628 1
        return $this->coll->current();
629
    }
630
631
    /**
632
     * Moves the internal iterator position to the next element.
633
     */
634
    public function next()
635
    {
636
        return $this->coll->next();
637
    }
638
639
    /**
640
     * {@inheritdoc}
641
     */
642 403
    public function unwrap()
643
    {
644 403
        return $this->coll;
645
    }
646
647
    /**
648
     * Cleanup internal state of cloned persistent collection.
649
     *
650
     * The following problems have to be prevented:
651
     * 1. Added documents are added to old PersistentCollection
652
     * 2. New collection is not dirty, if reused on other document nothing
653
     * changes.
654
     * 3. Snapshot leads to invalid diffs being generated.
655
     * 4. Lazy loading grabs entities from old owner object.
656
     * 5. New collection is connected to old owner and leads to duplicate keys.
657
     */
658 8
    public function __clone()
659
    {
660 8
        if (is_object($this->coll)) {
661 8
            $this->coll = clone $this->coll;
662
        }
663
664 8
        $this->initialize();
665
666 8
        $this->owner    = null;
667 8
        $this->snapshot = [];
668
669 8
        $this->changed();
670 8
    }
671
672
    /**
673
     * Actual logic for adding an element to the collection.
674
     *
675
     * @param mixed $value
676
     * @param bool  $arrayAccess
677
     *
678
     * @return bool
679
     */
680 185
    private function doAdd($value, $arrayAccess)
681
    {
682
        /* Initialize the collection before calling add() so this append operation
683
         * uses the appropriate key. Otherwise, we risk overwriting original data
684
         * when $newObjects are re-added in a later call to initialize().
685
         */
686 185
        if (isset($this->mapping['strategy']) && CollectionHelper::isHash($this->mapping['strategy'])) {
687 12
            $this->initialize();
688
        }
689 185
        $arrayAccess ? $this->coll->offsetSet(null, $value) : $this->coll->add($value);
690 185
        $this->changed();
691
692 185 View Code Duplication
        if ($this->uow !== null && $this->isOrphanRemovalEnabled() && $value !== null) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
693 105
            $this->uow->unscheduleOrphanRemoval($value);
694
        }
695
696 185
        return true;
697
    }
698
699
    /**
700
     * Actual logic for removing element by its key.
701
     *
702
     * @param mixed $offset
703
     * @param bool  $arrayAccess
704
     *
705
     * @return bool
706
     */
707 23
    private function doRemove($offset, $arrayAccess)
708
    {
709 23
        $this->initialize();
710 23
        if ($arrayAccess) {
711 19
            $this->coll->offsetUnset($offset);
712 19
            $removed = true;
713
        } else {
714 4
            $removed = $this->coll->remove($offset);
715
        }
716
717 23
        if (! $removed && ! $arrayAccess) {
718
            return $removed;
719
        }
720
721 23
        $this->changed();
722
723 23
        return $removed;
724
    }
725
726
    /**
727
     * Actual logic for setting an element in the collection.
728
     *
729
     * @param mixed $offset
730
     * @param mixed $value
731
     * @param bool  $arrayAccess
732
     */
733 36
    private function doSet($offset, $value, $arrayAccess)
734
    {
735 36
        $arrayAccess ? $this->coll->offsetSet($offset, $value) : $this->coll->set($offset, $value);
736
737
        // Handle orphanRemoval
738 36 View Code Duplication
        if ($this->uow !== null && $this->isOrphanRemovalEnabled() && $value !== null) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
739 34
            $this->uow->unscheduleOrphanRemoval($value);
740
        }
741
742 36
        $this->changed();
743 36
    }
744
745
    /**
746
     * Returns whether or not this collection has orphan removal enabled.
747
     *
748
     * Embedded documents are automatically considered as "orphan removal enabled" because they might have references
749
     * that require to trigger cascade remove operations.
750
     *
751
     * @return bool
752
     */
753 203
    private function isOrphanRemovalEnabled()
754
    {
755 203
        if ($this->mapping === null) {
756 11
            return false;
757
        }
758
759 192
        if (isset($this->mapping['embedded'])) {
760 114
            return true;
761
        }
762
763 88
        if (isset($this->mapping['reference']) && $this->mapping['isOwningSide'] && $this->mapping['orphanRemoval']) {
764 9
            return true;
765
        }
766
767 79
        return false;
768
    }
769
770
    /**
771
     * Checks whether collection owner needs to be scheduled for dirty change in case the collection is modified.
772
     *
773
     * @return bool
774
     */
775 210
    private function needsSchedulingForDirtyCheck()
776
    {
777 210
        return $this->owner && $this->dm && ! empty($this->mapping['isOwningSide'])
778 210
            && $this->dm->getClassMetadata(get_class($this->owner))->isChangeTrackingNotify();
779
    }
780
}
781