Passed
Push — master ( 61a477...8a3759 )
by Chris
07:43
created

CollectionKernel::getPropertyValue()   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 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\OperationProviderInterface;
21
use WebTheory\Collection\Contracts\OrderInterface;
22
use WebTheory\Collection\Contracts\PropertyResolverInterface;
23
use WebTheory\Collection\Enum\LoopAction;
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
    protected array $items = [];
34
35
    /**
36
     * Callback function to create a new instance of the interfacing collection.
37
     *
38
     * @var callable
39
     */
40
    protected $generator;
41
42
    /**
43
     * Property to use as primary identifier for items in the collection.
44
     */
45
    protected ?string $identifier = null;
46
47
    /**
48
     * Whether or not to map the identifier to items in the collection.
49
     */
50
    protected bool $map = false;
51
52
    protected PropertyResolverInterface $propertyResolver;
53
54
    protected OperationProviderInterface $operationProvider;
55
56
    protected JsonSerializerInterface $jsonSerializer;
57
58 165
    public function __construct(
59
        array $items,
60
        callable $generator,
61
        ?string $identifier = null,
62
        array $accessors = [],
63
        ?bool $map = false,
64
        ?JsonSerializerInterface $jsonSerializer = null,
65
        ?OperationProviderInterface $operationProvider = null
66
    ) {
67 165
        $this->generator = $generator;
68 165
        $this->identifier = $identifier;
69
70 165
        $this->map = $map ?? $this->map ?? false;
71 165
        $this->jsonSerializer = $jsonSerializer ?? new BasicJsonSerializer();
72 165
        $this->operationProvider = $operationProvider ?? new Operations();
73
74 165
        $this->propertyResolver = new PropertyResolver($accessors);
75
76 165
        array_map([$this, 'add'], $items);
77
    }
78
79
    public function __serialize(): array
80
    {
81
        return $this->toArray();
82
    }
83
84 3
    public function collect(object ...$items): void
85
    {
86 3
        foreach ($items as $item) {
87 3
            $this->add($item);
88
        }
89
    }
90
91 165
    public function add(object $item): bool
92
    {
93 165
        if ($this->alreadyHasItem($item)) {
94 3
            return false;
95
        }
96
97 165
        $this->isMapped()
98 3
            ? $this->items[$this->getPropertyValue($item, $this->identifier)] = $item
99 165
            : $this->items[] = $item;
100
101 165
        return true;
102
    }
103
104 9
    public function remove($item): bool
105
    {
106 9
        if (is_object($item)) {
107 6
            $position = array_search($item, $this->items, true);
108
109 6
            unset($this->items[$position]);
110
111 6
            return true;
112
        }
113
114 6
        if ($this->isMapped() && isset($this->items[$item])) {
115 3
            unset($this->items[$item]);
116
117 3
            return true;
118
        }
119
120 3
        if ($this->contains($item)) {
121 3
            return $this->remove($this->findById($item));
122
        }
123
124
        return false;
125
    }
126
127 3
    public function first(): object
128
    {
129 3
        return reset($this->items);
130
    }
131
132 3
    public function last(): object
133
    {
134 3
        $last = end($this->items);
135
136 3
        reset($this->items);
137
138 3
        return $last;
139
    }
140
141 6
    public function hasItems(): bool
142
    {
143 6
        return !empty($this->items);
144
    }
145
146 3
    public function contains($item): bool
147
    {
148 3
        if (is_object($item)) {
149
            return in_array($item, $this->items, true);
150
        }
151
152 3
        if ($this->isMapped()) {
153
            return isset($this->items[$item]);
154
        }
155
156 3
        if ($this->hasIdentifier()) {
157 3
            return !empty($this->find($this->identifier, $item));
158
        }
159
160
        return false;
161
    }
162
163 9
    public function find(string $property, $value): object
164
    {
165 9
        $items = $this->getItemsWhere($property, '=', $value);
166
167 9
        if (!empty($items)) {
168 6
            return $items[0];
169
        }
170
171 3
        throw new OutOfBoundsException(
172 3
            "Cannot find item where {$property} is equal to {$value}."
173
        );
174
    }
