Completed
Pull Request — master (#59)
by Marc
05:55 queued 03:46
created

Enum::__clone()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
crap 1
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
     * An array of available constants by class
34
     *
35
     * @var array ["$class" => ["$name" => $value, ...], ...]
36
     */
37
    private static $constants = array();
38
39
    /**
40
     * Already instantiated enumerators
41
     *
42
     * @var array ["$class" => ["$name" => $instance, ...], ...]
43
     */
44
    private static $instances = array();
45
46
    /**
47
     * Constructor
48
     *
49
     * @param null|bool|int|float|string $value   The value of the enumerator
50
     * @param int|null                   $ordinal The ordinal number of the enumerator
51
     */
52 29
    final private function __construct($value, $ordinal = null)
53
    {
54 29
        $this->value   = $value;
55 29
        $this->ordinal = $ordinal;
56 29
    }
57
58
    /**
59
     * Get the name of the enumerator
60
     *
61
     * @return string
62
     * @see getName()
63
     */
64 1
    public function __toString()
65
    {
66 1
        return $this->getName();
67
    }
68
69
    /**
70
     * @throws LogicException Enums are not cloneable
71
     *                        because instances are implemented as singletons
72
     */
73 1
    final private function __clone()
74
    {
75 1
        throw new LogicException('Enums are not cloneable');
76
    }
77
78
    /**
79
     * @throws LogicException Enums are not serializable
80
     *                        because instances are implemented as singletons
81
     */
82 1
    final public function __sleep()
83
    {
84 1
        throw new LogicException('Enums are not serializable');
85
    }
86
87
    /**
88
     * @throws LogicException Enums are not serializable
89
     *                        because instances are implemented as singletons
90
     */
91 1
    final public function __wakeup()
92
    {
93 1
        throw new LogicException('Enums are not serializable');
94
    }
95
96
    /**
97
     * Get the value of the enumerator
98
     *
99
     * @return null|bool|int|float|string
100
     */
101 9
    final public function getValue()
102
    {
103 9
        return $this->value;
104
    }
105
106
    /**
107
     * Get the name of the enumerator
108
     *
109
     * @return string
110
     */
111 6
    final public function getName()
112
    {
113 6
        return array_search($this->value, self::detectConstants(get_called_class()), true);
114
    }
115
116
    /**
117
     * Get the ordinal number of the enumerator
118
     *
119
     * @return int
120
     */
121 17
    final public function getOrdinal()
122
    {
123 17
        if ($this->ordinal !== null) {
124 15
            return $this->ordinal;
125
        }
126
127
        // detect ordinal
128 8
        $ordinal = 0;
129 8
        $value   = $this->value;
130 8
        foreach (self::detectConstants(get_called_class()) as $constValue) {
131 8
            if ($value === $constValue) {
132 8
                break;
133
            }
134 7
            ++$ordinal;
135 8
        }
136
137 8
        $this->ordinal = $ordinal;
138 8
        return $ordinal;
139
    }
140
141
    /**
142
     * Compare this enumerator against another and check if it's the same.
143
     *
144
     * @param mixed $enumerator
145
     * @return bool
146
     */
147 1
    final public function is($enumerator)
148
    {
149 1
        return $this === $enumerator || $this->value === $enumerator;
150
    }
151
152
    /**
153
     * Get an enumerator instance of the given value or instance
154
     *
155
     * @param static|null|bool|int|float|string $value
156
     * @return static
157
     * @throws InvalidArgumentException On an unknwon or invalid value
158
     * @throws LogicException           On ambiguous constant values
159
     */
160 40
    final public static function get($value)
161
    {
162 40
        if ($value instanceof static && get_class($value) === get_called_class()) {
163 15
            return $value;
164
        }
165
166 29
        $class     = get_called_class();
167 29
        $constants = self::detectConstants($class);
168 27
        $name      = array_search($value, $constants, true);
169 27 View Code Duplication
        if ($name === false) {
170 7
            $message = is_scalar($value)
171 7
                ? 'Unknown value ' . var_export($value, true)
172 7
                : 'Invalid value of type ' . (is_object($value) ? get_class($value) : gettype($value));
173 7
            throw new InvalidArgumentException($message);
174
        }
175
176 21
        if (!isset(self::$instances[$class][$name])) {
177 15
            self::$instances[$class][$name] = new $class($constants[$name]);
178 15
        }
179
180 21
        return self::$instances[$class][$name];
181
    }
182
183
    /**
184
     * Get an enumerator instance by the given name
185
     *
186
     * @param string $name The name of the enumerator
187
     * @return static
188
     * @throws InvalidArgumentException On an invalid or unknown name
189
     * @throws LogicException           On ambiguous values
190
     */
191 24
    final public static function getByName($name)
192
    {
193 24
        $name  = (string) $name;
194 24
        $class = get_called_class();
195 24
        if (isset(self::$instances[$class][$name])) {
196 16
            return self::$instances[$class][$name];
197
        }
198
199 12
        $const = $class . '::' . $name;
200 12
        if (!defined($const)) {
201 1
            throw new InvalidArgumentException($const . ' not defined');
202
        }
203
204 11
        return self::$instances[$class][$name] = new $class(constant($const));
205
    }
206
207
    /**
208
     * Get an enumeration instance by the given ordinal number
209
     *
210
     * @param int $ordinal The ordinal number or the enumerator
211
     * @return static
212
     * @throws InvalidArgumentException On an invalid ordinal number
213
     * @throws LogicException           On ambiguous values
214
     */
215 8
    final public static function getByOrdinal($ordinal)
216
    {
217 8
        $ordinal   = (int) $ordinal;
218 8
        $class     = get_called_class();
219 8
        $constants = self::detectConstants($class);
220 8
        $item      = array_slice($constants, $ordinal, 1, true);
221 8
        if (empty($item)) {
222 1
            throw new InvalidArgumentException(sprintf(
223 1
                'Invalid ordinal number, must between 0 and %s',
224 1
                count($constants) - 1
225 1
            ));
226
        }
227
228 7
        $name = key($item);
229 7
        if (isset(self::$instances[$class][$name])) {
230 5
            return self::$instances[$class][$name];
231
        }
232
233 3
        return self::$instances[$class][$name] = new $class(current($item), $ordinal);
234
    }
235
236
    /**
237
     * Clear all instantiated enumerators of the called class
238
     *
239
     * NOTE: This can break singleton behavior ... use it with caution!
240
     *
241
     * @return void
242
     */
243 31
    final public static function clear()
244
    {
245 31
        $class = get_called_class();
246 31
        unset(self::$instances[$class], self::$constants[$class]);
247 31
    }
248
249
    /**
250
     * Get a list of enumerator instances ordered by ordinal number
251
     *
252
     * @return static[]
253
     */
254 1
    final public static function getEnumerators()
255
    {
256 1
        return array_map('self::getByName', array_keys(self::detectConstants(get_called_class())));
257
    }
258
259
    /**
260
     * Get all available constants of the called class
261
     *
262
     * @return array
263
     * @throws LogicException On ambiguous constant values
264
     */
265 26
    final public static function getConstants()
266
    {
267 26
        return self::detectConstants(get_called_class());
268
    }
269
270
    /**
271
     * Is the given enumerator part of this enumeration
272
     * 
273
     * @param static|null|bool|int|float|string $value
274
     * @return bool
275
     */
276 1
    final public static function has($value)
277
    {
278 1
        if ($value instanceof static && get_class($value) === get_called_class()) {
279 1
            return true;
280
        }
281
282 1
        $class     = get_called_class();
283 1
        $constants = self::detectConstants($class);
284
285 1
        return in_array($value, $constants, true);
286
    }
287
288
    /**
289
     * Detect all available constants by the given class
290
     *
291
     * @param string $class
292
     * @return array
293
     * @throws LogicException On ambiguous constant values
294
     */
295 46
    private static function detectConstants($class)
296
    {
297 46
        if (!isset(self::$constants[$class])) {
298 29
            $reflection = new ReflectionClass($class);
299 29
            $constants  = array();
300
301
            do {
302 29
                $scopeConstants = array();
303 29
                if (PHP_VERSION_ID >= 70100) {
304 28
                    // Since PHP-7.1 visibility modifiers are allowed for class constants
305 28
                    // for enumerations we are only interected in public once.
306 2
                    foreach ($reflection->getReflectionConstants() as $reflConstant) {
307 2
                        if ($reflConstant->isPublic()) {
308 29
                            $scopeConstants[ $reflConstant->getName() ] = $reflConstant->getValue();
309 29
                        }
310 2
                    }
311
                } else {
312 2
                    // In PHP < 7.1 all class constants were public by definition
313 2
                    $scopeConstants = $reflection->getConstants();
314 2
                }
315 2
316
                $constants = $scopeConstants + $constants;
317
            } while (($reflection = $reflection->getParentClass()) && $reflection->name !== __CLASS__);
318
319 27
            // Detect ambiguous values and report names
320 8
            $ambiguous = array();
321 8
            foreach ($constants as $value) {
322
                $names = array_keys($constants, $value, true);
323 27
                if (count($names) > 1) {
324 27
                    $ambiguous[var_export($value, true)] = $names;
325
                }
326 44
            }
327
            if (!empty($ambiguous)) {
328
                throw new LogicException(
329
                    'All possible values needs to be unique. The following are ambiguous: '
330
                    . implode(', ', array_map(function ($names) use ($constants) {
331
                        return implode('/', $names) . '=' . var_export($constants[$names[0]], true);
332
                    }, $ambiguous))
333
                );
334
            }
335
336
            self::$constants[$class] = $constants;
337
        }
338
339
        return self::$constants[$class];
340
    }
341 21
342
    /**
343 21
     * Get an enumerator instance by the given name.
344
     *
345
     * This will be called automatically on calling a method
346
     * with the same name of a defined enumerator.
347
     *
348
     * @param string $method The name of the enumeraotr (called as method)
349
     * @param array  $args   There should be no arguments
350
     * @return static
351
     * @throws InvalidArgumentException On an invalid or unknown name
352
     * @throws LogicException           On ambiguous constant values
353
     */
354
    final public static function __callStatic($method, array $args)
355
    {
356
        return self::getByName($method);
357
    }
358
}
359