Passed
Push — master ( 7269b1...e79858 )
by Chris
07:53
created

CollectionKernel   F

Complexity

Total Complexity 78

Size/Duplication

Total Lines 440
Duplicated Lines 0 %

Test Coverage

Coverage 72.82%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 78
eloc 169
dl 0
loc 440
c 1
b 0
f 0
ccs 142
cts 195
cp 0.7282
rs 2.16

43 Methods

Rating   Name   Duplication   Size   Complexity  
A where() 0 7 1
A objectsMatch() 0 3 1
A difference() 0 8 1
A matches() 0 4 1
A sortCustom() 0 7 1
A add() 0 11 3
A __construct() 0 14 1
A isMapped() 0 3 2
A filter() 0 3 1
A hasItems() 0 3 1
A getResolvedObjectComparator() 0 10 2
A sortMapped() 0 13 2
A alreadyHasItem() 0 11 3
A find() 0 12 2
A walk() 0 3 1
A toArray() 0 3 1
A contains() 0 15 4
A intersection() 0 8 1
A jsonSerialize() 0 3 1
A spawn() 0 3 1
A getIterator() 0 3 1
A strip() 0 6 1
A findById() 0 9 2
A sortBy() 0 6 1
A foreach() 0 13 6
A first() 0 13 2
A column() 0 11 2
A notIn() 0 8 1
A map() 0 3 1
A collect() 0 3 1
A getResolvedCollectionComparator() 0 10 2
B itemMeetsCriteria() 0 26 9
A merge() 0 11 3
A hasIdentifier() 0 3 1
A __serialize() 0 3 1
A remove() 0 21 5
A sortWith() 0 7 1
A resolveValue() 0 3 1
A whereEquals() 0 3 1
A count() 0 3 1
A last() 0 13 2
A toJson() 0 3 1
A getFilteredItems() 0 5 1

How to fix   Complexity   

Complex Class

