Test Failed
Push — opt_Enum_detectConstnats ( ef883a )
by Marc
01:57
created

Enum::detectConstants()   B

Complexity

Conditions 6
Paths 3

Size

Total Lines 31
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 17
nc 3
nop 1
dl 0
loc 31
rs 8.439
c 0
b 0
f 0
1
<?php
2
3
namespace MabeEnum;
4
5
use ReflectionClass;
6
use InvalidArgumentException;
7
use LogicException;
8
9
/**
10
 * Class to implement enumerations for PHP 5 (without SplEnum)
11
 *
12
 * @link http://github.com/marc-mabe/php-enum for the canonical source repository
13
 * @copyright Copyright (c) 2017 Marc Bennewitz
14
 * @license http://github.com/marc-mabe/php-enum/blob/master/LICENSE.txt New BSD License
15
 */
16
abstract class Enum
17
{
18
    /**
19
     * The selected enumerator value
20
     *
21
     * @var null|bool|int|float|string
22
     */
23
    private $value;
24
25
    /**
26
     * The ordinal number of the enumerator
27
     *
28
     * @var null|int
29
     */
30
    private $ordinal;
31
32
    /**
33
     * A map of enumerator names and values by enumeration class
34
     *
35
     * @var array ["$class" => ["$name" => $value, ...], ...]
36
     */
37
    private static $constants = [];
38
39
    /**
40
     * A List of available enumerator names by enumeration class
41
     *
42
     * @var array ["$class" => ["$name0", ...], ...]
43
     */
44
    private static $names = [];
45
46
    /**
47
     * Already instantiated enumerators
48
     *
49
     * @var array ["$class" => ["$name" => $instance, ...], ...]
50
     */
51
    private static $instances = [];
52
53
    /**
54
     * Constructor
55
     *
56
     * @param null|bool|int|float|string $value   The value of the enumerator
57
     * @param int|null                   $ordinal The ordinal number of the enumerator
58
     */
59
    final private function __construct($value, $ordinal = null)
60
    {
61
        $this->value   = $value;
62
        $this->ordinal = $ordinal;
63
    }
64
65
    /**
66
     * Get the name of the enumerator
67
     *
68
     * @return string
69
     * @see getName()
70
     */
71
    public function __toString()
72
    {
73
        return $this->getName();
74
    }
75
76
    /**
77
     * @throws LogicException Enums are not cloneable
78
     *                        because instances are implemented as singletons
79
     */
80
    final private function __clone()
81
    {
82
        throw new LogicException('Enums are not cloneable');
83
    }
84
85
    /**
86
     * @throws LogicException Enums are not serializable
87
     *                        because instances are implemented as singletons
88
     */
89
    final public function __sleep()
90
    {
91
        throw new LogicException('Enums are not serializable');
92
    }
93
94
    /**
95
     * @throws LogicException Enums are not serializable
96
     *                        because instances are implemented as singletons
97
     */
98
    final public function __wakeup()
99
    {
100
        throw new LogicException('Enums are not serializable');
101
    }
102
103
    /**
104
     * Get the value of the enumerator
105
     *
106
     * @return null|bool|int|float|string
107
     */
108
    final public function getValue()
109
    {
110
        return $this->value;
111
    }
112
113
    /**
114
     * Get the name of the enumerator
115
     *
116
     * @return string
117
     */
118
    final public function getName()
119
    {
120
        $ordinal = $this->ordinal !== null ? $this->ordinal : $this->getOrdinal();
121
        return self::$names[static::class][$ordinal];
122
    }
123
124
    /**
125
     * Get the ordinal number of the enumerator
126
     *
127
     * @return int
128
     */
129
    final public function getOrdinal()
130
    {
131
        if ($this->ordinal === null) {
132
            $ordinal = 0;
133
            $value   = $this->value;
134
            foreach (self::detectConstants(static::class) as $constValue) {
135
                if ($value === $constValue) {
136
                    break;
137
                }
138
                ++$ordinal;
139
            }
140
141
            $this->ordinal = $ordinal;
142
        }
143
144
        return $this->ordinal;
145
    }
146
147
    /**
148
     * Compare this enumerator against another and check if it's the same.
149
     *
150
     * @param mixed $enumerator
151
     * @return bool
152
     */
153
    final public function is($enumerator)
154
    {
155
        return $this === $enumerator || $this->value === $enumerator
156
157
            // The following additional conditions are required only because of the issue of serializable singletons
158
            || ($enumerator instanceof static
159
                && \get_class($enumerator) === static::class
160
                && $enumerator->value === $this->value
161
            );
162
    }
163
164
    /**
165
     * Get an enumerator instance of the given enumerator value or instance
166
     *
167
     * @param static|null|bool|int|float|string $enumerator
168
     * @return static
169
     * @throws InvalidArgumentException On an unknwon or invalid value
170
     * @throws LogicException           On ambiguous constant values
171
     */
172
    final public static function get($enumerator)
173
    {
174
        if ($enumerator instanceof static && \get_class($enumerator) === static::class) {
175
            return $enumerator;
176
        }
177
178
        return static::byValue($enumerator);
179
    }
180
181
    /**
182
     * Get an enumerator instance by the given value
183
     *
184
     * @param mixed $value
185
     * @return static
186
     * @throws InvalidArgumentException On an unknwon or invalid value
187
     * @throws LogicException           On ambiguous constant values
188
     */
189
    final public static function byValue($value)
190
    {
191
        $constants = self::detectConstants(static::class);
192
        $name      = \array_search($value, $constants, true);
193 View Code Duplication
        if ($name === false) {
194
            $message = \is_scalar($value)
195
                ? 'Unknown value ' . \var_export($value, true)
196
                : 'Invalid value of type ' . (\is_object($value) ? \get_class($value) : \gettype($value));
197
            throw new InvalidArgumentException($message);
198
        }
199
200
        if (!isset(self::$instances[static::class][$name])) {
201
            self::$instances[static::class][$name] = new static($constants[$name]);
202
        }
203
204
        return self::$instances[static::class][$name];
205
    }
206
207
    /**
208
     * Get an enumerator instance by the given name
209
     *
210
     * @param string $name The name of the enumerator
211
     * @return static
212
     * @throws InvalidArgumentException On an invalid or unknown name
213
     * @throws LogicException           On ambiguous values
214
     */
215
    final public static function byName($name)
216
    {
217
        $name = (string) $name;
218 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...
219
            return self::$instances[static::class][$name];
220
        }
221
222
        $const = static::class . '::' . $name;
223
        if (!\defined($const)) {
224
            throw new InvalidArgumentException($const . ' not defined');
225
        }
226
227
        return self::$instances[static::class][$name] = new static(\constant($const));
228
    }
