Completed
Pull Request — master (#33)
by Freek
02:12
created

Enum   F

Complexity

Total Complexity 80

Size/Duplication

Total Lines 389
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 5

Importance

Changes 0
Metric Value
wmc 80
lcom 1
cbo 5
dl 0
loc 389
rs 2
c 0
b 0
f 0

31 Methods

Rating   Name   Duplication   Size   Complexity  
B __construct() 0 22 10
A __call() 0 8 2
A __callStatic() 0 16 5
A __toString() 0 4 1
A getIndex() 0 4 1
A getIndices() 0 4 1
A getValue() 0 4 1
A getValues() 0 4 1
A getName() 0 4 1
A getNames() 0 4 1
A isAny() 0 10 3
A isEqual() 0 12 4
A jsonSerialize() 0 4 1
B make() 0 25 8
A toArray() 0 4 1
A all() 0 6 1
A isValidIndex() 0 4 1
A isValidName() 0 4 1
A isValidValue() 0 4 1
B resolve() 0 57 8
A resolveFromDocBlocks() 0 18 3
B resolveFromStaticMethods() 0 22 6
A resolveByStaticCall() 0 16 3
A resolveByIndex() 0 7 1
A resolveByString() 0 12 3
A resolveByValue() 0 7 1
A resolveByName() 0 6 1
A startsWith() 0 4 2
A clearCache() 0 4 1
A getMappedIndex() 0 14 3
A getMappedValue() 0 14 3

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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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
namespace Spatie\Enum;
4
5
use TypeError;
6
use ReflectionClass;
7
use JsonSerializable;
8
use ReflectionMethod;
9
use ArgumentCountError;
10
use BadMethodCallException;
11
use Spatie\Enum\Exceptions\InvalidNameException;
12
use Spatie\Enum\Exceptions\InvalidIndexException;
13
use Spatie\Enum\Exceptions\InvalidValueException;
14
use Spatie\Enum\Exceptions\DuplicatedIndexException;
15
use Spatie\Enum\Exceptions\DuplicatedValueException;
16
17
abstract class Enum implements Enumerable, JsonSerializable
18
{
19
    /** @var array[] */
20
    protected static $cache = [];
21
22
    /** @var int */
23
    protected $index;
24
25
    /** @var string */
26
    protected $value;
27
28
    /** @var string */
29
    protected $name;
30
31
    /**
32
     * This construct is not part of the public API and COULD change in a minor release.
33
     * You SHOULD NOT use it by your own - instead you SHOULD use the make() method.
34
     *
35
     * @internal
36
     * @see \Spatie\Enum\Enum::make()
37
     *
38
     * @param string|null $name
39
     * @param string|null $value
40
     * @param int|null $index
41
     */
42
    public function __construct(?string $name = null, ?string $value = null, ?int $index = null)
43
    {
44
        if (is_null($name) && is_null($value) && is_null($index)) {
45
            ['name' => $name, 'value' => $value, 'index' => $index] = $this->resolveByStaticCall();
46
        }
47
48
        if (is_null($name) || ! static::isValidName($name)) {
49
            throw new InvalidNameException($name, static::class);
50
        }
51
52
        if (is_null($value) || ! static::isValidValue($value)) {
53
            throw new InvalidValueException($value, static::class);
54
        }
55
56
        if (is_null($index) || ! static::isValidIndex($index)) {
57
            throw new InvalidIndexException($index, static::class);
58
        }
59
60
        $this->name = $name;
61
        $this->value = $value;
62
        $this->index = $index;
63
    }
64
65
    public function __call($name, $arguments)
66
    {
67
        if (static::startsWith($name, 'is')) {
68
            return $this->isEqual(substr($name, 2));
69
        }
70
71
        throw new BadMethodCallException('Call to undefined method '.static::class.'->'.$name.'()');
72
    }
73
74
    public static function __callStatic($name, $arguments)
75
    {
76
        if (static::startsWith($name, 'is')) {
77
            if (! isset($arguments[0])) {
78
                throw new ArgumentCountError('Calling '.static::class.'::'.$name.'() in static context requires one argument');
79
            }
80
81
            return static::make($arguments[0])->$name();
82
        }
83
84
        if (static::isValidName($name) || static::isValidValue($name)) {
85
            return static::make($name);
86
        }
87
88
        throw new BadMethodCallException('Call to undefined method '.static::class.'::'.$name.'()');
89
    }
90
91
    public function __toString(): string
92
    {
93
        return $this->getValue();
94
    }
95
96
    public function getIndex(): int
97
    {
98
        return $this->index;
99
    }
100
101
    public static function getIndices(): array
102
    {
103
        return array_column(static::resolve(), 'index');
104
    }
105
106
    public function getValue(): string
107
    {
108
        return $this->value;
109
    }
110
111
    public static function getValues(): array
112
    {
113
        return array_column(static::resolve(), 'value');
114
    }
115
116
    public function getName(): string
117
    {
118
        return $this->name;
119
    }
120
121
    public static function getNames(): array
122
    {
123
        return array_keys(static::resolve());
124
    }
125
126
    public function isAny(array $values): bool
127
    {
128
        foreach ($values as $value) {
129
            if ($this->isEqual($value)) {
130
                return true;
131
            }
132
        }
133
134
        return false;
135
    }
136
137
    public function isEqual($value): bool
138
    {
139
        if (is_int($value) || is_string($value)) {
140
            $value = static::make($value);
141
        }
142
143
        if ($value instanceof $this) {
144
            return $value->getValue() === $this->getValue();
145
        }
146
147
        return false;
148
    }
149
150
    public function jsonSerialize()
151
    {
152
        return $this->getValue();
153
    }
154
155
    /**
156
     * @param int|string $value
157
     *
158
     * @return static
159
     */
160
    public static function make($value): Enumerable
161
    {
162
        if (! is_int($value) && ! is_string($value)) {
163
            throw new TypeError(static::class.'::make() expects string|int as argument but '.gettype($value).' given');
0 ignored issues
show
Unused Code introduced by
The call to TypeError::__construct() has too many arguments starting with static::class . '::make(...type($value) . ' given'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
164
        }
165
166
        $name = null;
167
        $index = null;
168
169
        if (is_int($value)) {
170
            if (! static::isValidIndex($value)) {
171
                throw new InvalidIndexException($value, static::class);
172
            }
173
174
            [$name, $index, $value] = static::resolveByIndex($value);
175
        } elseif (is_string($value)) {
176
            [$name, $index, $value] = static::resolveByString($value);
177
        }
178
179
        if (is_string($name) && method_exists(static::class, $name)) {
180
            return forward_static_call(static::class.'::'.$name);
181
        }
182
183
        return new static($name, $value, $index);
184
    }
185
186
    public static function toArray(): array
187
    {
188
        return array_combine(static::getValues(), static::getIndices());
189
    }
190
191
    public static function all(): array
192
    {
193
        return array_map(function(string $name) {
194
            return static::$name();
195
        }, static::getNames());
196
    }
197
198
    protected static function isValidIndex(int $index): bool
199
    {
200
        return in_array($index, static::getIndices(), true);
201
    }
202
203
    protected static function isValidName(string $value): bool
204
    {
205
        return in_array(strtoupper($value), static::getNames(), true);
206
    }
207
208
    protected static function isValidValue(string $value): bool
209
    {
210
        return in_array($value, static::getValues(), true);
211
    }
212
213
    protected static function resolve(): array
214
    {
215
        $values = [];
216
217
        $class = static::class;
218
219
        if (isset(self::$cache[$class])) {
220
            return self::$cache[$class];
221
        }
222
223
        self::$cache[$class] = [];
224
225
        $reflection = new ReflectionClass(static::class);
226
227
        foreach (self::resolveFromDocBlocks($reflection) as $value) {
228
            $values[] = $value;
229
        }
230
231
        foreach (self::resolveFromStaticMethods($reflection) as $value) {
232
            $values[] = $value;
233
        }
234
235
        foreach ($values as $index => $value) {
236
            $name = strtoupper($value);
237
238
            self::$cache[$class][$name] = [
239
                'name' => $name,
240
                'index' => static::getMappedIndex($name) ?? $index,
241
                'value' => static::getMappedValue($name) ?? $value,
242
            ];
243
        }
244
245
        foreach (self::$cache[$class] as $name => $enum) {
246
            self::$cache[$class][$name]['value'] = static::make($name)->getValue();
247
            self::$cache[$class][$name]['index'] = static::make($name)->getIndex();
248
        }
249
250
        $duplicatedValues = array_filter(array_count_values(static::getValues()), function (int $count) {
251
            return $count > 1;
252
        });
253
254
        if (! empty($duplicatedValues)) {
255
            self::clearCache();
256
            throw new DuplicatedValueException(array_keys($duplicatedValues), static::class);
257
        }
258
259
        $duplicatedIndices = array_filter(array_count_values(static::getIndices()), function (int $count) {
260
            return $count > 1;
261
        });
262
263
        if (! empty($duplicatedIndices)) {
264
            self::clearCache();
265
            throw new DuplicatedIndexException(array_keys($duplicatedIndices), static::class);
266
        }
267
268
        return self::$cache[$class];
269
    }
270
271
    protected static function resolveFromDocBlocks(ReflectionClass $reflection): array
272
    {
273
        $values = [];
274
275
        $docComment = $reflection->getDocComment();
276
277
        if (! $docComment) {
278
            return $values;
279
        }
280
281
        preg_match_all('/\@method static self ([\w]+)\(\)/', $docComment, $matches);
282
283
        foreach ($matches[1] ?? [] as $value) {
284
            $values[] = $value;
285
        }
286
287
        return $values;
288
    }
289
290
    protected static function resolveFromStaticMethods(ReflectionClass $reflection): array
291
    {
292
        $selfReflection = new ReflectionClass(self::class);
293
        $selfMethods = array_map(function (ReflectionMethod $method) {
294
            return $method->getName();
295
        }, $selfReflection->getMethods(ReflectionMethod::IS_STATIC | ReflectionMethod::IS_PUBLIC));
296
297
        $values = [];
298
        foreach ($reflection->getMethods(ReflectionMethod::IS_STATIC | ReflectionMethod::IS_PUBLIC) as $method) {
299
            if (
300
                $method->getDeclaringClass()->getName() === self::class
301
                || ! ($method->isPublic() && $method->isStatic())
302
                || in_array($method->getName(), $selfMethods)
303
            ) {
304
                continue;
305
            }
306
307
            $values[] = $method->getName();
308
        }
309
310
        return $values;
311
    }
312
313
    protected function resolveByStaticCall(): array
314
    {
315
        if (strpos(get_class($this), 'class@anonymous') !== 0) {
316
            throw new InvalidValueException(null, static::class);
317
        }
318
319
        $backtrace = debug_backtrace();
320
321
        $name = $backtrace[2]['function'];
322
323
        if (! static::isValidName($name)) {
324
            throw new InvalidValueException($name, static::class);
325
        }
326
327
        return static::resolve()[strtoupper($name)];
328
    }
329
330
    protected static function resolveByIndex(int $index): array
331
    {
332
        $name = array_combine(static::getIndices(), static::getNames())[$index];
333
        $value = array_search($index, static::toArray());
334
335
        return [$name, $index, $value];
336
    }
337
338
    protected static function resolveByString(string $string): array
339
    {
340
        if (static::isValidValue($string)) {
341
            return static::resolveByValue($string);
342
        }
343
344
        if (static::isValidName($string)) {
345
            return static::resolveByName($string);
346
        }
347
348
        throw new InvalidValueException($string, static::class);
349
    }
350
351
    protected static function resolveByValue(string $value): array
352
    {
353
        $index = static::toArray()[$value];
354
        $name = array_combine(static::getValues(), static::getNames())[$value];
355
356
        return [$name, $index, $value];
357
    }
358
359
    protected static function resolveByName(string $name): array
360
    {
361
        ['value' => $value, 'index' => $index] = static::resolve()[strtoupper($name)];
0 ignored issues
show
Bug introduced by
The variable $value does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $index does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
362
363
        return [$name, $index, $value];
364
    }
365
366
    protected static function startsWith(string $haystack, string $needle)
367
    {
368
        return strlen($haystack) > 2 && strpos($haystack, $needle) === 0;
369
    }
370
371
    protected static function clearCache()
372
    {
373
        unset(self::$cache[static::class]);
374
    }
375
376
    protected static function getMappedIndex(string $name): ?int
377
    {
378
        if (! defined(static::class.'::MAP_INDEX')) {
379
            return null;
380
        }
381
382
        $map = [];
383
384
        foreach (constant(static::class.'::MAP_INDEX') as $key => $index) {
385
            $map[strtoupper($key)] = $index;
386
        }
387
388
        return $map[$name] ?? null;
389
    }
390
391
    protected static function getMappedValue(string $name): ?string
392
    {
393
        if (! defined(static::class.'::MAP_VALUE')) {
394
            return null;
395
        }
396
397
        $map = [];
398
399
        foreach (constant(static::class.'::MAP_VALUE') as $key => $index) {
400
            $map[strtoupper($key)] = $index;
401
        }
402
403
        return $map[$name] ?? null;
404
    }
405
}
406