Completed
Pull Request — master (#1411)
by Maciej
12:22
created

PersistentCollectionTrait::setDocumentManager()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 5
ccs 4
cts 4
cp 1
rs 9.4285
cc 1
eloc 3
nc 1
nop 1
crap 1
1
<?php
2
/*
3
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the MIT license. For more information, see
17
 * <http://www.doctrine-project.org>.
18
 */
19
20
namespace Doctrine\ODM\MongoDB\PersistentCollection;
21
22
use Doctrine\Common\Collections\Collection as BaseCollection;
23
use Doctrine\ODM\MongoDB\DocumentManager;
24
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
25
use Doctrine\ODM\MongoDB\MongoDBException;
26
use Doctrine\ODM\MongoDB\UnitOfWork;
27
use Doctrine\ODM\MongoDB\Utility\CollectionHelper;
28
29
/**
30
 * Trait with methods needed to implement PersistentCollectionInterface.
31
 *
32
 * @since 1.1
33
 */
34
trait PersistentCollectionTrait
35
{
36
    /**
37
     * A snapshot of the collection at the moment it was fetched from the database.
38
     * This is used to create a diff of the collection at commit time.
39
     *
40
     * @var array
41
     */
42
    private $snapshot = array();
43
44
    /**
45
     * Collection's owning entity
46
     *
47
     * @var object
48
     */
49
    private $owner;
50
51
    /**
52
     * @var array
53
     */
54
    private $mapping;
55
56
    /**
57
     * Whether the collection is dirty and needs to be synchronized with the database
58
     * when the UnitOfWork that manages its persistent state commits.
59
     *
60
     * @var boolean
61
     */
62
    private $isDirty = false;
63
64
    /**
65
     * Whether the collection has already been initialized.
66
     *
67
     * @var boolean
68
     */
69
    private $initialized = true;
70
71
    /**
72
     * The wrapped Collection instance.
73
     *
74
     * @var BaseCollection
75
     */
76
    private $coll;
77
78
    /**
79
     * The DocumentManager that manages the persistence of the collection.
80
     *
81
     * @var DocumentManager
82
     */
83
    private $dm;
84
85
    /**
86
     * The UnitOfWork that manages the persistence of the collection.
87
     *
88
     * @var UnitOfWork
89
     */
90
    private $uow;
91
92
    /**
93
     * The raw mongo data that will be used to initialize this collection.
94
     *
95
     * @var array
96
     */
97
    private $mongoData = array();
98
99
    /**
100
     * Any hints to account for during reconstitution/lookup of the documents.
101
     *
102
     * @var array
103
     */
104
    private $hints = array();
105
106
    /** {@inheritdoc} */
107 2
    public function setDocumentManager(DocumentManager $dm)
108
    {
109 2
        $this->dm = $dm;
110 2
        $this->uow = $dm->getUnitOfWork();
111 2
    }
112
113
    /** {@inheritdoc} */
114 177
    public function setMongoData(array $mongoData)
115
    {
116 177
        $this->mongoData = $mongoData;
117 177
    }
118
119
    /** {@inheritdoc} */
120 153
    public function getMongoData()
121
    {
122 153
        return $this->mongoData;
123
    }
124
125
    /** {@inheritdoc} */
126 262
    public function setHints(array $hints)
127
    {
128 262
        $this->hints = $hints;
129 262
    }
130
131
    /** {@inheritdoc} */
132 167
    public function getHints()
133
    {
134 167
        return $this->hints;
135
    }
136
137
    /** {@inheritdoc} */
138 379
    public function initialize()
139
    {
140 379
        if ($this->initialized || ! $this->mapping) {
141 371
            return;
142
        }
143
144 165
        $newObjects = array();
145
146 165
        if ($this->isDirty) {
147
            // Remember any NEW objects added through add()
148 22
            $newObjects = $this->coll->toArray();
149
        }
150
151 165
        $this->initialized = true;
152
153 165
        $this->coll->clear();
154 165
        $this->uow->loadCollection($this);
155 165
        $this->takeSnapshot();
156
157 165
        $this->mongoData = array();
158
159
        // Reattach any NEW objects added through add()
160 165
        if ($newObjects) {
161 22
            foreach ($newObjects as $key => $obj) {
162 22
                if (CollectionHelper::isHash($this->mapping['strategy'])) {
163
                    $this->coll->set($key, $obj);
164
                } else {
165 22
                    $this->coll->add($obj);
166
                }
167
            }
168
169 22
            $this->isDirty = true;
170
        }
171 165
    }
172
173
    /**
174
     * Marks this collection as changed/dirty.
175
     */
176 193
    private function changed()
177
    {
178 193
        if ($this->isDirty) {
179 125
            return;
180
        }
181
182 193
        $this->isDirty = true;
183
184 193
        if ($this->needsSchedulingForDirtyCheck()) {
185 2
            $this->uow->scheduleForDirtyCheck($this->owner);
186
        }
187 193
    }
188
189
    /** {@inheritdoc} */
190 394
    public function isDirty()
191
    {
192 394
        if ($this->isDirty) {
193 250
            return true;
194
        }
195 350
        if (! $this->initialized && count($this->coll)) {
196
            // not initialized collection with added elements
197
            return true;
198
        }
199 350
        if ($this->initialized) {
200
            // if initialized let's check with last known snapshot
201 340
            return $this->coll->toArray() !== $this->snapshot;
202
        }
203 94
        return false;
204
    }
205
206
    /** {@inheritdoc} */
207 386
    public function setDirty($dirty)
208
    {
209 386
        $this->isDirty = $dirty;
210 386
    }
211
212
    /** {@inheritdoc} */
213 406
    public function setOwner($document, array $mapping)
214
    {
215 406
        $this->owner = $document;
216 406
        $this->mapping = $mapping;
217 406
    }
218
219
    /** {@inheritdoc} */
220 275
    public function takeSnapshot()
221
    {
222 275
        if (CollectionHelper::isList($this->mapping['strategy'])) {
223 261
            $array = $this->coll->toArray();
224 261
            $this->coll->clear();
225 261
            foreach ($array as $document) {
226 236
                $this->coll->add($document);
227
            }
228
        }
229 275
        $this->snapshot = $this->coll->toArray();
230 275
        $this->isDirty = false;
231 275
    }
232
233
    /** {@inheritdoc} */
234 23
    public function clearSnapshot()
235
    {
236 23
        $this->snapshot = array();
237 23
        $this->isDirty = $this->coll->count() ? true : false;
238 23
    }
239
240
    /** {@inheritdoc} */
241
    public function getSnapshot()
242
    {
243
        return $this->snapshot;
244
    }
245
246
    /** {@inheritdoc} */
247 87 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...
248
    {
249 87
        return array_udiff_assoc(
250 87
            $this->snapshot,
251 87
            $this->coll->toArray(),
252
            function ($a, $b) { return $a === $b ? 0 : 1; }
253
        );
254
    }
255
256
    /** {@inheritdoc} */
257 145 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...
258
    {
259
        $compare = function ($a, $b) {
260 99
            $compareA = is_object($a) ? spl_object_hash($a) : $a;
261 99
            $compareb = is_object($b) ? spl_object_hash($b) : $b;
262 99
            return $compareA === $compareb ? 0 : ($compareA > $compareb ? 1 : -1);
263 145
        };
264 145
        return array_values(array_udiff(
265 145
            $this->snapshot,
266 145
            $this->coll->toArray(),
267
            $compare
268
        ));
269
    }
270
271
    /** {@inheritdoc} */
272 87 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...
273
    {
274 87
        return array_udiff_assoc(
275 87
            $this->coll->toArray(),
276 87
            $this->snapshot,
277
            function ($a, $b) { return $a === $b ? 0 : 1; }
278
        );
279
    }
280
281
    /** {@inheritdoc} */
282 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...
283
    {
284 4
        $compare = function ($a, $b) {
285 4
            $compareA = is_object($a) ? spl_object_hash($a) : $a;
286 4
            $compareb = is_object($b) ? spl_object_hash($b) : $b;
287 4
            return $compareA === $compareb ? 0 : ($compareA > $compareb ? 1 : -1);
288 4
        };
289 4
        return array_values(array_udiff(
290 4
            $this->coll->toArray(),
291 4
            $this->snapshot,
292
            $compare
293
        ));
294
    }
295
296
    /** {@inheritdoc} */
297 395
    public function getOwner()
298
    {
299 395
        return $this->owner;
300
    }
301
302
    /** {@inheritdoc} */
303 268
    public function getMapping()
304
    {
305 268
        return $this->mapping;
306
    }
307
308
    /** {@inheritdoc} */
309 5
    public function getTypeClass()
310
    {
311
        switch (true) {
312 5
            case ($this->dm === null):
313 1
                throw new MongoDBException('No DocumentManager is associated with this PersistentCollection, please set one using setDocumentManager method.');
314
            case (empty($this->mapping)):
315 1
                throw new MongoDBException('No mapping is associated with this PersistentCollection, please set one using setOwner method.');
316 3
            case (empty($this->mapping['targetDocument'])):
317 1
                throw new MongoDBException('Specifying targetDocument is required for the ClassMetadata to be obtained.');
318
            default:
319 2
                return $this->dm->getClassMetadata($this->mapping['targetDocument']);
320
        }
321
    }
322
323
    /** {@inheritdoc} */
324 263
    public function setInitialized($bool)
325
    {
326 263
        $this->initialized = $bool;
327 263
    }
328
329
    /** {@inheritdoc} */
330 15
    public function isInitialized()
331
    {
332 15
        return $this->initialized;
333
    }
334
335
    /** {@inheritdoc} */
336 12
    public function first()
337
    {
338 12
        $this->initialize();
339 12
        return $this->coll->first();
340
    }
341
342
    /** {@inheritdoc} */
343 1
    public function last()
344
    {
345 1
        $this->initialize();
346 1
        return $this->coll->last();
347
    }
348
349
    /**
350
     * {@inheritdoc}
351
     */
352 21 View Code Duplication
    public function remove($key)
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...
353
    {
354 21
        $this->initialize();
355 21
        $removed = $this->coll->remove($key);
356
357 21
        if ( ! $removed) {
358
            return $removed;
359
        }
360
361 21
        $this->changed();
362
363 21
        return $removed;
364
    }
365
366
    /**
367
     * {@inheritdoc}
368
     */
369 17 View Code Duplication
    public function removeElement($element)
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...
370
    {
371 17
        $this->initialize();
372 17
        $removed = $this->coll->removeElement($element);
373
374 17
        if ( ! $removed) {
375
            return $removed;
376
        }
377
378 17
        $this->changed();
379
380 17
        return $removed;
381
    }
382
383
    /**
384
     * {@inheritdoc}
385
     */
386 1
    public function containsKey($key)
387
    {
388 1
        $this->initialize();
389 1
        return $this->coll->containsKey($key);
390
    }
391
392
    /**
393
     * {@inheritdoc}
394
     */
395 2
    public function contains($element)
396
    {
397 2
        $this->initialize();
398 2
        return $this->coll->contains($element);
399
    }
400
401
    /**
402
     * {@inheritdoc}
403
     */
404
    public function exists(\Closure $p)
405
    {
406
        $this->initialize();
407
        return $this->coll->exists($p);
408
    }
409
410
    /**
411
     * {@inheritdoc}
412
     */
413 2
    public function indexOf($element)
414
    {
415 2
        $this->initialize();
416 2
        return $this->coll->indexOf($element);
417
    }
418
419
    /**
420
     * {@inheritdoc}
421
     */
422 91
    public function get($key)
423
    {
424 91
        $this->initialize();
425 91
        return $this->coll->get($key);
426
    }
427
428
    /**
429
     * {@inheritdoc}
430
     */
431
    public function getKeys()
432
    {
433
        $this->initialize();
434
        return $this->coll->getKeys();
435
    }
436
437
    /**
438
     * {@inheritdoc}
439
     */
440
    public function getValues()
441
    {
442
        $this->initialize();
443
        return $this->coll->getValues();
444
    }
445
446
    /**
447
     * {@inheritdoc}
448
     */
449 375
    public function count()
450
    {
451 375
        $count = $this->coll->count();
452
453
        // If this collection is inversed and not initialized, add the count returned from the database
454 375
        if ($this->mapping['isInverseSide'] && ! $this->initialized) {
455 10
            $documentPersister = $this->uow->getDocumentPersister(get_class($this->owner));
456 10
            $count += empty($this->mapping['repositoryMethod'])
457 9
                ? $documentPersister->createReferenceManyInverseSideQuery($this)->count()
458 1
                : $documentPersister->createReferenceManyWithRepositoryMethodCursor($this)->count();
459
        }
460
461 375
        return $count + ($this->initialized ? 0 : count($this->mongoData));
462
    }
463
464
    /**
465
     * {@inheritdoc}
466
     */
467 35
    public function set($key, $value)
468
    {
469 35
        $this->coll->set($key, $value);
470
471
        // Handle orphanRemoval
472 35 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...
473 34
            $this->uow->unscheduleOrphanRemoval($value);
474
        }
475
476 35
        $this->changed();
477 35
    }
478
479
    /**
480
     * {@inheritdoc}
481
     */
482 167
    public function add($value)
483
    {
484
        /* Initialize the collection before calling add() so this append operation
485
         * uses the appropriate key. Otherwise, we risk overwriting original data
486
         * when $newObjects are re-added in a later call to initialize().
487
         */
488 167
        if (isset($this->mapping['strategy']) && CollectionHelper::isHash($this->mapping['strategy'])) {
489 12
            $this->initialize();
490
        }
491 167
        $this->coll->add($value);
492 167
        $this->changed();
493
494 167 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...
495 94
            $this->uow->unscheduleOrphanRemoval($value);
496
        }
497
498 167
        return true;
499
    }
