Completed
Pull Request — master (#1219)
by Maciej
14:45
created

PersistentCollectionTrait::removeElement()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 13
Code Lines 7

Duplication

Lines 13
Ratio 100 %

Code Coverage

Tests 6
CRAP Score 2.0116

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 13
loc 13
ccs 6
cts 7
cp 0.8571
rs 9.4285
cc 2
eloc 7
nc 2
nop 1
crap 2.0116
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
    /**
107
     * @var ClassMetadata
108
     */
109
    private $typeClass;
110
111
    /** {@inheritdoc} */
112
    public function setDocumentManager(DocumentManager $dm)
113
    {
114
        $this->dm = $dm;
115
        $this->uow = $dm->getUnitOfWork();
116
    }
117
118
    /** {@inheritdoc} */
119 172
    public function setMongoData(array $mongoData)
120
    {
121 172
        $this->mongoData = $mongoData;
122 172
    }
123
124
    /** {@inheritdoc} */
125 148
    public function getMongoData()
126
    {
127 148
        return $this->mongoData;
128
    }
129
130
    /** {@inheritdoc} */
131 256
    public function setHints(array $hints)
132
    {
133 256
        $this->hints = $hints;
134 256
    }
135
136
    /** {@inheritdoc} */
137 83
    public function getHints()
138
    {
139 83
        return $this->hints;
140
    }
141
142
    /** {@inheritdoc} */
143 369
    public function initialize()
144
    {
145 369
        if ($this->initialized || ! $this->mapping) {
146 362
            return;
147
        }
148
149 160
        $newObjects = array();
150
151 160
        if ($this->isDirty) {
152
            // Remember any NEW objects added through add()
153 21
            $newObjects = $this->coll->toArray();
154 21
        }
155
156 160
        $this->initialized = true;
157
158 160
        $this->coll->clear();
159 160
        $this->uow->loadCollection($this);
160 160
        $this->takeSnapshot();
161
162 160
        $this->mongoData = array();
163
164
        // Reattach any NEW objects added through add()
165 160
        if ($newObjects) {
166 21
            foreach ($newObjects as $key => $obj) {
167 21
                if (CollectionHelper::isHash($this->mapping['strategy'])) {
168
                    $this->coll->set($key, $obj);
169
                } else {
170 21
                    $this->coll->add($obj);
171
                }
172 21
            }
173
174 21
            $this->isDirty = true;
175 21
        }
176 160
    }
177
178
    /**
179
     * Marks this collection as changed/dirty.
180
     */
181 181
    private function changed()
182
    {
183 181
        if ($this->isDirty) {
184 118
            return;
185
        }
186
187 181
        $this->isDirty = true;
188
189 181
        if ($this->dm &&
190 181
            $this->mapping !== null &&
191 181
            $this->mapping['isOwningSide'] &&
192 181
            $this->owner &&
193 181
            $this->dm->getClassMetadata(get_class($this->owner))->isChangeTrackingNotify()) {
194 1
            $this->uow->scheduleForDirtyCheck($this->owner);
195 1
        }
196 181
    }
197
198
    /** {@inheritdoc} */
199 385
    public function isDirty()
200
    {
201 385
        return $this->isDirty;
202
    }
203
204
    /** {@inheritdoc} */
205 378
    public function setDirty($dirty)
206
    {
207 378
        $this->isDirty = $dirty;
208 378
    }
209
210
    /** {@inheritdoc} */
211 394
    public function setOwner($document, array $mapping)
212
    {
213 394
        $this->owner = $document;
214 394
        $this->mapping = $mapping;
215
216 394
        if ( ! empty($this->mapping['targetDocument'])) {
217 380
            $this->typeClass = $this->dm->getClassMetadata($this->mapping['targetDocument']);
218 380
        }
219 394
    }
220
221
    /** {@inheritdoc} */
222 263
    public function takeSnapshot()
223
    {
224 263
        if (CollectionHelper::isList($this->mapping['strategy'])) {
225 249
            $array = $this->coll->toArray();
226 249
            $this->coll->clear();
227 249
            foreach ($array as $document) {
228 224
                $this->coll->add($document);
229 249
            }
230 249
        }
231 263
        $this->snapshot = $this->coll->toArray();
232 263
        $this->isDirty = false;
233 263
    }
234
235
    /** {@inheritdoc} */
236 23
    public function clearSnapshot()
237
    {
238 23
        $this->snapshot = array();
239 23
        $this->isDirty = $this->coll->count() ? true : false;
240 23
    }
241
242
    /** {@inheritdoc} */
243
    public function getSnapshot()
244
    {
245
        return $this->snapshot;
246
    }
247
248
    /** {@inheritdoc} */
249 82 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...
250
    {
251 82
        return array_udiff_assoc(
252 82
            $this->snapshot,
253 82
            $this->coll->toArray(),
254
            function ($a, $b) { return $a === $b ? 0 : 1; }
255 82
        );
256
    }
257
258
    /** {@inheritdoc} */
259 142
    public function getDeletedDocuments()
260
    {
261
        $compare = function ($a, $b) {
262 97
            $compareA = is_object($a) ? spl_object_hash($a) : $a;
263 97
            $compareb = is_object($b) ? spl_object_hash($b) : $b;
264 97
            return $compareA === $compareb ? 0 : ($compareA > $compareb ? 1 : -1);
265 142
        };
266 142
        return array_udiff(
267 142
            $this->snapshot,
268 142
            $this->coll->toArray(),
269
            $compare
270 142
        );
271
    }
272
273
    /** {@inheritdoc} */
274 82 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...
275
    {
276 82
        return array_udiff_assoc(
277 82
            $this->coll->toArray(),
278 82
            $this->snapshot,
279
            function ($a, $b) { return $a === $b ? 0 : 1; }
280 82
        );
281
    }
282
283
    /** {@inheritdoc} */
284 385
    public function getOwner()
285
    {
286 385
        return $this->owner;
287
    }
288
289
    /** {@inheritdoc} */
290 261
    public function getMapping()
291
    {
292 261
        return $this->mapping;
293
    }
294
295
    /** {@inheritdoc} */
296 2
    public function getTypeClass()
297
    {
298 2
        if (empty($this->typeClass)) {
299 1
            throw new MongoDBException('Specifying targetDocument is required for the ClassMetadata to be obtained.');
300
        }
301
302 1
        return $this->typeClass;
303
    }
304
305
    /** {@inheritdoc} */
306 257
    public function setInitialized($bool)
307
    {
308 257
        $this->initialized = $bool;
309 257
    }
310
311
    /** {@inheritdoc} */
312 15
    public function isInitialized()
313
    {
314 15
        return $this->initialized;
315
    }
316
317
    /** {@inheritdoc} */
318 12
    public function first()
319
    {
320 12
        $this->initialize();
321 12
        return $this->coll->first();
322
    }
323
324
    /** {@inheritdoc} */
325 1
    public function last()
326
    {
327 1
        $this->initialize();
328 1
        return $this->coll->last();
329
    }
330
331
    /**
332
     * {@inheritdoc}
333
     */
334 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...
335
    {
336 21
        $this->initialize();
337 21
        $removed = $this->coll->remove($key);
338
339 21
        if ( ! $removed) {
340
            return $removed;
341
        }
342
343 21
        $this->changed();
344
345 21
        return $removed;
346
    }
347
348
    /**
349
     * {@inheritdoc}
350
     */
351 14 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...
352
    {
353 14
        $this->initialize();
354 14
        $removed = $this->coll->removeElement($element);
355
356 14
        if ( ! $removed) {
357
            return $removed;
358
        }
359
360 14
        $this->changed();
361
362 14
        return $removed;
363
    }
364
365
    /**
366
     * {@inheritdoc}
367
     */
368 1
    public function containsKey($key)
369
    {
370 1
        $this->initialize();
371 1
        return $this->coll->containsKey($key);
372
    }
373
374
    /**
375
     * {@inheritdoc}
376
     */
377 2
    public function contains($element)
378
    {
379 2
        $this->initialize();
380 2
        return $this->coll->contains($element);
381
    }
382
383
    /**
384
     * {@inheritdoc}
385
     */
386
    public function exists(\Closure $p)
387
    {
388
        $this->initialize();
389
        return $this->coll->exists($p);
390
    }
391
392
    /**
393
     * {@inheritdoc}
394
     */
395 2
    public function indexOf($element)
396
    {
397 2
        $this->initialize();
398 2
        return $this->coll->indexOf($element);
399
    }
400
401
    /**
402
     * {@inheritdoc}
403
     */
404 87
    public function get($key)
405
    {
406 87
        $this->initialize();
407 87
        return $this->coll->get($key);
408
    }
409
410
    /**
411
     * {@inheritdoc}
412
     */
413
    public function getKeys()
414
    {
415
        $this->initialize();
416
        return $this->coll->getKeys();
417
    }
418
419
    /**
420
     * {@inheritdoc}
421
     */
422
    public function getValues()
423
    {
424
        $this->initialize();
425
        return $this->coll->getValues();
426
    }
427
428
    /**
429
     * {@inheritdoc}
430
     */
431 366
    public function count()
432
    {
433 366
        $count = $this->coll->count();
434
435
        // If this collection is inversed and not initialized, add the count returned from the database
436 366
        if ($this->mapping['isInverseSide'] && ! $this->initialized) {
437 10
            $documentPersister = $this->uow->getDocumentPersister(get_class($this->owner));
438 10
            $count += empty($this->mapping['repositoryMethod'])
439 10
                ? $documentPersister->createReferenceManyInverseSideQuery($this)->count()
440 10
                : $documentPersister->createReferenceManyWithRepositoryMethodCursor($this)->count();
441 10
        }
442
443 366
        return count($this->mongoData) + $count;
444
    }
445
446
    /**
447
     * {@inheritdoc}
448
     */
449 35
    public function set($key, $value)
450
    {
451 35
        $this->coll->set($key, $value);
452
453
        // Handle orphanRemoval
454 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...
455 34
            $this->uow->unscheduleOrphanRemoval($value);
456 34
        }
457
458 35
        $this->changed();
459 35
    }
