Passed
Pull Request — master (#14)
by Pavel
03:16
created

ApiCollection::removeElement()   D

Complexity

Conditions 10
Paths 6

Size

Total Lines 28
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 19.1124

Importance

Changes 0
Metric Value
dl 0
loc 28
c 0
b 0
f 0
ccs 11
cts 20
cp 0.55
rs 4.8196
cc 10
eloc 18
nc 6
nop 1
crap 19.1124

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace Bankiru\Api\Doctrine\Proxy;
4
5
use Bankiru\Api\Doctrine\ApiEntityManager;
6
use Bankiru\Api\Doctrine\Mapping\ApiMetadata;
7
use Doctrine\Common\Collections\AbstractLazyCollection;
8
use Doctrine\Common\Collections\ArrayCollection;
9
use Doctrine\Common\Collections\Collection;
10
use Doctrine\Common\Collections\Criteria;
11
use Doctrine\Common\Collections\Selectable;
12
use ScayTrase\Api\Rpc\Exception\RpcExceptionInterface;
13
14
class ApiCollection extends AbstractLazyCollection implements Selectable
15
{
16
    /** @var  ApiEntityManager */
17
    private $manager;
18
    /** @var ApiMetadata */
19
    private $metadata;
20
    /** @var  object */
21
    private $owner;
22
    /** @var  array */
23
    private $association;
24
    /** @var  bool */
25
    private $isDirty = false;
26
    /** @var  array */
27
    private $snapshot = [];
28
    /** @var  string */
29
    private $backRefFieldName;
30
31
    /**
32
     * ApiCollection constructor.
33
     *
34
     * @param ApiEntityManager $manager
35
     * @param ApiMetadata      $class
36
     * @param Collection       $collection
37
     */
38 12
    public function __construct(
39
        ApiEntityManager $manager,
40
        ApiMetadata $class,
41
        Collection $collection = null
42
    ) {
43 12
        $this->manager     = $manager;
44 12
        $this->metadata    = $class;
45 12
        $this->collection  = $collection ?: new ArrayCollection();
46 12
        $this->initialized = true;
47 12
    }
48
49
    /**
50
     * @return boolean
51
     */
52 2
    public function isDirty()
53
    {
54 2
        return $this->isDirty;
55
    }
56
57 2
    public function unwrap()
58
    {
59 2
        return $this->collection;
60
    }
61
62
    /**
63
     * INTERNAL:
64
     * Tells this collection to take a snapshot of its current state.
65
     *
66
     * @return void
67
     */
68 4
    public function takeSnapshot()
69
    {
70 4
        $this->snapshot = $this->collection->toArray();
71 4
        $this->isDirty  = false;
72 4
    }
73
74 2
    public function getMapping()
75
    {
76 2
        return $this->association;
77
    }
78
79
    /**
80
     * @return object
81
     */
82 3
    public function getOwner()
83
    {
84 3
        return $this->owner;
85
    }
86
87
    /**
88
     * @param object $owner
89
     * @param array  $assoc
90
     */
91 12
    public function setOwner($owner, array $assoc)
92
    {
93 12
        $this->owner            = $owner;
94 12
        $this->association      = $assoc;
95 12
        $this->backRefFieldName = $assoc['inversedBy'] ?: $assoc['mappedBy'];
96 12
    }
97
98
    /**
99
     * @param bool $dirty
100
     */
101 2
    public function setDirty($dirty)
102
    {
103 2
        $this->isDirty = (bool)$dirty;
104 2
    }
105
106
    /**
107
     * Initializes the collection by loading its contents from the database
108
     * if the collection is not yet initialized.
109
     *
110
     * @return void
111
     */
112 3
    public function initialize()
113
    {
114 3
        if ($this->initialized || !$this->association) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->association of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
115 2
            return;
116
        }
117 2
        $this->doInitialize();
118 2
        $this->initialized = true;
119 2
    }
120
121
    /**
122
     * @return ApiMetadata
123
     */
124
    public function getMetadata()
125
    {
126
        return $this->metadata;
127
    }
128
129
    /**
130
     * INTERNAL:
131
     * Adds an element to a collection during hydration. This will automatically
132
     * complete bidirectional associations in the case of a one-to-many association.
133
     *
134
     * @param mixed $element The element to add.
135
     *
136
     * @return void
137
     */
138
    public function hydrateAdd($element)
139
    {
140
        $this->collection->add($element);
141
        // If _backRefFieldName is set and its a one-to-many association,
142
        // we need to set the back reference.
143
        if ($this->backRefFieldName && $this->association['type'] === ApiMetadata::ONE_TO_MANY) {
144
            // Set back reference to owner
145
            $this->metadata->getReflectionProperty($this->backRefFieldName)->setValue(
146
                $element,
147
                $this->owner
148
            );
149
            $this->manager->getUnitOfWork()->setOriginalEntityProperty(
150
                spl_object_hash($element),
151
                $this->backRefFieldName,
152
                $this->owner
153
            );
154
        }
155
    }
156
157
    /**
158
     * INTERNAL:
159
     * Sets a keyed element in the collection during hydration.
160
     *
161
     * @param mixed $key     The key to set.
162
     * @param mixed $element The element to set.
163
     *
164
     * @return void
165
     */
166
    public function hydrateSet($key, $element)
167
    {
168
        $this->collection->set($key, $element);
169
        // If _backRefFieldName is set, then the association is bidirectional
170
        // and we need to set the back reference.
171
        if ($this->backRefFieldName && $this->association['type'] === ApiMetadata::ONE_TO_MANY) {
172
            // Set back reference to owner
173
            $this->metadata->getReflectionProperty($this->backRefFieldName)->setValue(
174
                $element,
175
                $this->owner
176
            );
177
        }
178
    }
179
180
    /**
181
     * INTERNAL:
182
     * Returns the last snapshot of the elements in the collection.
183
     *
184
     * @return array The last snapshot of the elements.
185
     */
186
    public function getSnapshot()
187
    {
188
        return $this->snapshot;
189
    }
190
191 10
    public function setInitialized($state)
192
    {
193 10
        $this->initialized = (bool)$state;
194 10
    }
195
196
    /**
197
     * INTERNAL:
198
     * getDeleteDiff
199
     *
200
     * @return array
201
     */
202 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...
203
    {
204
        return array_udiff_assoc(
205
            $this->snapshot,
206
            $this->collection->toArray(),
207
            function ($a, $b) {
208
                return $a === $b ? 0 : 1;
209
            }
210
        );
211
    }
212
213
    /**
214
     * INTERNAL:
215
     * getInsertDiff
216
     *
217
     * @return array
218
     */
219 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...
220
    {
221
        return array_udiff_assoc(
222
            $this->collection->toArray(),
223
            $this->snapshot,
224
            function ($a, $b) {
225
                return $a === $b ? 0 : 1;
226
            }
227
        );
228
    }
229
230
    /**
231
     * {@inheritdoc}
232
     */
233 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...
234
    {
235
        $removed = parent::remove($key);
236
        if (!$removed) {
237
            return $removed;
238
        }
239
        $this->changed();
240
        if ($this->association !== null &&
241
            $this->association['type'] & ApiMetadata::TO_MANY &&
242
            $this->owner &&
243
            $this->association['orphanRemoval']
244
        ) {
245
            $this->manager->getUnitOfWork()->scheduleOrphanRemoval($removed);
246
        }
247
248
        return $removed;
249
    }
250
251
    /**
252
     * {@inheritdoc}
253
     */
254 1
    public function removeElement($element)
255
    {
256 1
        if (!$this->initialized && $this->association['fetch'] === ApiMetadata::FETCH_EXTRA_LAZY) {
257
            if ($this->collection->contains($element)) {
258
                return $this->collection->removeElement($element);
259
            }
260
            $persister = $this->manager->getUnitOfWork()->getCollectionPersister($this->association);
261
            if ($persister->removeElement($this, $element)) {
0 ignored issues
show
Bug introduced by
The method removeElement() does not seem to exist on object<Bankiru\Api\Doctr...er\CollectionPersister>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
262
                return $element;
263
            }
264
265
            return null;
266
        }
267 1
        $removed = parent::removeElement($element);
268 1
        if (!$removed) {
269
            return $removed;
270
        }
271 1
        $this->changed();
272 1
        if ($this->association !== null &&
273 1
            $this->association['type'] & ApiMetadata::TO_MANY &&
274 1
            $this->owner &&
275 1
            $this->association['orphanRemoval']
276 1
        ) {
277
            $this->manager->getUnitOfWork()->scheduleOrphanRemoval($element);
278
        }
279
280 1
        return $removed;
281
    }
282
283
    /**
284
     * {@inheritdoc}
285
     */
286 View Code Duplication
    public function containsKey($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...
287
    {
288
        if (!$this->initialized && $this->association['fetch'] === ApiMetadata::FETCH_EXTRA_LAZY
289
            && isset($this->association['indexBy'])
290
        ) {
291
            $persister = $this->manager->getUnitOfWork()->getCollectionPersister($this->association);
292
293
            return $this->collection->containsKey($key) || $persister->containsKey($this, $key);
0 ignored issues
show
Bug introduced by
The method containsKey() does not seem to exist on object<Bankiru\Api\Doctr...er\CollectionPersister>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
294
        }
295
296
        return parent::containsKey($key);
297
    }
298
299
    /**
300
     * {@inheritdoc}
301
     */
302 1 View Code Duplication
    public function contains($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...
303
    {
304 1
        if (!$this->initialized && $this->association['fetch'] === ApiMetadata::FETCH_EXTRA_LAZY) {
305
            $persister = $this->manager->getUnitOfWork()->getCollectionPersister($this->association);
306
307
            return $this->collection->contains($element) || $persister->contains($this, $element);
0 ignored issues
show
Bug introduced by
The method contains() does not seem to exist on object<Bankiru\Api\Doctr...er\CollectionPersister>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
308
        }
309
310 1
        return parent::contains($element);
311
    }
312
313
    /**
314
     * {@inheritdoc}
315
     */
316
    public function get($key)
317
    {
318
        if (!$this->initialized
319
            && $this->association['fetch'] === ApiMetadata::FETCH_EXTRA_LAZY
320
            && isset($this->association['indexBy'])
321
        ) {
322
            if (!$this->metadata->isIdentifierComposite() &&
323
                $this->metadata->isIdentifier($this->association['indexBy'])
324
            ) {
325
                return $this->manager->find($this->metadata->getName(), $key);
326
            }
327
328
            return $this->manager->getUnitOfWork()->getCollectionPersister($this->association)->get($this, $key);
0 ignored issues
show
Bug introduced by
The method get() does not seem to exist on object<Bankiru\Api\Doctr...er\CollectionPersister>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
329
        }
330
331
        return parent::get($key);
332
    }
333
334
    /**
335
     * {@inheritdoc}
336
     */
337 1
    public function count()
338
    {
339 1
        if (!$this->initialized && $this->association['fetch'] === ApiMetadata::FETCH_EXTRA_LAZY) {
340
            $persister = $this->manager->getUnitOfWork()->getCollectionPersister($this->association);
341
342
            return $persister->count($this) + ($this->isDirty ? $this->collection->count() : 0);
0 ignored issues
show
Bug introduced by
The method count() does not seem to exist on object<Bankiru\Api\Doctr...er\CollectionPersister>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
343
        }
344
345 1
        return parent::count();
346
    }
347
348
    /**
349
     * {@inheritdoc}
350
     */
351 View Code Duplication
    public function set($key, $value)
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
        parent::set($key, $value);
354
        $this->changed();
355
        if (is_object($value) && $this->manager) {
356
            $this->manager->getUnitOfWork()->cancelOrphanRemoval($value);
357
        }
358
    }
359
360
    /**
361
     * {@inheritdoc}
362
     */
363 2 View Code Duplication
    public function add($value)
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...
364
    {
365 2
        $this->collection->add($value);
366 2
        $this->changed();
367 2
        if (is_object($value) && $this->manager) {
368 2
            $this->manager->getUnitOfWork()->cancelOrphanRemoval($value);
369 2
        }
370
371 2
        return true;
372
    }
373
374
    /**
375
     * {@inheritdoc}
376
     */
377
    public function offsetExists($offset)
378
    {
379
        return $this->containsKey($offset);
380
    }
381
382
    /**
383
     * {@inheritdoc}
384
     */
385
    public function offsetGet($offset)
386
    {
387
        return $this->get($offset);
388
    }
389
    /* ArrayAccess implementation */
390
391
    /**
392
     * {@inheritdoc}
393
     */
394
    public function offsetSet($offset, $value)
395
    {
396
        if (!isset($offset)) {
397
            return $this->add($value);
398
        }
399
400
        return $this->set($offset, $value);
401
    }
402
403
    /**
404
     * {@inheritdoc}
405
     */
406
    public function offsetUnset($offset)
407
    {
408
        return $this->remove($offset);
409
    }
410
411
    /**
412
     * {@inheritdoc}
413
     */
414 2
    public function isEmpty()
415
    {
416 2
        return $this->collection->isEmpty() && $this->count() === 0;
417
    }
418
419
    /**
420
     * {@inheritdoc}
421
     */
422
    public function clear()
423
    {
424
        if ($this->initialized && $this->isEmpty()) {
425
            return;
426
        }
427
        $uow = $this->manager->getUnitOfWork();
428
        if ($this->association['type'] & ApiMetadata::TO_MANY &&
429
            $this->association['orphanRemoval'] &&
430
            $this->owner
431
        ) {
432
            // we need to initialize here, as orphan removal acts like implicit cascadeRemove,
433
            // hence for event listeners we need the objects in memory.
434
            $this->initialize();
435
            foreach ($this->collection as $element) {
436
                $uow->scheduleOrphanRemoval($element);
437
            }
438
        }
439
        $this->collection->clear();
440
        $this->initialized = true; // direct call, {@link initialize()} is too expensive
441
        if ($this->association['isOwningSide'] && $this->owner) {
442
            $this->changed();
443
            $uow->scheduleCollectionDeletion($this);
444
            $this->takeSnapshot();
445
        }
446
    }
447
448
    /**
449
     * Called by PHP when this collection is serialized. Ensures that only the
450
     * elements are properly serialized.
451
     *
452
     * Internal note: Tried to implement Serializable first but that did not work well
453
     *                with circular references. This solution seems simpler and works well.
454
     *
455
     * @return array
456
     */
457
    public function __sleep()
458
    {
459
        return ['collection', 'initialized'];
460
    }
461
462
    /**
463
     * Extracts a slice of $length elements starting at position $offset from the Collection.
464
     *
465
     * If $length is null it returns all elements from $offset to the end of the Collection.
466
     * Keys have to be preserved by this method. Calling this method will only return the
467
     * selected slice and NOT change the elements contained in the collection slice is called on.
468
     *
469
     * @param int      $offset
470
     * @param int|null $length
471
     *
472
     * @return array
473
     */
474 View Code Duplication
    public function slice($offset, $length = null)
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...
475
    {
476
        if (!$this->initialized && !$this->isDirty && $this->association['fetch'] === ApiMetadata::FETCH_EXTRA_LAZY) {
477
            $persister = $this->manager->getUnitOfWork()->getCollectionPersister($this->association);
478
479
            return $persister->slice($this, $offset, $length);
0 ignored issues
show
Bug introduced by
The method slice() does not seem to exist on object<Bankiru\Api\Doctr...er\CollectionPersister>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
480
        }
481
482
        return parent::slice($offset, $length);
483
    }
484
485
    /**
486
     * Cleans up internal state of cloned persistent collection.
487
     *
488
     * The following problems have to be prevented:
489
     * 1. Added entities are added to old PC
490
     * 2. New collection is not dirty, if reused on other entity nothing
491
     * changes.
492
     * 3. Snapshot leads to invalid diffs being generated.
493
     * 4. Lazy loading grabs entities from old owner object.
494
     * 5. New collection is connected to old owner and leads to duplicate keys.
495
     *
496
     * @return void
497
     */
498
    public function __clone()
499
    {
500
        if (is_object($this->collection)) {
501
            $this->collection = clone $this->collection;
502
        }
503
        $this->initialize();
504
        $this->owner    = null;
505
        $this->snapshot = [];
506
        $this->changed();
507
    }
508
509
    /**
510
     * Selects all elements from a selectable that match the expression and
511
     * return a new collection containing these elements.
512
     *
513
     * @param \Doctrine\Common\Collections\Criteria $criteria
514
     *
515
     * @return Collection
516
     *
517
     * @throws \RuntimeException
518
     */
519
    public function matching(Criteria $criteria)
520
    {
521
        if ($this->isDirty) {
522
            $this->initialize();
523
        }
524
        if ($this->initialized) {
525
            return $this->collection->matching($criteria);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Doctrine\Common\Collections\Collection as the method matching() does only exist in the following implementations of said interface: Bankiru\Api\Doctrine\Proxy\ApiCollection, Doctrine\Common\Collections\ArrayCollection.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
526
        }
527
528
        $builder         = Criteria::expr();
529
        $ownerExpression = $builder->eq($this->backRefFieldName, $this->owner);
530
        $expression      = $criteria->getWhereExpression();
531
        $expression      = $expression ? $builder->andX($expression, $ownerExpression) : $ownerExpression;
532
        $criteria        = clone $criteria;
533
        $criteria->where($expression);
534
        $persister = $this->manager->getUnitOfWork()->getEntityPersister($this->association['target']);
535
536
        return new LazyCriteriaCollection($persister, $criteria);
537
    }
538
539
    /**
540
     * Do the initialization logic
541
     *
542
     * @return void
543
     * @throws RpcExceptionInterface
544
     */
545 2
    protected function doInitialize()
546
    {
547
        // Has NEW objects added through add(). Remember them.
548 2
        $newObjects = [];
549 2
        if ($this->isDirty) {
550
            $newObjects = $this->collection->toArray();
551
        }
552 2
        $this->collection->clear();
553 2
        $this->manager->getUnitOfWork()->loadCollection($this);
554 2
        $this->takeSnapshot();
555
        // Reattach NEW objects added through add(), if any.
556 2
        if ($newObjects) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $newObjects of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
557
            foreach ($newObjects as $obj) {
558
                $this->collection->add($obj);
559
            }
560
            $this->isDirty = true;
561
        }
562 2
    }
563
564
    /**
565
     * Marks this collection as changed/dirty.
566
     *
567
     * @return void
568
     */
569 3
    private function changed()
570
    {
571 3
        $this->isDirty = true;
572 3
    }
573
}
574