500
501
    /**
502
     * {@inheritdoc}
503
     */
504 367
    public function isEmpty()
505
    {
506 367
        return $this->count() === 0;
507
    }
508
509
    /**
510
     * {@inheritdoc}
511
     */
512 321
    public function getIterator()
513
    {
514 321
        $this->initialize();
515 321
        return $this->coll->getIterator();
516
    }
517
518
    /**
519
     * {@inheritdoc}
520
     */
521 217
    public function map(\Closure $func)
522
    {
523 217
        $this->initialize();
524 217
        return $this->coll->map($func);
525
    }
526
527
    /**
528
     * {@inheritdoc}
529
     */
530
    public function filter(\Closure $p)
531
    {
532
        $this->initialize();
533
        return $this->coll->filter($p);
534
    }
535
536
    /**
537
     * {@inheritdoc}
538
     */
539
    public function forAll(\Closure $p)
540
    {
541
        $this->initialize();
542
        return $this->coll->forAll($p);
543
    }
544
545
    /**
546
     * {@inheritdoc}
547
     */
548
    public function partition(\Closure $p)
549
    {
550
        $this->initialize();
551
        return $this->coll->partition($p);
552
    }
553
554
    /**
555
     * {@inheritdoc}
556
     */
557 23
    public function toArray()
