Collection   C
last analyzed

Complexity

Total Complexity 79

Size/Duplication

Total Lines 492
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 8

Test Coverage

Coverage 83.97%

Importance

Changes 0
Metric Value
wmc 79
lcom 1
cbo 8
dl 0
loc 492
ccs 131
cts 156
cp 0.8397
rs 5.442
c 0
b 0
f 0

39 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 7 1
A unshift() 0 6 1
A first() 0 4 1
A last() 0 4 1
A key() 0 4 1
A next() 0 4 1
A current() 0 4 1
A remove() 0 11 3
A removeElement() 0 12 2
A offsetExists() 0 4 1
A offsetGet() 0 4 1
A offsetSet() 0 8 2
A offsetUnset() 0 4 1
A containsKey() 0 4 2
A contains() 0 4 1
A exists() 0 10 3
A indexOf() 0 4 1
A get() 0 4 2
A getKeys() 0 4 1
A getValues() 0 4 1
A count() 0 4 1
A isEmpty() 0 4 1
A getIterator() 0 4 1
A map() 0 4 1
A filter() 0 4 1
A forAll() 0 10 3
A partition() 0 14 3
A clear() 0 4 1
A slice() 0 4 1
C matching() 0 31 8
A add() 0 6 1
A set() 0 4 1
B __invoke() 0 19 5
A merge() 0 8 2
A valueOf() 0 4 1
A implode() 0 4 1
C asArray() 0 26 11
A getRealValue() 0 8 4
A unique() 0 14 4

How to fix   Complexity   

Complex Class

Complex classes like Collection 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 Collection, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Tdn\PhpTypes\Type;
6
7
use ArrayIterator;
8
use Closure;
9
use Doctrine\Common\Collections\Expr\ClosureExpressionVisitor;
10
use Doctrine\Common\Collections\Collection as CollectionInterface;
11
use Tdn\PhpTypes\Exception\InvalidTransformationException;
12
use Tdn\PhpTypes\Exception\InvalidTypeCastException;
13
use Tdn\PhpTypes\Type\Traits\Boxable;
14
use Tdn\PhpTypes\Type\Traits\Transmutable;
15
use Doctrine\Common\Collections\Criteria;
16
use Doctrine\Common\Collections\Selectable;
17
18
/**
19
 * Class Collection.
20
 *
21
 * A Collection is a TypeInterface implementation that wraps around a regular PHP array.
22
 * This object implements Doctrine's Collection and is analogous to Doctrine's
23
 * ArrayCollection, with extra functionality.
24
 *
25
 * This object can be extended to create type specific collections. (either primitive or compound)
26
 */
