Passed
Push — master ( 5ad58b...7269b1 )
by Chris
07:31
created

CollectionKernel::collect()   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 1
Bugs 0 Features 0
Metric Value
eloc 1
c 1
b 0
f 0
dl 0
loc 3
ccs 2
cts 2
cp 1
rs 10
cc 1
nc 1
nop 1
crap 1
1
<?php
2
3
namespace WebTheory\Collection\Kernel;
4
5
use ArrayIterator;
6
use Enum\LoopAction;
7
use IteratorAggregate;
8
use LogicException;
9
use OutOfBoundsException;
10
use Traversable;
11
use WebTheory\Collection\Comparison\HashBasedCollectionComparator;
12
use WebTheory\Collection\Comparison\HashBasedObjectComparator;
13
use WebTheory\Collection\Comparison\PropertyBasedCollectionComparator;
14
use WebTheory\Collection\Comparison\PropertyBasedObjectComparator;
15
use WebTheory\Collection\Comparison\RuntimeIdBasedCollectionComparator;
16
use WebTheory\Collection\Comparison\RuntimeIdBasedObjectComparator;
17
use WebTheory\Collection\Contracts\CollectionComparatorInterface;
18
use WebTheory\Collection\Contracts\CollectionKernelInterface;
19
use WebTheory\Collection\Contracts\CollectionSorterInterface;
20
use WebTheory\Collection\Contracts\ObjectComparatorInterface;
21
use WebTheory\Collection\Contracts\OrderInterface;
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
    protected PropertyBasedSorter $propertyBasedSorter;
39
40
    protected MapBasedSorter $mapBasedSorter;
41
42
    protected PropertyBasedCollectionComparator $propertyBasedCollectionComparator;
43
44
    protected HashBasedCollectionComparator $hashBasedCollectionComparator;
45
46
    protected RuntimeIdBasedCollectionComparator $idBasedCollectionComparator;
47
48
    protected PropertyBasedObjectComparator $propertyBasedObjectComparator;
49
50
    protected HashBasedObjectComparator $hashBasedObjectComparator;
51
52
    protected RuntimeIdBasedObjectComparator $idBasedObjectComparator;
53
54
    /**
55
     * @var callable
56
     */
57
    protected $factory;
58
59 90
    public function __construct(
60
        array $items,
61
        callable $factory,
62
        ?string $identifier = null,
63
        array $accessors = [],
64
        ?bool $map = false
65
    ) {
66 90
        $this->factory = $factory;
67 90
        $this->identifier = $identifier;
68 90
        $this->accessors = $accessors;
0 ignored issues
show
Bug Best Practice introduced by
The property accessors does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
69 90
        $this->map = $map ?? $this->map ?? false;
70
71 90
        $this->propertyResolver = new PropertyResolver($this->accessors);
72
73 90
        $this->propertyBasedSorter = new PropertyBasedSorter($this->propertyResolver);
74 90
        $this->mapBasedSorter = new MapBasedSorter($this->propertyResolver);
75
76 90
        $this->propertyBasedCollectionComparator = new PropertyBasedCollectionComparator($this->propertyResolver);
77 90
        $this->hashBasedCollectionComparator = new HashBasedCollectionComparator($this->propertyResolver);
0 ignored issues
show
Unused Code introduced by
The call to WebTheory\Collection\Com...mparator::__construct() has too many arguments starting with $this->propertyResolver. ( Ignorable by Annotation )

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

77
        $this->hashBasedCollectionComparator = /** @scrutinizer ignore-call */ new HashBasedCollectionComparator($this->propertyResolver);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
78 90
        $this->idBasedCollectionComparator = new RuntimeIdBasedCollectionComparator($this->propertyResolver);
0 ignored issues
show
Unused Code introduced by
The call to WebTheory\Collection\Com...mparator::__construct() has too many arguments starting with $this->propertyResolver. ( Ignorable by Annotation )

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

78
        $this->idBasedCollectionComparator = /** @scrutinizer ignore-call */ new RuntimeIdBasedCollectionComparator($this->propertyResolver);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
79
80 90
        $this->propertyBasedObjectComparator = new PropertyBasedObjectComparator($this->propertyResolver);
81 90
        $this->hashBasedObjectComparator = new HashBasedObjectComparator($this->propertyResolver);
0 ignored issues
show
Unused Code introduced by
The call to WebTheory\Collection\Com...mparator::__construct() has too many arguments starting with $this->propertyResolver. ( Ignorable by Annotation )

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

81
        $this->hashBasedObjectComparator = /** @scrutinizer ignore-call */ new HashBasedObjectComparator($this->propertyResolver);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
82 90
        $this->idBasedObjectComparator = new RuntimeIdBasedObjectComparator($this->propertyResolver);
0 ignored issues
show
Unused Code introduced by
The call to WebTheory\Collection\Com...mparator::__construct() has too many arguments starting with $this->propertyResolver. ( Ignorable by Annotation )

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

82
        $this->idBasedObjectComparator = /** @scrutinizer ignore-call */ new RuntimeIdBasedObjectComparator($this->propertyResolver);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
83
84 90
        array_map([$this, 'add'], $items);
85
    }