558
    {
559 23
        $this->initialize();
560 23
        return $this->coll->toArray();
561
    }
562
563
    /**
564
     * {@inheritdoc}
565
     */
566 28
    public function clear()
567
    {
568 28
        if ($this->initialized && $this->isEmpty()) {
569
            return;
570
        }
571
572 28
        if ($this->isOrphanRemovalEnabled()) {
573 27
            foreach ($this->coll as $element) {
574 25
                $this->uow->scheduleOrphanRemoval($element);
575
            }
576
        }
577
578 28
        $this->mongoData = array();
579 28
        $this->coll->clear();
580
581
        // Nothing to do for inverse-side collections
582 28
        if ( ! $this->mapping['isOwningSide']) {
583
            return;
584
        }
585
586
        // Nothing to do if the collection was initialized but contained no data
587 28
        if ($this->initialized && empty($this->snapshot)) {
588 2
            return;
589
        }
590
591 26
        $this->changed();
592 26
        $this->uow->scheduleCollectionDeletion($this);
593 26
        $this->takeSnapshot();
594 26
    }
595
596
    /**
597
     * {@inheritdoc}
598
     */
599 1
    public function slice($offset, $length = null)
600
    {
601 1
        $this->initialize();
602 1
        return $this->coll->slice($offset, $length);
603
    }
