Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
45 / 45
100.00% covered (success)
100.00%
8 / 8
CRAP
100.00% covered (success)
100.00%
1 / 1
CaseInsensitiveKeyArray
100.00% covered (success)
100.00%
45 / 45
100.00% covered (success)
100.00%
8 / 8
22
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
3
 toMapKey
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 offsetExists
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 offsetGet
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 offsetSet
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
5
 offsetUnset
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 count
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
1<?php
2
3declare(strict_types=1);
4
5namespace PeServer\Core\Collection;
6
7use ArrayAccess;
8use ArrayIterator;
9use Countable;
10use IteratorAggregate;
11use Traversable;
12use PeServer\Core\Text;
13use PeServer\Core\Throws\IndexOutOfRangeException;
14use PeServer\Core\Throws\KeyNotFoundException;
15use PeServer\Core\Throws\NotImplementedException;
16use PeServer\Core\Throws\NotSupportedException;
17
18/**
19 * キーとして大文字小文字を区別しない連想配列。
20 *
21 * * 追加という概念はない(`$array[] = 'xxx'`)
22 * * 数値も受け取れるけど仕方ないとして割り切る
23 *
24 * @template TKey of array-key
25 * @template TValue
26 * @implements ArrayAccess<TKey,TValue>
27 * @implements IteratorAggregate<TKey,TValue>
28 */
29class CaseInsensitiveKeyArray implements ArrayAccess, Countable, IteratorAggregate
30{
31    #region variable
32
33    /**
34     * 実データ。
35     *
36     * @var array<string|int,mixed>
37     * @phpstan-var array<array-key,TValue>
38     */
39    private array $data = [];
40    /**
41     * self::toMapKey適用キーに対する実キーをマッピング。
42     *
43     * @var array<string,string>
44     */
45    private array $map = [];
46
47    #endregion
48
49    /**
50     * 生成。
51     *
52     * @param array<string,string|int>|null $input
53     * @phpstan-param array<array-key,TValue>|null $input
54     */
55    public function __construct(array $input = null)
56    {
57        if ($input !== null) {
58            foreach ($input as $key => $value) {
59                $this->offsetSet($key, $value);
60            }
61        }
62    }
63
64    #region function
65
66    /**
67     * オフセット名へのマッピング名に変換。
68     *
69     * @param string $offset
70     * @return string
71     */
72    public function toMapKey(string $offset): string
73    {
74        return Text::toLower($offset);
75    }
76
77    #endregion
78
79    #region ArrayAccess
80
81    /**
82     * `ArrayAccess:offsetExists`
83     *
84     * @param string|int $offset
85     * @return bool
86     */
87    public function offsetExists(mixed $offset): bool
88    {
89        if (isset($this->data[$offset])) {
90            return true;
91        }
92        if (is_int($offset)) {
93            return false;
94        }
95
96        $mapOffset = $this->toMapKey($offset);
97        if (isset($this->map[$mapOffset])) {
98            return true;
99        }
100
101        return false;
102    }
103
104    /**
105     * `ArrayAccess:offsetGet`
106     *
107     * @param string|int $offset
108     * @return mixed
109     * @phpstan-return TValue
110     */
111    public function offsetGet(mixed $offset): mixed
112    {
113        if (isset($this->data[$offset])) {
114            return $this->data[$offset];
115        }
116        if (is_int($offset)) {
117            throw new IndexOutOfRangeException('$offset: ' . $offset);
118        }
119
120        $mapOffset = $this->toMapKey($offset);
121        if (isset($this->map[$mapOffset])) {
122            return $this->data[$this->map[$mapOffset]];
123        }
124
125        throw new KeyNotFoundException('$offset: ' . $offset);
126    }
127
128    /**
129     * `ArrayAccess:offsetSet`
130     *
131     * @param string|int|null $offset
132     * @param mixed $value
133     * @phpstan-param TValue $value
134     */
135    public function offsetSet(mixed $offset, mixed $value): void
136    {
137        if ($offset === null) {
138            throw new NotSupportedException();
139        }
140        if (is_int($offset)) {
141            $this->data[$offset] = $value;
142            return;
143        }
144        if (is_double($offset)) { //@phpstan-ignore-line サポートしているがドキュメント上は通常配列キー限定
145            $floatOffset = (string)$offset;
146            $this->data[$floatOffset] = $value;
147            return;
148        }
149
150        $mapOffset = $this->toMapKey($offset);
151        if (isset($this->map[$mapOffset])) {
152            $this->data[$this->map[$mapOffset]] = $value;
153        } else {
154            $this->data[$offset] = $value;
155            $this->map[$mapOffset] = $offset;
156        }
157    }
158
159    /**
160     * `ArrayAccess:offsetUnset`
161     *
162     * @param string|int $offset
163     */
164    public function offsetUnset(mixed $offset): void
165    {
166        if (is_int($offset)) {
167            unset($this->data[$offset]);
168            return;
169        }
170        if (is_double($offset)) { //@phpstan-ignore-line サポートしているがドキュメント上は通常配列キー限定
171            unset($this->data[(string)$offset]);
172            return;
173        }
174
175        $mapOffset = $this->toMapKey($offset);
176        unset($this->data[$this->map[$mapOffset]]);
177        unset($this->map[$mapOffset]);
178    }
179
180    #endregion
181
182    #region Countable
183
184    public function count(): int
185    {
186        return count($this->data);
187    }
188
189    #endregion
190
191    #region IteratorAggregate
192
193    public function getIterator(): Traversable
194    {
195        return new ArrayIterator($this->data);
196    }
197
198    #endregion
199}