PersistentRelatedEntitiesCollection   C
last analyzed

Complexity

Total Complexity 56

Size/Duplication

Total Lines 536
Duplicated Lines 4.29 %

Coupling/Cohesion

Components 1
Dependencies 5

Test Coverage

Coverage 25.66%

Importance

Changes 0
Metric Value
wmc 56
lcom 1
cbo 5
dl 23
loc 536
ccs 39
cts 152
cp 0.2566
rs 5.5199
c 0
b 0
f 0

34 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
A toArray() 0 6 1
A first() 0 6 1
A last() 0 6 1
A key() 0 6 1
A next() 0 6 1
A current() 0 6 1
A remove() 0 4 1
A removeElement() 0 4 1
A offsetExists() 0 6 1
A offsetGet() 0 6 1
A offsetSet() 0 4 1
A offsetUnset() 0 4 1
A containsKey() 0 6 1
A contains() 0 12 3
A exists() 11 11 3
A indexOf() 0 6 1
A get() 0 9 2
A getKeys() 0 6 1
A getValues() 0 6 1
A count() 0 6 1
A set() 0 4 1
A add() 0 4 1
A isEmpty() 0 6 1
A getIterator() 0 6 1
A map() 0 6 1
A filter() 0 6 1
A forAll() 12 12 3
A partition() 0 14 3
A __toString() 0 4 1
A clear() 0 4 1
A slice() 0 6 1
B matching() 0 31 7
B initialize() 0 47 8

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like PersistentRelatedEntitiesCollection often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use PersistentRelatedEntitiesCollection, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace JMS\JobQueueBundle\Entity\Listener;
4
5
use ArrayIterator;
6
use Closure;
7
use Doctrine\Common\Collections\ArrayCollection;
8
use Doctrine\Common\Collections\Collection;
9
use Doctrine\Common\Collections\Criteria;
10
use Doctrine\Common\Collections\Expr\ClosureExpressionVisitor;
11
use Doctrine\Common\Collections\Selectable;
12
use Doctrine\Persistence\ManagerRegistry;
13
use JMS\JobQueueBundle\Entity\Job;
14
15
/**
16
 * Collection for persistent related entities.
17
 *
18
 * We do not support all of Doctrine's built-in features.
19
 *
20
 * @author Johannes M. Schmitt <[email protected]>
21
 */
