Completed
Pull Request — master (#1757)
by Maciej
15:59
created

PersistentCollectionTrait::offsetUnset()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
cc 1
eloc 2
nc 1
nop 1
crap 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\ODM\MongoDB\PersistentCollection;
6
7
use Doctrine\Common\Collections\Collection as BaseCollection;
8
use Doctrine\ODM\MongoDB\DocumentManager;
9
use Doctrine\ODM\MongoDB\MongoDBException;
10
use Doctrine\ODM\MongoDB\UnitOfWork;
11
use Doctrine\ODM\MongoDB\Utility\CollectionHelper;
12
use function array_udiff;
13
use function array_udiff_assoc;
14
use function array_values;
15
use function count;
16
use function get_class;
17
use function is_object;
18
use function spl_object_hash;
19
20
/**
21
 * Trait with methods needed to implement PersistentCollectionInterface.
22
 *
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 151
    public function setMongoData(array $mongoData)
103
    {
104 151
        $this->mongoData = $mongoData;
105 151
    }
106
107
    /** {@inheritdoc} */
108 148
    public function getMongoData()
109
    {
110 148
        return $this->mongoData;
111
    }
112
113
    /** {@inheritdoc} */
114 238
    public function setHints(array $hints)
115
    {
116 238
        $this->hints = $hints;
117 238
    }
118
119
    /** {@inheritdoc} */
120 150
    public function getHints()
121
    {
122 150
        return $this->hints;
123
    }
124
125
    /** {@inheritdoc} */
126 371
    public function initialize()
127
    {
128 371
        if ($this->initialized || ! $this->mapping) {
129 363
            return;
130
        }
131
132 163
        $newObjects = [];
133
134 163
        if ($this->isDirty) {
135
            // Remember any NEW objects added through add()
136 15
            $newObjects = $this->coll->toArray();
137
        }
138
139 163
        $this->initialized = true;
140
141 163
        $this->coll->clear();
142 163
        $this->uow->loadCollection($this);
143 163
        $this->takeSnapshot();
144
145 163
        $this->mongoData = [];
146
147
        // Reattach any NEW objects added through add()
148 163
        if (! $newObjects) {
149 159
            return;
150
        }
151
152 15
        foreach ($newObjects as $key => $obj) {
153 15
            if (CollectionHelper::isHash($this->mapping['strategy'])) {
154
                $this->coll->set($key, $obj);
155
            } else {
156 15
                $this->coll->add($obj);
157
            }
158
        }
159
160 15
        $this->isDirty = true;
161 15
    }
162
163
    /**
164
     * Marks this collection as changed/dirty.
165
     */
166 188
    private function changed()
167
    {
168 188
        if ($this->isDirty) {
169 124
            return;
170
        }
171
172 188
        $this->isDirty = true;
173
174 188
        if (! $this->needsSchedulingForDirtyCheck()) {
175 186
            return;
176
        }
177
178 2
        $this->uow->scheduleForDirtyCheck($this->owner);
179 2
    }
180
181
    /** {@inheritdoc} */
182 376
    public function isDirty()
183
    {
184 376
        if ($this->isDirty) {
185 245
            return true;
186
        }
187 326
        if (! $this->initialized && count($this->coll)) {
188
            // not initialized collection with added elements
189
            return true;
190
        }
191 326
        if ($this->initialized) {
192
            // if initialized let's check with last known snapshot
193 320
            return $this->coll->toArray() !== $this->snapshot;
194
        }
195 77
        return false;
196
    }
197
198
    /** {@inheritdoc} */
199 365
    public function setDirty($dirty)
200
    {
201 365
        $this->isDirty = $dirty;
202 365
    }
203
204
    /** {@inheritdoc} */
205 386
    public function setOwner($document, array $mapping)
206
    {
207 386
        $this->owner = $document;
208 386
        $this->mapping = $mapping;
209 386
    }
210
211
    /** {@inheritdoc} */
212 269
    public function takeSnapshot()
213
    {
214 269
        if (CollectionHelper::isList($this->mapping['strategy'])) {
215 257
            $array = $this->coll->toArray();
216 257
            $this->coll->clear();
217 257
            foreach ($array as $document) {
218 241
                $this->coll->add($document);
219
            }
220
        }
221 269
        $this->snapshot = $this->coll->toArray();
222 269
        $this->isDirty = false;
223 269
    }
224
225
    /** {@inheritdoc} */
226 20
    public function clearSnapshot()
227
    {
228 20
        $this->snapshot = [];
229 20
        $this->isDirty = $this->coll->count() ? true : false;
230 20
    }
231
232
    /** {@inheritdoc} */
233
    public function getSnapshot()
234
    {
235
        return $this->snapshot;
236
    }
237
238
    /** {@inheritdoc} */
239 88 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 88
        return array_udiff_assoc(
242 88
            $this->snapshot,
243 88
            $this->coll->toArray(),
244 88
            function ($a, $b) {
245 44
                return $a === $b ? 0 : 1;
246 88
            }
247
        );
248
    }
