Issues (158)

src/EnumMap.php (22 issues)

1
<?php
2
3
declare(strict_types=1);
4
5
namespace MabeEnum;
6
7
use ArrayAccess;
8
use Countable;
9
use InvalidArgumentException;
10
use Iterator;
11
use IteratorAggregate;
12
use UnexpectedValueException;
13
14
/**
15
 * A map of enumerators and data values (EnumMap<T of Enum, mixed>).
16
 *
17
 * @template T of Enum
18
 * @implements ArrayAccess<T, mixed>
19
 * @implements IteratorAggregate<T, mixed>
20
 *
21
 * @copyright 2020, Marc Bennewitz
22
 * @license http://github.com/marc-mabe/php-enum/blob/master/LICENSE.txt New BSD License
23
 * @link http://github.com/marc-mabe/php-enum for the canonical source repository
24
 */
25
class EnumMap implements ArrayAccess, Countable, IteratorAggregate
26
{
27
    /**
28
     * The classname of the enumeration type
29
     * @var class-string<T>
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string<T> at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string<T>.
Loading history...
30
     */
31
    private $enumeration;
32
33
    /**
34
     * Internal map of ordinal number and data value
35
     * @var array<int, mixed>
36
     */
37
    private $map = [];
38
39
    /**
40
     * Constructor
41
     * @param class-string<T> $enumeration The classname of the enumeration type
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string<T> at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string<T>.
Loading history...
42
     * @param null|iterable<T|null|bool|int|float|string|array<mixed>, mixed> $map Initialize map
43
     * @throws InvalidArgumentException
44
     */
45 21
    public function __construct(string $enumeration, iterable $map = null)
46
    {
47 21
        if (!\is_subclass_of($enumeration, Enum::class)) {
48 1
            throw new InvalidArgumentException(\sprintf(
49 1
                '%s can handle subclasses of %s only',
50 1
                 __CLASS__,
51 1
                Enum::class
52
            ));
53
        }
54 20
        $this->enumeration = $enumeration;
55
56 20
        if ($map) {
57 3
            $this->addIterable($map);
58
        }
59 20
    }
60
61
    /**
62
     * Add virtual private property "__pairs" with a list of key-value-pairs
63
     * to the result of var_dump.
64
     *
65
     * This helps debugging as internally the map is using the ordinal number.
66
     *
67
     * @return array<string, mixed>
68
     */
69 1
    public function __debugInfo(): array {
70 1
        $dbg = (array)$this;
71 1
        $dbg["\0" . self::class . "\0__pairs"] = array_map(function ($k, $v) {
72 1
            return [$k, $v];
73 1
        }, $this->getKeys(), $this->getValues());
74 1
        return $dbg;
75
    }
76
77
    /* write access (mutable) */
78
79
    /**
80
     * Adds the given enumerator (object or value) mapping to the specified data value.
81
     * @param T|null|bool|int|float|string|array<mixed> $enumerator
0 ignored issues
show
Missing parameter comment
Loading history...
82
     * @param mixed                                                 $value
0 ignored issues
show
Missing parameter comment
Loading history...
83
     * @throws InvalidArgumentException On an invalid given enumerator
84
     * @see offsetSet()
85
     */
86 13
    public function add($enumerator, $value): void
87
    {
88 13
        $ord = ($this->enumeration)::get($enumerator)->getOrdinal();
89 12
        $this->map[$ord] = $value;
90 12
    }
91
92
    /**
93
     * Adds the given iterable, mapping enumerators (objects or values) to data values.
94
     * @param iterable<T|null|bool|int|float|string|array<mixed>, mixed> $map
0 ignored issues
show
Missing parameter comment
Loading history...
95
     * @throws InvalidArgumentException On an invalid given enumerator
96
     */
97 5
    public function addIterable(iterable $map): void
98
    {
99 5
        $innerMap = $this->map;
100 5
        foreach ($map as $enumerator => $value) {
101 5
            $ord = ($this->enumeration)::get($enumerator)->getOrdinal();
102 5
            $innerMap[$ord] = $value;
103
        }
104 5
        $this->map = $innerMap;
105 5
    }
106
107
    /**
108
     * Removes the given enumerator (object or value) mapping.
109
     * @param T|null|bool|int|float|string|array<mixed> $enumerator
0 ignored issues
show
Missing parameter comment
Loading history...
110
     * @throws InvalidArgumentException On an invalid given enumerator
111
     * @see offsetUnset()
112
     */
113 5
    public function remove($enumerator): void
114
    {
115 5
        $ord = ($this->enumeration)::get($enumerator)->getOrdinal();
116 5
        unset($this->map[$ord]);
117 5
    }
118
119
    /**
120
     * Removes the given iterable enumerator (object or value) mappings.
121
     * @param iterable<T|null|bool|int|float|string|array<mixed>> $enumerators
0 ignored issues
show
Missing parameter comment
Loading history...
122
     * @throws InvalidArgumentException On an invalid given enumerator
123
     */
124 3
    public function removeIterable(iterable $enumerators): void
125
    {
126 3
        $map = $this->map;
127 3
        foreach ($enumerators as $enumerator) {
128 3
            $ord = ($this->enumeration)::get($enumerator)->getOrdinal();
129 3
            unset($map[$ord]);
130
        }
131
132 3
        $this->map = $map;
133 3
    }
134
135
    /* write access (immutable) */
136
137
    /**
138
     * Creates a new map with the given enumerator (object or value) mapping to the specified data value added.
139
     * @param T|null|bool|int|float|string|array<mixed> $enumerator
0 ignored issues
show
Missing parameter comment
Loading history...
140
     * @param mixed                                                 $value
0 ignored issues
show
Missing parameter comment
Loading history...
141
     * @return static
142
     * @throws InvalidArgumentException On an invalid given enumerator
143
     */
144 1
    public function with($enumerator, $value): self
145
    {
146 1
        $clone = clone $this;
147 1
        $clone->add($enumerator, $value);
148 1
        return $clone;
149
    }
150
151
    /**
152
     * Creates a new map with the given iterable mapping enumerators (objects or values) to data values added.
153
     * @param iterable<T|null|bool|int|float|string|array<mixed>, mixed> $map
0 ignored issues
show
Missing parameter comment
Loading history...
154
     * @return static
155
     * @throws InvalidArgumentException On an invalid given enumerator
156
     */
157 1
    public function withIterable(iterable $map): self
158
    {
159 1
        $clone = clone $this;
160 1
        $clone->addIterable($map);
161 1
        return $clone;
162
    }
163
164
    /**
165
     * Create a new map with the given enumerator mapping removed.
166
     * @param T|null|bool|int|float|string|array<mixed> $enumerator
0 ignored issues
show
Missing parameter comment
Loading history...
167
     * @return static
168
     * @throws InvalidArgumentException On an invalid given enumerator
169
     */
170 1
    public function without($enumerator): self
171
    {
172 1
        $clone = clone $this;
173 1
        $clone->remove($enumerator);
174 1
        return $clone;
175
    }
176
177
    /**
178
     * Creates a new map with the given iterable enumerator (object or value) mappings removed.
179
     * @param iterable<T|null|bool|int|float|string|array<mixed>> $enumerators
0 ignored issues
show
Missing parameter comment
Loading history...
180
     * @return static
181
     * @throws InvalidArgumentException On an invalid given enumerator
182
     */
183 1
    public function withoutIterable(iterable $enumerators): self
184
    {
185 1
        $clone = clone $this;
186 1
        $clone->removeIterable($enumerators);
187 1
        return $clone;
188
    }
189
190
    /* read access */
191
192
    /**
193
     * Get the classname of the enumeration type.
194
     * @return class-string<T>
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string<T> at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string<T>.
Loading history...
195
     */
196 1
    public function getEnumeration(): string
197
    {
198 1
        return $this->enumeration;
199
    }
200
201
    /**
202
     * Get the mapped data value of the given enumerator (object or value).
203
     * @param T|null|bool|int|float|string|array<mixed> $enumerator
0 ignored issues
show
Missing parameter comment
Loading history...
204
     * @return mixed
205
     * @throws InvalidArgumentException On an invalid given enumerator
206
     * @throws UnexpectedValueException If the given enumerator does not exist in this map
207
     * @see offsetGet()
208
     */
209 10
    public function get($enumerator)
210
    {
211 10
        $enumerator = ($this->enumeration)::get($enumerator);
212 10
        $ord = $enumerator->getOrdinal();
213 10
        if (!\array_key_exists($ord, $this->map)) {
214 2
            throw new UnexpectedValueException(sprintf(
215 2
                'Enumerator %s could not be found',
216 2
                \var_export($enumerator->getValue(), true)
217
            ));
218
        }
219
220 8
        return $this->map[$ord];
221
    }
222
223
    /**
224
     * Get a list of enumerator keys.
225
     * @return T[]
226
     *
227
     * @phpstan-return array<int, T>
228
     * @psalm-return list<T>
229
     */
230 8
    public function getKeys(): array
231
    {
232
        /** @var callable $byOrdinalFn */
233 8
        $byOrdinalFn = [$this->enumeration, 'byOrdinal'];
234
235 8
        return \array_map($byOrdinalFn, \array_keys($this->map));
236
    }
237
238
    /**
239
     * Get a list of mapped data values.
240
     * @return mixed[]
241
     *
242
     * @phpstan-return array<int, mixed>
243
     * @psalm-return list<mixed>
244
     */
245 8
    public function getValues(): array
246
    {
247 8
        return \array_values($this->map);
248
    }
249
250
    /**
251
     * Search for the given data value.
252
     * @param mixed $value
0 ignored issues
show
Missing parameter comment
Loading history...
253
     * @param bool $strict Use strict type comparison
254
     * @return T|null The enumerator object of the first matching data value or NULL
255
     */
256 2
    public function search($value, bool $strict = false)
257
    {
258
        /** @var false|int $ord */
259 2
        $ord = \array_search($value, $this->map, $strict);
260 2
        if ($ord !== false) {
261 2
            return ($this->enumeration)::byOrdinal($ord);
262
        }
263
264 2
        return null;
265
    }
266
267
    /**
268
     * Test if the given enumerator key (object or value) exists.
269
     * @param T|null|bool|int|float|string|array<mixed> $enumerator
0 ignored issues
show
Missing parameter comment
Loading history...
270
     * @return bool
271
     * @see offsetExists()
272
     */
273 8
    public function has($enumerator): bool
274
    {
275
        try {
276 8
            $ord = ($this->enumeration)::get($enumerator)->getOrdinal();
277 7
            return \array_key_exists($ord, $this->map);
278 1
        } catch (InvalidArgumentException $e) {
279
            // An invalid enumerator can't be contained in this map
280 1
            return false;
281
        }
282
    }
283
284
    /**
285
     * Test if the given enumerator key (object or value) exists.
286
     * @param T|null|bool|int|float|string|array<mixed> $enumerator
0 ignored issues
show
Missing parameter comment
Loading history...
287
     * @return bool
288
     * @see offsetExists()
289
     * @see has()
290
     * @deprecated Will trigger deprecation warning in last 4.x and removed in 5.x
291
     */
292 1
    public function contains($enumerator): bool
293
    {
294 1
        return $this->has($enumerator);
295
    }
296
297
    /* ArrayAccess */
298
299
    /**
300
     * Test if the given enumerator key (object or value) exists and is not NULL
301
     * @param T|null|bool|int|float|string|array<mixed> $enumerator
0 ignored issues
show
Missing parameter comment
Loading history...
302
     * @return bool
303
     * @see contains()
304
     */
305 5
    public function offsetExists($enumerator): bool
306
    {
307
        try {
308 5
            return isset($this->map[($this->enumeration)::get($enumerator)->getOrdinal()]);
309 1
        } catch (InvalidArgumentException $e) {
310
            // An invalid enumerator can't be an offset of this map
311 1
            return false;
312
        }
313
    }
314
315
    /**
316
     * Get the mapped data value of the given enumerator (object or value).
317
     * @param T|null|bool|int|float|string|array<mixed> $enumerator
0 ignored issues
show
Missing parameter comment
Loading history...
318
     * @return mixed The mapped date value of the given enumerator or NULL
319
     * @throws InvalidArgumentException On an invalid given enumerator
320
     * @see get()
321
     */
322 4
    public function offsetGet($enumerator)
323
    {
324
        try {
325 4
            return $this->get($enumerator);
326 1
        } catch (UnexpectedValueException $e) {
327 1
            return null;
328
        }
329
    }
330
331
    /**
332
     * Adds the given enumerator (object or value) mapping to the specified data value.
333
     * @param T|null|bool|int|float|string|array<mixed> $enumerator
0 ignored issues
show
Missing parameter comment
Loading history...
334
     * @param mixed                                     $value
0 ignored issues
show
Missing parameter comment
Loading history...
335
     * @return void
336
     * @throws InvalidArgumentException On an invalid given enumerator
337
     * @see add()
338
     */
339 7
    public function offsetSet($enumerator, $value = null): void
340
    {
341 7
        $this->add($enumerator, $value);
342 6
    }
343
344
    /**
345
     * Removes the given enumerator (object or value) mapping.
346
     * @param T|null|bool|int|float|string|array<mixed> $enumerator
0 ignored issues
show
Missing parameter comment
Loading history...
347
     * @return void
348
     * @throws InvalidArgumentException On an invalid given enumerator
349
     * @see remove()
350
     */
351 2
    public function offsetUnset($enumerator): void
352
    {
353 2
        $this->remove($enumerator);
354 2
    }
355
356
    /* IteratorAggregate */
357
358
    /**
359
     * Get a new Iterator.
360
     *
361
     * @return Iterator<T, mixed> Iterator<K extends Enum, V>
362
     */
363 2
    public function getIterator(): Iterator
364
    {
365 2
        $map = $this->map;
366 2
        foreach ($map as $ordinal => $value) {
367 2
            yield ($this->enumeration)::byOrdinal($ordinal) => $value;
368
        }
369 2
    }
370
371
    /* Countable */
372
373
    /**
374
     * Count the number of elements
375
     *
376
     * @return int
377
     */
378 3
    public function count(): int
379
    {
380 3
        return \count($this->map);
381
    }
382
383
    /**
384
     * Tests if the map is empty
385
     *
386
     * @return bool
387
     */
388 1
    public function isEmpty(): bool
389
    {
390 1
        return empty($this->map);
391
    }
392
}
393