Passed
Push — master ( f7b3b1...ee6571 )
by Chris
09:41
created

CollectionKernel   D

Complexity

Total Complexity 59

Size/Duplication

Total Lines 371
Duplicated Lines 0 %

Test Coverage

Coverage 91.95%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 59
eloc 122
c 1
b 0
f 0
dl 0
loc 371
ccs 137
cts 149
cp 0.9195
rs 4.08

43 Methods

Rating   Name   Duplication   Size   Complexity  
A getItemsWhere() 0 4 1
A objectsMatch() 0 3 1
A where() 0 3 1
A matches() 0 3 1
A spawnFrom() 0 8 1
A sortCustom() 0 7 1
A diff() 0 4 1
A add() 0 11 3
A __construct() 0 19 1
A isMapped() 0 3 2
A hasItems() 0 3 1
A filter() 0 4 1
A contrast() 0 4 1
A spawnWith() 0 3 1
A sortMapped() 0 5 1
A loop() 0 3 1
A alreadyHasItem() 0 11 3
A walk() 0 3 1
A toArray() 0 3 1
A contains() 0 15 4
A findBy() 0 10 2
A jsonSerialize() 0 4 1
A getIterator() 0 3 1
A sortBy() 0 5 1
A foreach() 0 3 1
A first() 0 3 1
A column() 0 5 1
A map() 0 3 1
A intersect() 0 4 1
A collect() 0 4 2
A merge() 0 4 1
A getCollectionComparator() 0 10 2
A hasIdentifier() 0 3 1
A __serialize() 0 3 1
A remove() 0 21 5
A sortWith() 0 3 1
A getObjectComparator() 0 10 2
A getPropertyValue() 0 3 1
A count() 0 3 1
A last() 0 7 1
A getBasicQuery() 0 8 1
A toJson() 0 3 1
A query() 0 3 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 OutOfBoundsException;
8
use ReturnTypeWillChange;
9
use Traversable;
10
use WebTheory\Collection\Comparison\PropertyBasedCollectionComparator;
11
use WebTheory\Collection\Comparison\PropertyBasedObjectComparator;
12
use WebTheory\Collection\Comparison\RuntimeIdBasedCollectionComparator;
13
use WebTheory\Collection\Comparison\RuntimeIdBasedObjectComparator;
14
use WebTheory\Collection\Contracts\CollectionComparatorInterface;
15
use WebTheory\Collection\Contracts\CollectionKernelInterface;
16
use WebTheory\Collection\Contracts\CollectionQueryInterface;
17
use WebTheory\Collection\Contracts\CollectionSorterInterface;
18
use WebTheory\Collection\Contracts\JsonSerializerInterface;
19
use WebTheory\Collection\Contracts\LoopInterface;
20
use WebTheory\Collection\Contracts\ObjectComparatorInterface;
21
use WebTheory\Collection\Contracts\OperationProviderInterface;
22
use WebTheory\Collection\Contracts\PropertyResolverInterface;
23
use WebTheory\Collection\Enum\Order;
24
use WebTheory\Collection\Iteration\ForeachLoop;
25
use WebTheory\Collection\Json\BasicJsonSerializer;
26
use WebTheory\Collection\Query\BasicQuery;
27
use WebTheory\Collection\Query\Operation\Operations;
28
use WebTheory\Collection\Resolution\PropertyResolver;
29
use WebTheory\Collection\Sorting\MapBasedSorter;
30
use WebTheory\Collection\Sorting\PropertyBasedSorter;
31
32
class CollectionKernel implements CollectionKernelInterface, IteratorAggregate
33
{
34
    /**
35
     * Array of objects to be operated on.
36
     *
37
     * @var array<int,object>|array<string,object>
38
     */
39
    protected array $items = [];
40
41
    /**
42
     * Callback function to create a new instance of the interfacing collection.
43
     *
44
     * @var callable
45
     */
46
    protected $generator;
47
48
    /**
49
     * Property to use as primary identifier for items in the collection.
50
     */
51
    protected ?string $identifier = null;
52
53
    /**
54
     * Whether or not to map the identifier to items in the collection.
55
     */
56
    protected bool $map = false;
57
58
    protected PropertyResolverInterface $propertyResolver;
59
60
    protected OperationProviderInterface $operationProvider;
61
62
    protected JsonSerializerInterface $jsonSerializer;
63
64 198
    public function __construct(
65
        array $items,
66
        callable $generator,
67
        ?string $identifier = null,
68
        array $accessors = [],
69
        ?bool $map = false,
70
        ?JsonSerializerInterface $jsonSerializer = null,
71
        ?OperationProviderInterface $operationProvider = null
72
    ) {
73 198
        $this->generator = $generator;
74 198
        $this->identifier = $identifier;
75
76 198
        $this->map = $map ?? $this->map ?? false;
77 198
        $this->jsonSerializer = $jsonSerializer ?? new BasicJsonSerializer();
78 198
        $this->operationProvider = $operationProvider ?? new Operations();
79
80 198
        $this->propertyResolver = new PropertyResolver($accessors);
81
82 198
        $this->collect(...$items);
83
    }
84
85
    public function __serialize(): array
86
    {
87
        return $this->toArray();
88
    }
89
90 198
    public function collect(object ...$items): void
91
    {
92 198
        foreach ($items as $item) {
93 198
            $this->add($item);
94
        }
95
    }
96
97 198
    public function add(object $item): bool
98
    {
99 198
        if ($this->alreadyHasItem($item)) {
100 9
            return false;
101
        }
102
103 198
        $this->isMapped()
104 27
            ? $this->items[$this->getPropertyValue($item, $this->identifier)] = $item
105 198
            : $this->items[] = $item;
106
107 198
        return true;
108
    }
109
110 9
    public function remove($item): bool
111
    {
112 9
        if (is_object($item)) {
113 6
            $position = array_search($item, $this->items, true);
114
115 6
            unset($this->items[$position]);
116
117 6
            return true;
118
        }
119
120 6
        if ($this->isMapped() && isset($this->items[$item])) {
121 3
            unset($this->items[$item]);
122
123 3
            return true;
124
        }
125
126 3
        if ($this->contains($item)) {
127 3
            return $this->remove($this->findBy($this->identifier, $item));
0 ignored issues
show
Bug introduced by
It seems like $this->identifier can also be of type null; however, parameter $property of WebTheory\Collection\Ker...lectionKernel::findBy() 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

127
            return $this->remove($this->findBy(/** @scrutinizer ignore-type */ $this->identifier, $item));
Loading history...
128
        }
129
130
        return false;
131
    }
132
133 3
    public function contains($item): bool
134
    {
135 3
        if (is_object($item)) {
136
            return in_array($item, $this->items, true);
137
        }
138
139 3
        if ($this->isMapped()) {
140
            return isset($this->items[$item]);
141
        }
142
143 3
        if ($this->hasIdentifier()) {
144 3
            return !empty($this->findBy($this->identifier, $item));
145
        }
146
147
        return false;
148
    }
149
150 3
    public function first(): object
151
    {
152 3
        return reset($this->items);
153
    }
154
155 3
    public function last(): object
156
    {
157 3
        $last = end($this->items);
158
159 3
        reset($this->items);
160
161 3
        return $last;
162
    }
163
164 6
    public function hasItems(): bool
165
    {
166 6
        return !empty($this->items);
167
    }
168
169 6
    public function column(string $property): array
170
    {
171 6
        return array_map(
172 6
            fn ($item) => $this->getPropertyValue($item, $property),
173 6
            $this->items
174
        );
175
    }
176
177 9
    public function findBy(string $property, $value): object
178
    {
179 9
        $items = $this->getItemsWhere($property, '=', $value);
180
181 9
        if (!empty($items)) {
182 6
            return $items[0];
183
        }
184
185 3
        throw new OutOfBoundsException(
186 3
            "Cannot find item where {$property} is equal to {$value}."
187
        );
188
    }
189
190 30
    public function query(CollectionQueryInterface $query): object
191
    {
192 30
        return $this->spawnFrom(...$query->query($this->items));
193
    }
194
195 18
    public function where(string $property, string $operator, $value): object
196
    {
197 18
        return $this->query($this->getBasicQuery($property, $operator, $value));
198
    }
199
200 12
    public function filter(callable $callback): object
201
    {
202 12
        return $this->spawnFrom(
203 12
            ...array_values(array_filter($this->items, $callback))
204
        );
205
    }
206
207 6
    public function matches(array $collection): bool
208
    {
209 6
        return $this->getCollectionComparator()->matches($this->items, $collection);
210
    }
211
212 12
    public function diff(array $collection): object
213
    {
214 12
        return $this->spawnFrom(
215 12
            ...$this->getCollectionComparator()->diff($this->items, $collection)
216
        );
217
    }
218
219 15
    public function contrast(array $collection): object
220
    {
221 15
        return $this->spawnFrom(
222 15
            ...$this->getCollectionComparator()->contrast($this->items, $collection)
0 ignored issues
show
Bug introduced by
$this->getCollectionComp...is->items, $collection) of type array is incompatible with the type object expected by parameter $items of WebTheory\Collection\Ker...tionKernel::spawnFrom(). ( Ignorable by Annotation )

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

222
            /** @scrutinizer ignore-type */ ...$this->getCollectionComparator()->contrast($this->items, $collection)
Loading history...
223
        );
224
    }
