Completed
Push — optimize_byOrdinal ( c42b49 )
by Marc
01:22
created

Enum::byOrdinal()   B

Complexity

Conditions 4
Paths 6

Size

Total Lines 24
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 4.0058

Importance

Changes 0
Metric Value
dl 0
loc 24
ccs 13
cts 14
cp 0.9286
rs 8.6845
c 0
b 0
f 0
cc 4
eloc 14
nc 6
nop 1
crap 4.0058
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) 2015 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 = array();
38
39
    /**
40
     * A List of of available enumerator names by enumeration class
41
     *
42
     * @var array ["$class" => ["$name0", ...], ...]
43
     */
44
    private static $names = array();
45
46
    /**
47
     * Already instantiated enumerators
48
     *
49
     * @var array ["$class" => ["$name" => $instance, ...], ...]
50
     */
51
    private static $instances = array();
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 14
    final private function __construct($value, $ordinal = null)
60
    {
61 14
        $this->value   = $value;
62 14
        $this->ordinal = $ordinal;
63 14
    }
64
65
    /**
66
     * Get the name of the enumerator
67
     *
68
     * @return string
69
     * @see getName()
70
     */
71 1
    public function __toString()
72
    {
73 1
        return $this->getName();
74
    }
75
76
    /**
77
     * @throws LogicException Enums are not cloneable
78
     *                        because instances are implemented as singletons
79
     */
80 1
    final private function __clone()