86
87
    public function __serialize(): array
88
    {
89
        return $this->toArray();
90
    }
91
92 90
    public function add(object $item): bool
93
    {
94 90
        if ($this->alreadyHasItem($item)) {
95 3
            return false;
96
        }
97
98 90
        $this->isMapped()
99 3
            ? $this->items[$this->resolveValue($item, $this->identifier)] = $item
100 90
            : $this->items[] = $item;
101
102 90
        return true;
103
    }
104
105 3
    public function contains($item): bool
106
    {
107 3
        if (is_object($item)) {
108
            return in_array($item, $this->items, true);
109
        }
110
111 3
        if ($this->isMapped()) {
112
            return isset($this->items[$item]);
113
        }
114
115 3
        if ($this->hasIdentifier()) {
116 3
            return !empty($this->find($this->identifier, $item));
117
        }
118
119
        return false;
120
    }
121
122 9
    public function remove($item): bool
123
    {
124 9
        if (is_object($item)) {
125 6
            $position = array_search($item, $this->items, true);
126
127 6
            unset($this->items[$position]);
128
129 6
            return true;
130
        }
131
132 6
        if ($this->isMapped() && isset($this->items[$item])) {
133 3
            unset($this->items[$item]);
134
135 3
            return true;
136
        }
137
138 3
        if ($this->contains($item)) {
139 3
            return $this->remove($this->findById($item));
140
        }
141
142
        return false;
143
    }
144
145 6
    public function column(string $property): array
146
    {
147 6
        $temp = [];
148
149 6
        foreach ($this->items as $item) {
150 6
            $value = $this->resolveValue($item, $property);
151
152 6
            $temp[] = $value;
153
        }
154
155 6
        return $temp;
156
    }
157
158 3
    public function first(): object
159
    {
160 3
        if (!$this->hasItems()) {
161
            throw new OutOfBoundsException(
162
                "Can't determine first item. Collection is empty"
163
            );
164
        }
165
166 3
        reset($this->items);
167
168 3
        $first = current($this->items);
169
170 3
        return $first;
171
    }
172
173 3
    public function last(): object
174
    {
175 3
        if (!$this->hasItems()) {
176
            throw new OutOfBoundsException(
177
                'Can\'t determine last item. Collection is empty'
178
            );
179
        }
180
181 3
        $last = end($this->items);
182
183 3
        reset($this->items);
184
185 3
        return $last;
186
    }
187
188 9
    public function sortBy(string $property, string $order = OrderInterface::ASC): object
189
    {
190 9
        $sorter = $this->propertyBasedSorter
191 9
            ->setProperty($property);
192
193 9
        return $this->sortWith($sorter, $order);
194
    }
195
196 12
    public function sortMapped(array $map, $order = OrderInterface::ASC, string $property = null): object
197
    {
198 12
        if (!$property ??= $this->identifier) {
199 3
            throw new LogicException(
200
                'Cannot sort by map without property or item identifier set.'
201
            );
202
        }
203
204 9
        $sorter = $this->mapBasedSorter
205 9
            ->setMap($map)
206 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

206
            ->setProperty(/** @scrutinizer ignore-type */ $property);
Loading history...
207
208 9
        return $this->sortWith($sorter, $order);
209
    }