225
226 15
    public function intersect(array $collection): object
227
    {
228 15
        return $this->spawnFrom(
229 15
            ...$this->getCollectionComparator()->intersect($this->items, $collection)
230
        );
231
    }
232
233 3
    public function merge(array ...$collections): object
234
    {
235 3
        return $this->spawnFrom(
236 3
            ...array_merge($this->items, ...array_values($collections))
237
        );
238
    }
239
240 42
    public function sortWith(CollectionSorterInterface $sorter, string $order = Order::Asc): object
241
    {
242 42
        return $this->spawnFrom(...$sorter->sort($this->items, $order));
243
    }
244
245 21
    public function sortBy(string $property, string $order = Order::Asc): object
246
    {
247 21
        return $this->sortWith(
248 21
            new PropertyBasedSorter($this->propertyResolver, $property),
249
            $order
250
        );
251
    }
252
253 21
    public function sortMapped(array $map, string $property, string $order = Order::Asc): object
254
    {
255 21
        return $this->sortWith(
256 21
            new MapBasedSorter($this->propertyResolver, $property, $map),
257
            $order
258
        );
259
    }
260
261
    public function sortCustom(callable $callback): object
262
    {
263
        $clone = clone $this;
264
265
        usort($clone->items, $callback);
266
267
        return $this->spawnWith($clone);
268
    }