Complex classes like CollectionKernel 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.

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 CollectionKernel, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace WebTheory\Collection\Kernel;
4
5
use ArrayIterator;
6
use IteratorAggregate;
7
use LogicException;
8
use OutOfBoundsException;
9
use Traversable;
10
use WebTheory\Collection\Comparison\HashBasedCollectionComparator;
11
use WebTheory\Collection\Comparison\HashBasedObjectComparator;
12
use WebTheory\Collection\Comparison\PropertyBasedCollectionComparator;
13
use WebTheory\Collection\Comparison\PropertyBasedObjectComparator;
14
use WebTheory\Collection\Comparison\RuntimeIdBasedCollectionComparator;
15
use WebTheory\Collection\Comparison\RuntimeIdBasedObjectComparator;
16
use WebTheory\Collection\Contracts\CollectionComparatorInterface;
17
use WebTheory\Collection\Contracts\CollectionKernelInterface;
18
use WebTheory\Collection\Contracts\CollectionSorterInterface;
19
use WebTheory\Collection\Contracts\ObjectComparatorInterface;
20
use WebTheory\Collection\Contracts\OrderInterface;
21
use WebTheory\Collection\Enum\LoopAction;
22
use WebTheory\Collection\Resolution\PropertyResolver;
23
use WebTheory\Collection\Sorting\MapBasedSorter;
24
use WebTheory\Collection\Sorting\PropertyBasedSorter;
25
26
class CollectionKernel implements CollectionKernelInterface, IteratorAggregate
27
{
28
    protected array $items = [];
29
30
    protected ?string $identifier = null;
31
32
    protected bool $map = false;
33
34
    protected int $jsonFlags;
35
36
    protected PropertyResolver $propertyResolver;
37
38
    /**
39
     * @var callable
40
     */
41
    protected $factory;
42
43 93
    public function __construct(
44
        array $items,
45
        callable $factory,
46
        ?string $identifier = null,
47
        array $accessors = [],
48
        ?bool $map = false
49
    ) {
50 93
        $this->factory = $factory;
51 93
        $this->identifier = $identifier;
52 93
        $this->map = $map ?? $this->map ?? false;
53
54 93
        $this->propertyResolver = new PropertyResolver($accessors);
55
56 93
        array_map([$this, 'add'], $items);
57
    }
58
59
    public function __serialize(): array
60
    {
61
        return $this->toArray();
62
    }
63
64 93
    public function add(object $item): bool
65
    {
66 93
        if ($this->alreadyHasItem($item)) {
67 3
            return false;
68
        }
69
70 93
        $this->isMapped()
71 3
            ? $this->items[$this->resolveValue($item, $this->identifier)] = $item
72 93
            : $this->items[] = $item;
73
74 93
        return true;
75
    }
76
77 3
    public function contains($item): bool
78
    {
79 3
        if (is_object($item)) {
80
            return in_array($item, $this->items, true);
81
        }
82
83 3
        if ($this->isMapped()) {
84
            return isset($this->items[$item]);
85
        }
86
87 3
        if ($this->hasIdentifier()) {
88 3
            return !empty($this->find($this->identifier, $item));
89
        }
90
91
        return false;
92
    }
93
94 9
    public function remove($item): bool
95
    {
96 9
        if (is_object($item)) {
97 6
            $position = array_search($item, $this->items, true);
98
99 6
            unset($this->items[$position]);
100
101 6
            return true;
102
        }
103
104 6
        if ($this->isMapped() && isset($this->items[$item])) {
105 3
            unset($this->items[$item]);
106
107 3
            return true;
108
        }
109
110 3
        if ($this->contains($item)) {
111 3
            return $this->remove($this->findById($item));
112
        }
113
114
        return false;
115
    }
116
117 6
    public function column(string $property): array
118
    {
119 6
        $temp = [];
120
121 6
        foreach ($this->items as $item) {
122 6
            $value = $this->resolveValue($item, $property);
123
124 6
            $temp[] = $value;
125
        }
126
127 6
        return $temp;
128
    }
129
130 3
    public function first(): object
131
    {
132 3
        if (!$this->hasItems()) {
133
            throw new OutOfBoundsException(
134
                "Can't determine first item. Collection is empty"
135
            );
136
        }
137
138 3
        reset($this->items);
139
140 3
        $first = current($this->items);
141
142 3
        return $first;
143
    }
144
145 3
    public function last(): object
146
    {
147 3
        if (!$this->hasItems()) {
148
            throw new OutOfBoundsException(
149
                'Can\'t determine last item. Collection is empty'
150
            );
151
        }
152
153 3
        $last = end($this->items);
154
155 3
        reset($this->items);
156
157 3
        return $last;
158
    }
159
160 9
    public function sortBy(string $property, string $order = OrderInterface::ASC): object
161
    {
162 9
        $sorter = (new PropertyBasedSorter($this->propertyResolver))
163 9
            ->setProperty($property);
164
165 9
        return $this->sortWith($sorter, $order);
166
    }
167
168 12
    public function sortMapped(array $map, $order = OrderInterface::ASC, string $property = null): object
169
    {
170 12
        if (!$property ??= $this->identifier) {
171 3
            throw new LogicException(
172
                'Cannot sort by map without property or item identifier set.'
173
            );
174
        }
175
176 9
        $sorter = (new MapBasedSorter($this->propertyResolver))
177 9
            ->setMap($map)
178 9
            ->setProperty($property);
0 ignored issues
show
Bug introduced by
It seems like $property can also be of type null; however, parameter $property of WebTheory\Collection\Sor...edSorter::setProperty() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

178
            ->setProperty(/** @scrutinizer ignore-type */ $property);
Loading history...
179
180 9
        return $this->sortWith($sorter, $order);
181
    }
182
183 18
    public function sortWith(CollectionSorterInterface $sorter, $order = OrderInterface::ASC): object
184
    {
185 18
        $collection = clone $this;
186
187 18
        $items = $sorter->sort($collection->items, $order);
188
189 12
        return $this->collect(...$items);
190
    }
191
192
    public function sortCustom(callable $callback): object
193
    {
194
        $collection = clone $this;
195
196
        usort($collection->items, $callback);
197
198
        return $this->collect(...$collection->items);
199
    }
200
201 9
    public function find(string $property, $value): object
202
    {
203 9
        $items = $this->getFilteredItems(
204 9
            fn ($item) => $this->resolveValue($item, $property) === $value
205
        );
206
207 9
        if ($items) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $items 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...
208 6
            return $items[0];
209
        }
210
211 3
        throw new OutOfBoundsException(
212 3
            "Can't find item with {$property} is equal to {$value}."
213
        );
214
    }
