Passed
Push — master ( e79858...61a477 )
by Chris
07:44
created

CollectionKernel::getItemsWhere()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 2
c 0
b 0
f 0
dl 0
loc 4
ccs 3
cts 3
cp 1
rs 10
cc 1
nc 1
nop 3
crap 1
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\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\ObjectComparatorInterface;
20
use WebTheory\Collection\Contracts\OrderInterface;
21
use WebTheory\Collection\Enum\LoopAction;
22
use WebTheory\Collection\Json\BasicJsonSerializer;
23
use WebTheory\Collection\Query\BasicQuery;
24
use WebTheory\Collection\Resolution\PropertyResolver;
25
use WebTheory\Collection\Sorting\MapBasedSorter;
26
use WebTheory\Collection\Sorting\PropertyBasedSorter;
27
28
class CollectionKernel implements CollectionKernelInterface, IteratorAggregate
29
{
30
    protected array $items = [];
31
32
    /**
33
     * @var callable
34
     */
35
    protected $generator;
36
37
    protected ?string $identifier = null;
38
39
    protected bool $map = false;
40
41
    protected JsonSerializerInterface $jsonSerializer;
42
43
    protected PropertyResolver $propertyResolver;
44
45 108
    public function __construct(
46
        array $items,
47
        callable $generator,
48
        ?string $identifier = null,
49
        array $accessors = [],
50
        ?bool $map = false,
51
        ?JsonSerializerInterface $jsonSerializer = null
52
    ) {
53 108
        $this->generator = $generator;
54 108
        $this->identifier = $identifier;
55
56 108
        $this->map = $map ?? $this->map ?? false;
57 108
        $this->jsonSerializer = $jsonSerializer ?? new BasicJsonSerializer();
58
59 108
        $this->propertyResolver = new PropertyResolver($accessors);
60
61 108
        array_map([$this, 'add'], $items);
62
    }
63
64
    public function __serialize(): array
65
    {
66
        return $this->toArray();
67
    }
68
69 3
    public function collect(object ...$items): void
70
    {
71 3
        foreach ($items as $item) {
72 3
            $this->add($item);
73
        }
74
    }
75
76 108
    public function add(object $item): bool
77
    {
78 108
        if ($this->alreadyHasItem($item)) {
79 3
            return false;
80
        }
81
82 108
        $this->isMapped()
83 3
            ? $this->items[$this->resolveValue($item, $this->identifier)] = $item
84 108
            : $this->items[] = $item;
85
86 108
        return true;
87
    }
88
89 9
    public function remove($item): bool
90
    {
91 9
        if (is_object($item)) {
92 6
            $position = array_search($item, $this->items, true);
93
94 6
            unset($this->items[$position]);
95
96 6
            return true;
97
        }
98
99 6
        if ($this->isMapped() && isset($this->items[$item])) {
100 3
            unset($this->items[$item]);
101
102 3
            return true;
103
        }
104
105 3
        if ($this->contains($item)) {
106 3
            return $this->remove($this->findById($item));
107
        }
108
109
        return false;
110
    }
111
112 3
    public function contains($item): bool
113
    {
114 3
        if (is_object($item)) {
115
            return in_array($item, $this->items, true);
116
        }
117
118 3
        if ($this->isMapped()) {
119
            return isset($this->items[$item]);
120
        }
121
122 3
        if ($this->hasIdentifier()) {
123 3
            return !empty($this->find($this->identifier, $item));
124
        }
125
126
        return false;
127
    }
128
129 21
    public function sortWith(CollectionSorterInterface $sorter, $order = OrderInterface::ASC): object
130
    {
131 21
        return $this->spawnFrom(...$sorter->sort($this->items, $order));
132
    }
133
134 9
    public function sortBy(string $property, string $order = OrderInterface::ASC): object
135
    {
136 9
        return $this->sortWith(
137 9
            new PropertyBasedSorter($this->propertyResolver, $property),
138
            $order
139
        );
140
    }
141
142 15
    public function sortMapped(array $map, $order = OrderInterface::ASC, string $property = null): object
143
    {
144 15
        if (!$property ??= $this->identifier) {
145 3
            throw new LogicException(
146
                'Cannot sort by map without property or item identifier set.'
147
            );
148
        }
149
150 12
        return $this->sortWith(
151 12
            new MapBasedSorter($this->propertyResolver, $property, $map),
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::__construct() 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

151
            new MapBasedSorter($this->propertyResolver, /** @scrutinizer ignore-type */ $property, $map),
Loading history...
152
            $order
153
        );
154
    }
155
156
    public function sortCustom(callable $callback): object
157
    {
158
        $clone = clone $this;
159
160
        usort($clone->items, $callback);
161
162
        return $this->spawn($clone);
163
    }
164
165 3
    public function first(): object
166
    {
167 3
        return reset($this->items);
168
    }
169
170 3
    public function last(): object
171
    {
172 3
        $last = end($this->items);
173
174 3
        reset($this->items);
175
176 3
        return $last;
177
    }
178
179 9
    public function find(string $property, $value): object
180
    {
181 9
        $items = $this->getItemsWhere($property, '=', $value);
182
183 9
        if (!empty($items)) {
184 6
            return $items[0];
185
        }
186
187 3
        throw new OutOfBoundsException(
188 3
            "Can't find item where {$property} is equal to {$value}."
189
        );
190
    }
191
192 9
    public function findById($id)
193
    {
194 9
        if (!$this->hasIdentifier()) {
195 3
            throw new LogicException(
196 3
                "Use of " . __METHOD__ . " requires an identifier."
197
            );
198
        }
199
200 6
        return $this->find($this->identifier, $id);
201
    }
202
203 3
    public function filter(callable $callback): object
204
    {
205 3
        return $this->spawnFrom(
206 3
            ...array_values(array_filter($this->items, $callback))
207
        );
208
    }
209
210 15
    public function query(CollectionQueryInterface $query): object
211
    {
212 15
        return $this->spawnFrom(...$query->query($this->items));
213
    }
214
215 12
    public function where(string $property, string $operator, $value): object
216
    {
217 12
        return $this->query($this->getBasicQuery($property, $operator, $value));
218
    }
219
220 6
    public function whereEquals(string $property, $value): object
221
    {
222 6
        return $this->where($property, '=', $value);
223
    }
224
225 3
    public function whereNotIn($property, $values): object
226
    {
227 3
        return $this->where($property, 'not in', $values);
228
    }
229
230 6
    public function column(string $property): array
231
    {
232 6
        return array_map(
233 6
            fn ($item) => $this->resolveValue($item, $property),
234 6
            $this->items
235
        );
236
    }
237
238
    public function map(callable $callback)
239
    {
240
        return array_map($callback, $this->items);
241
    }
242
243
    public function walk(callable $callback): void
244
    {
245
        array_walk($this->items, $callback);
246
    }
247
248
    public function foreach(callable $callback): void
249
    {
250
        foreach ($this->items as $key => $item) {
251
            $action = $callback($item, $key, $this->items);
252
253
            if ($action instanceof LoopAction) {
254
                switch ($action->getValue()) {
255
                    case LoopAction::Break:
256
                        break 2;
257
258
                    case LoopAction::Continue:
259
                        continue 2;
260
                }
261
            }
262
        }
263
    }
264
265
    public function notIn(array $items): object
266
    {
267
        $clone = clone $this;
268
269
        $clone->items = $this->getResolvedCollectionComparator()
270
            ->notIn($clone->items, $items);
271
272
        return $this->spawn($clone);
273
    }
274
275 3
    public function difference(array $items): object
276
    {
277 3
        $clone = clone $this;
278
279 3
        $clone->items = $this->getResolvedCollectionComparator()
280 3
            ->difference($clone->items, $items);
281
282 3
        return $this->spawn($clone);
283
    }
284
285 3
    public function intersection(array $items): object
286
    {
287 3
        $clone = clone $this;
288
289 3
        $clone->items = $this->getResolvedCollectionComparator()
290 3
            ->intersection($clone->items, $items);
291
292 3
        return $this->spawn($clone);
293
    }
294
295 6
    public function matches(array $items): bool
296
    {
297 6
        return $this->getResolvedCollectionComparator()
298 6
            ->matches($this->items, $items);
299
    }
300
301 3
    public function merge(array ...$collections): object
302
    {
303 3
        $clone = clone $this;
304
305 3
        foreach ($collections as $collection) {
306 3
            $clone->collect(...$collection);
307
        }
308
309 3
        return $this->spawn($clone);
310
    }
311
312 66
    public function toArray(): array
313
    {
314 66
        return $this->items;
315
    }
316
317
    public function toJson(): string
318
    {
319
        return $this->jsonSerializer->serialize($this->items);
320
    }
321
322
    public function jsonSerialize(): mixed
323
    {
324
        return $this->items;
325
    }
326
327 6
    public function hasItems(): bool
328
    {
329 6
        return !empty($this->items);
330
    }
331
332 3
    public function getIterator(): Traversable
333
    {
334 3
        return new ArrayIterator($this->items);
335
    }
336
337 3
    public function count(): int
338
    {
339 3
        return count($this->items);
340
    }
341
342 12
    protected function getResolvedCollectionComparator(): CollectionComparatorInterface
343
    {
344 12
        if ($this->hasIdentifier()) {
345 12
            $comparator = new PropertyBasedCollectionComparator($this->propertyResolver);
346 12
            $comparator->setProperty($this->identifier);
347
        } else {
348
            $comparator = new RuntimeIdBasedCollectionComparator();
349
        }
350
351 12
        return $comparator;
352
    }
353
354 108
    protected function getResolvedObjectComparator(): ObjectComparatorInterface
355
    {
356 108
        if ($this->hasIdentifier()) {
357 108
            $comparator = new PropertyBasedObjectComparator($this->propertyResolver);
358 108
            $comparator->setProperty($this->identifier);
359
        } else {
360 18
            $comparator = new RuntimeIdBasedObjectComparator();
361
        }
362
363 108
        return $comparator;
364
    }
365
366 21
    protected function getBasicQuery(string $property, string $operator, $value): BasicQuery
367
    {
368 21
        return new BasicQuery($this->propertyResolver, $property, $operator, $value);
369
    }
370
371 9
    protected function getItemsWhere(string $property, string $operator, $value): array
372
    {
373 9
        return $this->getBasicQuery($property, $operator, $value)
374 9
            ->query($this->items);
375
    }
376
377
    protected function objectsMatch(object $a, object $b): bool
378
    {
379
        return $this->getResolvedObjectComparator()->matches($a, $b);
380
    }
381
382 108
    protected function alreadyHasItem(object $object): bool
383
    {
384 108
        $comparator = $this->getResolvedObjectComparator();
385
386 108
        foreach ($this->items as $item) {
387 108
            if ($comparator->matches($item, $object)) {
388 3
                return true;
389
            }
390
        }
391
392 108
        return false;
393
    }
394
395 9
    protected function resolveValue(object $item, string $property)
396
    {
397 9
        return $this->propertyResolver->resolveProperty($item, $property);
398
    }
399
400 108
    protected function hasIdentifier(): bool
401
    {
402 108
        return !empty($this->identifier);
403
    }
404
405 108
    protected function isMapped(): bool
406
    {
407 108
        return $this->hasIdentifier() && true === $this->map;
408
    }
409
410 42
    protected function spawn(self $clone): object
411
    {
412 42
        return ($this->generator)($clone);
413
    }
414
415 33
    protected function spawnFrom(object ...$items): object
416
    {
417 33
        $clone = clone $this;
418
419 33
        $clone->items = $items;
420
421 33
        return $this->spawn($clone);
422
    }
423
}
424