210
211 18
    public function sortWith(CollectionSorterInterface $sorter, $order = OrderInterface::ASC): object
212
    {
213 18
        $collection = clone $this;
214
215 18
        $items = $sorter->sort($collection->items, $order);
216
217 12
        return $this->collect(...$items);
218
    }
219
220
    public function sortCustom(callable $callback): object
221
    {
222
        $collection = clone $this;
223
224
        usort($collection->items, $callback);
225
226
        return $this->collect(...$collection->items);
227
    }
228
229 9
    public function find(string $property, $value): object
230
    {
231 9
        $items = $this->getFilteredItems(
232 9
            fn ($item) => $this->resolveValue($item, $property) === $value
233
        );
234
235 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...
236 6
            return $items[0];
237
        }
238
239 3
        throw new OutOfBoundsException(
240 3
            "Can't find item with {$property} is equal to {$value}."
241
        );
242
    }
243
244 9
    public function findById($id)
245
    {
246 9
        if (!$this->hasIdentifier()) {
247 3
            throw new LogicException(
248 3
                "Use of " . __METHOD__ . " requires an identifier."
249
            );
250
        }
251
252 6
        return $this->find($this->identifier, $id);
253
    }
254
255 3
    public function filter(callable $callback): object
256
    {
257 3
        return $this->collect(...$this->getFilteredItems($callback));
258
    }
259
260 3
    public function strip($property, $values): object
261
    {
262 3
        return $this->filter(
263 3
            fn ($item) => !in_array(
264 3
                $this->resolveValue($item, $property),
265
                $values
266
            )
267
        );
268
    }
269
270
    public function whereEquals(string $property, $value): object
271
    {
272
        return $this->filter(
273
            fn ($item) => $this->resolveValue($item, $property) === $value
274
        );
275
    }
276
277
    // public function whereNotEquals(string $property, $value): object
278
    // {
279
    //     return $this->filter(
280
    //         fn ($item) => $this->resolveValue($item, $property) !== $value
281
    //     );
282
    // }
283
284
    // public function whereGreaterThan(string $property, $value): object
285
    // {
286
    //     return $this->filter(
287
    //         fn ($item) => $this->resolveValue($item, $property) > $value
288
    //     );
289
    // }
290
291
    // public function whereLessThan(string $property, $value): object
292
    // {
293
    //     return $this->filter(
294
    //         fn ($item) => $this->resolveValue($item, $property) < $value
295
    //     );
296
    // }
297
298
    // public function whereGreaterThanOrEquals(string $property, $value): object
299
    // {
300
    //     return $this->filter(
301
    //         fn ($item) => $this->resolveValue($item, $property) >= $value
302
    //     );
303
    // }
304
305
    // public function whereLessThanOrEquals(string $property, $value): object
306
    // {
307
    //     return $this->filter(
308
    //         fn ($item) => $this->resolveValue($item, $property) <= $value
309
    //     );
310
    // }
311
312
    // public function whereIn(string $property, array $values): object
313
    // {
314
    //     return $this->filter(
315
    //         fn ($item) => in_array($this->resolveValue($item, $property), $values)
316
    //     );
317
    // }
318
319
    // public function whereNotIn(string $property, array $values): object
320
    // {
321
    //     return $this->filter(
322
    //         fn ($item) => !in_array($this->resolveValue($item, $property), $values)
323
    //     );
324
    // }
325
326
    public function spawn(callable $callback): object
327
    {
328
        return $this->collect(...$this->map($callback));
329
    }
330
331
    public function map(callable $callback)
332
    {
333
        return array_map($callback, $this->items);
334
    }
335
336
    public function walk(callable $callback): void
337
    {
338
        array_walk($this->items, $callback);
339
    }
340
341
    public function foreach(callable $callback): void
342
    {
343
        foreach ($this->items as $key => $item) {
344
            $action = $callback($item, $key, $this->items);
345
346
            switch ($action) {
347
                case LoopAction::Break():
0 ignored issues
show
Bug introduced by
The method Break() does not exist on Enum\LoopAction. Since you implemented __callStatic, consider adding a @method annotation. ( Ignorable by Annotation )

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

347
                case LoopAction::/** @scrutinizer ignore-call */ Break():
Loading history...
348
                case true:
349
                    break 2;
350
351
                case LoopAction::Continue():
352
                case false:
353
                    continue 2;
354
            }
355
        }
356
    }
