Enum::getValues()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace MabeEnum;
6
7
use ReflectionClass;
8
use InvalidArgumentException;
9
use LogicException;
10
11
/**
12
 * Abstract base enumeration class.
13
 *
14
 * @copyright 2020, Marc Bennewitz
0 ignored issues
show
Coding Style introduced by
@copyright tag must contain a year and the name of the copyright holder
Loading history...
15
 * @license http://github.com/marc-mabe/php-enum/blob/master/LICENSE.txt New BSD License
16
 * @link http://github.com/marc-mabe/php-enum for the canonical source repository
17
 *
18
 * @psalm-immutable
19
 */
20
abstract class Enum
21
{
22
    /**
23
     * The selected enumerator value
24
     *
25
     * @var null|bool|int|float|string|array<mixed>
26
     */
27
    private $value;
0 ignored issues
show
Coding Style introduced by
Expected 1 blank line before member var; 0 found
Loading history...
28
29
    /**
30
     * The ordinal number of the enumerator
31
     *
32
     * @var null|int
33
     */
34
    private $ordinal;
35
36
    /**
37
     * A map of enumerator names and values by enumeration class
38
     *
39
     * @var array<class-string<Enum>, array<string, null|bool|int|float|string|array<mixed>>>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<class-string<Enum>...t|string|array<mixed>>> at position 2 could not be parsed: Unknown type name 'class-string' at position 2 in array<class-string<Enum>, array<string, null|bool|int|float|string|array<mixed>>>.
Loading history...
40
     */
41
    private static $constants = [];
42
43
    /**
44
     * A List of available enumerator names by enumeration class
45
     *
46
     * @var array<class-string<Enum>, string[]>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<class-string<Enum>, string[]> at position 2 could not be parsed: Unknown type name 'class-string' at position 2 in array<class-string<Enum>, string[]>.
Loading history...
47
     */
48
    private static $names = [];
49
50
    /**
51
     * A map of enumerator names and instances by enumeration class
52
     *
53
     * @var array<class-string<Enum>, array<string, Enum>>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<class-string<Enum>, array<string, Enum>> at position 2 could not be parsed: Unknown type name 'class-string' at position 2 in array<class-string<Enum>, array<string, Enum>>.
Loading history...
54
     */
55
    private static $instances = [];
56
57
    /**
58
     * Constructor
59
     *
60
     * @param null|bool|int|float|string|array<mixed> $value   The value of the enumerator
61
     * @param int|null                                $ordinal The ordinal number of the enumerator
62
     */
63 46
    final private function __construct($value, $ordinal = null)
64
    {
65 46
        $this->value   = $value;
66 46
        $this->ordinal = $ordinal;
67 46
    }
68
69
    /**
70
     * Get the name of the enumerator
71
     *
72
     * @return string
73
     * @see getName()
74
     */
75 1
    public function __toString(): string
76
    {
77 1
        return $this->getName();
78
    }
79
80
    /**
81
     * @throws LogicException Enums are not cloneable
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
82
     *                        because instances are implemented as singletons
83
     */
0 ignored issues
show
introduced by
Missing @return tag in function comment
Loading history...
84 1
    final protected function __clone()
85
    {
86 1
        throw new LogicException('Enums are not cloneable');
87
    }
88
89
    /**
90
     * @throws LogicException Enums are not serializable
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
91
     *                        because instances are implemented as singletons
92
     *
93
     * @psalm-return never-return
94
     */
0 ignored issues
show
introduced by
Missing @return tag in function comment
Loading history...
95 1
    final public function __sleep()
96
    {
97 1
        throw new LogicException('Enums are not serializable');
98
    }
99
100
    /**
101
     * @throws LogicException Enums are not serializable
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
102
     *                        because instances are implemented as singletons
103
     *
104
     * @psalm-return never-return
105
     */
0 ignored issues
show
introduced by
Missing @return tag in function comment
Loading history...
106 1
    final public function __wakeup()
107
    {
108 1
        throw new LogicException('Enums are not serializable');
109
    }
110
111
    /**
112
     * Get the value of the enumerator
113
     *
114
     * @return null|bool|int|float|string|array<mixed>
115
     */
116 33
    final public function getValue()
117
    {
118 33
        return $this->value;
119
    }
120
121
    /**
122
     * Get the name of the enumerator
123
     *
124
     * @return string
125
     *
126
     * @phpstan-return string
127
     * @psalm-return non-empty-string
128
     */
129 8
    final public function getName()
130
    {
131 8
        return self::$names[static::class][$this->ordinal ?? $this->getOrdinal()];
132
    }
133
134
    /**
135
     * Get the ordinal number of the enumerator
136
     *
137
     * @return int
138
     */
139 111
    final public function getOrdinal()
140
    {
141 111
        if ($this->ordinal === null) {
142 23
            $ordinal   = 0;
143 23
            $value     = $this->value;
144 23
            $constants = self::$constants[static::class] ?? static::getConstants();
145 23
            foreach ($constants as $constValue) {
146 23
                if ($value === $constValue) {
147 23
                    break;
148
                }
149 18
                ++$ordinal;
150
            }
151
152 23
            $this->ordinal = $ordinal;
153
        }
154
155 111
        return $this->ordinal;
156
    }
157
158
    /**
159
     * Compare this enumerator against another and check if it's the same.
160
     *
161
     * @param static|null|bool|int|float|string|array<mixed> $enumerator An enumerator object or value
162
     * @return bool
163
     */
164 2
    final public function is($enumerator)
165
    {
166 2
        return $this === $enumerator || $this->value === $enumerator
167
168
            // The following additional conditions are required only because of the issue of serializable singletons
169 2
            || ($enumerator instanceof static
170 2
                && \get_class($enumerator) === static::class
171 2
                && $enumerator->value === $this->value
172
            );
173
    }
174
175
    /**
176
     * Get an enumerator instance of the given enumerator value or instance
177
     *
178
     * @param static|null|bool|int|float|string|array<mixed> $enumerator An enumerator object or value
179
     * @return static
180
     * @throws InvalidArgumentException On an unknown or invalid value
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
181
     * @throws LogicException           On ambiguous constant values
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
182
     *
183
     * @psalm-pure
184
     */
185 122
    final public static function get($enumerator)
186
    {
187 122
        if ($enumerator instanceof static) {
188 41
            if (\get_class($enumerator) !== static::class) {
189 4
                throw new InvalidArgumentException(sprintf(
190 4
                    'Invalid value of type %s for enumeration %s',
191 4
                    \get_class($enumerator),
192 4
                    static::class
193
                ));
194
            }
195
196 37
            return $enumerator;
197
        }
198
199 93
        return static::byValue($enumerator);
200
    }
201
202
    /**
203
     * Get an enumerator instance by the given value
204
     *
205
     * @param null|bool|int|float|string|array<mixed> $value Enumerator value
206
     * @return static
207
     * @throws InvalidArgumentException On an unknown or invalid value
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
208
     * @throws LogicException           On ambiguous constant values
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
209
     *
210
     * @psalm-pure
211
     */
212 93
    final public static function byValue($value)
213
    {
214
        /** @var mixed $value */
215
216 93
        $constants = self::$constants[static::class] ?? static::getConstants();
217
218 91
        $name = \array_search($value, $constants, true);
219 91
        if ($name === false) {
220 9
            throw new InvalidArgumentException(sprintf(
221 9
                'Unknown value %s for enumeration %s',
222 9
                \is_scalar($value)
223 7
                    ? \var_export($value, true)
224 9
                    : 'of type ' . (\is_object($value) ? \get_class($value) : \gettype($value)),
225 9
                static::class
226
            ));
227
        }
228
229
        /** @var static $instance */
230 84
        $instance = self::$instances[static::class][$name]
231 84
            ?? self::$instances[static::class][$name] = new static($constants[$name]);
0 ignored issues
show
Coding Style introduced by
Expected 1 space before "??"; newline found
Loading history...
232
233 84
        return $instance;
234
    }
235
236
    /**
237
     * Get an enumerator instance by the given name
238
     *
239
     * @param string $name The name of the enumerator
240
     * @return static
241
     * @throws InvalidArgumentException On an invalid or unknown name
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
242
     * @throws LogicException           On ambiguous values
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
243
     *
244
     * @psalm-pure
245
     */
246 61
    final public static function byName(string $name)
247
    {
248 61
        if (isset(self::$instances[static::class][$name])) {
249
            /** @var static $instance */
250 46
            $instance = self::$instances[static::class][$name];
251 46
            return $instance;
252
        }
253
254 20
        $const = static::class . "::{$name}";
0 ignored issues
show
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $name instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
255 20
        if (!\defined($const)) {
256 1
            throw new InvalidArgumentException("{$const} not defined");
0 ignored issues
show
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $const instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
257
        }
258
259 19
        assert(
260 19
            self::noAmbiguousValues(static::getConstants()),
261 18
            'Ambiguous enumerator values detected for ' . static::class
262
        );
263
264 18
        return self::$instances[static::class][$name] = new static(\constant($const));
265
    }
266
267
    /**
268
     * Get an enumeration instance by the given ordinal number
269
     *
270
     * @param int $ordinal The ordinal number of the enumerator
271
     * @return static
272
     * @throws InvalidArgumentException On an invalid ordinal number
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
273
     * @throws LogicException           On ambiguous values
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
274
     *
275
     * @psalm-pure
276
     */
277 41
    final public static function byOrdinal(int $ordinal)
278
    {
279 41
        $constants = self::$constants[static::class] ?? static::getConstants();
280
281 41
        if (!isset(self::$names[static::class][$ordinal])) {
282 1
            throw new InvalidArgumentException(\sprintf(
283 1
                'Invalid ordinal number %s, must between 0 and %s',
284 1
                $ordinal,
285 1
                \count(self::$names[static::class]) - 1
286
            ));
287
        }
288
289 40
        $name = self::$names[static::class][$ordinal];
290
291
        /** @var static $instance */
292 40
        $instance = self::$instances[static::class][$name]
293 40
            ?? self::$instances[static::class][$name] = new static($constants[$name], $ordinal);
0 ignored issues
show
Coding Style introduced by
Expected 1 space before "??"; newline found
Loading history...
294
295 40
        return $instance;
296
    }
297
298
    /**
299
     * Get a list of enumerator instances ordered by ordinal number
300
     *
301
     * @return static[]
302
     *
303
     * @phpstan-return array<int, static>
304
     * @psalm-return list<static>
305
     * @psalm-pure
306
     */
307 19
    final public static function getEnumerators()
308
    {
309 19
        if (!isset(self::$names[static::class])) {
310 1
            static::getConstants();
311
        }
312
313
        /** @var callable $byNameFn */
314 19
        $byNameFn = [static::class, 'byName'];
315 19
        return \array_map($byNameFn, self::$names[static::class]);
316
    }
317
318
    /**
319
     * Get a list of enumerator values ordered by ordinal number
320
     *
321
     * @return (null|bool|int|float|string|array)[]
322
     *
323
     * @phpstan-return array<int, null|bool|int|float|string|array>
324
     * @psalm-return list<null|bool|int|float|string|array>
325
     * @psalm-pure
326
     */
327 8
    final public static function getValues()
328
    {
329 8
        return \array_values(self::$constants[static::class] ?? static::getConstants());
330
    }
331
332
    /**
333
     * Get a list of enumerator names ordered by ordinal number
334
     *
335
     * @return string[]
336
     *
337
     * @phpstan-return array<int, string>
338
     * @psalm-return list<non-empty-string>
339
     * @psalm-pure
340
     */
341 3
    final public static function getNames()
342
    {
343 3
        if (!isset(self::$names[static::class])) {
344 1
            static::getConstants();
345
        }
346 3
        return self::$names[static::class];
347
    }
348
349
    /**
350
     * Get a list of enumerator ordinal numbers
351
     *
352
     * @return int[]
353
     *
354
     * @phpstan-return array<int, int>
355
     * @psalm-return list<int>
356
     * @psalm-pure
357
     */
358 1
    final public static function getOrdinals()
359
    {
360 1
        $count = \count(self::$constants[static::class] ?? static::getConstants());
361 1
        return $count ? \range(0, $count - 1) : [];
362
    }
363
364
    /**
365
     * Get all available constants of the called class
366
     *
367
     * @return (null|bool|int|float|string|array)[]
368
     * @throws LogicException On ambiguous constant values
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
369
     *
370
     * @phpstan-return array<string, null|bool|int|float|string|array>
371
     * @psalm-return array<non-empty-string, null|bool|int|float|string|array>
372
     * @psalm-pure
373
     */
374 154
    final public static function getConstants()
375
    {
376 154
        if (isset(self::$constants[static::class])) {
377 118
            return self::$constants[static::class];
378
        }
379
380 51
        $reflection = new ReflectionClass(static::class);
381 51
        $constants  = [];
382
383
        do {
384 51
            $scopeConstants = [];
385
            // Enumerators must be defined as public class constants
386 51
            foreach ($reflection->getReflectionConstants() as $reflConstant) {
387 50
                if ($reflConstant->isPublic()) {
388 50
                    $scopeConstants[ $reflConstant->getName() ] = $reflConstant->getValue();
389
                }
390
            }
391
392 51
            $constants = $scopeConstants + $constants;
393 51
        } while (($reflection = $reflection->getParentClass()) && $reflection->name !== __CLASS__);
394
395 51
        assert(
396 51
            self::noAmbiguousValues($constants),
397 51
            'Ambiguous enumerator values detected for ' . static::class
398
        );
399
400 48
        self::$names[static::class] = \array_keys($constants);
401 48
        return self::$constants[static::class] = $constants;
402
    }
403
404
    /**
405
     * Test that the given constants does not contain ambiguous values
406
     * @param array<string, null|bool|int|float|string|array<mixed>> $constants
0 ignored issues
show
Documentation introduced by
Missing parameter comment
Loading history...
407
     * @return bool
408
     */
409 54
    private static function noAmbiguousValues($constants)
410
    {
411 54
        foreach ($constants as $value) {
412 53
            $names = \array_keys($constants, $value, true);
413 53
            if (\count($names) > 1) {
414 5
                return false;
415
            }
416
        }
417
418 49
        return true;
419
    }
420
421
    /**
422
     * Test if the given enumerator is part of this enumeration
423
     *
424
     * @param static|null|bool|int|float|string|array<mixed> $enumerator
0 ignored issues
show
Documentation introduced by
Missing parameter comment
Loading history...
425
     * @return bool
426
     *
427
     * @psalm-pure
428
     */
429 1
    final public static function has($enumerator)
430
    {
431 1
        if ($enumerator instanceof static) {
432 1
            return \get_class($enumerator) === static::class;
433
        }
434
435 1
        return static::hasValue($enumerator);
436
    }
437
438
    /**
439
     * Test if the given enumerator value is part of this enumeration
440
     *
441
     * @param null|bool|int|float|string|array<mixed> $value
0 ignored issues
show
Documentation introduced by
Missing parameter comment
Loading history...
442
     * @return bool
443
     *
444
     * @psalm-pure
445
     */
446 2
    final public static function hasValue($value)
447
    {
448 2
        return \in_array($value, self::$constants[static::class] ?? static::getConstants(), true);
449
    }
450
451
    /**
452
     * Test if the given enumerator name is part of this enumeration
453
     *
454
     * @param string $name
0 ignored issues
show
Documentation introduced by
Missing parameter comment
Loading history...
455
     * @return bool
456
     *
457
     * @psalm-pure
458
     */
459 1
    final public static function hasName(string $name)
460
    {
461 1
        return \defined("static::{$name}");
0 ignored issues
show
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $name instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
462
    }
463
464
    /**
465
     * Get an enumerator instance by the given name.
466
     *
467
     * This will be called automatically on calling a method
468
     * with the same name of a defined enumerator.
469
     *
470
     * @param string       $method The name of the enumerator (called as method)
471
     * @param array<mixed> $args   There should be no arguments
472
     * @return static
473
     * @throws InvalidArgumentException On an invalid or unknown name
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
474
     * @throws LogicException           On ambiguous constant values
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
475
     *
476
     * @psalm-pure
477
     */
478 39
    final public static function __callStatic(string $method, array $args)
479
    {
480 39
        return static::byName($method);
481
    }
482
}
483