215
216 9
    public function findById($id)
217
    {
218 9
        if (!$this->hasIdentifier()) {
219 3
            throw new LogicException(
220 3
                "Use of " . __METHOD__ . " requires an identifier."
221
            );
222
        }
223
224 6
        return $this->find($this->identifier, $id);
225
    }
226
227 6
    public function filter(callable $callback): object
228
    {
229 6
        return $this->collect(...$this->getFilteredItems($callback));
230
    }
231
232 3
    public function strip($property, $values): object
233
    {
234 3
        return $this->filter(
235 3
            fn ($item) => !in_array(
236 3
                $this->resolveValue($item, $property),
237
                $values
238
            )
239
        );
240
    }
241
242 3
    public function where(string $property, string $operator, $value): object
243
    {
244 3
        return $this->filter(
245 3
            fn ($item) => $this->itemMeetsCriteria(
246 3
                $this->resolveValue($item, $property),
247
                $operator,
248
                $value
249
            )
250
        );
251
    }
252
253 3
    public function whereEquals(string $property, $value): object
254
    {
255 3
        return $this->where($property, '=', $value);
256
    }
257
258
    public function spawn(callable $callback): object
259
    {
260
        return $this->collect(...$this->map($callback));
261
    }
262
263
    public function map(callable $callback)
264
    {
265
        return array_map($callback, $this->items);
266
    }
267
268
    public function walk(callable $callback): void
269
    {
270
        array_walk($this->items, $callback);
271
    }
272
273
    public function foreach(callable $callback): void
274
    {
275
        foreach ($this->items as $key => $item) {
276
            $action = $callback($item, $key, $this->items);
277
278
            switch ($action) {
279
                case LoopAction::Break():
280
                case true:
281
                    break 2;
282
283
                case LoopAction::Continue():
284
                case false:
285
                    continue 2;
286
            }
287
        }
288
    }
289
290
    public function notIn(array $items): object
291
    {
292
        $collection = clone $this;
293
294
        $items = $this->getResolvedCollectionComparator()
295
            ->notIn($collection->items, $items);
296
297
        return $this->collect(...$items);
298
    }
299
300 3
    public function difference(array $items): object