22
class PersistentRelatedEntitiesCollection implements Collection, Selectable
23
{
24
    private $registry;
25
    private $job;
26
    private $entities;
27
28 32
    public function __construct(ManagerRegistry $registry, Job $job)
29
    {
30 32
        $this->registry = $registry;
31 32
        $this->job = $job;
32 32
    }
33
34
    /**
35
     * Gets the PHP array representation of this collection.
36
     *
37
     * @return array<object> The PHP array representation of this collection.
38
     */
39
    public function toArray()
40
    {
41
        $this->initialize();
42
43
        return $this->entities;
44
    }
45
46
    /**
47
     * Sets the internal iterator to the first element in the collection and
48
     * returns this element.
49
     *
50
     * @return object|false
51
     */
52 2
    public function first()
53
    {
54 2
        $this->initialize();
55
56 2
        return reset($this->entities);
57
    }
58
59
    /**
60
     * Sets the internal iterator to the last element in the collection and
61
     * returns this element.
62
     *
63
     * @return object|false
64
     */
65
    public function last()
66
    {
67
        $this->initialize();
68
69
        return end($this->entities);
70
    }
71
72
    /**
73
     * Gets the current key/index at the current internal iterator position.
74
     *
75
     * @return string|integer
76
     */
77
    public function key()
78
    {
79
        $this->initialize();
80
81
        return key($this->entities);
82
    }
83
84
    /**
85
     * Moves the internal iterator position to the next element.
86
     *
87
     * @return object|false
88
     */
89
    public function next()
90
    {
91
        $this->initialize();
92
93
        return next($this->entities);
94
    }
95
96
    /**
97
     * Gets the element of the collection at the current internal iterator position.
98
     *
99
     * @return object|false
100
     */
101
    public function current()
102
    {
103
        $this->initialize();
104
105
        return current($this->entities);
106
    }
107
108
    /**
109
     * Removes an element with a specific key/index from the collection.
110
     *
111
     * @param string|integer $key
112
     * @return object|null The removed element or NULL, if no element exists for the given key.
113
     */
114
    public function remove($key)
115
    {
116
        throw new \LogicException('remove() is not supported.');
117
    }
118
119
    /**
120
     * Removes the specified element from the collection, if it is found.
121
     *
122
     * @param object $element The element to remove.
123
     * @return boolean TRUE if this collection contained the specified element, FALSE otherwise.
124
     */
125
    public function removeElement($element)
126
    {
127
        throw new \LogicException('removeElement() is not supported.');
128
    }
129
130
    /**
131
     * ArrayAccess implementation of offsetExists()
132
     *
133
     * @see containsKey()
134
     *
135
     * @param mixed $offset
136
     * @return bool
137
     */
138
    public function offsetExists($offset)
139
    {
140
        $this->initialize();
141
142
        return $this->containsKey($offset);
143
    }
144
145
    /**
146
     * ArrayAccess implementation of offsetGet()
147
     *
148
     * @see get()
149
     *
150
     * @param mixed $offset
151
     * @return mixed
152
     */
153
    public function offsetGet($offset)
154
    {
155
        $this->initialize();
156
157
        return $this->get($offset);
158
    }
159
160
    /**
161
     * ArrayAccess implementation of offsetSet()
162
     *
163
     * @see add()
164
     * @see set()
165
     *
166
     * @param mixed $offset
167
     * @param mixed $value
168
     * @return bool
169
     */
170
    public function offsetSet($offset, $value)
171
    {
172
        throw new \LogicException('Adding new related entities is not supported after initial creation.');
173
    }
174
175
    /**
176
     * ArrayAccess implementation of offsetUnset()
177
     *
178
     * @see remove()
179
     *
180
     * @param mixed $offset
181
     * @return mixed
182
     */
183
    public function offsetUnset($offset)
184
    {
185
        throw new \LogicException('unset() is not supported.');
186
    }
187
188
    /**
189
     * Checks whether the collection contains a specific key/index.
190
     *
191
     * @param mixed $key The key to check for.
192
     * @return boolean TRUE if the given key/index exists, FALSE otherwise.
193
     */
194
    public function containsKey($key)
195
    {
196
        $this->initialize();
197
198
        return isset($this->entities[$key]);
199
    }
200
201
    /**
202
     * Checks whether the given element is contained in the collection.
203
     * Only element values are compared, not keys. The comparison of two elements
204
     * is strict, that means not only the value but also the type must match.
205
     * For objects this means reference equality.
206
     *
207
     * @param mixed $element
208
     * @return boolean TRUE if the given element is contained in the collection,
209
     *          FALSE otherwise.
210
     */
211
    public function contains($element)
212
    {
213
        $this->initialize();
214
215
        foreach ($this->entities as $collectionElement) {
216
            if ($element === $collectionElement) {
217
                return true;
218
            }
219
        }
220
221
        return false;
222
    }
223
224
    /**
225
     * Tests for the existence of an element that satisfies the given predicate.
226
     *
227
     * @param Closure $p The predicate.
228
     * @return boolean TRUE if the predicate is TRUE for at least one element, FALSE otherwise.
229
     */
230 View Code Duplication
    public function exists(Closure $p)
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...
231
    {
232
        $this->initialize();
233
234
        foreach ($this->entities as $key => $element) {
235
            if ($p($key, $element)) {
236
                return true;
237
            }
238
        }
239
        return false;
240
    }
241
242
    /**
243
     * Searches for a given element and, if found, returns the corresponding key/index
244
     * of that element. The comparison of two elements is strict, that means not
245
     * only the value but also the type must match.
246
     * For objects this means reference equality.
247
     *
248
     * @param mixed $element The element to search for.
249
     * @return mixed The key/index of the element or FALSE if the element was not found.
250
     */
251
    public function indexOf($element)
252
    {
253
        $this->initialize();
254
255
        return array_search($element, $this->entities, true);
256
    }
257
258
    /**
259
     * Gets the element with the given key/index.
260
     *
261
     * @param mixed $key The key.
262
     * @return mixed The element or NULL, if no element exists for the given key.
263
     */
264
    public function get($key)
265
    {
266
        $this->initialize();
267
268
        if (isset($this->entities[$key])) {
269
            return $this->entities[$key];
270
        }
271
        return null;
272
    }
273
274
    /**
275
     * Gets all keys/indexes of the collection elements.
276
     *
277
     * @return array
278
     */
279
    public function getKeys()
280
    {
281
        $this->initialize();
282
283
        return array_keys($this->entities);
284
    }
285
286
    /**
287
     * Gets all elements.
288
     *
289
     * @return array
290
     */
291
    public function getValues()
292
    {
293
        $this->initialize();
294
295
        return array_values($this->entities);
296
    }
297
298
    /**
299
     * Returns the number of elements in the collection.
300
     *
301
     * Implementation of the Countable interface.
302
     *
303
     * @return integer The number of elements in the collection.
304
     */
305 2
    public function count()
306
    {
307 2
        $this->initialize();
308
309 2
        return count($this->entities);
310
    }
311
312
    /**
313
     * Adds/sets an element in the collection at the index / with the specified key.
314
     *
315
     * When the collection is a Map this is like put(key,value)/add(key,value).
316
     * When the collection is a List this is like add(position,value).
317
     *
318
     * @param mixed $key
319
     * @param mixed $value
320
     */
321
    public function set($key, $value)
322
    {
323
        throw new \LogicException('set() is not supported.');
324
    }
325
326
    /**
327
     * Adds an element to the collection.
328
     *
329
     * @param mixed $value
330
     * @return boolean Always TRUE.
331
     */
332
    public function add($value)
333
    {
334
        throw new \LogicException('Adding new entities is not supported after creation.');
335
    }
336
337
    /**
338
     * Checks whether the collection is empty.
339
     *
340
     * Note: This is preferable over count() == 0.
341
     *
342
     * @return boolean TRUE if the collection is empty, FALSE otherwise.
343
     */
344
    public function isEmpty()
345
    {
346
        $this->initialize();
347
348
        return !$this->entities;
349
    }
350
351
    /**
352
     * Gets an iterator for iterating over the elements in the collection.
353
     *
354
     * @return ArrayIterator
355
     */
356 2
    public function getIterator()
357
    {
358 2
        $this->initialize();
359
360 2
        return new ArrayIterator($this->entities);
361
    }
362
363
    /**
364
     * Applies the given function to each element in the collection and returns
365
     * a new collection with the elements returned by the function.
366
     *
367
     * @param Closure $func
368
     * @return Collection
369
     */
370
    public function map(Closure $func)
371
    {
372
        $this->initialize();
373
374
        return new ArrayCollection(array_map($func, $this->entities));
375
    }
376
377
    /**
378
     * Returns all the elements of this collection that satisfy the predicate p.
379
     * The order of the elements is preserved.
380
     *
381
     * @param Closure $p The predicate used for filtering.
382
     * @return Collection A collection with the results of the filter operation.
383
     */
384
    public function filter(Closure $p)
385
    {
386
        $this->initialize();
387
388
        return new ArrayCollection(array_filter($this->entities, $p));
389
    }
390
391
    /**
392
     * Applies the given predicate p to all elements of this collection,
393
     * returning true, if the predicate yields true for all elements.
394
     *
395
     * @param Closure $p The predicate.
396
     * @return boolean TRUE, if the predicate yields TRUE for all elements, FALSE otherwise.
397
     */
398 View Code Duplication
    public function forAll(Closure $p)
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...
399
    {
400
        $this->initialize();
401
402
        foreach ($this->entities as $key => $element) {
403
            if (!$p($key, $element)) {
404
                return false;
405
            }
406
        }
407
408
        return true;
409
    }
410
411
    /**
412
     * Partitions this collection in two collections according to a predicate.
413
     * Keys are preserved in the resulting collections.
414
     *
415
     * @param Closure $p The predicate on which to partition.
416
     * @return array An array with two elements. The first element contains the collection
417
     *               of elements where the predicate returned TRUE, the second element
418
     *               contains the collection of elements where the predicate returned FALSE.
419
     */
420
    public function partition(Closure $p)
421
    {
422
        $this->initialize();
423
424
        $coll1 = $coll2 = array();
425
        foreach ($this->entities as $key => $element) {
426
            if ($p($key, $element)) {
427
                $coll1[$key] = $element;
428
            } else {
429
                $coll2[$key] = $element;
430
            }
431
        }
432
        return array(new ArrayCollection($coll1), new ArrayCollection($coll2));
433
    }
434
435
    /**
436
     * Returns a string representation of this object.
437
     *
438
     * @return string
439
     */
440
    public function __toString()
441
    {
442
        return __CLASS__ . '@' . spl_object_hash($this);
443
    }
444
445
    /**
446
     * Clears the collection.
447
     */
448
    public function clear()
449
    {
450
        throw new \LogicException('clear() is not supported.');
451
    }
452
453
    /**
454
     * Extract a slice of $length elements starting at position $offset from the Collection.
455
     *
456
     * If $length is null it returns all elements from $offset to the end of the Collection.
457
     * Keys have to be preserved by this method. Calling this method will only return the
458
     * selected slice and NOT change the elements contained in the collection slice is called on.
459
     *
460
     * @param int $offset
461
     * @param int $length
462
     * @return array
463
     */
464
    public function slice($offset, $length = null)
465
    {
466
        $this->initialize();
467
468
        return array_slice($this->entities, $offset, $length, true);
469
    }
470
471
    /**
472
     * Select all elements from a selectable that match the criteria and
473
     * return a new collection containing these elements.
474
     *
475
     * @param  Criteria $criteria
476
     * @return Collection
477
     */
478
    public function matching(Criteria $criteria)
479
    {
480
        $this->initialize();
481
482
        $expr     = $criteria->getWhereExpression();
483
        $filtered = $this->entities;
484
485
        if ($expr) {
486
            $visitor  = new ClosureExpressionVisitor();
487
            $filter   = $visitor->dispatch($expr);
488
            $filtered = array_filter($filtered, $filter);
489
        }
490
491
        if (null !== $orderings = $criteria->getOrderings()) {
492
            $next = null;
493
            foreach (array_reverse($orderings) as $field => $ordering) {
494
                $next = ClosureExpressionVisitor::sortByField($field, $ordering == 'DESC' ? -1 : 1, $next);
495
            }
496
497
            usort($filtered, $next);
498
        }
499
500
        $offset = $criteria->getFirstResult();
501
        $length = $criteria->getMaxResults();
502
503
        if ($offset || $length) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $offset of type integer|null is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
Bug Best Practice introduced by
The expression $length of type integer|null is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
504
            $filtered = array_slice($filtered, (int)$offset, $length);
505
        }
506
507
        return new ArrayCollection($filtered);
508
    }
