Completed
Push — travis_php72 ( e0b08a )
by Marc
10:19 queued 08:19
created

Enum::detectConstants()   B

Complexity

Conditions 6
Paths 3

Size

Total Lines 31
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 6

Importance

Changes 0
Metric Value
cc 6
eloc 17
nc 3
nop 1
dl 0
loc 31
ccs 16
cts 16
cp 1
crap 6
rs 8.439
c 0
b 0
f 0
1
<?php
2
3
namespace MabeEnum;
4
5
use AssertionError;
6
use ReflectionClass;
7
use InvalidArgumentException;
8
use LogicException;
9
10
/**
11
 * Class to implement enumerations for PHP 5 (without SplEnum)
12
 *
13
 * @link http://github.com/marc-mabe/php-enum for the canonical source repository
14
 * @copyright Copyright (c) 2017 Marc Bennewitz
15
 * @license http://github.com/marc-mabe/php-enum/blob/master/LICENSE.txt New BSD License
16
 */
17
abstract class Enum
18
{
19
    /**
20
     * The selected enumerator value
21
     *
22
     * @var null|bool|int|float|string
23
     */
24
    private $value;
25
26
    /**
27
     * The ordinal number of the enumerator
28
     *
29
     * @var null|int
30
     */
31
    private $ordinal;
32
33
    /**
34
     * A map of enumerator names and values by enumeration class
35
     *
36
     * @var array ["$class" => ["$name" => $value, ...], ...]
37
     */
38
    private static $constants = [];
39
40
    /**
41
     * A List of available enumerator names by enumeration class
42
     *
43
     * @var array ["$class" => ["$name0", ...], ...]
44
     */
45
    private static $names = [];
46
47
    /**
48
     * Already instantiated enumerators
49
     *
50
     * @var array ["$class" => ["$name" => $instance, ...], ...]
51
     */
52
    private static $instances = [];
53
54
    /**
55
     * Constructor
56
     *
57
     * @param null|bool|int|float|string $value   The value of the enumerator
58
     * @param int|null                   $ordinal The ordinal number of the enumerator
59
     */
60 111
    final private function __construct($value, $ordinal = null)
61
    {
62 111
        $this->value   = $value;
63 111
        $this->ordinal = $ordinal;
64 111
    }
65
66
    /**
67
     * Get the name of the enumerator
68
     *
69
     * @return string
70
     * @see getName()
71
     */
72 3
    public function __toString()
73
    {
74 3
        return $this->getName();
75
    }
76
77
    /**
78
     * @throws LogicException Enums are not cloneable
79
     *                        because instances are implemented as singletons
80
     */
81 3
    final private function __clone()
82
    {
83 3
        throw new LogicException('Enums are not cloneable');
84
    }
85
86
    /**
87
     * @throws LogicException Enums are not serializable
88
     *                        because instances are implemented as singletons
89
     */
90 3
    final public function __sleep()
91
    {
92 3
        throw new LogicException('Enums are not serializable');
93
    }
94
95
    /**
96
     * @throws LogicException Enums are not serializable
97
     *                        because instances are implemented as singletons
98
     */
99 3
    final public function __wakeup()
100
    {
101 3
        throw new LogicException('Enums are not serializable');
102
    }
103
104
    /**
105
     * Get the value of the enumerator
106
     *
107
     * @return null|bool|int|float|string
108
     */
109 72
    final public function getValue()
110
    {
111 72
        return $this->value;
112
    }
113
114
    /**
115
     * Get the name of the enumerator
116
     *
117
     * @return string
118
     */
119 27
    final public function getName()
120
    {
121 27
        $ordinal = $this->ordinal !== null ? $this->ordinal : $this->getOrdinal();
122 27
        return self::$names[static::class][$ordinal];
123
    }
124
125
    /**
126
     * Get the ordinal number of the enumerator
127
     *
128
     * @return int
129
     */
130 192
    final public function getOrdinal()
131
    {
132 192
        if ($this->ordinal === null) {
133 60
            $ordinal = 0;
134 60
            $value   = $this->value;
135 60
            foreach (self::detectConstants(static::class) as $constValue) {
136 60
                if ($value === $constValue) {
137 60
                    break;
138
                }
139 48
                ++$ordinal;
140
            }
141
142 60
            $this->ordinal = $ordinal;
143
        }
144
145 192
        return $this->ordinal;
146
    }
147
148
    /**
149
     * Compare this enumerator against another and check if it's the same.
150
     *
151
     * @param mixed $enumerator
152
     * @return bool
153
     */
154 6
    final public function is($enumerator)
155
    {
156 6
        return $this === $enumerator || $this->value === $enumerator
157
158
            // The following additional conditions are required only because of the issue of serializable singletons
159 6
            || ($enumerator instanceof static
160 6
                && \get_class($enumerator) === static::class
161 6
                && $enumerator->value === $this->value
162
            );
163
    }
164
165
    /**
166
     * Get an enumerator instance of the given enumerator value or instance
167
     *
168
     * @param static|null|bool|int|float|string $enumerator
169
     * @return static
170
     * @throws InvalidArgumentException On an unknwon or invalid value
171
     * @throws LogicException           On ambiguous constant values
172
     */
173 228
    final public static function get($enumerator)
174
    {
175 228
        if ($enumerator instanceof static && \get_class($enumerator) === static::class) {
176 84
            return $enumerator;
177
        }
178
179 165
        return static::byValue($enumerator);
180
    }
181
182
    /**
183
     * Get an enumerator instance by the given value
184
     *
185
     * @param mixed $value
186
     * @return static
187
     * @throws InvalidArgumentException On an unknwon or invalid value
188
     * @throws LogicException           On ambiguous constant values
189
     */
190 165
    final public static function byValue($value)
191
    {
192 165
        $constants = self::detectConstants(static::class);
193 159
        $name      = \array_search($value, $constants, true);
194 159
        if ($name === false) {
195 30
            throw new InvalidArgumentException(sprintf(
196 30
                'Unknown value %s for enumeration %s',
197 30
                \is_scalar($value)
198 15
                    ? \var_export($value, true)
199 30
                    : 'of type ' . (\is_object($value) ? \get_class($value) : \gettype($value)),
200 30
                static::class
201
            ));
202
        }
203
204 132
        if (!isset(self::$instances[static::class][$name])) {
205 51
            self::$instances[static::class][$name] = new static($constants[$name]);
206
        }
207
208 132
        return self::$instances[static::class][$name];
209
    }
210
211
    /**
212
     * Get an enumerator instance by the given name
213
     *
214
     * @param string $name The name of the enumerator
215
     * @return static
216
     * @throws InvalidArgumentException On an invalid or unknown name
217
     * @throws LogicException           On ambiguous values
218
     */
219 135
    final public static function byName($name)
220
    {
221 135
        $name = (string) $name;
222 135 View Code Duplication
        if (isset(self::$instances[static::class][$name])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
223 96
            return self::$instances[static::class][$name];
224
        }
225
226 54
        $const = static::class . '::' . $name;
227 54
        if (!\defined($const)) {
228 3
            throw new InvalidArgumentException($const . ' not defined');
229
        }
230
231 51
        return self::$instances[static::class][$name] = new static(\constant($const));
232
    }
233
234
    /**
235
     * Get an enumeration instance by the given ordinal number
236
     *
237
     * @param int $ordinal The ordinal number or the enumerator
238
     * @return static
239
     * @throws InvalidArgumentException On an invalid ordinal number
240
     * @throws LogicException           On ambiguous values
241
     */
242 90
    final public static function byOrdinal($ordinal)
243
    {
244 90
        $ordinal = (int) $ordinal;
245
246 90
        if (!isset(self::$names[static::class])) {
247 6
            self::detectConstants(static::class);
248
        }
249
250 90
        if (!isset(self::$names[static::class][$ordinal])) {
251 3
            throw new InvalidArgumentException(\sprintf(
252 3
                'Invalid ordinal number, must between 0 and %s',
253 3
                \count(self::$names[static::class]) - 1
254
            ));
255
        }
256
257 87
        $name = self::$names[static::class][$ordinal];
258 87 View Code Duplication
        if (isset(self::$instances[static::class][$name])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
259 81
            return self::$instances[static::class][$name];
260
        }
261
262 9
        $const = static::class . '::' . $name;
263 9
        return self::$instances[static::class][$name] = new static(\constant($const), $ordinal);
264
    }
265
266
    /**
267
     * Get a list of enumerator instances ordered by ordinal number
268
     *
269
     * @return static[]
270
     */
271 48
    final public static function getEnumerators()
272
    {
273 48
        if (!isset(self::$names[static::class])) {
274 3
            self::detectConstants(static::class);
275
        }
276 48
        return \array_map([static::class, 'byName'], self::$names[static::class]);
277
    }
278
279
    /**
280
     * Get a list of enumerator values ordered by ordinal number
281
     *
282
     * @return mixed[]
283
     */
284 3
    final public static function getValues()
285
    {
286 3
        return \array_values(self::detectConstants(static::class));
287
    }
288
289
    /**
290
     * Get a list of enumerator names ordered by ordinal number
291
     *
292
     * @return string[]
293
     */
294 6
    final public static function getNames()
295
    {
296 6
        if (!isset(self::$names[static::class])) {
297 3
            self::detectConstants(static::class);
298
        }
299 6
        return self::$names[static::class];
300
    }
301
    /*
302
     * Get a list of enumerator ordinal numbers
303
     *
304
     * @return int[]
305
     */
306 3
    final public static function getOrdinals()
307
    {
308 3
        $count = \count(self::detectConstants(static::class));
309 3
        return $count === 0 ? [] : \range(0, $count - 1);
310
    }
311
312
    /**
313
     * Get all available constants of the called class
314
     *
315
     * @return array
316
     * @throws LogicException On ambiguous constant values
317
     */
318 238
    final public static function getConstants()
319
    {
320 238
        return self::detectConstants(static::class);
321
    }
322
323
    /**
324
     * Is the given enumerator part of this enumeration
325
     * 
326
     * @param static|null|bool|int|float|string $value
327
     * @return bool
328
     */
329 3
    final public static function has($value)
330
    {
331 3
        if ($value instanceof static && \get_class($value) === static::class) {
332 3
            return true;
333
        }
334
335 3
        $constants = self::detectConstants(static::class);
336 3
        return \in_array($value, $constants, true);
337
    }
338
339
    /**
340
     * Detect all public available constants of given enumeration class
341
     *
342
     * @param string $class
343
     * @return array
344
     * @throws AssertionError On ambiguous constant values
345
     */
346 322
    private static function detectConstants($class)
347
    {
348 322
        if (!isset(self::$constants[$class])) {
349 121
            $reflection = new ReflectionClass($class);
350 121
            $publicConstants  = [];
351
352
            do {
353 121
                $scopeConstants = [];
354 121
                if (\PHP_VERSION_ID >= 70100) {
355
                    // Since PHP-7.1 visibility modifiers are allowed for class constants
356
                    // for enumerations we are only interested in public once.
357 82
                    foreach ($reflection->getReflectionConstants() as $reflConstant) {
0 ignored issues
show
Bug introduced by
The method getReflectionConstants() does not exist on ReflectionClass. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

357
                    foreach ($reflection->/** @scrutinizer ignore-call */ getReflectionConstants() as $reflConstant) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
358 80
                        if ($reflConstant->isPublic()) {
359 82
                            $scopeConstants[ $reflConstant->getName() ] = $reflConstant->getValue();
360
                        }
361
                    }
362
                } else {
363
                    // In PHP < 7.1 all class constants were public by definition
364 39
                    $scopeConstants = $reflection->getConstants();
365
                }
366
367 121
                $publicConstants = $scopeConstants + $publicConstants;
368 121
            } while (($reflection = $reflection->getParentClass()) && $reflection->name !== __CLASS__);
369
370 121
            assert(self::noAmbiguousValues($publicConstants));
371
372 115
            self::$constants[$class] = $publicConstants;
373 115
            self::$names[$class] = \array_keys($publicConstants);
374
        }
375
376 316
        return self::$constants[$class];
377
    }
378
379
    /**
380
     * Assert that the given enumeration class doesn't define ambiguous enumerator values
381
     * @param array $constants
382
     * @return bool
383
     */
384 121
    private static function noAmbiguousValues(array $constants)
385
    {
386 121
        foreach ($constants as $value) {
387 118
            $names = \array_keys($constants, $value, true);
388 118
            if (\count($names) > 1) {
389 118
                return false;
390
            }
391
        }
392
393 109
        return true;
394
    }
395
396
    /**
397
     * Get an enumerator instance by the given name.
398
     *
399
     * This will be called automatically on calling a method
400
     * with the same name of a defined enumerator.
401
     *
402
     * @param string $method The name of the enumeraotr (called as method)
403
     * @param array  $args   There should be no arguments
404
     * @return static
405
     * @throws InvalidArgumentException On an invalid or unknown name
406
     * @throws LogicException           On ambiguous constant values
407
     */
408 78
    final public static function __callStatic($method, array $args)
409
    {
410 78
        return self::byName($method);
411
    }
412
}
413