Passed
Push — doc-generics ( 5914b1...8f05b5 )
by Marc
04:50
created

Enum   B

Complexity

Total Complexity 49

Size/Duplication

Total Lines 418
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 9
Bugs 1 Features 0
Metric Value
eloc 105
c 9
b 1
f 0
dl 0
loc 418
ccs 122
cts 122
cp 1
rs 8.48
wmc 49

23 Methods

Rating   Name   Duplication   Size   Complexity  
A __toString() 0 3 1
A __sleep() 0 3 1
A __construct() 0 4 1
A getValue() 0 3 1
A __clone() 0 3 1
A __wakeup() 0 3 1
A getName() 0 3 1
A is() 0 8 5
A getOrdinal() 0 17 4
A byValue() 0 22 4
A get() 0 15 3
A byOrdinal() 0 19 2
A byName() 0 19 3
A hasName() 0 3 1
A getNames() 0 6 2
A getEnumerators() 0 9 2
A hasValue() 0 3 1
A __callStatic() 0 3 1
A getConstants() 0 28 6
A getOrdinals() 0 4 2
A getValues() 0 3 1
A noAmbiguousValues() 0 10 3
A has() 0 7 2

How to fix   Complexity   

Complex Class

Complex classes like Enum often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Enum, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace MabeEnum;
6
7
use ReflectionClass;
8
use InvalidArgumentException;
9
use LogicException;
10
11
/**
12
 * Abstract base enumeration class.
13
 *
14
 * @copyright 2019 Marc Bennewitz
15
 * @license http://github.com/marc-mabe/php-enum/blob/master/LICENSE.txt New BSD License
16
 * @link http://github.com/marc-mabe/php-enum for the canonical source repository
17
 */