509
510 4
    private function initialize()
511
    {
512 4
        if (null !== $this->entities) {
513 2
            return;
514
        }
515
516 4
        $con = $this->registry->getManagerForClass('JMSJobQueueBundle:Job')->getConnection();
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Doctrine\Persistence\ObjectManager as the method getConnection() does only exist in the following implementations of said interface: Doctrine\ORM\Decorator\EntityManagerDecorator, Doctrine\ORM\EntityManager.

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...
517 4
        $entitiesPerClass = array();
518 4
        $count = 0;
519 4
        foreach ($con->query("SELECT related_class, related_id FROM jms_job_related_entities WHERE job_id = " . $this->job->getId()) as $data) {
520 4
            $count += 1;
521 4
            $entitiesPerClass[$data['related_class']][] = json_decode($data['related_id'], true);
522
        }
523
524 4
        if (0 === $count) {
525
            $this->entities = array();
526
527
            return;
528
        }
529
530 4
        $entities = array();
531 4
        foreach ($entitiesPerClass as $className => $ids) {
532 4
            $em = $this->registry->getManagerForClass($className);
533 4
            $qb = $em->createQueryBuilder()
534 4
                ->select('e')->from($className, 'e');
535
536 4
            $i = 0;
537 4
            foreach ($ids as $id) {
538 4
                $expr = null;
539 4
                foreach ($id as $k => $v) {
540 4
                    if (null === $expr) {
541 4
                        $expr = $qb->expr()->eq('e.' . $k, '?' . (++$i));
542
                    } else {
543
                        $expr = $qb->expr()->andX($expr, $qb->expr()->eq('e.' . $k, '?' . (++$i)));
544
                    }
545
546 4
                    $qb->setParameter($i, $v);
547
                }
548
549 4
                $qb->orWhere($expr);
550
            }
551
552 4
            $entities = array_merge($entities, $qb->getQuery()->getResult());
553
        }
554
555 4
        $this->entities = $entities;
556 4
    }
557
}
558