Completed
Push — master ( 08b9e1...e0c601 )
by Andreas
13s
created

isOrphanRemovalEnabled()   A

Complexity

Conditions 6
Paths 4

Size

Total Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 16
ccs 8
cts 8
cp 1
rs 9.1111
c 0
b 0
f 0
cc 6
nc 4
nop 0
crap 6
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 152
    public function setMongoData(array $mongoData)
103
    {
104 152
        $this->mongoData = $mongoData;
105 152
    }
106
107
    /** {@inheritdoc} */
108 149
    public function getMongoData()
109
    {
110 149
        return $this->mongoData;
111
    }
112
113
    /** {@inheritdoc} */
114 239
    public function setHints(array $hints)
115
    {
116 239
        $this->hints = $hints;
117 239
    }
118
119
    /** {@inheritdoc} */
120 151
    public function getHints()
121
    {
122 151
        return $this->hints;
123
    }
124
125
    /** {@inheritdoc} */
126 375
    public function initialize()
127
    {
128 375
        if ($this->initialized || ! $this->mapping) {
129 367
            return;
130
        }
131
132 164
        $newObjects = [];
133
134 164
        if ($this->isDirty) {
135
            // Remember any NEW objects added through add()
136 15
            $newObjects = $this->coll->toArray();
137
        }
138
139 164
        $this->initialized = true;
140
141 164
        $this->coll->clear();
142 164
        $this->uow->loadCollection($this);
143 164
        $this->takeSnapshot();
144
145 164
        $this->mongoData = [];
146
147
        // Reattach any NEW objects added through add()
148 164
        if (! $newObjects) {
149 160
            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 189
    private function changed()
167
    {
168 189
        if ($this->isDirty) {
169 125
            return;
170
        }
171
172 189
        $this->isDirty = true;
173
174 189
        if (! $this->needsSchedulingForDirtyCheck()) {
175 187
            return;
176
        }
177
178 2
        $this->uow->scheduleForDirtyCheck($this->owner);
179 2
    }
180
181
    /** {@inheritdoc} */
182 380
    public function isDirty()
183
    {
184 380
        if ($this->isDirty) {
185 248
            return true;
186
        }
187 330
        if (! $this->initialized && count($this->coll)) {
188
            // not initialized collection with added elements
189
            return true;
190
        }
191 330
        if ($this->initialized) {
192
            // if initialized let's check with last known snapshot
193 324
            return $this->coll->toArray() !== $this->snapshot;
194
        }
195 78
        return false;
196
    }
197
198
    /** {@inheritdoc} */
199 369
    public function setDirty($dirty)
200
    {
201 369
        $this->isDirty = $dirty;
202 369
    }
203
204
    /** {@inheritdoc} */
205 390
    public function setOwner($document, array $mapping)
206
    {
207 390
        $this->owner = $document;
208 390
        $this->mapping = $mapping;
209 390
    }
210
211
    /** {@inheritdoc} */
212 272
    public function takeSnapshot()
213
    {
214 272
        if (CollectionHelper::isList($this->mapping['strategy'])) {
215 260
            $array = $this->coll->toArray();
216 260
            $this->coll->clear();
217 260
            foreach ($array as $document) {
218 244
                $this->coll->add($document);
219
            }
220
        }
221 272
        $this->snapshot = $this->coll->toArray();
222 272
        $this->isDirty = false;
223 272
    }
224
225
    /** {@inheritdoc} */
226 22
    public function clearSnapshot()
227
    {
228 22
        $this->snapshot = [];
229 22
        $this->isDirty = $this->coll->count() ? true : false;
230 22
    }
231
232
    /** {@inheritdoc} */
233
    public function getSnapshot()
234
    {
235
        return $this->snapshot;
236
    }
237
238
    /** {@inheritdoc} */
239 89 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 89
        return array_udiff_assoc(
242 89
            $this->snapshot,
243 89
            $this->coll->toArray(),
244
            function ($a, $b) {
245 45
                return $a === $b ? 0 : 1;
246 89
            }
247
        );
248
    }
249
250
    /** {@inheritdoc} */
251 130 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 = function ($a, $b) {
254 90
            $compareA = is_object($a) ? spl_object_hash($a) : $a;
255 90
            $compareb = is_object($b) ? spl_object_hash($b) : $b;
256 90
            return $compareA === $compareb ? 0 : ($compareA > $compareb ? 1 : -1);
257 130
        };
258 130
        return array_values(array_udiff(
259 130
            $this->snapshot,
260 130
            $this->coll->toArray(),
261 130
            $compare
262
        ));
263
    }
264
265
    /** {@inheritdoc} */
266 89 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 89
        return array_udiff_assoc(
269 89
            $this->coll->toArray(),
270 89
            $this->snapshot,
271
            function ($a, $b) {
272 45
                return $a === $b ? 0 : 1;
273 89
            }
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 = 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 379
    public function getOwner()
294
    {
295 379
        return $this->owner;
296
    }
297
298
    /** {@inheritdoc} */
299 263
    public function getMapping()
300
    {
301 263
        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 242
    public function setInitialized($bool)
321
    {
322 242
        $this->initialized = $bool;
323 242
    }
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 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 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 102
    public function count()
437
    {
438
        // Workaround around not being able to directly count inverse collections anymore
439 102
        $this->initialize();
440
441 102
        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 160
    public function add($value)
456
    {
457 160
        return $this->doAdd($value, false);
458
    }
459
460
    /**
461
     * {@inheritdoc}
462
     */
463 353
    public function isEmpty()
464
    {
465 353
        return $this->initialized ? $this->coll->isEmpty() : $this->count() === 0;
466
    }
467
468
    /**
469
     * {@inheritdoc}
470
     */
471 304
    public function getIterator()
472
    {
473 304
        $this->initialize();
474 304
        return $this->coll->getIterator();
475
    }
476
477
    /**
478
     * {@inheritdoc}
479
     */
480 205
    public function map(\Closure $func)
481
    {
482 205
        $this->initialize();
483 205
        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 379
    public function unwrap()
643
    {
644 379
        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 172
    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 172
        if (isset($this->mapping['strategy']) && CollectionHelper::isHash($this->mapping['strategy'])) {
686 9
            $this->initialize();
687
        }
688 172
        $arrayAccess ? $this->coll->offsetSet(null, $value) : $this->coll->add($value);
689 172
        $this->changed();
690
691 172 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 172
        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 182
    private function isOrphanRemovalEnabled()
747
    {
748 182
        if ($this->mapping === null) {
749 11
            return false;
750
        }
751
752 171
        if (isset($this->mapping['embedded'])) {
753 94
            return true;
754
        }
755
756 87
        if (isset($this->mapping['reference']) && $this->mapping['isOwningSide'] && $this->mapping['orphanRemoval']) {
757 9
            return true;
758
        }
759
760 78
        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 189
    private function needsSchedulingForDirtyCheck()
769
    {
770 189
        return $this->owner && $this->dm && ! empty($this->mapping['isOwningSide'])
771 189
            && $this->dm->getClassMetadata(get_class($this->owner))->isChangeTrackingNotify();
772
    }
773
}
774