Passed
Push — master ( 4f5a04...f7b3b1 )
by Chris
07:47
created

CollectionKernel::loop()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 1
c 0
b 0
f 0
dl 0
loc 3
ccs 2
cts 2
cp 1
rs 10
cc 1
nc 1
nop 2
crap 1
1
<?php
2
3
namespace WebTheory\Collection\Kernel;
4
5
use ArrayIterator;
6
use IteratorAggregate;
7
use OutOfBoundsException;
8
use Traversable;
9
use WebTheory\Collection\Comparison\PropertyBasedCollectionComparator;
10
use WebTheory\Collection\Comparison\PropertyBasedObjectComparator;
11
use WebTheory\Collection\Comparison\RuntimeIdBasedCollectionComparator;
12
use WebTheory\Collection\Comparison\RuntimeIdBasedObjectComparator;
13
use WebTheory\Collection\Contracts\CollectionComparatorInterface;
14
use WebTheory\Collection\Contracts\CollectionKernelInterface;
15
use WebTheory\Collection\Contracts\CollectionQueryInterface;
16
use WebTheory\Collection\Contracts\CollectionSorterInterface;
17
use WebTheory\Collection\Contracts\JsonSerializerInterface;
18
use WebTheory\Collection\Contracts\LoopInterface;
19
use WebTheory\Collection\Contracts\ObjectComparatorInterface;
20
use WebTheory\Collection\Contracts\OperationProviderInterface;
21
use WebTheory\Collection\Contracts\PropertyResolverInterface;
22
use WebTheory\Collection\Enum\Order;
23
use WebTheory\Collection\Iteration\ForeachLoop;
24
use WebTheory\Collection\Json\BasicJsonSerializer;
25
use WebTheory\Collection\Query\BasicQuery;
26
use WebTheory\Collection\Query\Operation\Operations;
27
use WebTheory\Collection\Resolution\PropertyResolver;
28
use WebTheory\Collection\Sorting\MapBasedSorter;
29
use WebTheory\Collection\Sorting\PropertyBasedSorter;
30
31
class CollectionKernel implements CollectionKernelInterface, IteratorAggregate
32
{
33
    /**
34
     * Array of objects to be operated on.
35
     *
36
     * @var array<int,object>|array<string,object>
37
     */
38
    protected array $items = [];
39
40
    /**
41
     * Callback function to create a new instance of the interfacing collection.
42
     *
43
     * @var callable
44
     */
45
    protected $generator;
46
47
    /**
48
     * Property to use as primary identifier for items in the collection.
49
     */
50
    protected ?string $identifier = null;
51
52
    /**
53
     * Whether or not to map the identifier to items in the collection.
54
     */
55
    protected bool $map = false;
56
57
    protected PropertyResolverInterface $propertyResolver;
58
59
    protected OperationProviderInterface $operationProvider;
60
61
    protected JsonSerializerInterface $jsonSerializer;
62
63 192
    public function __construct(
64
        array $items,
65
        callable $generator,
66
        ?string $identifier = null,
67
        array $accessors = [],
68
        ?bool $map = false,
69
        ?JsonSerializerInterface $jsonSerializer = null,
70
        ?OperationProviderInterface $operationProvider = null
71
    ) {
72 192
        $this->generator = $generator;
73 192
        $this->identifier = $identifier;
74
75 192
        $this->map = $map ?? $this->map ?? false;
76 192
        $this->jsonSerializer = $jsonSerializer ?? new BasicJsonSerializer();
77 192
        $this->operationProvider = $operationProvider ?? new Operations();
78
79 192
        $this->propertyResolver = new PropertyResolver($accessors);
80
81 192
        $this->collect(...$items);
82
    }
83
84
    public function __serialize(): array
85
    {
86
        return $this->toArray();
87
    }
88
89 192
    public function collect(object ...$items): void
90
    {
91 192
        foreach ($items as $item) {
92 192
            $this->add($item);
93
        }
94
    }
95
96 192
    public function add(object $item): bool
97
    {
98 192
        if ($this->alreadyHasItem($item)) {
99 15
            return false;
100
        }
101
102 192
        $this->isMapped()
103 27
            ? $this->items[$this->getPropertyValue($item, $this->identifier)] = $item
104 192
            : $this->items[] = $item;
105
106 192
        return true;
107
    }
108
109 9
    public function remove($item): bool
110
    {
111 9
        if (is_object($item)) {
112 6
            $position = array_search($item, $this->items, true);
113
114 6
            unset($this->items[$position]);
115
116 6
            return true;
117
        }
118
119 6
        if ($this->isMapped() && isset($this->items[$item])) {
120 3
            unset($this->items[$item]);
121
122 3
            return true;
123
        }
124
125 3
        if ($this->contains($item)) {
126 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

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

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