Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
81.63% |
40 / 49 |
|
78.57% |
11 / 14 |
CRAP | |
0.00% |
0 / 1 |
ItemList | |
81.63% |
40 / 49 |
|
78.57% |
11 / 14 |
32.86 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
count | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
first | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
toArray | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getIterator | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
jsonSerialize | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
offsetExists | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
offsetGet | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
getType | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
offsetCheck | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
5 | |||
typeCheck | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
append | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
offsetSet | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
offsetUnset | |
71.43% |
5 / 7 |
|
0.00% |
0 / 1 |
3.21 |
1 | <?php |
2 | namespace Apie\Core\Lists; |
3 | |
4 | use Apie\Core\Exceptions\IndexNotFoundException; |
5 | use Apie\Core\Exceptions\InvalidTypeException; |
6 | use Apie\Core\Exceptions\ObjectIsEmpty; |
7 | use Apie\Core\Exceptions\ObjectIsImmutable; |
8 | use Apie\Core\TypeUtils; |
9 | use Apie\Core\ValueObjects\Utils; |
10 | use ArrayIterator; |
11 | use Iterator; |
12 | use ReflectionClass; |
13 | use ReflectionType; |
14 | |
15 | /** |
16 | * @template T |
17 | * @implements ItemListInterface<T> |
18 | */ |
19 | class 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 array<int, T> |
62 | */ |
63 | public function toArray(): array |
64 | { |
65 | return $this->internal; |
66 | } |
67 | |
68 | /** |
69 | * @return Iterator<int, T> |
70 | */ |
71 | public function getIterator(): Iterator |
72 | { |
73 | return new ArrayIterator($this->internal); |
74 | } |
75 | |
76 | /** |
77 | * @return array<int, T> |
78 | */ |
79 | public function jsonSerialize(): array |
80 | { |
81 | return $this->internal; |
82 | } |
83 | |
84 | public function offsetExists(mixed $offset): bool |
85 | { |
86 | return array_key_exists($offset, $this->internal); |
87 | } |
88 | |
89 | |
90 | /** |
91 | * @return T |
92 | */ |
93 | public function offsetGet(mixed $offset): mixed |
94 | { |
95 | if (!array_key_exists($offset, $this->internal)) { |
96 | throw new IndexNotFoundException($offset); |
97 | } |
98 | return $this->internal[$offset]; |
99 | } |
100 | |
101 | protected function getType(): ReflectionType |
102 | { |
103 | $currentClass = static::class; |
104 | if (!isset(self::$typeMapping[$currentClass])) { |
105 | self::$typeMapping[$currentClass] = (new ReflectionClass($currentClass))->getMethod('offsetGet')->getReturnType(); |
106 | } |
107 | return self::$typeMapping[$currentClass]; |
108 | } |
109 | |
110 | protected function offsetCheck(mixed $value): int |
111 | { |
112 | if ($value === null) { // append |
113 | return count($this->internal); |
114 | } |
115 | $value = Utils::toInt($value); |
116 | if ($value < 0) { |
117 | throw new IndexNotFoundException($value); |
118 | } |
119 | // we check if null is allowed. If it is allowed we accept the current offset as it will expand the array. |
120 | if ($value > count($this->internal) && !TypeUtils::matchesType($this->getType(), null)) { |
121 | throw new IndexNotFoundException($value); |
122 | } |
123 | return $value; |
124 | } |
125 | |
126 | protected function typeCheck(mixed $value): void |
127 | { |
128 | if (static::class === ItemList::class) { |
129 | return; |
130 | } |
131 | $type = $this->getType(); |
132 | if (!TypeUtils::matchesType($type, $value)) { |
133 | throw new InvalidTypeException($value, $type->__toString()); |
134 | } |
135 | } |
136 | |
137 | public function append(mixed $value): self |
138 | { |
139 | $this->typeCheck($value); |
140 | $returnValue = $this->mutable ? $this : clone $this; |
141 | $returnValue->internal[] = $value; |
142 | |
143 | return $returnValue; |
144 | } |
145 | |
146 | /** |
147 | * @param T $value |
148 | */ |
149 | public function offsetSet(mixed $offset, mixed $value): void |
150 | { |
151 | if (!$this->mutable) { |
152 | throw new ObjectIsImmutable($this); |
153 | } |
154 | $offset = $this->offsetCheck($offset); |
155 | $this->typeCheck($value); |
156 | $this->internal[$offset] = $value; |
157 | } |
158 | |
159 | public function offsetUnset(mixed $offset): void |
160 | { |
161 | if (!$this->mutable) { |
162 | throw new ObjectIsImmutable($this); |
163 | } |
164 | $offset = Utils::toInt($offset); |
165 | // a value can only be deleted if it is the last item in the array or if null is allowed |
166 | if (($offset + 1) === count($this->internal)) { |
167 | array_pop($this->internal); |
168 | return; |
169 | } |
170 | array_splice($this->internal, $offset, 1); |
171 | } |
172 | } |