175
176 9
    public function findById($id)
177
    {
178 9
        if ($this->hasIdentifier()) {
179 6
            return $this->find($this->identifier, $id);
180
        }
181
182 3
        throw new LogicException(
183 3
            "Use of " . __METHOD__ . " requires an identifier."
184
        );
185
    }
186
187 6
    public function matches(array $items): bool
188
    {
189 6
        return $this->getCollectionComparator()->matches($this->items, $items);
190
    }
191
192 6
    public function column(string $property): array
193
    {
194 6
        return array_map(
195 6
            fn ($item) => $this->getPropertyValue($item, $property),
196 6
            $this->items
197
        );
198
    }
199
200 3
    public function merge(array ...$collections): object
201
    {
202 3
        $clone = clone $this;
203
204 3
        foreach ($collections as $collection) {
205 3
            $clone->collect(...$collection);
206
        }
207
208 3
        return $this->spawnWith($clone);
209
    }
210
211 6
    public function filter(callable $callback): object
212
    {
213 6
        return $this->spawnFrom(
214 6
            ...array_values(array_filter($this->items, $callback))
215
        );
216
    }
217
218 42
    public function query(CollectionQueryInterface $query): object
219
    {
220 42
        return $this->spawnFrom(...$query->query($this->items));
221
    }
222
223 36
    public function where(string $property, string $operator, $value): object
224
    {
225 36
        return $this->query($this->getBasicQuery($property, $operator, $value));
226
    }
227
228 9
    public function whereEquals(string $property, $value): object
229
    {
230 9
        return $this->where($property, '=', $value);
231
    }
232
233 6
    public function whereNotEquals(string $property, $value): object
234
    {
235 6
        return $this->where($property, '!=', $value);
236
    }
237
238 6
    public function whereIn(string $property, array $values): object
239
    {
240 6
        return $this->where($property, 'in', $values);
241
    }
242
243 9
    public function whereNotIn($property, $values): object
244
    {
245 9
        return $this->where($property, 'not in', $values);
246
    }
247
248 6
    public function notIn(array $items): object
249
    {
250 6
        return $this->spawnFrom(
251 6
            ...$this->getCollectionComparator()->notIn($this->items, $items)
252
        );
253
    }
254
255 9
    public function difference(array $items): object
256
    {
257 9
        return $this->spawnFrom(
258 9
            ...$this->getCollectionComparator()->difference($this->items, $items)
0 ignored issues
show
Bug introduced by
$this->getCollectionComp...e($this->items, $items) 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

258
            /** @scrutinizer ignore-type */ ...$this->getCollectionComparator()->difference($this->items, $items)
Loading history...
259
        );
260
    }
261
262 9
    public function intersection(array $items): object
263
    {
264 9
        return $this->spawnFrom(
265 9
            ...$this->getCollectionComparator()->intersection($this->items, $items)
266
        );
267
    }
268
269 30
    public function sortWith(CollectionSorterInterface $sorter, $order = OrderInterface::ASC): object
270
    {
271 30
        return $this->spawnFrom(...$sorter->sort($this->items, $order));
272
    }
273
274 15
    public function sortBy(string $property, string $order = OrderInterface::ASC): object
275
    {
276 15
        return $this->sortWith(
277 15
            new PropertyBasedSorter($this->propertyResolver, $property),
278
            $order
279
        );
280
    }
281
282 18
    public function sortMapped(array $map, $order = OrderInterface::ASC, string $property = null): object
283
    {
284 18
        if ($property ??= $this->identifier) {
285 15
            return $this->sortWith(
286 15
                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

286
                new MapBasedSorter($this->propertyResolver, /** @scrutinizer ignore-type */ $property, $map),
Loading history...
287
                $order
288
            );
289
        }
290
291 3
        throw new LogicException(
292
            'Cannot sort by map without property or item identifier set.'
293
        );
294
    }
295
296
    public function sortCustom(callable $callback): object