269
270 3
    public function map(callable $callback): array
271
    {
272 3
        return array_map($callback, $this->items);
273
    }
274
275 3
    public function walk(callable $callback): void
276
    {
277 3
        array_walk($this->items, $callback);
278
    }
279
280 3
    public function loop(LoopInterface $loop, callable $callback): void
281
    {
282 3
        $loop->iterate($this->items, $callback);
283
    }
284
285 3
    public function foreach(callable $callback): void
286
    {
287 3
        $this->loop(new ForeachLoop(), $callback);
288
    }
289
290 147
    public function toArray(): array
291
    {
292 147
        return $this->items;
293
    }
294
295 3
    public function toJson(): string
296
    {
297 3
        return $this->jsonSerializer->serialize($this->items);
298
    }
299
300 3
    public function count(): int
301
    {
302 3
        return count($this->items);
303
    }
304
305 2
    #[ReturnTypeWillChange]
306 1
    public function jsonSerialize()
307
    {
308 3
        return $this->items;
309
    }
310
311 3
    public function getIterator(): Traversable
312
    {
313 3
        return new ArrayIterator($this->items);
314
    }
315
316 48
    protected function getCollectionComparator(): CollectionComparatorInterface
317
    {
318 48
        if ($this->hasIdentifier()) {
319 30
            $comparator = new PropertyBasedCollectionComparator($this->propertyResolver);
320 30
            $comparator->setProperty($this->identifier);
321
        } else {
322 18
            $comparator = new RuntimeIdBasedCollectionComparator();
323
        }
324
325 48
        return $comparator;
326
    }
327
328 198
    protected function getObjectComparator(): ObjectComparatorInterface
329
    {
330 198
        if ($this->hasIdentifier()) {
331 198
            $comparator = new PropertyBasedObjectComparator($this->propertyResolver);
332 198
            $comparator->setProperty($this->identifier);
333
        } else {
334 69
            $comparator = new RuntimeIdBasedObjectComparator();
335
        }
336
337 198
        return $comparator;
338
    }
339
340 27
    protected function getBasicQuery(string $property, string $operator, $value): CollectionQueryInterface
341
    {
342 27
        return new BasicQuery(
343 27
            $this->propertyResolver,
344
            $property,
345
            $operator,
346
            $value,
347 27
            $this->operationProvider
348
        );
349
    }
350
351
    protected function objectsMatch(object $a, object $b): bool
352
    {
353
        return $this->getObjectComparator()->matches($a, $b);
354
    }
355
356 198
    protected function alreadyHasItem(object $object): bool
357
    {
358 198
        $comparator = $this->getObjectComparator();
359
360 198
        foreach ($this->items as $item) {
361 198
            if ($comparator->matches($item, $object)) {
362 9
                return true;
363
            }
364
        }
365
366 198
        return false;
367
    }
368
369 33
    protected function getPropertyValue(object $item, string $property)
370
    {
371 33
        return $this->propertyResolver->resolveProperty($item, $property);
372
    }
373
374 9
    protected function getItemsWhere(string $property, string $operator, $value): array
375
    {
376 9
        return $this->getBasicQuery($property, $operator, $value)
377 9
            ->query($this->items);
378
    }
379
380 198
    protected function hasIdentifier(): bool
381
    {
382 198
        return !empty($this->identifier);
383
    }
384
385 198
    protected function isMapped(): bool
386
    {
387 198
        return $this->hasIdentifier() && true === $this->map;
388
    }
389
390 123
    protected function spawnWith(self $clone): object
391
    {
392 123
        return ($this->generator)($clone);
393
    }
394
395 123
    protected function spawnFrom(object ...$items): object
396
    {
397 123
        $clone = clone $this;
398
399 123
        $clone->items = [];
400 123
        $clone->collect(...$items);
401
402 123
        return $this->spawnWith($clone);
403
    }
404
}
405