301
    {
302 3
        $collection = clone $this;
303
304 3
        $items = $this->getResolvedCollectionComparator()
305 3
            ->difference($collection->items, $items);
306
307 3
        return $this->collect(...$items);
0 ignored issues
show
Bug introduced by
$items of type array is incompatible with the type object expected by parameter $items of WebTheory\Collection\Ker...ectionKernel::collect(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

307
        return $this->collect(/** @scrutinizer ignore-type */ ...$items);
Loading history...
308
    }
309
310 3
    public function intersection(array $items): object
311
    {
312 3
        $collection = clone $this;
313
314 3
        $items = $this->getResolvedCollectionComparator()
315 3
            ->intersection($collection->items, $items);
316
317 3
        return $this->collect(...$items);
318
    }
319
320 6
    public function matches(array $items): bool
321
    {
322 6
        return $this->getResolvedCollectionComparator()
323 6
            ->matches($this->items, $items);
324
    }
325
326 3
    public function merge(array ...$collections): object
327
    {
328 3
        $clone = clone $this;
329
330 3
        foreach ($collections as $collection) {
331 3
            foreach ($collection as $value) {
332 3
                $clone->add($value);
333
            }
334
        }
335
336 3
        return $this->collect(...$clone->items);
337
    }
338
339 27
    public function collect(object ...$items): object
340
    {
341 27
        return ($this->factory)(...$items);
342
    }
343
344 24
    public function toArray(): array
345
    {
346 24
        return $this->items;
347
    }
348
349
    public function toJson(): string
350
    {
351
        return json_encode($this->items, JSON_THROW_ON_ERROR | $this->jsonFlags);
352
    }
353
354
    public function jsonSerialize(): mixed
355
    {
356
        return $this->items;
357
    }
358
359 12
    public function hasItems(): bool
360
    {
361 12
        return !empty($this->items);
362
    }
363
364 3
    public function getIterator(): Traversable
365
    {
366 3
        return new ArrayIterator($this->items);
367
    }
368
369 3
    public function count(): int
370
    {
371 3
        return count($this->items);
372
    }
373
374 15
    protected function getFilteredItems(callable $callback): array
375
    {
376 15
        $collection = clone $this;
377
378 15
        return array_values(array_filter($collection->items, $callback));
379
        // return array_merge([], array_filter($collection->items, $callback));
380
    }
381
382 12
    protected function getResolvedCollectionComparator(): CollectionComparatorInterface
383
    {
384 12
        if ($this->hasIdentifier()) {
385 12
            $comparator = new PropertyBasedCollectionComparator($this->propertyResolver);
386 12
            $comparator->setProperty($this->identifier);
387
        } else {
388
            $comparator = new RuntimeIdBasedCollectionComparator();
389
        }
390
391 12
        return $comparator;
392
    }
393
394 93
    protected function getResolvedObjectComparator(): ObjectComparatorInterface
395
    {
396 93
        if ($this->hasIdentifier()) {
397 93
            $comparator = new PropertyBasedObjectComparator($this->propertyResolver);
398 93
            $comparator->setProperty($this->identifier);
399
        } else {
400 18
            $comparator = new RuntimeIdBasedObjectComparator();
401
        }
402
403 93
        return $comparator;
404
    }
405
406
    protected function objectsMatch(object $a, object $b): bool
407
    {
408
        return $this->getResolvedObjectComparator()->matches($a, $b);
409
    }
410
411 93
    protected function alreadyHasItem(object $object): bool
412
    {
413 93
        $comparator = $this->getResolvedObjectComparator();
414
415 93
        foreach ($this->items as $item) {
416 93
            if ($comparator->matches($item, $object)) {
417 3
                return true;
418
            }
419
        }
420
421 93
        return false;
422
    }
423
424 3
    protected function itemMeetsCriteria($resolved, string $operator, $value): bool
425
    {
426 3
        switch ($operator) {
427 3
            case '=':
428 3
                return $resolved === $value;
429
430
            case '!=':
431
                return $resolved !== $value;
432
433
            case '>':
434
                return $resolved > $value;
435
436
            case '<':
437
                return $resolved < $value;
438
439
            case '>=':
440
                return $resolved >= $value;
441
442
            case '<=':
443
                return $resolved <= $value;
444
445
            case 'in':
446
                return in_array($resolved, $value);
447
448
            case 'not in':
0 ignored issues
show
introduced by
The function implicitly returns null when this case condition does not match. This is incompatible with the type-hinted return boolean. Consider adding a default case to the switch.
Loading history...
449
                return !in_array($resolved, $value);
450
        }
451
    }
452
453 24
    protected function resolveValue(object $item, string $property)
454
    {
455 24
        return $this->propertyResolver->resolveProperty($item, $property);
456
    }
457
458 93
    protected function hasIdentifier(): bool
459
    {
460 93
        return !empty($this->identifier);
461
    }
462
463 93
    protected function isMapped(): bool
464
    {
465 93
        return $this->hasIdentifier() && true === $this->map;
466
    }
467
}
468