460
461
    /**
462
     * {@inheritdoc}
463
     */
464 155
    public function add($value)
465
    {
466
        /* Initialize the collection before calling add() so this append operation
467
         * uses the appropriate key. Otherwise, we risk overwriting original data
468
         * when $newObjects are re-added in a later call to initialize().
469
         */
470 155
        if (isset($this->mapping['strategy']) && CollectionHelper::isHash($this->mapping['strategy'])) {
471 12
            $this->initialize();
472 12
        }
473 155
        $this->coll->add($value);
474 155
        $this->changed();
475
476 155 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...
477 91
            $this->uow->unscheduleOrphanRemoval($value);
478 91
        }
479
480 155
        return true;
481
    }
482
483
    /**
484
     * {@inheritdoc}
485
     */
486 359
    public function isEmpty()
487
    {
488 359
        return $this->count() === 0;
489
    }
490
491
    /**
492
     * {@inheritdoc}
493
     */
494 318
    public function getIterator()
495
    {
496 318
        $this->initialize();
497 318
        return $this->coll->getIterator();
498
    }
499
500
    /**
501
     * {@inheritdoc}
502
     */
503 214
    public function map(\Closure $func)
504
    {
505 214
        $this->initialize();
506 214
        return $this->coll->map($func);
507
    }