229
230
    /**
231
     * Get an enumeration instance by the given ordinal number
232
     *
233
     * @param int $ordinal The ordinal number or the enumerator
234
     * @return static
235
     * @throws InvalidArgumentException On an invalid ordinal number
236
     * @throws LogicException           On ambiguous values
237
     */
238
    final public static function byOrdinal($ordinal)
239
    {
240
        $ordinal = (int) $ordinal;
241
242
        if (!isset(self::$names[static::class])) {
243
            self::detectConstants(static::class);
244
        }
245
246
        if (!isset(self::$names[static::class][$ordinal])) {
247
            throw new InvalidArgumentException(\sprintf(
248
                'Invalid ordinal number, must between 0 and %s',
249
                \count(self::$names[static::class]) - 1
250
            ));
251
        }
252
253
        $name = self::$names[static::class][$ordinal];
254 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...
255
            return self::$instances[static::class][$name];
256
        }
257
258
        $const = static::class . '::' . $name;
259
        return self::$instances[static::class][$name] = new static(\constant($const), $ordinal);
260
    }
261
262
    /**
263
     * Get a list of enumerator instances ordered by ordinal number
264
     *
265
     * @return static[]
266
     */
267
    final public static function getEnumerators()
268
    {
269
        if (!isset(self::$names[static::class])) {
270
            self::detectConstants(static::class);
271
        }
272
        return \array_map([static::class, 'byName'], self::$names[static::class]);
273
    }
