Passed
Push — master ( b92096...2e9258 )
by Marc
01:57
created

Enum::__callStatic()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 2
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
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 148
    final private function __construct($value, $ordinal = null)
60
    {
61 148
        $this->value   = $value;
62 148
        $this->ordinal = $ordinal;
63 148
    }
64
65
    /**
66
     * Get the name of the enumerator
67
     *
68
     * @return string
69
     * @see getName()
70
     */
71 4
    public function __toString()
72
    {
73 4
        return $this->getName();
74
    }
75
76
    /**
77
     * @throws LogicException Enums are not cloneable
78
     *                        because instances are implemented as singletons
79
     */
80 4
    final private function __clone()
81
    {
82 4
        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 4
    final public function __sleep()
90
    {
91 4
        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 4
    final public function __wakeup()
99
    {
100 4
        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 96
    final public function getValue()
109
    {
110 96
        return $this->value;
111
    }
112
113
    /**
114
     * Get the name of the enumerator
115
     *
116
     * @return string
117
     */
118 36
    final public function getName()
119
    {
120 36
        $ordinal = $this->ordinal !== null ? $this->ordinal : $this->getOrdinal();
121 36
        return self::$names[static::class][$ordinal];
122
    }
123
124
    /**
125
     * Get the ordinal number of the enumerator
126
     *
127
     * @return int
128
     */
129 256
    final public function getOrdinal()
130
    {
131 256
        if ($this->ordinal === null) {
132 80
            $ordinal = 0;
133 80
            $value   = $this->value;
134 80
            foreach (self::detectConstants(static::class) as $constValue) {
135 80
                if ($value === $constValue) {
136 80
                    break;
137
                }
138 64
                ++$ordinal;
139 20
            }
140
141 80
            $this->ordinal = $ordinal;
142 20
        }
143
144 256
        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 8
    final public function is($enumerator)
154
    {
155 8
        return $this === $enumerator || $this->value === $enumerator
156
157
            // The following additional conditions are required only because of the issue of serializable singletons
158 8
            || ($enumerator instanceof static
159 8
                && \get_class($enumerator) === static::class
160 8
                && $enumerator->value === $this->value
161 2
            );
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 304
    final public static function get($enumerator)
173
    {
174 304
        if ($enumerator instanceof static && \get_class($enumerator) === static::class) {
175 112
            return $enumerator;
176
        }
177
178 220
        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 220
    final public static function byValue($value)
190
    {
191 220
        if (!isset(self::$names[static::class])) {
192 64
            self::detectConstants(static::class);
193 14
        }
194
195 212
        $name = \array_search($value, self::$constants[static::class], true);
196 212
        if ($name === false) {
197 40
            throw new InvalidArgumentException(sprintf(
198 40
                'Unknown value %s for enumeration %s',
199 40
                \is_scalar($value)
200 25
                    ? \var_export($value, true)
201 40
                    : 'of type ' . (\is_object($value) ? \get_class($value) : \gettype($value)),
202 40
                static::class
203 10
            ));
204
        }
205
206 176
        if (!isset(self::$instances[static::class][$name])) {
207 68
            self::$instances[static::class][$name] = new static(self::$constants[static::class][$name]);
208 17
        }
209
210 176
        return self::$instances[static::class][$name];
211
    }
212
213
    /**
214
     * Get an enumerator instance by the given name
215
     *
216
     * @param string $name The name of the enumerator
217
     * @return static
218
     * @throws InvalidArgumentException On an invalid or unknown name
219
     * @throws LogicException           On ambiguous values
220
     */
221 180
    final public static function byName($name)
222
    {
223 180
        $name = (string) $name;
224 180 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...
225 128
            return self::$instances[static::class][$name];
226
        }
227
228 72
        $const = static::class . '::' . $name;
229 72
        if (!\defined($const)) {
230 4
            throw new InvalidArgumentException($const . ' not defined');
231
        }
232
233 68
        return self::$instances[static::class][$name] = new static(\constant($const));
234
    }
235
236
    /**
237
     * Get an enumeration instance by the given ordinal number
238
     *
239
     * @param int $ordinal The ordinal number or the enumerator
240
     * @return static
241
     * @throws InvalidArgumentException On an invalid ordinal number
242
     * @throws LogicException           On ambiguous values
243
     */
244 120
    final public static function byOrdinal($ordinal)
245
    {
246 120
        $ordinal = (int) $ordinal;
247
248 120
        if (!isset(self::$names[static::class])) {
249 8
            self::detectConstants(static::class);
250 2
        }
251
252 120
        if (!isset(self::$names[static::class][$ordinal])) {
253 4
            throw new InvalidArgumentException(\sprintf(
254 4
                'Invalid ordinal number, must between 0 and %s',
255 4
                \count(self::$names[static::class]) - 1
256 1
            ));
257
        }
258
259 116
        $name = self::$names[static::class][$ordinal];
260 116 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...
261 108
            return self::$instances[static::class][$name];
262
        }
263
264 12
        return self::$instances[static::class][$name] = new static(self::$constants[static::class][$name], $ordinal);
265
    }
266
267
    /**
268
     * Get a list of enumerator instances ordered by ordinal number
269
     *
270
     * @return static[]
271
     */
272 64
    final public static function getEnumerators()
273
    {
274 64
        if (!isset(self::$names[static::class])) {
275 4
            self::detectConstants(static::class);
276 1
        }
277 64
        return \array_map([static::class, 'byName'], self::$names[static::class]);
278
    }
279
280
    /**
281
     * Get a list of enumerator values ordered by ordinal number
282
     *
283
     * @return mixed[]
284
     */
285 4
    final public static function getValues()
286
    {
287 4
        return \array_values(self::detectConstants(static::class));
288
    }
289
290
    /**
291
     * Get a list of enumerator names ordered by ordinal number
292
     *
293
     * @return string[]
294
     */
295 8
    final public static function getNames()
296
    {
297 8
        if (!isset(self::$names[static::class])) {
298 4
            self::detectConstants(static::class);
299 1
        }
300 8
        return self::$names[static::class];
301
    }
302
    /*
303
     * Get a list of enumerator ordinal numbers
304
     *
305
     * @return int[]
306
     */
307 4
    final public static function getOrdinals()
308
    {
309 4
        $count = \count(self::detectConstants(static::class));
310 4
        return $count === 0 ? [] : \range(0, $count - 1);
311
    }
312
313
    /**
314
     * Get all available constants of the called class
315
     *
316
     * @return array
317
     * @throws LogicException On ambiguous constant values
318
     */
319 316
    final public static function getConstants()
320
    {
321 316
        return self::detectConstants(static::class);
322
    }
323
324
    /**
325
     * Is the given enumerator part of this enumeration
326
     * 
327
     * @param static|null|bool|int|float|string $value
328
     * @return bool
329
     */
330 4
    final public static function has($value)
331
    {
332 4
        if ($value instanceof static && \get_class($value) === static::class) {
333 4
            return true;
334
        }
335
336 4
        $constants = self::detectConstants(static::class);
337 4
        return \in_array($value, $constants, true);
338
    }
339
340
    /**
341
     * Detect all public available constants of given enumeration class
342
     *
343
     * @param string $class
344
     * @return array
345
     */
346 400
    private static function detectConstants($class)
347
    {
348 400
        if (!isset(self::$constants[$class])) {
349 160
            $reflection = new ReflectionClass($class);
350 160
            $publicConstants  = [];
351
352
            do {
353 160
                $scopeConstants = [];
354 160
                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 78
                    $scopeConstants = $reflection->getConstants();
365
                }
366
367 160
                $publicConstants = $scopeConstants + $publicConstants;
368 160
            } while (($reflection = $reflection->getParentClass()) && $reflection->name !== __CLASS__);
369
370 160
            assert(self::noAmbiguousValues($publicConstants));
371
372 152
            self::$constants[$class] = $publicConstants;
373 152
            self::$names[$class] = \array_keys($publicConstants);
374 37
        }
375
376 392
        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 160
    private static function noAmbiguousValues(array $constants)
385
    {
386 160
        foreach ($constants as $value) {
387 156
            $names = \array_keys($constants, $value, true);
388 156
            if (\count($names) > 1) {
389 122
                return false;
390
            }
391 37
        }
392
393 144
        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 104
    final public static function __callStatic($method, array $args)
409
    {
410 104
        return self::byName($method);
411
    }
412
}
413