508
509
    /**
510
     * {@inheritdoc}
511
     */
512
    public function filter(\Closure $p)
513
    {
514
        $this->initialize();
515
        return $this->coll->filter($p);
516
    }
517
518
    /**
519
     * {@inheritdoc}
520
     */
521
    public function forAll(\Closure $p)
522
    {
523
        $this->initialize();
524
        return $this->coll->forAll($p);
525
    }
526
527
    /**
528
     * {@inheritdoc}
529
     */
530
    public function partition(\Closure $p)
531
    {
532
        $this->initialize();
533
        return $this->coll->partition($p);
534
    }
535
536
    /**
537
     * {@inheritdoc}
538
     */
539 23
    public function toArray()
540
    {
541 23
        $this->initialize();
542 23
        return $this->coll->toArray();
543
    }
544
545
    /**
546
     * {@inheritdoc}
547
     */
548 28
    public function clear()
549
    {
550 28
        if ($this->initialized && $this->isEmpty()) {
551
            return;
552
        }
553
554 28
        if ($this->isOrphanRemovalEnabled()) {
555 27
            foreach ($this->coll as $element) {
556 25
                $this->uow->scheduleOrphanRemoval($element);
557 27
            }
558 27
        }
559
560 28
        $this->mongoData = array();
561 28
        $this->coll->clear();
562
563
        // Nothing to do for inverse-side collections
564 28
        if ( ! $this->mapping['isOwningSide']) {
565
            return;
566
        }
567
568
        // Nothing to do if the collection was initialized but contained no data
569 28
        if ($this->initialized && empty($this->snapshot)) {
570 2
            return;
571
        }
572
573 26
        $this->changed();
574 26
        $this->uow->scheduleCollectionDeletion($this);
575 26
        $this->takeSnapshot();
576 26
    }