249
250
    /** {@inheritdoc} */
251 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 128
        $compare = function ($a, $b) {
254 89
            $compareA = is_object($a) ? spl_object_hash($a) : $a;
255 89
            $compareb = is_object($b) ? spl_object_hash($b) : $b;
256 89
            return $compareA === $compareb ? 0 : ($compareA > $compareb ? 1 : -1);
257 128
        };
258 128
        return array_values(array_udiff(
259 128
            $this->snapshot,
260 128
            $this->coll->toArray(),
261 128
            $compare
262
        ));
263
    }
264
265
    /** {@inheritdoc} */
266 88 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 88
        return array_udiff_assoc(
269 88
            $this->coll->toArray(),
270 88
            $this->snapshot,
271 88
            function ($a, $b) {
272 44
                return $a === $b ? 0 : 1;
273 88
            }
274
        );
275
    }
276
277
    /** {@inheritdoc} */
278 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 4
        $compare = 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 375
    public function getOwner()
294
    {
295 375
        return $this->owner;
296
    }
297
298
    /** {@inheritdoc} */
299 260
    public function getMapping()
300
    {
301 260
        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 241
    public function setInitialized($bool)
321
    {
322 241
        $this->initialized = $bool;
323 241
    }
324
325
    /** {@inheritdoc} */
326 18
    public function isInitialized()
327
    {
328 18
        return $this->initialized;
329
    }
330
331
    /** {@inheritdoc} */
332 9
    public function first()
333
    {
334 9
        $this->initialize();
335 9
        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 2
    public function contains($element)
383
    {
384 2
        $this->initialize();
385 2
        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 18
    public function get($key)
410
    {
411 18
        $this->initialize();
412 18
        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 101
    public function count()
437
    {
438
        // Workaround around not being able to directly count inverse collections anymore
439 101
        $this->initialize();
440
441 101
        return $this->coll->count();
442
    }
443
444
    /**
445
     * {@inheritdoc}
446
     */
447 20
    public function set($key, $value)
448
    {
449 20
        return $this->doSet($key, $value, false);
450
    }
451
452
    /**
453
     * {@inheritdoc}
454
     */
455 159
    public function add($value)
456
    {
457 159
        return $this->doAdd($value, false);
458
    }
459
460
    /**
461
     * {@inheritdoc}
462
     */
463 349
    public function isEmpty()
464
    {
465 349
        return $this->initialized ? $this->coll->isEmpty() : $this->count() === 0;
466
    }
467
468
    /**
469
     * {@inheritdoc}
470
     */
471 301
    public function getIterator()
472
    {
473 301
        $this->initialize();
474 301
        return $this->coll->getIterator();
475
    }
476
477
    /**
478
     * {@inheritdoc}
479
     */
480 202
    public function map(\Closure $func)
481
    {
482 202
        $this->initialize();
483 202
        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 24
    public function clear()
526
    {
527 24
        if ($this->initialized && $this->isEmpty()) {
528
            return;
529
        }
530
531 24
        if ($this->isOrphanRemovalEnabled()) {
532 24
            $this->initialize();
533 24
            foreach ($this->coll as $element) {
534 24
                $this->uow->scheduleOrphanRemoval($element);
535
            }
536
        }
537
538 24
        $this->mongoData = [];
539 24
        $this->coll->clear();
540
541
        // Nothing to do for inverse-side collections
542 24
        if (! $this->mapping['isOwningSide']) {
543
            return;
544
        }
545
546
        // Nothing to do if the collection was initialized but contained no data
547 24
        if ($this->initialized && empty($this->snapshot)) {
548 2
            return;
549
        }
550
551 22
        $this->changed();
552 22
        $this->uow->scheduleCollectionDeletion($this);
553 22
        $this->takeSnapshot();
554 22
    }
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 1
    public function offsetExists($offset)
583
    {
584 1
        $this->initialize();
585 1
        return $this->coll->offsetExists($offset);
586
    }
587
588
    /**
589
     * @see get()
590
     */
591 66
    public function offsetGet($offset)
592
    {
593 66
        $this->initialize();
594 66
        return $this->coll->offsetGet($offset);
595
    }
596
597
    /**
598
     * @see add()
599
     * @see set()
600
     */
601 34
    public function offsetSet($offset, $value)
602
    {
603 34
        if (! isset($offset)) {
604 34
            return $this->doAdd($value, true);
605
        }
606
607 2
        return $this->doSet($offset, $value, true);
608
    }
609
610
    /**
611
     * @see remove()
612
     */
613 17
    public function offsetUnset($offset)
614
    {
615 17
        return $this->doRemove($offset, true);
616
    }
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 375
    public function unwrap()
643
    {
644 375
        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
     * @return bool
678
     */
679 171
    private function doAdd($value, $arrayAccess)
680
    {
681
        /* Initialize the collection before calling add() so this append operation
682
         * uses the appropriate key. Otherwise, we risk overwriting original data
683
         * when $newObjects are re-added in a later call to initialize().
684
         */
685 171
        if (isset($this->mapping['strategy']) && CollectionHelper::isHash($this->mapping['strategy'])) {
686 9
            $this->initialize();
687
        }
688 171
        $arrayAccess ? $this->coll->offsetSet(null, $value) : $this->coll->add($value);
689 171
        $this->changed();
690
691 171 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...
692 93
            $this->uow->unscheduleOrphanRemoval($value);
693
        }
694
695 171
        return true;
696
    }
697
698
    /**
699
     * Actual logic for removing element by its key.
700
     *
701
     * @param mixed $offset
702
     * @param bool  $arrayAccess
703
     * @return mixed|void
704
     */
705 21
    private function doRemove($offset, $arrayAccess)
706
    {
707 21
        $this->initialize();
708 21
        $removed = $arrayAccess ? $this->coll->offsetUnset($offset) : $this->coll->remove($offset);
709
710 21
        if (! $removed && ! $arrayAccess) {
711
            return $removed;
712
        }
713
714 21
        $this->changed();
715
716 21
        return $removed;
717
    }
718
719
    /**
720
     * Actual logic for setting an element in the collection.
721
     *
722
     * @param mixed $offset
723
     * @param mixed $value
724
     * @param bool  $arrayAccess
725
     */
726 21
    private function doSet($offset, $value, $arrayAccess)
727
    {
728 21
        $arrayAccess ? $this->coll->offsetSet($offset, $value) : $this->coll->set($offset, $value);
729
730
        // Handle orphanRemoval
731 21 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...
732 19
            $this->uow->unscheduleOrphanRemoval($value);
733
        }
734
735 21
        $this->changed();
736 21
    }
737
738
    /**
739
     * Returns whether or not this collection has orphan removal enabled.
740
     *
741
     * Embedded documents are automatically considered as "orphan removal enabled" because they might have references
742
     * that require to trigger cascade remove operations.
743
     *
744
     * @return bool
745
     */
746 181
    private function isOrphanRemovalEnabled()
747
    {
748 181
        if ($this->mapping === null) {
749 11
            return false;
750
        }
751
752 170
        if (isset($this->mapping['embedded'])) {
753 94
            return true;
754
        }
755
756 86
        if (isset($this->mapping['reference']) && $this->mapping['isOwningSide'] && $this->mapping['orphanRemoval']) {
757 9
            return true;
758
        }
759
760 77
        return false;
761
    }
762
763
    /**
764
     * Checks whether collection owner needs to be scheduled for dirty change in case the collection is modified.
765
     *
766
     * @return bool
767
     */
768 188
    private function needsSchedulingForDirtyCheck()
769
    {
770 188
        return $this->owner && $this->dm && ! empty($this->mapping['isOwningSide'])
771 188
            && $this->dm->getClassMetadata(get_class($this->owner))->isChangeTrackingNotify();
772
    }
773
}
774