604
605
    /**
606
     * Called by PHP when this collection is serialized. Ensures that only the
607
     * elements are properly serialized.
608
     *
609
     * @internal Tried to implement Serializable first but that did not work well
610
     *           with circular references. This solution seems simpler and works well.
611
     */
612 4
    public function __sleep()
613
    {
614 4
        return array('coll', 'initialized');
615
    }
616
617
    /* ArrayAccess implementation */
618
619
    /**
620
     * @see containsKey()
621
     */
622 1
    public function offsetExists($offset)
623
    {
624 1
        return $this->containsKey($offset);
625
    }
626
627
    /**
628
     * @see get()
629
     */
630 79
    public function offsetGet($offset)
631
    {
632 79
        return $this->get($offset);
633
    }
634
635
    /**
636
     * @see add()
637
     * @see set()
638
     */
639 40
    public function offsetSet($offset, $value)
640
    {
641 40
        if ( ! isset($offset)) {
642 39
            return $this->add($value);
643
        }
644
645 2
        return $this->set($offset, $value);
646
    }
647
648
    /**
649
     * @see remove()
650
     */
651 18
    public function offsetUnset($offset)
652
    {
653 18
        return $this->remove($offset);
654
    }
655
656
    public function key()
657
    {
658
        return $this->coll->key();
659
    }
