Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
80.77% covered (warning)
80.77%
42 / 52
73.33% covered (warning)
73.33%
11 / 15
CRAP
0.00% covered (danger)
0.00%
0 / 1
ItemList
80.77% covered (warning)
80.77%
42 / 52
73.33% covered (warning)
73.33%
11 / 15
36.40
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 count
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 first
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 last
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 toArray
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getIterator
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 jsonSerialize
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 offsetExists
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 offsetGet
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getType
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 offsetCheck
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
5
 typeCheck
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 append
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 offsetSet
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 offsetUnset
71.43% covered (warning)
71.43%
5 / 7
0.00% covered (danger)
0.00%
0 / 1
3.21
1<?php
2namespace Apie\Core\Lists;
3
4use Apie\Core\Exceptions\IndexNotFoundException;
5use Apie\Core\Exceptions\InvalidTypeException;
6use Apie\Core\Exceptions\ObjectIsEmpty;
7use Apie\Core\Exceptions\ObjectIsImmutable;
8use Apie\Core\TypeUtils;
9use Apie\Core\ValueObjects\Utils;
10use ArrayIterator;
11use Iterator;
12use ReflectionClass;
13use ReflectionType;
14
15/**
16 * @template T
17 * @implements ItemListInterface<T>
18 */
19class ItemList implements ItemListInterface
20{
21    /**
22     * @var array<int, T>
23     */
24    protected array $internal = [];
25
26    /** @var ReflectionType[] */
27    private static $typeMapping = [];
28
29    protected bool $mutable = true;
30
31    /**
32     * @param array<int|string, T> $input
33     */
34    final public function __construct(array $input = [])
35    {
36        $oldMutable = $this->mutable;
37        $this->mutable = true;
38        foreach ($input as $item) {
39            $this->offsetSet(null, $item);
40        }
41        $this->mutable = $oldMutable;
42    }
43
44    public function count(): int
45    {
46        return count($this->internal);
47    }
48
49    /**
50     * @return T
51     */
52    public function first(): mixed
53    {
54        if (empty($this->internal)) {
55            throw ObjectIsEmpty::createForList();
56        }
57        return reset($this->internal);
58    }
59
60    /**
61     * @return T
62     */
63    public function last(): mixed
64    {
65        if (empty($this->internal)) {
66            throw ObjectIsEmpty::createForList();
67        }
68        return end($this->internal);
69    }
70
71    /**
72     * @return array<int, T>
73     */
74    public function toArray(): array
75    {
76        return $this->internal;
77    }
78
79    /**
80     * @return Iterator<int, T>
81     */
82    public function getIterator(): Iterator
83    {
84        return new ArrayIterator($this->internal);
85    }
86
87    /**
88     * @return array<int, T>
89     */
90    public function jsonSerialize(): array
91    {
92        return $this->internal;
93    }
94
95    public function offsetExists(mixed $offset): bool
96    {
97        return array_key_exists($offset, $this->internal);
98    }
99
100
101    /**
102     * @return T
103     */
104    public function offsetGet(mixed $offset): mixed
105    {
106        if (!array_key_exists($offset, $this->internal)) {
107            throw new IndexNotFoundException($offset);
108        }
109        return $this->internal[$offset];
110    }
111
112    protected function getType(): ReflectionType
113    {
114        $currentClass = static::class;
115        if (!isset(self::$typeMapping[$currentClass])) {
116            self::$typeMapping[$currentClass] = (new ReflectionClass($currentClass))->getMethod('offsetGet')->getReturnType();
117        }
118        return self::$typeMapping[$currentClass];
119    }
120
121    protected function offsetCheck(mixed $value): int
122    {
123        if ($value === null) { // append
124            return count($this->internal);
125        }
126        $value = Utils::toInt($value);
127        if ($value < 0) {
128            throw new IndexNotFoundException($value);
129        }
130        // we check if null is allowed. If it is allowed we accept the current offset as it will expand the array.
131        if ($value > count($this->internal) && !TypeUtils::matchesType($this->getType(), null)) {
132            throw new IndexNotFoundException($value);
133        }
134        return $value;
135    }
136
137    protected function typeCheck(mixed $value): void
138    {
139        if (static::class === ItemList::class) {
140            return;
141        }
142        $type = $this->getType();
143        if (!TypeUtils::matchesType($type, $value)) {
144            throw new InvalidTypeException($value, $type->__toString());
145        }
146    }
147
148    public function append(mixed $value): self
149    {
150        $this->typeCheck($value);
151        $returnValue = $this->mutable ? $this : clone $this;
152        $returnValue->internal[] = $value;
153
154        return $returnValue;
155    }
156
157    /**
158     * @param T $value
159     */
160    public function offsetSet(mixed $offset, mixed $value): void
161    {
162        if (!$this->mutable) {
163            throw new ObjectIsImmutable($this);
164        }
165        $offset = $this->offsetCheck($offset);
166        $this->typeCheck($value);
167        $this->internal[$offset] = $value;
168    }
169
170    public function offsetUnset(mixed $offset): void
171    {
172        if (!$this->mutable) {
173            throw new ObjectIsImmutable($this);
174        }
175        $offset = Utils::toInt($offset);
176        // a value can only be deleted if it is the last item in the array or if null is allowed
177        if (($offset + 1) === count($this->internal)) {
178            array_pop($this->internal);
179            return;
180        }
181        array_splice($this->internal, $offset, 1);
182    }
183}