577
578
    /**
579
     * {@inheritdoc}
580
     */
581 1
    public function slice($offset, $length = null)
582
    {
583 1
        $this->initialize();
584 1
        return $this->coll->slice($offset, $length);
585
    }
586
587
    /**
588
     * Called by PHP when this collection is serialized. Ensures that only the
589
     * elements are properly serialized.
590
     *
591
     * @internal Tried to implement Serializable first but that did not work well
592
     *           with circular references. This solution seems simpler and works well.
593
     */
594 1
    public function __sleep()
595
    {
596 1
        return array('coll', 'initialized');
597
    }
598
599
    /* ArrayAccess implementation */
600
601
    /**
602
     * @see containsKey()
603
     */
604 1
    public function offsetExists($offset)
605
    {
606 1
        return $this->containsKey($offset);
607
    }
608
609
    /**
610
     * @see get()
611
     */
612 75
    public function offsetGet($offset)
613
    {
614 75
        return $this->get($offset);
615
    }
616
617
    /**
618
     * @see add()
619
     * @see set()
620
     */
621 38
    public function offsetSet($offset, $value)
622
    {
623 38
        if ( ! isset($offset)) {
624 37
            return $this->add($value);
625
        }
626
627 2
        return $this->set($offset, $value);
628
    }
629
630
    /**
631
     * @see remove()
632
     */
633 18
    public function offsetUnset($offset)
634
    {
635 18
        return $this->remove($offset);
636
    }
637
638
    public function key()
639
    {
640
        return $this->coll->key();
641
    }
642
643
    /**
644
     * Gets the element of the collection at the current iterator position.
645
     */
646 1
    public function current()
647
    {
648 1
        return $this->coll->current();
649
    }
650
651
    /**
652
     * Moves the internal iterator position to the next element.
653
     */
654
    public function next()
655
    {
656
        return $this->coll->next();
657
    }
658
659
    /**
660
     * Retrieves the wrapped Collection instance.
661
     */
662 386
    public function unwrap()
663
    {
664 386
        return $this->coll;
665
    }
666
667
    /**
668
     * Cleanup internal state of cloned persistent collection.
669
     *
670
     * The following problems have to be prevented:
671
     * 1. Added documents are added to old PersistentCollection
672
     * 2. New collection is not dirty, if reused on other document nothing
673
     * changes.
674
     * 3. Snapshot leads to invalid diffs being generated.
675
     * 4. Lazy loading grabs entities from old owner object.
676
     * 5. New collection is connected to old owner and leads to duplicate keys.
677
     */
678 8
    public function __clone()
679
    {
680 8
        if (is_object($this->coll)) {
681 8
            $this->coll = clone $this->coll;
682 8
        }
683
684 8
        $this->initialize();
685
686 8
        $this->owner = null;
687 8
        $this->snapshot = array();
688
689 8
        $this->changed();
690 8
    }
691
692
    /**
693
     * Returns whether or not this collection has orphan removal enabled.
694
     *
695
     * Embedded documents are automatically considered as "orphan removal enabled" because they might have references
696
     * that require to trigger cascade remove operations.
697
     *
698
     * @return boolean
699
     */
700 175
    private function isOrphanRemovalEnabled()
701
    {
702 175
        if ($this->mapping === null) {
703 5
            return false;
704
        }
705
706 170
        if (isset($this->mapping['embedded'])) {
707 102
            return true;
708
        }
709
710 73
        if (isset($this->mapping['reference']) && $this->mapping['isOwningSide'] && $this->mapping['orphanRemoval']) {
711 8
            return true;
712
        }
713
714 65
        return false;
715
    }
716
}
717