Completed
Pull Request — master (#94)
by Marc
03:13 queued 01:19
created

Enum::getValue()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 1
cts 1
cp 1
crap 1
rs 10
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 108
     */
60
    final private function __construct($value, $ordinal = null)
61 108
    {
62 108
        $this->value   = $value;
63 108
        $this->ordinal = $ordinal;
64
    }
65
66
    /**
67
     * Get the name of the enumerator
68
     *
69
     * @return string
70
     * @see getName()
71 3
     */
72
    public function __toString()
73 3
    {
74
        return $this->getName();
75
    }
76
77
    /**
78
     * @throws LogicException Enums are not cloneable
79
     *                        because instances are implemented as singletons
80 3
     */
81
    final private function __clone()
82 3
    {
83
        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 3
     */
90
    final public function __sleep()
91 3
    {
92
        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 3
     */
99
    final public function __wakeup()
100 3
    {
101
        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 69
     */
109
    final public function getValue()
110 69
    {
111
        return $this->value;
112
    }
113
114
    /**
115
     * Get the name of the enumerator
116
     *
117
     * @return string
118 24
     */
119
    final public function getName()
120 24
    {
121 24
        $ordinal = $this->ordinal !== null ? $this->ordinal : $this->getOrdinal();
122
        return self::$names[static::class][$ordinal];
123
    }
124
125
    /**
126
     * Get the ordinal number of the enumerator
127
     *
128
     * @return int
129 189
     */
130
    final public function getOrdinal()
131 189
    {
132 57
        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
                    break;
138 48
                }
139 19
                ++$ordinal;
140
            }
141 57
142 19
            $this->ordinal = $ordinal;
143
        }
144 189
145
        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 6
     */
154
    final public function is($enumerator)
155 6
    {
156
        return $this === $enumerator || $this->value === $enumerator
157
158 6
            // 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 2
                && $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 219
     */
173
    final public static function get($enumerator)
174 219
    {
175 84
        if ($enumerator instanceof static && \get_class($enumerator) === static::class) {
176
            return $enumerator;
177
        }
178 156
179
        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 156
     */
190
    final public static function byValue($value)
191 156
    {
192 150
        $constants = self::detectConstants(static::class);
193 150
        $name      = \array_search($value, $constants, true);
194 21 View Code Duplication
        if ($name === false) {
195 13
            $message = \is_scalar($value)
196 21
                ? 'Unknown value ' . \var_export($value, true)
197 21
                : 'Invalid value of type ' . (\is_object($value) ? \get_class($value) : \gettype($value));
198
            throw new InvalidArgumentException($message);
199
        }
200 132
201 51
        if (!isset(self::$instances[static::class][$name])) {
202 17
            self::$instances[static::class][$name] = new static($constants[$name]);
203
        }
204 132
205
        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 132
     */
216
    final public static function byName($name)
217 132
    {
218 132
        $name = (string) $name;
219 96 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
            return self::$instances[static::class][$name];
221
        }
222 51
223 51
        $const = static::class . '::' . $name;
224 3
        if (!\defined($const)) {
225
            throw new InvalidArgumentException($const . ' not defined');
226
        }
227 48
228
        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 90
     */
239
    final public static function byOrdinal($ordinal)
240 90
    {
241
        $ordinal = (int) $ordinal;
242 90
243 6
        if (!isset(self::$names[static::class])) {
244 2
            self::detectConstants(static::class);
245
        }
246 90
247 3
        if (!isset(self::$names[static::class][$ordinal])) {
248 3
            throw new InvalidArgumentException(\sprintf(
249 3
                'Invalid ordinal number, must between 0 and %s',
250 1
                \count(self::$names[static::class]) - 1
251
            ));
252
        }
253 87
254 87
        $name = self::$names[static::class][$ordinal];
255 81 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
            return self::$instances[static::class][$name];
257
        }
258 9
259 9
        $const = static::class . '::' . $name;
260
        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 48
     */
268
    final public static function getEnumerators()
269 48
    {
270 3
        if (!isset(self::$names[static::class])) {
271 1
            self::detectConstants(static::class);
272 48
        }
273
        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 3
     */
281
    final public static function getValues()
282 3
    {
283
        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 6
     */
291
    final public static function getNames()
292 6
    {
293 3
        if (!isset(self::$names[static::class])) {
294 1
            self::detectConstants(static::class);
295 6
        }
296
        return self::$names[static::class];
297
    }
298
    /*
299
     * Get a list of enumerator ordinal numbers
300
     *
301
     * @return int[]
302 3
     */
303
    final public static function getOrdinals()
304 3
    {
305 3
        $count = \count(self::detectConstants(static::class));
306
        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 236
     */
315
    final public static function getConstants()
316 236
    {
317
        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 3
     */
326
    final public static function has($value)
327 3
    {
328 3
        if ($value instanceof static && \get_class($value) === static::class) {
329
            return true;
330
        }
331 3
332 3
        $constants = self::detectConstants(static::class);
333
        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 308
     */
343
    private static function detectConstants($class)
344 308
    {
345 107
        if (!isset(self::$constants[$class])) {
346 107
            $reflection = new ReflectionClass($class);
347
            $constants  = [];
348
349 107
            do {
350 107
                $scopeConstants = [];
351
                if (\PHP_VERSION_ID >= 70100) {
352
                    // Since PHP-7.1 visibility modifiers are allowed for class constants
353 37
                    // for enumerations we are only interested in public once.
354 36
                    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 37
                        if ($reflConstant->isPublic()) {
356
                            $scopeConstants[ $reflConstant->getName() ] = $reflConstant->getValue();
357
                        }
358
                    }
359
                } else {
360 70
                    // In PHP < 7.1 all class constants were public by definition
361
                    $scopeConstants = $reflection->getConstants();
362
                }
363 107
364 107
                $constants = $scopeConstants + $constants;
365
            } while (($reflection = $reflection->getParentClass()) && $reflection->name !== __CLASS__);
366
367 107
            self::$constants[$class] = $constants;
368 107
            self::$names[$class] = \array_keys($constants);
369 104
370 104
            assert('self::assertNonAmbiguous($class)');
371 72
        }
372 2
373 35
        return self::$constants[$class];
374 107
    }
375 6
376
    /**
377 6
     * Assert that the given enumeration class doesn't define ambiguous enumerator values
378 6
     * @param string $class
379 6
     * @return bool
380 2
     */
381
    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...
382
    {
383 101
        $constants = self::$constants[$class];
384 101
        $ambiguous = [];
385 33
        foreach ($constants as $value) {
386
            $names = \array_keys($constants, $value, true);
387 302
            if (\count($names) > 1) {
388
                $ambiguous[\var_export($value, true)] = $names;
389
            }
390
        }
391
392
        return empty($ambiguous);
393
    }
394
395
    /**
396
     * Get an enumerator instance by the given name.
397
     *
398
     * This will be called automatically on calling a method
399
     * with the same name of a defined enumerator.
400
     *
401
     * @param string $method The name of the enumeraotr (called as method)
402 78
     * @param array  $args   There should be no arguments
403
     * @return static
404 78
     * @throws InvalidArgumentException On an invalid or unknown name
405
     * @throws LogicException           On ambiguous constant values
406
     */
407
    final public static function __callStatic($method, array $args)
408
    {
409
        return self::byName($method);
410
    }
411
}
412