Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
60 / 60
100.00% covered (success)
100.00%
23 / 23
CRAP
100.00% covered (success)
100.00%
1 / 1
ArrayProxy
100.00% covered (success)
100.00%
60 / 60
100.00% covered (success)
100.00%
23 / 23
44
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 unwrap
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 is
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 in
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 rewind
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 current
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 key
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 next
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 valid
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%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 offsetSet
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 offsetUnset
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 count
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 exists
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 contains
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 merge
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 map
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 filter
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 reduce
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 sorted
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 sort
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
8
 usort
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
1<?php
2
3declare(strict_types=1);
4
5namespace Celema\Boiler\Proxy;
6
7use ArrayAccess;
8use Celema\Boiler\Contract\Wrapper;
9use Celema\Boiler\Exception\OutOfBoundsException;
10use Celema\Boiler\Exception\RuntimeException;
11use Celema\Boiler\Exception\UnexpectedValueException;
12use Countable;
13use Iterator;
14use Override;
15
16/**
17 * @api
18 *
19 * @psalm-type ArrayCallable = callable(mixed, mixed):int
20 * @psalm-type FilterCallable = callable(mixed):mixed
21 *
22 * @template-implements ArrayAccess<array-key, mixed>
23 * @template-implements Iterator<mixed>
24 * @implements Proxy<array<array-key, mixed>>
25 */
26final class ArrayProxy implements ArrayAccess, Iterator, Countable, Proxy
27{
28    /** @var list<array-key> */
29    private array $keys;
30    private int $position;
31
32    /**
33     * @param array<array-key, mixed> $array
34     */
35    public function __construct(
36        private array $array,
37        private readonly Wrapper $wrapper,
38    ) {
39        $this->array = $array;
40        $this->keys = array_keys($array);
41        $this->position = 0;
42    }
43
44    #[Override]
45    public function unwrap(): array
46    {
47        return $this->array;
48    }
49
50    #[Override]
51    public function is(mixed $other): bool
52    {
53        return $this->array === ($other instanceof Proxy ? $other->unwrap() : $other);
54    }
55
56    /** @param self|array<array-key, mixed> $haystack */
57    #[Override]
58    public function in(self|array $haystack): bool
59    {
60        if ($haystack instanceof self) {
61            $haystack = $haystack->unwrap();
62        }
63
64        /** @var mixed $item */
65        foreach ($haystack as $item) {
66            if ($this->is($item)) {
67                return true;
68            }
69        }
70
71        return false;
72    }
73
74    #[Override]
75    public function rewind(): void
76    {
77        $this->position = 0;
78    }
79
80    #[Override]
81    public function current(): mixed
82    {
83        $key = $this->keys[$this->position];
84
85        return $this->wrapper->wrap($this->array[$key]);
86    }
87
88    /**
89     * @return array-key
90     */
91    #[Override]
92    public function key(): mixed
93    {
94        return $this->keys[$this->position];
95    }
96
97    #[Override]
98    public function next(): void
99    {
100        $this->position++;
101    }
102
103    #[Override]
104    public function valid(): bool
105    {
106        return isset($this->keys[$this->position]);
107    }
108
109    /** @param array-key $offset */
110    #[Override]
111    public function offsetExists(mixed $offset): bool
112    {
113        return array_key_exists($offset, $this->array);
114    }
115
116    /** @param array-key $offset */
117    #[Override]
118    public function offsetGet(mixed $offset): mixed
119    {
120        if (array_key_exists($offset, $this->array)) {
121            return $this->wrapper->wrap($this->array[$offset]);
122        }
123
124        $key = is_numeric($offset) ? (string) $offset : "'{$offset}'";
125
126        throw new OutOfBoundsException("Undefined array key {$key}");
127    }
128
129    #[Override]
130    public function offsetSet(mixed $offset, mixed $value): void
131    {
132        if ($offset === null) {
133            $this->array[] = $this->wrapper->unwrap($value);
134        } else {
135            $this->array[$offset] = $this->wrapper->unwrap($value);
136        }
137
138        $this->keys = array_keys($this->array);
139    }
140
141    #[Override]
142    public function offsetUnset(mixed $offset): void
143    {
144        unset($this->array[$offset]);
145        $this->keys = array_keys($this->array);
146    }
147
148    #[Override]
149    public function count(): int
150    {
151        return count($this->array);
152    }
153
154    /** @param array-key $key */
155    public function exists(mixed $key): bool
156    {
157        return array_key_exists($key, $this->array);
158    }
159
160    public function contains(mixed $value): bool
161    {
162        return in_array($value instanceof Proxy ? $value->unwrap() : $value, $this->array, true);
163    }
164
165    public function merge(array|self $array): self
166    {
167        return new self(array_merge(
168            $this->array,
169            $array instanceof self ? $array->unwrap() : $array,
170        ), $this->wrapper);
171    }
172
173    /** @psalm-param ArrayCallable $callable */
174    public function map(callable $callable): self
175    {
176        return new self(array_map($callable, $this->array), $this->wrapper);
177    }
178
179    /** @psalm-param FilterCallable $callable */
180    public function filter(callable $callable): self
181    {
182        return new self(array_filter($this->array, $callable), $this->wrapper);
183    }
184
185    /** @psalm-param ArrayCallable $callable */
186    public function reduce(callable $callable, mixed $initial = null): mixed
187    {
188        return $this->wrapper->wrap(array_reduce($this->array, $callable, $initial));
189    }
190
191    /** @psalm-param ArrayCallable $callable */
192    public function sorted(string $mode = '', ?callable $callable = null): self
193    {
194        $mode = strtolower(trim($mode));
195
196        if (str_starts_with($mode, 'u')) {
197            if (!is_callable($callable)) {
198                throw new RuntimeException('No callable provided for user defined sorting');
199            }
200
201            return $this->usort($this->array, $mode, $callable);
202        }
203
204        return $this->sort($this->array, $mode);
205    }
206
207    private function sort(array $array, string $mode): self
208    {
209        match ($mode) {
210            '' => sort($array),
211            'ar' => arsort($array),
212            'a' => asort($array),
213            'kr' => krsort($array),
214            'k' => ksort($array),
215            'r' => rsort($array),
216            default => throw new UnexpectedValueException("Sort mode '{$mode}' not supported"),
217        };
218
219        return new self($array, $this->wrapper);
220    }
221
222    /** @psalm-param ArrayCallable $callable */
223    private function usort(array $array, string $mode, callable $callable): self
224    {
225        match ($mode) {
226            'ua' => uasort($array, $callable),
227            'u' => usort($array, $callable),
228            default => throw new UnexpectedValueException("Sort mode '{$mode}' not supported"),
229        };
230
231        return new self($array, $this->wrapper);
232    }
233}