Passed
Pull Request — master (#100)
by
unknown
60:31
created

Enum::has()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
nc 2
nop 1
dl 0
loc 8
ccs 2
cts 2
cp 1
crap 3
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 implements \JsonSerializable
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 when enumerator is jsonSerialized
105
     *
106
     * @return bool|float|int|mixed|null|string
107
     */
108 96
109
    final public function jsonSerialize()
110 96
    {
111
        return $this->getValue();
112
    }
113
114
    /**
115
     * Get the value of the enumerator
116
     *
117
     * @return null|bool|int|float|string
118 36
     */
119
    final public function getValue()
120 36
    {
121 36
        return $this->value;
122
    }
123
124
    /**
125
     * Get the name of the enumerator
126
     *
127
     * @return string
128
     */
129 256
    final public function getName()
130
    {
131 256
        $ordinal = $this->ordinal !== null ? $this->ordinal : $this->getOrdinal();
132 80
        return self::$names[static::class][$ordinal];
133 80
    }
134 80
135 80
    /**
136 80
     * Get the ordinal number of the enumerator
137
     *
138 64
     * @return int
139 20
     */
140
    final public function getOrdinal()
141 80
    {
142 20
        if ($this->ordinal === null) {
143
            $ordinal = 0;
144 256
            $value   = $this->value;
145
            foreach (self::detectConstants(static::class) as $constValue) {
146
                if ($value === $constValue) {
147
                    break;
148
                }
149
                ++$ordinal;
150
            }
151
152
            $this->ordinal = $ordinal;
153 8
        }
154
155 8
        return $this->ordinal;
156
    }
157
158 8
    /**
159 8
     * Compare this enumerator against another and check if it's the same.
160 8
     *
161 2
     * @param mixed $enumerator
162
     * @return bool
163
     */
164
    final public function is($enumerator)
165
    {
166
        return $this === $enumerator || $this->value === $enumerator
167
168
            // The following additional conditions are required only because of the issue of serializable singletons
169
            || ($enumerator instanceof static
170
                && \get_class($enumerator) === static::class
171
                && $enumerator->value === $this->value
172 304
            );
173
    }
174 304
175 112
    /**
176
     * Get an enumerator instance of the given enumerator value or instance
177
     *
178 220
     * @param static|null|bool|int|float|string $enumerator
179
     * @return static
180
     * @throws InvalidArgumentException On an unknwon or invalid value
181
     * @throws LogicException           On ambiguous constant values
182
     */
183
    final public static function get($enumerator)
184
    {
185
        if ($enumerator instanceof static && \get_class($enumerator) === static::class) {
186
            return $enumerator;
187
        }
188
189 220
        return static::byValue($enumerator);
190
    }
191 220
192 64
    /**
193 14
     * Get an enumerator instance by the given value
194
     *
195 212
     * @param mixed $value
196 212
     * @return static
197 40
     * @throws InvalidArgumentException On an unknwon or invalid value
198 40
     * @throws LogicException           On ambiguous constant values
199 40
     */
200 25
    final public static function byValue($value)
201 40
    {
202 40
        if (!isset(self::$constants[static::class])) {
203 10
            self::detectConstants(static::class);
204
        }
205
206 176
        $name = \array_search($value, self::$constants[static::class], true);
207 68
        if ($name === false) {
208 17
            throw new InvalidArgumentException(sprintf(
209
                'Unknown value %s for enumeration %s',
210 176
                \is_scalar($value)
211
                    ? \var_export($value, true)
212
                    : 'of type ' . (\is_object($value) ? \get_class($value) : \gettype($value)),
213
                static::class
214
            ));
215
        }
216
217
        if (!isset(self::$instances[static::class][$name])) {
218
            self::$instances[static::class][$name] = new static(self::$constants[static::class][$name]);
219
        }
220
221 180
        return self::$instances[static::class][$name];
222
    }
223 180
224 180
    /**
225 128
     * Get an enumerator instance by the given name
226
     *
227
     * @param string $name The name of the enumerator
228 72
     * @return static
229 72
     * @throws InvalidArgumentException On an invalid or unknown name
230 4
     * @throws LogicException           On ambiguous values
231
     */
232
    final public static function byName($name)
233 68
    {
234
        $name = (string) $name;
235
        if (isset(self::$instances[static::class][$name])) {
236
            return self::$instances[static::class][$name];
237
        }
238
239
        $const = static::class . '::' . $name;
240
        if (!\defined($const)) {
241
            throw new InvalidArgumentException($const . ' not defined');
242
        }
243
244 120
        return self::$instances[static::class][$name] = new static(\constant($const));
245
    }
246 120
247
    /**
248 120
     * Get an enumeration instance by the given ordinal number
249 8
     *
250 2
     * @param int $ordinal The ordinal number or the enumerator
251
     * @return static
252 120
     * @throws InvalidArgumentException On an invalid ordinal number
253 4
     * @throws LogicException           On ambiguous values
254 4
     */
255 4
    final public static function byOrdinal($ordinal)
256 1
    {
257
        $ordinal = (int) $ordinal;
258
259 116
        if (!isset(self::$names[static::class])) {
260 116
            self::detectConstants(static::class);
261 108
        }
262
263
        if (!isset(self::$names[static::class][$ordinal])) {
264 12
            throw new InvalidArgumentException(\sprintf(
265
                'Invalid ordinal number, must between 0 and %s',
266
                \count(self::$names[static::class]) - 1
267
            ));
268
        }
269
270
        $name = self::$names[static::class][$ordinal];
271
        if (isset(self::$instances[static::class][$name])) {
272 64
            return self::$instances[static::class][$name];
273
        }
274 64
275 4
        return self::$instances[static::class][$name] = new static(self::$constants[static::class][$name], $ordinal);
276 1
    }
277 64
278
    /**
279
     * Get a list of enumerator instances ordered by ordinal number
280
     *
281
     * @return static[]
282
     */
283
    final public static function getEnumerators()
284
    {
285 4
        if (!isset(self::$names[static::class])) {
286
            self::detectConstants(static::class);
287 4
        }
288
        return \array_map([static::class, 'byName'], self::$names[static::class]);
289
    }
290
291
    /**
292
     * Get a list of enumerator values ordered by ordinal number
293
     *
294
     * @return mixed[]
295 8
     */
296
    final public static function getValues()
297 8
    {
298 4
        return \array_values(self::detectConstants(static::class));
299 1
    }
300 8
301
    /**
302
     * Get a list of enumerator names ordered by ordinal number
303
     *
304
     * @return string[]
305
     */
306
    final public static function getNames()
307 4
    {
308
        if (!isset(self::$names[static::class])) {
309 4
            self::detectConstants(static::class);
310 4
        }
311
        return self::$names[static::class];
312
    }
313
    /*
314
     * Get a list of enumerator ordinal numbers
315
     *
316
     * @return int[]
317
     */
318
    final public static function getOrdinals()
319 316
    {
320
        $count = \count(self::detectConstants(static::class));
321 316
        return $count === 0 ? [] : \range(0, $count - 1);
322
    }
323
324
    /**
325
     * Get all available constants of the called class
326
     *
327
     * @return array
328
     * @throws LogicException On ambiguous constant values
329
     */
330 4
    final public static function getConstants()
331
    {
332 4
        return self::detectConstants(static::class);
333 4
    }
334
335
    /**
336 4
     * Is the given enumerator part of this enumeration
337 4
     * 
338
     * @param static|null|bool|int|float|string $value
339
     * @return bool
340
     */
341
    final public static function has($value)
342
    {
343
        if ($value instanceof static && \get_class($value) === static::class) {
344
            return true;
345
        }
346 400
347
        $constants = self::detectConstants(static::class);
348 400
        return \in_array($value, $constants, true);
349 160
    }
350 160
351
    /**
352
     * Detect all public available constants of given enumeration class
353 160
     *
354 160
     * @param string $class
355
     * @return array
356
     */
357
    private static function detectConstants($class)
358
    {
359 82
        if (!isset(self::$constants[$class])) {
360 80
            $reflection = new ReflectionClass($class);
361 82
            $publicConstants  = [];
362
363
            do {
364
                $scopeConstants = [];
365
                if (\PHP_VERSION_ID >= 70100 && method_exists(ReflectionClass::class, 'getReflectionConstants')) {
366 78
                    // Since PHP-7.1 visibility modifiers are allowed for class constants
367
                    // for enumerations we are only interested in public once.
368
                    // NOTE: HHVM > 3.26.2 still does not support private/protected constants.
369 160
                    //       It allows the visibility keyword but ignores it.
370 160
                    foreach ($reflection->getReflectionConstants() as $reflConstant) {
371
                        if ($reflConstant->isPublic()) {
372 160
                            $scopeConstants[ $reflConstant->getName() ] = $reflConstant->getValue();
373 160
                        }
374 160
                    }
375 39
                } else {
376
                    // In PHP < 7.1 all class constants were public by definition
377 152
                    $scopeConstants = $reflection->getConstants();
378 152
                }
379 37
380
                $publicConstants = $scopeConstants + $publicConstants;
381 392
            } while (($reflection = $reflection->getParentClass()) && $reflection->name !== __CLASS__);
382
383
            assert(
384
                self::noAmbiguousValues($publicConstants),
385
                "Ambiguous enumerator values detected for {$class}"
386
            );
387
388
            self::$constants[$class] = $publicConstants;
389 160
            self::$names[$class] = \array_keys($publicConstants);
390
        }
391 160
392 156
        return self::$constants[$class];
393 156
    }
394 122
395
    /**
396 37
     * Test that the given constants does not contain ambiguous values
397
     * @param array $constants
398 144
     * @return bool
399
     */
400
    private static function noAmbiguousValues(array $constants)
401
    {
402
        foreach ($constants as $value) {
403
            $names = \array_keys($constants, $value, true);
404
            if (\count($names) > 1) {
405
                return false;
406
            }
407
        }
408
409
        return true;
410
    }
411
412
    /**
413 104
     * Get an enumerator instance by the given name.
414
     *
415 104
     * This will be called automatically on calling a method
416
     * with the same name of a defined enumerator.
417
     *
418
     * @param string $method The name of the enumeraotr (called as method)
419
     * @param array  $args   There should be no arguments
420
     * @return static
421
     * @throws InvalidArgumentException On an invalid or unknown name
422
     * @throws LogicException           On ambiguous constant values
423
     */
424
    final public static function __callStatic($method, array $args)
425
    {
426
        return self::byName($method);
427
    }
428
}
429