18
abstract class Enum
19
{
20
    /**
21
     * The selected enumerator value
22
     *
23
     * @var null|bool|int|float|string|array<int|string, mixed>
24
     */
25
    private $value;
0 ignored issues
show
Coding Style introduced by
Expected 1 blank line before member var; 0 found
Loading history...
26
27
    /**
28
     * The ordinal number of the enumerator
29
     *
30
     * @var null|int
31
     */
32
    private $ordinal;
33
34
    /**
35
     * A map of enumerator names and values by enumeration class
36
     *
37
     * @var array<class-string<Enum>, array<string, null|bool|int|float|string|array<int|string, mixed>>>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<class-string<Enum>...ay<int|string, mixed>>> at position 2 could not be parsed: Unknown type name 'class-string' at position 2 in array<class-string<Enum>, array<string, null|bool|int|float|string|array<int|string, mixed>>>.
Loading history...
38
     */
39
    private static $constants = [];
40
41
    /**
42
     * A List of available enumerator names by enumeration class
43
     *
44
     * @var array<class-string<Enum>, string[]>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<class-string<Enum>, string[]> at position 2 could not be parsed: Unknown type name 'class-string' at position 2 in array<class-string<Enum>, string[]>.
Loading history...
45
     */
46
    private static $names = [];
47
48
    /**
49
     * A map of enumerator names and instances by enumeration class
50
     *
51
     * @var array<class-string<Enum>, array<string, Enum>>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<class-string<Enum>, array<string, Enum>> at position 2 could not be parsed: Unknown type name 'class-string' at position 2 in array<class-string<Enum>, array<string, Enum>>.
Loading history...
52
     */
53
    private static $instances = [];
54
55
    /**
56
     * Constructor
57
     *
58
     * @param null|bool|int|float|string|array<int|string, mixed> $value   The value of the enumerator
59
     * @param int|null                                            $ordinal The ordinal number of the enumerator
60
     */
61 45
    final private function __construct($value, $ordinal = null)
62
    {
63 45
        $this->value   = $value;
64 45
        $this->ordinal = $ordinal;
65 45
    }
66
67
    /**
68
     * Get the name of the enumerator
69
     *
70
     * @return string
71
     * @see getName()
72
     */
73 1
    public function __toString(): string
74
    {
75 1
        return $this->getName();
76
    }
77
78
    /**
79
     * @throws LogicException Enums are not cloneable
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
80
     *                        because instances are implemented as singletons
81
     */
0 ignored issues
show
introduced by
Missing @return tag in function comment
Loading history...
82 1
    final private function __clone()
83
    {
84 1
        throw new LogicException('Enums are not cloneable');
85
    }
86
87
    /**
88
     * @throws LogicException Enums are not serializable
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
89
     *                        because instances are implemented as singletons
90
     */
0 ignored issues
show
introduced by
Missing @return tag in function comment
Loading history...
91 1
    final public function __sleep()
92
    {
93 1
        throw new LogicException('Enums are not serializable');
94
    }
95
96
    /**
97
     * @throws LogicException Enums are not serializable
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
98
     *                        because instances are implemented as singletons
99
     */
0 ignored issues
show
introduced by
Missing @return tag in function comment
Loading history...
100 1
    final public function __wakeup()
101
    {
102 1
        throw new LogicException('Enums are not serializable');
103
    }
104
105
    /**
106
     * Get the value of the enumerator
107
     *
108
     * @return null|bool|int|float|string|array<int|string, mixed>
109
     */
110 32
    final public function getValue()
111
    {
112 32
        return $this->value;
113
    }
114
115
    /**
116
     * Get the name of the enumerator
117
     *
118
     * @return string
119
     */
120 8
    final public function getName()
121
    {
122 8
        return self::$names[static::class][$this->ordinal ?? $this->getOrdinal()];
123
    }
124
125
    /**
126
     * Get the ordinal number of the enumerator
127
     *
128
     * @return int
129
     */
130 109
    final public function getOrdinal()
131
    {
132 109
        if ($this->ordinal === null) {
133 22
            $ordinal   = 0;
134 22
            $value     = $this->value;
135 22
            $constants = self::$constants[static::class] ?? static::getConstants();
136 22
            foreach ($constants as $constValue) {
137 22
                if ($value === $constValue) {
138 22
                    break;
139
                }
140 17
                ++$ordinal;
141
            }
142
143 22
            $this->ordinal = $ordinal;
144
        }
145
146 109
        return $this->ordinal;
147
    }
148
149
    /**
150
     * Compare this enumerator against another and check if it's the same.
151
     *
152
     * @param static|null|bool|int|float|string|array<int|string, mixed> $enumerator An enumerator object or value
153
     * @return bool
154
     */
155 2
    final public function is($enumerator)
156
    {
157 2
        return $this === $enumerator || $this->value === $enumerator
158
159
            // The following additional conditions are required only because of the issue of serializable singletons
160 2
            || ($enumerator instanceof static
161 2
                && \get_class($enumerator) === static::class
162 2
                && $enumerator->value === $this->value
163
            );
164
    }
165
166
    /**
167
     * Get an enumerator instance of the given enumerator value or instance
168
     *
169
     * @param static|null|bool|int|float|string|array<int|string, mixed> $enumerator An enumerator object or value
170
     * @return static
171
     * @throws InvalidArgumentException On an unknown or invalid value
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
172
     * @throws LogicException           On ambiguous constant values
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
173
     */
174 120
    final public static function get($enumerator)
175
    {
176 120
        if ($enumerator instanceof static) {
177 39
            if (\get_class($enumerator) !== static::class) {
178 4
                throw new InvalidArgumentException(sprintf(
179 4
                    'Invalid value of type %s for enumeration %s',
180 4
                    \get_class($enumerator),
181 4
                    static::class
182
                ));
183
            }
184
185 35
            return $enumerator;
186
        }
187
188 93
        return static::byValue($enumerator);
189
    }
190
191
    /**
192
     * Get an enumerator instance by the given value
193
     *
194
     * @param null|bool|int|float|string|array<int|string, mixed> $value Enumerator value
195
     * @return static
196
     * @throws InvalidArgumentException On an unknown or invalid value
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
197
     * @throws LogicException           On ambiguous constant values
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
198
     */
199 93
    final public static function byValue($value)
200
    {
201
        /** @var mixed $value */
202
203 93
        $constants = self::$constants[static::class] ?? static::getConstants();
204
205 91
        $name = \array_search($value, $constants, true);
206 91
        if ($name === false) {
207 9
            throw new InvalidArgumentException(sprintf(
208 9
                'Unknown value %s for enumeration %s',
209 9
                \is_scalar($value)
210 7
                    ? \var_export($value, true)
211 9
                    : 'of type ' . (\is_object($value) ? \get_class($value) : \gettype($value)),
212 9
                static::class
213
            ));
214
        }
215
216
        /** @var static $instance */
217 84
        $instance = self::$instances[static::class][$name]
218 84
            ?? self::$instances[static::class][$name] = new static($constants[$name]);
0 ignored issues
show
Coding Style introduced by
Expected 1 space before "??"; newline found
Loading history...
219
220 84
        return $instance;
221
    }
222
223
    /**
224
     * Get an enumerator instance by the given name
225
     *
226
     * @param string $name The name of the enumerator
227
     * @return static
228
     * @throws InvalidArgumentException On an invalid or unknown name
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
229
     * @throws LogicException           On ambiguous values
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
230
     */
231 59
    final public static function byName(string $name)
232
    {
233 59
        if (isset(self::$instances[static::class][$name])) {
234
            /** @var static $instance */
235 44
            $instance = self::$instances[static::class][$name];
236 44
            return $instance;
237
        }
238
239 19
        $const = static::class . "::{$name}";
0 ignored issues
show
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $name instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
240 19
        if (!\defined($const)) {
241 1
            throw new InvalidArgumentException("{$const} not defined");
0 ignored issues
show
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $const instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
242
        }
243
244 18
        assert(
245 18
            self::noAmbiguousValues(static::getConstants()),
246 17
            'Ambiguous enumerator values detected for ' . static::class
247
        );
248
249 17
        return self::$instances[static::class][$name] = new static(\constant($const));
250
    }
251
252
    /**
253
     * Get an enumeration instance by the given ordinal number
254
     *
255
     * @param int $ordinal The ordinal number of the enumerator
256
     * @return static
257
     * @throws InvalidArgumentException On an invalid ordinal number
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
258
     * @throws LogicException           On ambiguous values
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
259
     */
260 39
    final public static function byOrdinal(int $ordinal)
261
    {
262 39
        $constants = self::$constants[static::class] ?? static::getConstants();
263
264 39
        if (!isset(self::$names[static::class][$ordinal])) {
265 1
            throw new InvalidArgumentException(\sprintf(
266 1
                'Invalid ordinal number %s, must between 0 and %s',
267 1
                $ordinal,
268 1
                \count(self::$names[static::class]) - 1
269
            ));
270
        }
271
272 38
        $name = self::$names[static::class][$ordinal];
273
274
        /** @var static $instance */
275 38
        $instance = self::$instances[static::class][$name]
276 38
            ?? self::$instances[static::class][$name] = new static($constants[$name], $ordinal);
0 ignored issues
show
Coding Style introduced by
Expected 1 space before "??"; newline found
Loading history...
277
278 38
        return $instance;
279
    }
280
281
    /**
282
     * Get a list of enumerator instances ordered by ordinal number
283
     *
284
     * @return static[]
285
     */
286 17
    final public static function getEnumerators()
287
    {
288 17
        if (!isset(self::$names[static::class])) {
289 1
            static::getConstants();
290
        }
291
292
        /** @var callable $byNameFn */
293 17
        $byNameFn = [static::class, 'byName'];
294 17
        return \array_map($byNameFn, self::$names[static::class]);
295
    }
296
297
    /**
298
     * Get a list of enumerator values ordered by ordinal number
299
     *
300
     * @return (null|bool|int|float|string|array<int|string, mixed>)[]
301
     */
302 7
    final public static function getValues()
303
    {
304 7
        return \array_values(self::$constants[static::class] ?? static::getConstants());
305
    }
306
307
    /**
308
     * Get a list of enumerator names ordered by ordinal number
309
     *
310
     * @return string[]
311
     */
312 3
    final public static function getNames()
313
    {
314 3
        if (!isset(self::$names[static::class])) {
315 1
            static::getConstants();
316
        }
317 3
        return self::$names[static::class];
318
    }
319
    
320
    /**
321
     * Get a list of enumerator ordinal numbers
322
     *
323
     * @return int[]
324
     */
325 1
    final public static function getOrdinals()
326
    {
327 1
        $count = \count(self::$constants[static::class] ?? static::getConstants());
328 1
        return $count ? \range(0, $count - 1) : [];
329
    }
330
331
    /**
332
     * Get all available constants of the called class
333
     *
334
     * @return array<string, null|bool|int|float|string|array<int|string, mixed>>
335
     * @throws LogicException On ambiguous constant values
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
336
     */
337 152
    final public static function getConstants()
338
    {
339 152
        if (isset(self::$constants[static::class])) {
340 116
            return self::$constants[static::class];
341
        }
342
343 51
        $reflection = new ReflectionClass(static::class);
344 51
        $constants  = [];
345
346
        do {
347 51
            $scopeConstants = [];
348
            // Enumerators must be defined as public class constants
349 51
            foreach ($reflection->getReflectionConstants() as $reflConstant) {
350 50
                if ($reflConstant->isPublic()) {
351 50
                    $scopeConstants[ $reflConstant->getName() ] = $reflConstant->getValue();
352
                }
353
            }
354
355 51
            $constants = $scopeConstants + $constants;
356 51
        } while (($reflection = $reflection->getParentClass()) && $reflection->name !== __CLASS__);
357
358 51
        assert(
359 51
            self::noAmbiguousValues($constants),
360 51
            'Ambiguous enumerator values detected for ' . static::class
361
        );
362
363 48
        self::$names[static::class] = \array_keys($constants);
364 48
        return self::$constants[static::class] = $constants;
365
    }
366
367
    /**
368
     * Test that the given constants does not contain ambiguous values
369
     * @param array<string, null|bool|int|float|string|array<int|string, mixed>> $constants
0 ignored issues
show
Documentation introduced by
Missing parameter comment
Loading history...
370
     * @return bool
371
     */
372 53
    private static function noAmbiguousValues($constants)
373
    {
374 53
        foreach ($constants as $value) {
375 52
            $names = \array_keys($constants, $value, true);
376 52
            if (\count($names) > 1) {
377 52
                return false;
378
            }
379
        }
380
381 48
        return true;
382
    }
383
384
    /**
385
     * Test if the given enumerator is part of this enumeration
386
     * 
387
     * @param static|null|bool|int|float|string|array<int|string, mixed> $enumerator
0 ignored issues
show
Documentation introduced by
Missing parameter comment
Loading history...
388
     * @return bool
389
     */
390 1
    final public static function has($enumerator)
391
    {
392 1
        if ($enumerator instanceof static) {
393 1
            return \get_class($enumerator) === static::class;
394
        }
395
396 1
        return static::hasValue($enumerator);
397
    }
398
399
    /**
400
     * Test if the given enumerator value is part of this enumeration
401
     *
402
     * @param null|bool|int|float|string|array<int|string, mixed> $value
0 ignored issues
show
Documentation introduced by
Missing parameter comment
Loading history...
403
     * @return bool
404
     */
405 2
    final public static function hasValue($value)
406
    {
407 2
        return \in_array($value, self::$constants[static::class] ?? static::getConstants(), true);
408
    }
409
410
    /**
411
     * Test if the given enumerator name is part of this enumeration
412
     *
413
     * @param string $name
0 ignored issues
show
Documentation introduced by
Missing parameter comment
Loading history...
414
     * @return bool
415
     */
416 1
    final public static function hasName(string $name)
417
    {
418 1
        return \defined("static::{$name}");
0 ignored issues
show
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $name instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
419
    }
420
421
    /**
422
     * Get an enumerator instance by the given name.
423
     *
424
     * This will be called automatically on calling a method
425
     * with the same name of a defined enumerator.
426
     *
427
     * @param string       $method The name of the enumerator (called as method)
428
     * @param array<mixed> $args   There should be no arguments
429
     * @return static
430
     * @throws InvalidArgumentException On an invalid or unknown name
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
431
     * @throws LogicException           On ambiguous constant values
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
432
     */
433 39
    final public static function __callStatic(string $method, array $args)
434
    {
435 39
        return static::byName($method);
436
    }
437
}
438