Completed
Push — feature/74 ( 60c27e...f551d9 )
by Marc
04:30
created

Enum::getEnumerators()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

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