81
    {
82 1
        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 1
    final public function __sleep()
90
    {
91 1
        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 1
    final public function __wakeup()
99
    {
100 1
        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 21
    final public function getValue()
109
    {
110 21
        return $this->value;
111
    }
112
113
    /**
114
     * Get the name of the enumerator
115
     *
116
     * @return string
117
     */
118 7
    final public function getName()
119
    {
120 7
        if ($this->ordinal !== null) {
121 7
            return self::$names[static::class][$this->ordinal];
122
        } else {
123
            return array_search($this->value, self::detectConstants(static::class), true);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The expression array_search($this->valu...(static::class), true); of type false|integer|string adds false to the return on line 123 which is incompatible with the return type documented by MabeEnum\Enum::getName of type string. It seems like you forgot to handle an error condition.
Loading history...
124
        }
125
    }
126
127
    /**
128
     * Get the ordinal number of the enumerator
129
     *
130
     * @return int
131
     */
132 40
    final public function getOrdinal()
133
    {
134 40
        if ($this->ordinal !== null) {
135 39
            return $this->ordinal;
136
        }
137
138
        // detect ordinal
139 9
        $ordinal = 0;
140 9
        $value   = $this->value;
141 9
        foreach (self::detectConstants(static::class) as $constValue) {
142 9
            if ($value === $constValue) {
143 9
                break;
144
            }
145 9
            ++$ordinal;
146
        }
147
148 9
        $this->ordinal = $ordinal;
149 9
        return $ordinal;
150
    }
151
152
    /**
153
     * Compare this enumerator against another and check if it's the same.
154
     *
155
     * @param mixed $enumerator
156
     * @return bool
157
     */
158 2
    final public function is($enumerator)
159
    {
160 2
        return $this === $enumerator || $this->value === $enumerator
161
162
            // The following additional conditions are required only because of the issue of serializable singletons
163 2
            || ($enumerator instanceof static
164 2
                && get_class($enumerator) === static::class
165 2
                && $enumerator->value === $this->value
166
            );
167
    }
168
169
    /**
170
     * Get an enumerator instance of the given value or instance
171
     *
172
     * @param static|null|bool|int|float|string $value
173
     * @return static
174
     * @throws InvalidArgumentException On an unknwon or invalid value
175
     * @throws LogicException           On ambiguous constant values
176
     */
177 61
    final public static function get($value)
178
    {
179 61
        if ($value instanceof static && get_class($value) === static::class) {
180 22
            return $value;
181
        }
182
183 46
        return static::byValue($value);
184
    }
185
186
    /**
187
     * Get an enumerator instance by the given value
188
     *
189
     * @param mixed $value
190
     * @return static
191
     * @throws InvalidArgumentException On an unknwon or invalid value
192
     * @throws LogicException           On ambiguous constant values
193
     */
194 46
    final public static function byValue($value)
195
    {
196 46
        $class     = static::class;
197 46
        $constants = self::detectConstants($class);
198 44
        $name      = array_search($value, $constants, true);
199 44 View Code Duplication
        if ($name === false) {
200 7
            $message = is_scalar($value)
201 3
                ? 'Unknown value ' . var_export($value, true)
202 7
                : 'Invalid value of type ' . (is_object($value) ? get_class($value) : gettype($value));
203 7
            throw new InvalidArgumentException($message);
204
        }
205
206 38
        if (!isset(self::$instances[$class][$name])) {
207 7
            self::$instances[$class][$name] = new $class($constants[$name]);
208
        }
209
210 38
        return self::$instances[$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 32
    final public static function byName($name)
222
    {
223 32
        $name  = (string) $name;
224 32
        $class = static::class;
225 32
        if (isset(self::$instances[$class][$name])) {
226 27
            return self::$instances[$class][$name];
227
        }
228
229 6
        $const = $class . '::' . $name;
230 6
        if (!defined($const)) {
231 1
            throw new InvalidArgumentException($const . ' not defined');
232
        }
233
234 5
        return self::$instances[$class][$name] = new $class(constant($const));
235
    }
236
237
    /**
238
     * Get an enumeration instance by the given ordinal number
239
     *
240
     * @param int $ordinal The ordinal number or the enumerator
241
     * @return static
242
     * @throws InvalidArgumentException On an invalid ordinal number
243
     * @throws LogicException           On ambiguous values
244
     */
245 19
    final public static function byOrdinal($ordinal)
246
    {
247 19
        $ordinal   = (int) $ordinal;
248 19
        $class     = static::class;
249
250 19
        if (!isset(self::$names[$class])) {
251
            self::detectConstants($class);
252
        }
253
254 19
        if (!isset(self::$names[$class][$ordinal])) {
255 1
            throw new InvalidArgumentException(sprintf(
256 1
                'Invalid ordinal number, must between 0 and %s',
257 1
                count(self::$names[$class]) - 1
258
            ));
259
        }
260
261 18
        $name = self::$names[$class][$ordinal];
262 18
        if (isset(self::$instances[$class][$name])) {
263 17
            return self::$instances[$class][$name];
264
        }
265
266 2
        $const = $class . '::' . $name;
267 2
        return self::$instances[$class][$name] = new $class(constant($const));
268
    }
269
270
    /**
271
     * Get a list of enumerator instances ordered by ordinal number
272
     *
273
     * @return static[]
274
     */
275 12
    final public static function getEnumerators()
276
    {
277 12
        if (!isset(self::$names[static::class])) {
278
            self::detectConstants(static::class);
279
        }
280 12
        return array_map([static::class, 'byName'], self::$names[static::class]);
281
    }
282
283
    /**
284
     * Get a list of enumerator values ordered by ordinal number
285
     *
286
     * @return mixed[]
287
     */
288 1
    final public static function getValues()
289
    {
290 1
        return array_values(self::detectConstants(static::class));
291
    }
292
293
    /**
294
     * Get a list of enumerator names ordered by ordinal number
295
     *
296
     * @return string[]
297
     */
298 1
    final public static function getNames()
299
    {
300 1
        if (!isset(self::$names[static::class])) {
301
            self::detectConstants(static::class);
302
        }
303 1
        return self::$names[static::class];
304
    }
305
    /*
306
     * Get a list of enumerator ordinal numbers
307
     *
308
     * @return int[]
309
     */
310 1
    final public static function getOrdinals()
311
    {
312 1
        $count = count(self::detectConstants(static::class));
313 1
        return $count === 0 ? array() : range(0, $count - 1);
314
    }
315
316
    /**
317
     * Get all available constants of the called class
318
     *
319
     * @return array
320
     * @throws LogicException On ambiguous constant values
321
     */
322 63
    final public static function getConstants()
323
    {
324 63
        return self::detectConstants(static::class);
325
    }
326
327
    /**
328
     * Is the given enumerator part of this enumeration
329
     * 
330
     * @param static|null|bool|int|float|string $value
331
     * @return bool
332
     */
333 1
    final public static function has($value)
334
    {
335 1
        if ($value instanceof static && get_class($value) === static::class) {
336 1
            return true;
337
        }
338
339 1
        $constants = self::detectConstants(static::class);
340 1
        return in_array($value, $constants, true);
341
    }
342
343
    /**
344
     * Detect all public available constants of given enumeration class
345
     *
346
     * @param string $class
347
     * @return array
348
     * @throws LogicException On ambiguous constant values
349
     */
350 80
    private static function detectConstants($class)
351
    {
352 80
        if (!isset(self::$constants[$class])) {
353 13
            $reflection = new ReflectionClass($class);
354 13
            $constants  = array();
355
356
            do {
357 13
                $scopeConstants = array();
358 13
                if (PHP_VERSION_ID >= 70100) {
359
                    // Since PHP-7.1 visibility modifiers are allowed for class constants
360
                    // for enumerations we are only interested in public once.
361 13
                    foreach ($reflection->getReflectionConstants() as $reflConstant) {
362 12
                        if ($reflConstant->isPublic()) {
363 13
                            $scopeConstants[ $reflConstant->getName() ] = $reflConstant->getValue();
364
                        }
365
                    }
366
                } else {
367
                    // In PHP < 7.1 all class constants were public by definition
368
                    $scopeConstants = $reflection->getConstants();
369
                }
370
371 13
                $constants = $scopeConstants + $constants;
372 13
            } while (($reflection = $reflection->getParentClass()) && $reflection->name !== __CLASS__);
373
374
            // Detect ambiguous values and report names
375 13
            $ambiguous = array();
376 13
            foreach ($constants as $value) {
377 12
                $names = array_keys($constants, $value, true);
378 12
                if (count($names) > 1) {
379 12
                    $ambiguous[var_export($value, true)] = $names;
380
                }
381
            }
382 13
            if (!empty($ambiguous)) {
383 2
                throw new LogicException(
384
                    'All possible values needs to be unique. The following are ambiguous: '
385 2
                    . implode(', ', array_map(function ($names) use ($constants) {
386 2
                        return implode('/', $names) . '=' . var_export($constants[$names[0]], true);
387 2
                    }, $ambiguous))
388
                );
389
            }
390
391 11
            self::$constants[$class] = $constants;
392 11
            self::$names[$class] = array_keys($constants);
393
        }
394
395 78
        return self::$constants[$class];
396
    }
397
398
    /**
399
     * Get an enumerator instance by the given name.
400
     *
401
     * This will be called automatically on calling a method
402
     * with the same name of a defined enumerator.
403
     *
404
     * @param string $method The name of the enumeraotr (called as method)
405
     * @param array  $args   There should be no arguments
406
     * @return static
407
     * @throws InvalidArgumentException On an invalid or unknown name
408
     * @throws LogicException           On ambiguous constant values
409
     */
410 18
    final public static function __callStatic($method, array $args)
411
    {
412 18
        return self::byName($method);
413
    }
414
}
415