297
    {
298
        $clone = clone $this;
299
300
        usort($clone->items, $callback);
301
302
        return $this->spawnWith($clone);
303
    }
304
305
    public function map(callable $callback)
306
    {
307
        return array_map($callback, $this->items);
308
    }
309
310
    public function walk(callable $callback): void
311
    {
312
        array_walk($this->items, $callback);
313
    }
314
315
    public function foreach(callable $callback): void
316
    {
317
        foreach ($this->items as $key => $item) {
318
            $action = $callback($item, $key, $this->items);
319
320
            if ($action instanceof LoopAction) {
321
                switch ($action->getValue()) {
322
                    case LoopAction::Break:
323
                        break 2;
324
325
                    case LoopAction::Continue:
326
                        continue 2;
327
                }
328
            }
329
        }
330
    }
331
332 3
    public function count(): int
333
    {
334 3
        return count($this->items);
335
    }
336
337 123
    public function toArray(): array
338
    {
339 123
        return $this->items;
340
    }
341
342
    public function toJson(): string
343
    {
344
        return $this->jsonSerializer->serialize($this->items);
345
    }
346
347
    public function jsonSerialize(): mixed
348
    {
349
        return $this->items;
350
    }
351
352 3
    public function getIterator(): Traversable
353
    {
354 3
        return new ArrayIterator($this->items);
355
    }
356
357 30
    protected function getCollectionComparator(): CollectionComparatorInterface
358
    {
359 30
        if ($this->hasIdentifier()) {
360 12
            $comparator = new PropertyBasedCollectionComparator($this->propertyResolver);
361 12
            $comparator->setProperty($this->identifier);
362
        } else {
363 18
            $comparator = new RuntimeIdBasedCollectionComparator();
364
        }
365
366 30
        return $comparator;
367
    }
368
369 165
    protected function getObjectComparator(): ObjectComparatorInterface
370
    {
371 165
        if ($this->hasIdentifier()) {
372 165
            $comparator = new PropertyBasedObjectComparator($this->propertyResolver);
373 165
            $comparator->setProperty($this->identifier);
374
        } else {
375 90
            $comparator = new RuntimeIdBasedObjectComparator();
376
        }
377
378 165
        return $comparator;
379
    }
380
381 45
    protected function getBasicQuery(string $property, string $operator, $value): CollectionQueryInterface
382
    {
383 45
        return new BasicQuery(
384 45
            $this->propertyResolver,
385
            $property,
386
            $operator,
387
            $value,
388 45
            $this->operationProvider
389
        );
390
    }
391
392
    protected function objectsMatch(object $a, object $b): bool
393
    {
394
        return $this->getObjectComparator()->matches($a, $b);
395
    }
396
397 165
    protected function alreadyHasItem(object $object): bool
398
    {
399 165
        $comparator = $this->getObjectComparator();
400
401 165
        foreach ($this->items as $item) {
402 165
            if ($comparator->matches($item, $object)) {
403 3
                return true;
404
            }
405
        }
406
407 165
        return false;
408
    }
409
410 9
    protected function getPropertyValue(object $item, string $property)
411
    {
412 9
        return $this->propertyResolver->resolveProperty($item, $property);
413
    }
414
415 9
    protected function getItemsWhere(string $property, string $operator, $value): array
416
    {
417 9
        return $this->getBasicQuery($property, $operator, $value)
418 9
            ->query($this->items);
419
    }
420
421 165
    protected function hasIdentifier(): bool
422
    {
423 165
        return !empty($this->identifier);
424
    }
425
426 165
    protected function isMapped(): bool
427
    {
428 165
        return $this->hasIdentifier() && true === $this->map;
429
    }
430
431 99
    protected function spawnWith(self $clone): object
432
    {
433 99
        return ($this->generator)($clone);
434
    }
435
436 96
    protected function spawnFrom(object ...$items): object
437
    {
438 96
        $clone = clone $this;
439
440 96
        $clone->items = $items;
441
442 96
        return $this->spawnWith($clone);
443
    }
444
}
445