660
661
    /**
662
     * Gets the element of the collection at the current iterator position.
663
     */
664 1
    public function current()
665
    {
666 1
        return $this->coll->current();
667
    }
668
669
    /**
670
     * Moves the internal iterator position to the next element.
671
     */
672
    public function next()
673
    {
674
        return $this->coll->next();
675
    }
676
677
    /**
678
     * {@inheritdoc}
679
     */
680 395
    public function unwrap()
681
    {
682 395
        return $this->coll;
683
    }
684
685
    /**
686
     * Cleanup internal state of cloned persistent collection.
687
     *
688
     * The following problems have to be prevented:
689
     * 1. Added documents are added to old PersistentCollection
690
     * 2. New collection is not dirty, if reused on other document nothing
691
     * changes.
692
     * 3. Snapshot leads to invalid diffs being generated.
693
     * 4. Lazy loading grabs entities from old owner object.
694
     * 5. New collection is connected to old owner and leads to duplicate keys.
695
     */
696 8
    public function __clone()
697
    {
698 8
        if (is_object($this->coll)) {
699 8
            $this->coll = clone $this->coll;
700
        }
701
702 8
        $this->initialize();
703
704 8
        $this->owner = null;
705 8
        $this->snapshot = array();
706
707 8
        $this->changed();
708 8
    }
709
710
    /**
711
     * Returns whether or not this collection has orphan removal enabled.
712
     *
713
     * Embedded documents are automatically considered as "orphan removal enabled" because they might have references
714
     * that require to trigger cascade remove operations.
715
     *
716
     * @return boolean
717
     */
718 187
    private function isOrphanRemovalEnabled()
719
    {
720 187
        if ($this->mapping === null) {
721 10
            return false;
722
        }
723
724 177
        if (isset($this->mapping['embedded'])) {
725 105
            return true;
726
        }
727
728 77
        if (isset($this->mapping['reference']) && $this->mapping['isOwningSide'] && $this->mapping['orphanRemoval']) {
729 8
            return true;
730
        }
731
732 69
        return false;
733
    }
734
735
    /**
736
     * Checks whether collection owner needs to be scheduled for dirty change in case the collection is modified.
737
     *
738
     * @return bool
739
     */
740 193
    private function needsSchedulingForDirtyCheck()
741
    {
742 193
        return $this->owner && $this->dm && ! empty($this->mapping['isOwningSide'])
743 193
            && $this->dm->getClassMetadata(get_class($this->owner))->isChangeTrackingNotify();
744
    }
745
}
746