Passed
Push — opt_Enum_detectConstnats ( 87a4b7...42f65d )
by Marc
09:30
created

Enum::noAmbiguousValues()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 6
nc 3
nop 1
dl 0
loc 11
ccs 8
cts 8
cp 1
crap 3
rs 9.4285
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 108
    final private function __construct($value, $ordinal = null)
61
    {
62 108
        $this->value   = $value;
63 108
        $this->ordinal = $ordinal;
64 108
    }
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 69
    final public function getValue()
110
    {
111 69
        return $this->value;
112
    }
113
114
    /**
115
     * Get the name of the enumerator
116
     *
117
     * @return string
118
     */
119 24
    final public function getName()
120
    {
121 24
        $ordinal = $this->ordinal !== null ? $this->ordinal : $this->getOrdinal();
122 24
        return self::$names[static::class][$ordinal];
123
    }
124
125
    /**
126
     * Get the ordinal number of the enumerator
127
     *
128
     * @return int
129
     */
130 189
    final public function getOrdinal()
131
    {
132 189
        if ($this->ordinal === null) {
133 57
            $ordinal = 0;
134 57
            $value   = $this->value;
135 57
            foreach (self::detectConstants(static::class) as $constValue) {
136 57
                if ($value === $constValue) {
137 57
                    break;
138
                }
139 48
                ++$ordinal;
140 19
            }
141
142 57
            $this->ordinal = $ordinal;
143 19
        }
144
145 189
        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 2
            );
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 225
    final public static function get($enumerator)
174
    {
175 225
        if ($enumerator instanceof static && \get_class($enumerator) === static::class) {
176 84
            return $enumerator;
177
        }
178
179 162
        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 162
    final public static function byValue($value)
191
    {
192 162
        $constants = self::detectConstants(static::class);
193 156
        $name      = \array_search($value, $constants, true);
194 156 View Code Duplication
        if ($name === false) {
195 27
            $message = \is_scalar($value)
196 19
                ? 'Unknown value ' . \var_export($value, true)
197 27
                : 'Invalid value of type ' . (\is_object($value) ? \get_class($value) : \gettype($value));
198 27
            throw new InvalidArgumentException($message);
199
        }
200
201 132
        if (!isset(self::$instances[static::class][$name])) {
202 51
            self::$instances[static::class][$name] = new static($constants[$name]);
203 17
        }
204
205 132
        return self::$instances[static::class][$name];
206
    }
207
208
    /**
209
     * Get an enumerator instance by the given name
210
     *
211
     * @param string $name The name of the enumerator
212
     * @return static
213
     * @throws InvalidArgumentException On an invalid or unknown name
214
     * @throws LogicException           On ambiguous values
215
     */
216 132
    final public static function byName($name)
217
    {
218 132
        $name = (string) $name;
219 132 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...
220 96
            return self::$instances[static::class][$name];
221
        }
222
223 51
        $const = static::class . '::' . $name;
224 51
        if (!\defined($const)) {
225 3
            throw new InvalidArgumentException($const . ' not defined');
226
        }
227
228 48
        return self::$instances[static::class][$name] = new static(\constant($const));
229
    }
230
231
    /**
232
     * Get an enumeration instance by the given ordinal number
233
     *
234
     * @param int $ordinal The ordinal number or the enumerator
235
     * @return static
236
     * @throws InvalidArgumentException On an invalid ordinal number
237
     * @throws LogicException           On ambiguous values
238
     */
239 90
    final public static function byOrdinal($ordinal)
240
    {
241 90
        $ordinal = (int) $ordinal;
242
243 90
        if (!isset(self::$names[static::class])) {
244 6
            self::detectConstants(static::class);
245 2
        }
246
247 90
        if (!isset(self::$names[static::class][$ordinal])) {
248 3
            throw new InvalidArgumentException(\sprintf(
249 3
                'Invalid ordinal number, must between 0 and %s',
250 3
                \count(self::$names[static::class]) - 1
251 1
            ));
252
        }
253
254 87
        $name = self::$names[static::class][$ordinal];
255 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...
256 81
            return self::$instances[static::class][$name];
257
        }
258
259 9
        $const = static::class . '::' . $name;
260 9
        return self::$instances[static::class][$name] = new static(\constant($const), $ordinal);
261
    }
262
263
    /**
264
     * Get a list of enumerator instances ordered by ordinal number
265
     *
266
     * @return static[]
267
     */
268 48
    final public static function getEnumerators()
269
    {
270 48
        if (!isset(self::$names[static::class])) {
271 3
            self::detectConstants(static::class);
272 1
        }
273 48
        return \array_map([static::class, 'byName'], self::$names[static::class]);
274
    }
275
276
    /**
277
     * Get a list of enumerator values ordered by ordinal number
278
     *
279
     * @return mixed[]
280
     */
281 3
    final public static function getValues()
282
    {
283 3
        return \array_values(self::detectConstants(static::class));
284
    }
285
286
    /**
287
     * Get a list of enumerator names ordered by ordinal number
288
     *
289
     * @return string[]
290
     */
291 6
    final public static function getNames()
292
    {
293 6
        if (!isset(self::$names[static::class])) {
294 3
            self::detectConstants(static::class);
295 1
        }
296 6
        return self::$names[static::class];
297
    }
298
    /*
299
     * Get a list of enumerator ordinal numbers
300
     *
301
     * @return int[]
302
     */
303 3
    final public static function getOrdinals()
304
    {
305 3
        $count = \count(self::detectConstants(static::class));
306 3
        return $count === 0 ? [] : \range(0, $count - 1);
307
    }
308
309
    /**
310
     * Get all available constants of the called class
311
     *
312
     * @return array
313
     * @throws LogicException On ambiguous constant values
314
     */
315 236
    final public static function getConstants()
316
    {
317 236
        return self::detectConstants(static::class);
318
    }
319
320
    /**
321
     * Is the given enumerator part of this enumeration
322
     * 
323
     * @param static|null|bool|int|float|string $value
324
     * @return bool
325
     */
326 3
    final public static function has($value)
327
    {
328 3
        if ($value instanceof static && \get_class($value) === static::class) {
329 3
            return true;
330
        }
331
332 3
        $constants = self::detectConstants(static::class);
333 3
        return \in_array($value, $constants, true);
334
    }
335
336
    /**
337
     * Detect all public available constants of given enumeration class
338
     *
339
     * @param string $class
340
     * @return array
341
     * @throws AssertionError On ambiguous constant values
342
     */
343 314
    private static function detectConstants($class)
344
    {
345 314
        if (!isset(self::$constants[$class])) {
346 113
            $reflection = new ReflectionClass($class);
347 113
            $publicConstants  = [];
348
349
            do {
350 113
                $scopeConstants = [];
351 113
                if (\PHP_VERSION_ID >= 70100) {
352
                    // Since PHP-7.1 visibility modifiers are allowed for class constants
353
                    // for enumerations we are only interested in public once.
354 39
                    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

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