Completed
Pull Request — master (#33)
by Freek
01:26
created

Enum::getAll()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 6
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
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
    /**
192
     * @return \Spatie\Enum\Enumerable[]
193
     */
194
    public static function getAll(): array
195
    {
196
        return array_map(function(string $value) {
197
            return static::make($value);
198
        }, static::getValues());
199
    }
200
201
    protected static function isValidIndex(int $index): bool
202
    {
203
        return in_array($index, static::getIndices(), true);
204
    }
205
206
    protected static function isValidName(string $value): bool
207
    {
208
        return in_array(strtoupper($value), static::getNames(), true);
209
    }
210
211
    protected static function isValidValue(string $value): bool
212
    {
213
        return in_array($value, static::getValues(), true);
214
    }
215
216
    protected static function resolve(): array
217
    {
218
        $values = [];
219
220
        $class = static::class;
221
222
        if (isset(self::$cache[$class])) {
223
            return self::$cache[$class];
224
        }
225
226
        self::$cache[$class] = [];
227
228
        $reflection = new ReflectionClass(static::class);
229
230
        foreach (self::resolveFromDocBlocks($reflection) as $value) {
231
            $values[] = $value;
232
        }
233
234
        foreach (self::resolveFromStaticMethods($reflection) as $value) {
235
            $values[] = $value;
236
        }
237
238
        foreach ($values as $index => $value) {
239
            $name = strtoupper($value);
240
241
            self::$cache[$class][$name] = [
242
                'name' => $name,
243
                'index' => static::getMappedIndex($name) ?? $index,
244
                'value' => static::getMappedValue($name) ?? $value,
245
            ];
246
        }
247
248
        foreach (self::$cache[$class] as $name => $enum) {
249
            self::$cache[$class][$name]['value'] = static::make($name)->getValue();
250
            self::$cache[$class][$name]['index'] = static::make($name)->getIndex();
251
        }
252
253
        $duplicatedValues = array_filter(array_count_values(static::getValues()), function (int $count) {
254
            return $count > 1;
255
        });
256
257
        if (! empty($duplicatedValues)) {
258
            self::clearCache();
259
            throw new DuplicatedValueException(array_keys($duplicatedValues), static::class);
260
        }
261
262
        $duplicatedIndices = array_filter(array_count_values(static::getIndices()), function (int $count) {
263
            return $count > 1;
264
        });
265
266
        if (! empty($duplicatedIndices)) {
267
            self::clearCache();
268
            throw new DuplicatedIndexException(array_keys($duplicatedIndices), static::class);
269
        }
270
271
        return self::$cache[$class];
272
    }
273
274
    protected static function resolveFromDocBlocks(ReflectionClass $reflection): array
275
    {
276
        $values = [];
277
278
        $docComment = $reflection->getDocComment();
279
280
        if (! $docComment) {
281
            return $values;
282
        }
283
284
        preg_match_all('/\@method static self ([\w]+)\(\)/', $docComment, $matches);
285
286
        foreach ($matches[1] ?? [] as $value) {
287
            $values[] = $value;
288
        }
289
290
        return $values;
291
    }
292
293
    protected static function resolveFromStaticMethods(ReflectionClass $reflection): array
294
    {
295
        $selfReflection = new ReflectionClass(self::class);
296
        $selfMethods = array_map(function (ReflectionMethod $method) {
297
            return $method->getName();
298
        }, $selfReflection->getMethods(ReflectionMethod::IS_STATIC | ReflectionMethod::IS_PUBLIC));
299
300
        $values = [];
301
        foreach ($reflection->getMethods(ReflectionMethod::IS_STATIC | ReflectionMethod::IS_PUBLIC) as $method) {
302
            if (
303
                $method->getDeclaringClass()->getName() === self::class
304
                || ! ($method->isPublic() && $method->isStatic())
305
                || in_array($method->getName(), $selfMethods)
306
            ) {
307
                continue;
308
            }
309
310
            $values[] = $method->getName();
311
        }
312
313
        return $values;
314
    }
315
316
    protected function resolveByStaticCall(): array
317
    {
318
        if (strpos(get_class($this), 'class@anonymous') !== 0) {
319
            throw new InvalidValueException(null, static::class);
320
        }
321
322
        $backtrace = debug_backtrace();
323
324
        $name = $backtrace[2]['function'];
325
326
        if (! static::isValidName($name)) {
327
            throw new InvalidValueException($name, static::class);
328
        }
329
330
        return static::resolve()[strtoupper($name)];
331
    }
332
333
    protected static function resolveByIndex(int $index): array
334
    {
335
        $name = array_combine(static::getIndices(), static::getNames())[$index];
336
        $value = array_search($index, static::toArray());
337
338
        return [$name, $index, $value];
339
    }
340
341
    protected static function resolveByString(string $string): array
342
    {
343
        if (static::isValidValue($string)) {
344
            return static::resolveByValue($string);
345
        }
346
347
        if (static::isValidName($string)) {
348
            return static::resolveByName($string);
349
        }
350
351
        throw new InvalidValueException($string, static::class);
352
    }
353
354
    protected static function resolveByValue(string $value): array
355
    {
356
        $index = static::toArray()[$value];
357
        $name = array_combine(static::getValues(), static::getNames())[$value];
358
359
        return [$name, $index, $value];
360
    }
361
362
    protected static function resolveByName(string $name): array
363
    {
364
        ['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...
365
366
        return [$name, $index, $value];
367
    }
368
369
    protected static function startsWith(string $haystack, string $needle)
370
    {
371
        return strlen($haystack) > 2 && strpos($haystack, $needle) === 0;
372
    }
373
374
    protected static function clearCache()
375
    {
376
        unset(self::$cache[static::class]);
377
    }
378
379
    protected static function getMappedIndex(string $name): ?int
380
    {
381
        if (! defined(static::class.'::MAP_INDEX')) {
382
            return null;
383
        }
384
385
        $map = [];
386
387
        foreach (constant(static::class.'::MAP_INDEX') as $key => $index) {
388
            $map[strtoupper($key)] = $index;
389
        }
390
391
        return $map[$name] ?? null;
392
    }
393
394
    protected static function getMappedValue(string $name): ?string
395
    {
396
        if (! defined(static::class.'::MAP_VALUE')) {
397
            return null;
398
        }
399
400
        $map = [];
401
402
        foreach (constant(static::class.'::MAP_VALUE') as $key => $index) {
403
            $map[strtoupper($key)] = $index;
404
        }
405
406
        return $map[$name] ?? null;
407
    }
408
}
409