Passed
Push — micro-optimizations ( 83b250 )
by Marc
03:59
created

Enum::detectConstants()   A

Complexity

Conditions 6
Paths 2

Size

Total Lines 28
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 6

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
eloc 16
c 1
b 0
f 0
nc 2
nop 1
dl 0
loc 28
ccs 16
cts 16
cp 1
crap 6
rs 9.1111

2 Methods

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