274
275
    /**
276
     * Get a list of enumerator values ordered by ordinal number
277
     *
278
     * @return mixed[]
279
     */
280
    final public static function getValues()
281
    {
282
        return \array_values(self::detectConstants(static::class));
283
    }
284
285
    /**
286
     * Get a list of enumerator names ordered by ordinal number
287
     *
288
     * @return string[]
289
     */
290
    final public static function getNames()
291
    {
292
        if (!isset(self::$names[static::class])) {
293
            self::detectConstants(static::class);
294
        }
295
        return self::$names[static::class];
296
    }
297
    /*
298
     * Get a list of enumerator ordinal numbers
299
     *
300
     * @return int[]
301
     */
302
    final public static function getOrdinals()
303
    {
304
        $count = \count(self::detectConstants(static::class));
305
        return $count === 0 ? [] : \range(0, $count - 1);
306
    }
307
308
    /**
309
     * Get all available constants of the called class
310
     *
311
     * @return array
312
     * @throws LogicException On ambiguous constant values
313
     */
314
    final public static function getConstants()
315
    {
316
        return self::detectConstants(static::class);
317
    }
318
319
    /**
320
     * Is the given enumerator part of this enumeration
321
     * 
322
     * @param static|null|bool|int|float|string $value
323
     * @return bool
324
     */
325
    final public static function has($value)
326
    {
327
        if ($value instanceof static && \get_class($value) === static::class) {
328
            return true;
329
        }
330
331
        $constants = self::detectConstants(static::class);
332
        return \in_array($value, $constants, true);
333
    }
334
335
    /**
336
     * Detect all public available constants of given enumeration class
337
     *
338
     * @param string $class
339
     * @return array
340
     * @throws LogicException On ambiguous constant values
341
     */
342
    private static function detectConstants($class)
343
    {
344
        if (!isset(self::$constants[$class])) {
345
            $reflection = new ReflectionClass($class);
346
            $constants  = [];
347
348
            do {
349
                $scopeConstants = [];
350
                if (\PHP_VERSION_ID >= 70100) {
351
                    // Since PHP-7.1 visibility modifiers are allowed for class constants
352
                    // for enumerations we are only interested in public once.
353
                    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

353
                    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...
354
                        if ($reflConstant->isPublic()) {
355
                            $scopeConstants[ $reflConstant->getName() ] = $reflConstant->getValue();
356
                        }
357
                    }
358
                } else {
359
                    // In PHP < 7.1 all class constants were public by definition
360
                    $scopeConstants = $reflection->getConstants();
361
                }
362
363
                $constants = $scopeConstants + $constants;
364
            } while (($reflection = $reflection->getParentClass()) && $reflection->name !== __CLASS__);
365
366
            self::$constants[$class] = $constants;
367
            self::$names[$class] = \array_keys($constants);
368
369
            assert('self::assertNonAmbiguous($class)');
370
        }
371
372
        return self::$constants[$class];
373
    }
374
375
    /**
376
     * Assert that the given enumeration class doesn't define ambiguous enumerator values
377
     * @param string $class
378
     * @return bool
379
     */
380
    private static function assertNonAmbiguous($class)
0 ignored issues
show
Unused Code introduced by
The method assertNonAmbiguous() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
381
    {
382
        $constants = self::$constants[$class];
383
        $ambiguous = [];
384
        foreach ($constants as $value) {
385
            $names = \array_keys($constants, $value, true);
386
            if (\count($names) > 1) {
387
                $ambiguous[\var_export($value, true)] = $names;
388
            }
389
        }
390
391
        return empty($ambiguous);
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
    final public static function __callStatic($method, array $args)
407
    {
408
        return self::byName($method);
409
    }
410
}
411