Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
52 / 52
100.00% covered (success)
100.00%
20 / 20
CRAP
100.00% covered (success)
100.00%
1 / 1
ArrayProxy
100.00% covered (success)
100.00%
52 / 52
100.00% covered (success)
100.00%
20 / 20
36
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
 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
 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 Celemas\Boiler\Proxy;
6
7use ArrayAccess;
8use Celemas\Boiler\Contract\Wrapper;
9use Celemas\Boiler\Exception\OutOfBoundsException;
10use Celemas\Boiler\Exception\RuntimeException;
11use Celemas\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 rewind(): void
52    {
53        $this->position = 0;
54    }
55
56    #[Override]
57    public function current(): mixed
58    {
59        $key = $this->keys[$this->position];
60
61        return $this->wrapper->wrap($this->array[$key]);
62    }
63
64    /**
65     * @return array-key
66     */
67    #[Override]
68    public function key(): mixed
69    {
70        return $this->keys[$this->position];
71    }
72
73    #[Override]
74    public function next(): void
75    {
76        $this->position++;
77    }
78
79    #[Override]
80    public function valid(): bool
81    {
82        return isset($this->keys[$this->position]);
83    }
84
85    /** @param array-key $offset */
86    #[Override]
87    public function offsetExists(mixed $offset): bool
88    {
89        return array_key_exists($offset, $this->array);
90    }
91
92    /** @param array-key $offset */
93    #[Override]
94    public function offsetGet(mixed $offset): mixed
95    {
96        if (array_key_exists($offset, $this->array)) {
97            return $this->wrapper->wrap($this->array[$offset]);
98        }
99
100        $key = is_numeric($offset) ? (string) $offset : "'{$offset}'";
101
102        throw new OutOfBoundsException("Undefined array key {$key}");
103    }
104
105    #[Override]
106    public function offsetSet(mixed $offset, mixed $value): void
107    {
108        if ($offset === null) {
109            $this->array[] = $this->wrapper->unwrap($value);
110        } else {
111            $this->array[$offset] = $this->wrapper->unwrap($value);
112        }
113
114        $this->keys = array_keys($this->array);
115    }
116
117    #[Override]
118    public function offsetUnset(mixed $offset): void
119    {
120        unset($this->array[$offset]);
121        $this->keys = array_keys($this->array);
122    }
123
124    #[Override]
125    public function count(): int
126    {
127        return count($this->array);
128    }
129
130    /** @param array-key $key */
131    public function exists(mixed $key): bool
132    {
133        return array_key_exists($key, $this->array);
134    }
135
136    public function merge(array|self $array): self
137    {
138        return new self(array_merge(
139            $this->array,
140            $array instanceof self ? $array->unwrap() : $array,
141        ), $this->wrapper);
142    }
143
144    /** @psalm-param ArrayCallable $callable */
145    public function map(callable $callable): self
146    {
147        return new self(array_map($callable, $this->array), $this->wrapper);
148    }
149
150    /** @psalm-param FilterCallable $callable */
151    public function filter(callable $callable): self
152    {
153        return new self(array_filter($this->array, $callable), $this->wrapper);
154    }
155
156    /** @psalm-param ArrayCallable $callable */
157    public function reduce(callable $callable, mixed $initial = null): mixed
158    {
159        return $this->wrapper->wrap(array_reduce($this->array, $callable, $initial));
160    }
161
162    /** @psalm-param ArrayCallable $callable */
163    public function sorted(string $mode = '', ?callable $callable = null): self
164    {
165        $mode = strtolower(trim($mode));
166
167        if (str_starts_with($mode, 'u')) {
168            if (!is_callable($callable)) {
169                throw new RuntimeException('No callable provided for user defined sorting');
170            }
171
172            return $this->usort($this->array, $mode, $callable);
173        }
174
175        return $this->sort($this->array, $mode);
176    }
177
178    private function sort(array $array, string $mode): self
179    {
180        match ($mode) {
181            '' => sort($array),
182            'ar' => arsort($array),
183            'a' => asort($array),
184            'kr' => krsort($array),
185            'k' => ksort($array),
186            'r' => rsort($array),
187            default => throw new UnexpectedValueException("Sort mode '{$mode}' not supported"),
188        };
189
190        return new self($array, $this->wrapper);
191    }
192
193    /** @psalm-param ArrayCallable $callable */
194    private function usort(array $array, string $mode, callable $callable): self
195    {
196        match ($mode) {
197            'ua' => uasort($array, $callable),
198            'u' => usort($array, $callable),
199            default => throw new UnexpectedValueException("Sort mode '{$mode}' not supported"),
200        };
201
202        return new self($array, $this->wrapper);
203    }
204}