357
358
    public function notIn(array $items): object
359
    {
360
        $collection = clone $this;
361
362
        $items = $this->getResolvedCollectionComparator()
363
            ->notIn($collection->items, $items);
364
365
        return $this->collect(...$items);
366
    }
367
368 3
    public function difference(array $items): object
369
    {
370 3
        $collection = clone $this;
371
372 3
        $items = $this->getResolvedCollectionComparator()
373 3
            ->difference($collection->items, $items);
374
375 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

375
        return $this->collect(/** @scrutinizer ignore-type */ ...$items);
Loading history...
376
    }
377
378 3
    public function intersection(array $items): object
379
    {
380 3
        $collection = clone $this;
381
382 3
        $items = $this->getResolvedCollectionComparator()
383 3
            ->intersection($collection->items, $items);
384
385 3
        return $this->collect(...$items);
386
    }
387
388 6
    public function matches(array $items): bool
389
    {
390 6
        return $this->getResolvedCollectionComparator()
391 6
            ->matches($this->items, $items);
392
    }
393
394 3
    public function merge(array ...$collections): object
395
    {
396 3
        $clone = clone $this;
397
398 3
        foreach ($collections as $collection) {
399 3
            foreach ($collection as $value) {
400 3
                $clone->add($value);
401
            }
402
        }
403
404 3
        return $this->collect(...$clone->items);
405
    }
406
407 24
    public function collect(object ...$items): object
408
    {
409 24
        return ($this->factory)(...$items);
410
    }
411
412 24
    public function toArray(): array
413
    {
414 24
        return $this->items;
415
    }
416
417
    public function toJson(): string
418
    {
419
        return json_encode($this->items, JSON_THROW_ON_ERROR | $this->jsonFlags);
420
    }
421
422
    public function jsonSerialize(): mixed
423
    {
424
        return $this->items;
425
    }
426
427 12
    public function hasItems(): bool
428
    {
429 12
        return !empty($this->items);
430
    }
431
432 3
    public function getIterator(): Traversable
433
    {
434 3
        return new ArrayIterator($this->items);
435
    }
436
437 3
    public function count(): int
438
    {
439 3
        return count($this->items);
440
    }
441
442 12
    protected function getFilteredItems(callable $callback): array
443
    {
444 12
        $collection = clone $this;
445
446 12
        return array_values(array_filter($collection->items, $callback));
447
        // return array_merge([], array_filter($collection->items, $callback));
448
    }
449
450 12
    protected function getResolvedCollectionComparator(): CollectionComparatorInterface
451
    {
452 12
        return ($this->hasIdentifier())
453 12
            ? $this->propertyBasedCollectionComparator->setProperty($this->identifier)
454 12
            : $this->idBasedCollectionComparator;
455
    }
456
457 90
    protected function getResolvedObjectComparator(): ObjectComparatorInterface
458
    {
459 90
        return $this->hasIdentifier()
460 90
            ? $this->propertyBasedObjectComparator->setProperty($this->identifier)
461 90
            : $this->idBasedObjectComparator;
462
    }
463
464
    protected function objectsMatch(object $a, object $b): bool
465
    {
466
        return $this->getResolvedObjectComparator()->matches($a, $b);
467
    }
468
469 90
    protected function alreadyHasItem(object $object): bool
470
    {
471 90
        $comparator = $this->getResolvedObjectComparator();
472
473 90
        foreach ($this->items as $item) {
474 90
            if ($comparator->matches($item, $object)) {
475 3
                return true;
476
            }
477
        }
478
479 90
        return false;
480
    }
481
482 21
    protected function resolveValue(object $item, string $property)
483
    {
484 21
        return $this->propertyResolver->resolveProperty($item, $property);
485
    }
486
487 90
    protected function hasIdentifier(): bool
488
    {
489 90
        return !empty($this->identifier);
490
    }
491
492 90
    protected function isMapped(): bool
493
    {
494 90
        return $this->hasIdentifier() && true === $this->map;
495
    }
496
}
497