Passed
Push — master ( 93b7bb...5c048f )
by Marc
01:43 queued 11s
created

Enum::__callStatic()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 2
dl 0
loc 3
ccs 0
cts 0
cp 0
crap 2
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 2019 Marc Bennewitz
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
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" => ["$name" => $value, ...], ...]
40
     */
41
    private static $constants = [];
42
43
    /**
44
     * A List of available enumerator names by enumeration class
45
     *
46
     * @var array ["$class" => ["$name0", ...], ...]
47
     */
48
    private static $names = [];
49
50
    /**
51
     * Already instantiated enumerators
52
     *
53
     * @var array ["$class" => ["$name" => $instance, ...], ...]
54
     */
55
    private static $instances = [];
56
57
    /**
58
     * Constructor
59
     *
60
     * @param null|bool|int|float|string|array $value   The value of the enumerator
61 46
     * @param int|null                         $ordinal The ordinal number of the enumerator
62
     */
63 46
    final private function __construct($value, $ordinal = null)
64 46
    {
65 46
        $this->value   = $value;
66
        $this->ordinal = $ordinal;
67
    }
68
69
    /**
70
     * Get the name of the enumerator
71
     *
72
     * @return string
73 1
     * @see getName()
74
     */
75 1
    public function __toString(): string
76
    {
77
        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 1
     *                        because instances are implemented as singletons
83
     */
0 ignored issues
show
introduced by
Missing @return tag in function comment
Loading history...
84 1
    final private function __clone()
85
    {
86
        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 1
     *                        because instances are implemented as singletons
92
     *
93 1
     * @psalm-return never-return
94
     */
0 ignored issues
show
introduced by
Missing @return tag in function comment
Loading history...
95
    final public function __sleep()
96
    {
97
        throw new LogicException('Enums are not serializable');
98
    }
99
100 1
    /**
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 1
     *                        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
    final public function __wakeup()
107
    {
108
        throw new LogicException('Enums are not serializable');
109
    }
110 33
111
    /**
112 33
     * Get the value of the enumerator
113
     *
114
     * @return null|bool|int|float|string|array
115
     */
116
    final public function getValue()
117
    {
118
        return $this->value;
119
    }
120 8
121
    /**
122 8
     * Get the name of the enumerator
123
     *
124
     * @return string
125
     *
126
     * @psalm-return non-empty-string
127
     */
128
    final public function getName()
129
    {
130 111
        return self::$names[static::class][$this->ordinal ?? $this->getOrdinal()];
131
    }
132 111
133 23
    /**
134 23
     * Get the ordinal number of the enumerator
135 23
     *
136 23
     * @return int
137 23
     */
138 23
    final public function getOrdinal()
139
    {
140 18
        if ($this->ordinal === null) {
141
            $ordinal   = 0;
142
            $value     = $this->value;
143 23
            $constants = self::$constants[static::class] ?? static::getConstants();
144
            foreach ($constants as $constValue) {
145
                if ($value === $constValue) {
146 111
                    break;
147
                }
148
                ++$ordinal;
149
            }
150
151
            $this->ordinal = $ordinal;
152
        }
153
154
        return $this->ordinal;
155 2
    }
156
157 2
    /**
158
     * Compare this enumerator against another and check if it's the same.
159
     *
160 2
     * @param static|null|bool|int|float|string|array $enumerator An enumerator object or value
161 2
     * @return bool
162 2
     */
163
    final public function is($enumerator)
164
    {
165
        return $this === $enumerator || $this->value === $enumerator
166
167
            // The following additional conditions are required only because of the issue of serializable singletons
168
            || ($enumerator instanceof static
169
                && \get_class($enumerator) === static::class
170
                && $enumerator->value === $this->value
171
            );
172
    }
173
174 122
    /**
175
     * Get an enumerator instance of the given enumerator value or instance
176 122
     *
177 37
     * @param static|null|bool|int|float|string|array $enumerator An enumerator object or value
178
     * @return static
179
     * @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...
180 95
     * @throws LogicException           On ambiguous constant values
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
181
     *
182
     * @psalm-pure
183
     */
184
    final public static function get($enumerator)
185
    {
186
        if ($enumerator instanceof static && \get_class($enumerator) === static::class) {
187
            return $enumerator;
188
        }
189
190
        return static::byValue($enumerator);
191 95
    }
192
193 95
    /**
194
     * Get an enumerator instance by the given value
195 93
     *
196 93
     * @param null|bool|int|float|string|array $value Enumerator value
197 12
     * @return static
198 12
     * @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...
199 12
     * @throws LogicException           On ambiguous constant values
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
200 7
     *
201 12
     * @psalm-pure
202 12
     */
203
    final public static function byValue($value)
204
    {
205
        $constants = self::$constants[static::class] ?? static::getConstants();
206 84
207 84
        $name = \array_search($value, $constants, true);
208
        if ($name === false) {
209
            throw new InvalidArgumentException(sprintf(
210
                'Unknown value %s for enumeration %s',
211
                \is_scalar($value)
212
                    ? \var_export($value, true)
213
                    : 'of type ' . (\is_object($value) ? \get_class($value) : \gettype($value)),
214
                static::class
215
            ));
216
        }
217
218 61
        return self::$instances[static::class][$name]
219
            ?? 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...
220 61
    }
221 46
222
    /**
223
     * Get an enumerator instance by the given name
224 20
     *
225 20
     * @param string $name The name of the enumerator
226 1
     * @return static
227
     * @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...
228
     * @throws LogicException           On ambiguous values
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
229 19
     *
230 19
     * @psalm-pure
231 18
     */
232
    final public static function byName(string $name)
233
    {
234 18
        if (isset(self::$instances[static::class][$name])) {
235
            return self::$instances[static::class][$name];
236
        }
237
238
        $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...
239
        if (!\defined($const)) {
240
            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...
241
        }
242
243
        assert(
244
            self::noAmbiguousValues(static::getConstants()),
245 41
            'Ambiguous enumerator values detected for ' . static::class
246
        );
247 41
248
        return self::$instances[static::class][$name] = new static(\constant($const));
249 41
    }
250 1
251 1
    /**
252 1
     * Get an enumeration instance by the given ordinal number
253 1
     *
254
     * @param int $ordinal The ordinal number of the enumerator
255
     * @return static
256
     * @throws InvalidArgumentException On an invalid ordinal number
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
257 40
     * @throws LogicException           On ambiguous values
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
258 40
     *
259 40
     * @psalm-pure
260
     */
261
    final public static function byOrdinal(int $ordinal)
262
    {
263
        $constants = self::$constants[static::class] ?? static::getConstants();
264
265
        if (!isset(self::$names[static::class][$ordinal])) {
266
            throw new InvalidArgumentException(\sprintf(
267 19
                'Invalid ordinal number %s, must between 0 and %s',
268
                $ordinal,
269 19
                \count(self::$names[static::class]) - 1
270 1
            ));
271
        }
272 19
273
        $name = self::$names[static::class][$ordinal];
274
        return self::$instances[static::class][$name]
275
            ?? 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...
276
    }
277
278
    /**
279
     * Get a list of enumerator instances ordered by ordinal number
280 8
     *
281
     * @return static[]
282 8
     *
283
     * @psalm-return list<static>
284
     * @psalm-pure
285
     */
286
    final public static function getEnumerators()
287
    {
288
        if (!isset(self::$names[static::class])) {
289
            static::getConstants();
290 3
        }
291
        return \array_map([static::class, 'byName'], self::$names[static::class]);
292 3
    }
293 1
294
    /**
295 3
     * Get a list of enumerator values ordered by ordinal number
296
     *
297
     * @return mixed[]
298
     *
299
     * @psalm-return list<null|bool|int|float|string|array>
300
     * @psalm-pure
301
     */
302
    final public static function getValues()
303 1
    {
304
        return \array_values(self::$constants[static::class] ?? static::getConstants());
305 1
    }
306 1
307
    /**
308
     * Get a list of enumerator names ordered by ordinal number
309
     *
310
     * @return string[]
311
     *
312
     * @psalm-return list<non-empty-string>
313
     * @psalm-pure
314
     */
315 154
    final public static function getNames()
316
    {
317 154
        if (!isset(self::$names[static::class])) {
318 118
            static::getConstants();
319
        }
320
        return self::$names[static::class];
321 51
    }
322 51
323
    /**
324
     * Get a list of enumerator ordinal numbers
325 51
     *
326
     * @return int[]
327 51
     *
328 50
     * @psalm-return list<int>
329 50
     * @psalm-pure
330
     */
331
    final public static function getOrdinals()
332
    {
333 51
        $count = \count(self::$constants[static::class] ?? static::getConstants());
334 51
        return $count ? \range(0, $count - 1) : [];
335
    }
336 51
337 51
    /**
338 51
     * Get all available constants of the called class
339
     *
340
     * @return mixed[]
341 48
     * @throws LogicException On ambiguous constant values
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
342 48
     *
343
     * @psalm-return array<non-empty-string, null|bool|int|float|string|array>
344
     * @psalm-pure
345
     */
346
    final public static function getConstants()
347
    {
348
        if (isset(self::$constants[static::class])) {
349
            return self::$constants[static::class];
350 54
        }
351
352 54
        $reflection = new ReflectionClass(static::class);
353 53
        $constants  = [];
354 53
355 5
        do {
356
            $scopeConstants = [];
357
            // Enumerators must be defined as public class constants
358
            foreach ($reflection->getReflectionConstants() as $reflConstant) {
359 49
                if ($reflConstant->isPublic()) {
360
                    $scopeConstants[ $reflConstant->getName() ] = $reflConstant->getValue();
361
                }
362
            }
363
364
            $constants = $scopeConstants + $constants;
365
        } while (($reflection = $reflection->getParentClass()) && $reflection->name !== __CLASS__);
366
367
        assert(
368 1
            self::noAmbiguousValues($constants),
369
            'Ambiguous enumerator values detected for ' . static::class
370 1
        );
371 1
372
        self::$names[static::class] = \array_keys($constants);
373
        return self::$constants[static::class] = $constants;
374
    }
375
376
    /**
377
     * Test that the given constants does not contain ambiguous values
378
     * @param array $constants
0 ignored issues
show
Documentation introduced by
Missing parameter comment
Loading history...
379
     * @return bool
380 2
     */
381
    private static function noAmbiguousValues($constants)
0 ignored issues
show
introduced by
Type hint "array" missing for $constants
Loading history...
382 2
    {
383
        foreach ($constants as $value) {
384
            $names = \array_keys($constants, $value, true);
385
            if (\count($names) > 1) {
386
                return false;
387
            }
388
        }
389
390
        return true;
391 1
    }
392
393 1
    /**
394
     * Test if the given enumerator is part of this enumeration
395
     *
396
     * @param static|null|bool|int|float|string|array $enumerator
0 ignored issues
show
Documentation introduced by
Missing parameter comment
Loading history...
397
     * @return bool
398
     *
399
     * @psalm-pure
400
     */
401
    final public static function has($enumerator)
402
    {
403
        return ($enumerator instanceof static && \get_class($enumerator) === static::class)
404
            || static::hasValue($enumerator);
405
    }
406
407
    /**
408 39
     * Test if the given enumerator value is part of this enumeration
409
     *
410 39
     * @param null|bool|int|float|string|array $value
0 ignored issues
show
Documentation introduced by
Missing parameter comment
Loading history...
411
     * @return bool
412
     *
413
     * @psalm-pure
414
     */
415
    final public static function hasValue($value)
416
    {
417
        return \in_array($value, self::$constants[static::class] ?? static::getConstants(), true);
418
    }
419
420
    /**
421
     * Test if the given enumerator name is part of this enumeration
422
     *
423
     * @param string $name
0 ignored issues
show
Documentation introduced by
Missing parameter comment
Loading history...
424
     * @return bool
425
     *
426
     * @psalm-pure
427
     */
428
    final public static function hasName(string $name)
429
    {
430
        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...
431
    }
432
433
    /**
434
     * Get an enumerator instance by the given name.
435
     *
436
     * This will be called automatically on calling a method
437
     * with the same name of a defined enumerator.
438
     *
439
     * @param string $method The name of the enumerator (called as method)
440
     * @param array  $args   There should be no arguments
441
     * @return static
442
     * @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...
443
     * @throws LogicException           On ambiguous constant values
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
444
     *
445
     * @psalm-pure
446
     */
447
    final public static function __callStatic(string $method, array $args)
448
    {
449
        return static::byName($method);
450
    }
451
}
452