27
class Collection implements TransmutableTypeInterface, CollectionInterface, Selectable
28
{
29
    use Transmutable;
30
    use Boxable;
31
32
    /**
33
     * @var null|string
34
     */
35
    private $type;
36
37
    /**
38
     * @var array
39
     */
40
    private $elements;
41
42
    /**
43
     * @param array       $elements
44
     * @param null|string $type
45
     */
46 70
    public function __construct(array $elements = array(), string $type = null)
47
    {
48 70
        $this->type = $type;
49
        $this->elements = array_map(function ($element) {
50 65
            return $this->getRealValue($element);
51 70
        }, $elements);
52 69
    }
53
54
    /**
55
     * @param $element
56
     *
57
     * @return bool
58
     */
59 1
    public function unshift($element)
60
    {
61 1
        array_unshift($this->elements, $this->getRealValue($element));
62
63 1
        return true;
64
    }
65
66
    /**
67
     * {@inheritdoc}
68
     */
69 3
    public function first()
70
    {
71 3
        return reset($this->elements);
72
    }
73
74
    /**
75
     * {@inheritdoc}
76
     */
77 3
    public function last()
78
    {
79 3
        return end($this->elements);
80
    }
81
82
    /**
83
     * {@inheritdoc}
84
     */
85 6
    public function key()
86
    {
87 6
        return key($this->elements);
88
    }
89
90
    /**
91
     * {@inheritdoc}
92
     */
93 9
    public function next()
94
    {
95 9
        return next($this->elements);
96
    }
97
98
    /**
99
     * {@inheritdoc}
100
     */
101 6
    public function current()
102
    {
103 6
        return current($this->elements);
104
    }
105
106
    /**
107
     * {@inheritdoc}
108
     */
109 1
    public function remove($key)
110
    {
111 1
        if (!isset($this->elements[$key]) && !array_key_exists($key, $this->elements)) {
112 1
            return null;
113
        }
114
115 1
        $removed = $this->elements[$key];
116 1
        unset($this->elements[$key]);
117
118 1
        return $removed;
119
    }
120
121
    /**
122
     * {@inheritdoc}
123
     */
124 1
    public function removeElement($element)
125
    {
126 1
        $key = array_search($element, $this->elements, true);
127
128 1
        if ($key === false) {
129 1
            return false;
130
        }
131
132 1
        unset($this->elements[$key]);
133
134 1
        return true;
135
    }
136
137
    /**
138
     * Required by interface ArrayAccess.
139
     *
140
     * {@inheritdoc}
141
     */
142 1
    public function offsetExists($offset)
143
    {
144 1
        return $this->containsKey($offset);
145
    }
146
147
    /**
148
     * Required by interface ArrayAccess.
149
     *
150
     * {@inheritdoc}
151
     */
152 1
    public function offsetGet($offset)
153
    {
154 1
        return $this->get($offset);
155
    }
156
157
    /**
158
     * Required by interface ArrayAccess.
159
     *
160
     * {@inheritdoc}
161
     */
162
    public function offsetSet($offset, $value)
163
    {
164
        if (!isset($offset)) {
165
            return $this->add($value);
166
        }
167
168
        $this->set($offset, $value);
169
    }
170
171
    /**
172
     * Required by interface ArrayAccess.
173
     *
174
     * {@inheritdoc}
175
     */
176 1
    public function offsetUnset($offset)
177
    {
178 1
        return $this->remove($offset);
179
    }
180
181
    /**
182
     * {@inheritdoc}
183
     */
184 1
    public function containsKey($key)
185
    {
186 1
        return isset($this->elements[$key]) || array_key_exists($key, $this->elements);
187
    }
188
189
    /**
190
     * {@inheritdoc}
191
     */
192 1
    public function contains($element)
193
    {
194 1
        return in_array($element, $this->elements, true);
195
    }
196
197
    /**
198
     * {@inheritdoc}
199
     */
200 1
    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...
201
    {
202 1
        foreach ($this->elements as $key => $element) {
203 1
            if ($p($key, $element)) {
204 1
                return true;
205
            }
206
        }
207
208 1
        return false;
209
    }
210
211
    /**
212
     * {@inheritdoc}
213
     */
214 1
    public function indexOf($element)
215
    {
216 1
        return array_search($element, $this->elements, true);
217
    }
218
219
    /**
220
     * {@inheritdoc}
221
     */
222 1
    public function get($key)
223
    {
224 1
        return isset($this->elements[$key]) ? $this->elements[$key] : null;
225
    }
226
227
    /**
228
     * {@inheritdoc}
229
     */
230 3
    public function getKeys()
231
    {
232 3
        return array_keys($this->elements);
233
    }
234
235
    /**
236
     * {@inheritdoc}
237
     */
238 3
    public function getValues()
239
    {
240 3
        return array_values($this->elements);
241
    }
242
243
    /**
244
     * {@inheritdoc}
245
     */
246 5
    public function count()
247
    {
248 5
        return count($this->elements);
249
    }
250
251
    /**
252
     * {@inheritdoc}
253
     */
254 1
    public function isEmpty()
255
    {
256 1
        return empty($this->elements);
257
    }
258
259
    /**
260
     * Required by interface IteratorAggregate.
261
     *
262
     * {@inheritdoc}
263
     */
264 10
    public function getIterator()
265
    {
266 10
        return new ArrayIterator($this->elements);
267
    }
268
269
    /**
270
     * {@inheritdoc}
271
     *
272
     * @return Collection
273
     */
274
    public function map(Closure $func)
275
    {
276
        return new static(array_map($func, $this->elements), $this->type);
277
    }
278
279
    /**
280
     * {@inheritdoc}
281
     *
282
     * @return Collection
283
     */
284
    public function filter(Closure $p)
285
    {
286
        return new static(array_filter($this->elements, $p), $this->type);
287
    }
288
289
    /**
290
     * {@inheritdoc}
291
     */
292 2
    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...
293
    {
294 2
        foreach ($this->elements as $key => $element) {
295 2
            if (!$p($key, $element)) {
296 2
                return false;
297
            }
298
        }
299
300 1
        return true;
301
    }
302
303
    /**
304
     * {@inheritdoc}
305
     *
306
     * @return Collection
0 ignored issues
show
Documentation introduced by
Should the return type not be Collection[]?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
307
     */
308
    public function partition(Closure $p)
309
    {
310
        $matches = $noMatches = array();
311
312
        foreach ($this->elements as $key => $element) {
313
            if ($p($key, $element)) {
314
                $matches[$key] = $element;
315
            } else {
316
                $noMatches[$key] = $element;
317
            }
318
        }
319
320
        return array(new static($matches, $this->type), new static($noMatches, $this->type));
321
    }
322
323
    /**
324
     * {@inheritdoc}
325
     */
326
    public function clear()
327
    {
328
        $this->elements = array();
329
    }
330
331
    /**
332
     * {@inheritdoc}
333
     */
334
    public function slice($offset, $length = null)
335
    {
336
        return array_slice($this->elements, $offset, $length, true);
337
    }
338
339
    /**
340
     * {@inheritdoc}
341
     *
342
     * @return Collection
343
     */
344 1
    public function matching(Criteria $criteria)
345
    {
346 1
        $expr = $criteria->getWhereExpression();
347 1
        $filtered = $this->elements;
348
349 1
        if ($expr) {
350
            $visitor = new ClosureExpressionVisitor();
351
            $filter = $visitor->dispatch($expr);
352
            $filtered = array_filter($filtered, $filter);
353
        }
354
355 1
        if ($orderings = $criteria->getOrderings()) {
356 1
            $next = null;
357 1
            foreach (array_reverse($orderings) as $field => $ordering) {
358 1
                $next = ClosureExpressionVisitor::sortByField($field, $ordering == Criteria::DESC ? -1 : 1);
359
            }
360
361 1
            if (null !== $next) {
362 1
                uasort($filtered, $next);
363
            }
364
        }
365
366 1
        $offset = $criteria->getFirstResult();
367 1
        $length = $criteria->getMaxResults();
368
369 1
        if (null !== $offset || null !== $length) {
370
            $filtered = array_slice($filtered, (int) $offset, $length);
371
        }
372
373 1
        return new static($filtered, $this->type);
374
    }
375
376
    /**
377
     * @throws \LogicException when not collection is untyped or collection type does not contain __toString method
378
     *
379
     * @return Collection
380
     */
381
    public function unique()
382
    {
383 2
        $closure = function ($key, $value) {
384 2
            return is_string($value);
385 2
        };
386
387 2
        if ((null !== $this->type && method_exists($this->type, '__toString')) || $this->forAll($closure)) {
388 1
            $result = new static(array_unique($this->elements, SORT_STRING), $this->type);
389
390 1
            return $result;
391
        }
392
393 1
        throw new \LogicException('Collection instance is not typed, or type has no string support.');
394
    }
395
396
    /**
397
     * @param mixed $value
398
     */
399 2
    public function add($value)
400
    {
401 2
        $this->elements[] = $this->getRealValue($value);
402
403 2
        return true;
404
    }
405
406 1
    public function set($key, $value)
407
    {
408 1
        $this->elements[$key] = $this->getRealValue($value);
409 1
    }
410
411
    /**
412
     * {@inheritdoc}
413
     *
414
     * @return string|array|int
415
     */
416 21
    public function __invoke(int $toType = Type::ARRAY)
417
    {
418 21
        $e = null;
419
        switch ($toType) {
420 21
            case Type::INT:
421 1
                return $this->count();
422 21
            case Type::ARRAY:
423 19
                return $this->elements;
424 4
            case Type::STRING:
425
                try {
426 2
                    return (StringType::valueOf($this))(Type::STRING);
427 1
                } catch (\Throwable $e) {
428 1
                    $e = new \ErrorException($e->getMessage());
429
                }
430
                // Intentionally throwing exception below.
431
            default:
432 3
                throw new InvalidTypeCastException(static::class, $this->getTranslatedType($toType), null, 0, $e);
433
        }
434
    }
435
436
    /**
437
     * @param CollectionInterface $collection
438
     * @param bool                $keepDupes
439
     *
440
     * @return Collection
441
     */
442 2
    public function merge(CollectionInterface $collection, $keepDupes = false): Collection
443
    {
444 2
        if ($keepDupes) {
445 1
            return new self(array_merge($this->toArray(), $collection->toArray()));
446
        }
447
448 1
        return new self($this->toArray() + $collection->toArray());
449
    }
450
451
    /**
452
     * {@inheritdoc}
453
     *
454
     * @return Collection
455
     */
456 4
    public static function valueOf($mixed): Collection
457
    {
458 4
        return new static(self::asArray($mixed));
459
    }
460
461
    /**
462
     * @param string $delimeter
463
     *
464
     * @return StringType
465
     */
466 1
    public function implode(string $delimeter): StringType
467
    {
468 1
        return StringType::create(implode($delimeter, $this->toArray()));
469
    }
470
471
    /**
472
     * Returns a mixed variable as an array.
473
     *
474
     * @param $mixed
475
     *
476
     * @return array
477
     */
478 4
    private static function asArray($mixed): array
479
    {
480 4
        if ($mixed instanceof CollectionInterface) {
481 2
            return $mixed->toArray();
482
        }
483
484 3
        if ($mixed instanceof TypeInterface) {
485 2
            return [$mixed()];
486
        }
487
488 2
        $type = strtolower(gettype($mixed));
489
        switch ($type) {
490 2
            case 'integer':
491 2
            case 'double':
492 2
            case 'float':
493 2
            case 'string':
494 2
            case 'object':
495 2
            case 'resource':
496 2
            case 'boolean':
497 1
                return [$mixed];
498 2
            case 'array':
499 1
                return $mixed;
500
            default:
501 1
                throw new InvalidTransformationException($type, static::class);
502
        }
503
    }
504
505
    /**
506
     * @param $value
507
     *
508
     * @return mixed
509
     */
510 66
    private function getRealValue($value)
511
    {
512 66
        if (null !== $this->type && class_exists($this->type) && !$value instanceof $this->type) {
513 10
            return new $this->type($value);
514
        }
515
516 56
        return $value;
517
    }
518
}
519