Completed
Pull Request — master (#55)
by
unknown
01:08
created

Enum::toArray()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
1
<?php
2
3
namespace Spatie\Enum;
4
5
use ArgumentCountError;
6
use BadMethodCallException;
7
use JsonSerializable;
8
use ReflectionClass;
9
use ReflectionMethod;
10
use Spatie\Enum\Exceptions\DuplicatedIndexException;
11
use Spatie\Enum\Exceptions\DuplicatedValueException;
12
use Spatie\Enum\Exceptions\InvalidIndexException;
13
use Spatie\Enum\Exceptions\InvalidNameException;
14
use Spatie\Enum\Exceptions\InvalidValueException;
15
use TypeError;
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(string $name, array $arguments)
66
    {
67
        if (static::startsWith($name, 'is')) {
68
            return $this->isEqual(substr($name, 2));
69
        }
70
71
        if (static::isValidName($name)) {
72
            return static::make($name);
73
        }
74
75
        throw new BadMethodCallException('Call to undefined method '.static::class.'->'.$name.'()');
76
    }
77
78
    /**
79
     * @param string $name
80
     * @param mixed[] $arguments
81
     *
82
     * @return \Spatie\Enum\Enumerable|bool
83
     */
84
    public static function __callStatic(string $name, array $arguments)
85
    {
86
        if (static::startsWith($name, 'is')) {
87
            if (! isset($arguments[0])) {
88
                throw new ArgumentCountError('Calling '.static::class.'::'.$name.'() in static context requires one argument');
89
            }
90
91
            return static::make($arguments[0])->$name();
92
        }
93
94
        if (static::isValidName($name) || static::isValidValue($name)) {
95
            return static::make($name);
96
        }
97
98
        throw new BadMethodCallException('Call to undefined method '.static::class.'::'.$name.'()');
99
    }
100
101
    public function __toString(): string
102
    {
103
        return $this->getValue();
104
    }
105
106
    public function getIndex(): int
107
    {
108
        return $this->index;
109
    }
110
111
    public static function getIndices(): array
112
    {
113
        return array_column(static::resolve(), 'index');
114
    }
115
116
    public function getValue(): string
117
    {
118
        return $this->value;
119
    }
120
121
    public static function getValues(): array
122
    {
123
        return array_column(static::resolve(), 'value');
124
    }
125
126
    public function getName(): string
127
    {
128
        return $this->name;
129
    }
130
131
    public static function getNames(): array
132
    {
133
        return array_keys(static::resolve());
134
    }
135
136
    public function isAny(array $values): bool
137
    {
138
        foreach ($values as $value) {
139
            if ($this->isEqual($value)) {
140
                return true;
141
            }
142
        }
143
144
        return false;
145
    }
146
147
    public function isEqual($value): bool
148
    {
149
        if (is_int($value) || is_string($value)) {
150
            try {
151
                $value = static::make($value);
152
            } catch (InvalidValueException $error) {
153
                return false;
154
            } catch (InvalidIndexException $error) {
155
                return false;
156
            }
157
        }
158
159
        if ($value instanceof $this) {
160
            return $value->getValue() === $this->getValue();
161
        }
162
163
        return false;
164
    }
165
166
    public function jsonSerialize(): string
167
    {
168
        return $this->getValue();
169
    }
170
171
    /**
172
     * @param int|string $value
173
     *
174
     * @return static
175
     */
176
    public static function make($value): Enumerable
177
    {
178
        if (! is_int($value) && ! is_string($value)) {
179
            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...
180
        }
181
182
        $name = null;
183
        $index = null;
184
185
        if (is_int($value)) {
186
            if (! static::isValidIndex($value)) {
187
                throw new InvalidIndexException($value, static::class);
188
            }
189
190
            [$name, $index, $value] = static::resolveByIndex($value);
191
        } elseif (is_string($value)) {
192
            [$name, $index, $value] = static::resolveByString($value);
193
        }
194
195
        if (is_string($name) && method_exists(static::class, $name)) {
196
            return forward_static_call(static::class.'::'.$name);
197
        }
198
199
        return new static($name, $value, $index);
200
    }
201
202
    /**
203
     * @param int $index
204
     *
205
     * @return static
206
     */
207
    public static function makeByIndex(int $index): Enumerable
208
    {
209
        if (! static::isValidIndex($index)) {
210
            throw new InvalidIndexException($index, static::class);
211
        }
212
213
        [$name, $index, $value] = static::resolveByIndex($index);
0 ignored issues
show
Bug introduced by
The variable $name 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 $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...
214
215
        return new static($name, $value, $index);
216
    }
217
218
    /**
219
     * @param string $value
220
     *
221
     * @return static
222
     */
223
    public static function makeByValue(string $value): Enumerable
224
    {
225
        [$name, $index, $value] = static::resolveByString($value);
0 ignored issues
show
Bug introduced by
The variable $name 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...
226
227
        if (is_string($name) && method_exists(static::class, $name)) {
228
            return forward_static_call(static::class.'::'.$name);
229
        }
230
231
        return new static($name, $value, $index);
232
    }
233
234
    public static function toArray(): array
235
    {
236
        return array_combine(static::getValues(), static::getIndices());
237
    }
238
239
    /**
240
     * @return \Spatie\Enum\Enumerable[]
241
     */
242
    public static function getAll(): array
243
    {
244
        return array_map(static function (int $index): Enumerable {
245
            return static::make($index);
246
        }, static::getIndices());
247
    }
248
249
    public static function isValidIndex(int $index): bool
250
    {
251
        return in_array($index, static::getIndices(), true);
252
    }
253
254
    public static function isValidName(string $value): bool
255
    {
256
        return in_array(strtoupper($value), static::getNames(), true);
257
    }
258
259
    public static function isValidValue(string $value): bool
260
    {
261
        return in_array($value, static::getValues(), true);
262
    }
263
264
    protected static function resolve(): array
265
    {
266
        $values = [];
267
268
        $class = static::class;
269
270
        if (isset(self::$cache[$class])) {
271
            return self::$cache[$class];
272
        }
273
274
        self::$cache[$class] = [];
275
276
        $reflection = new ReflectionClass(static::class);
277
278
        foreach (static::resolveFromDocBlocks($reflection) as $value) {
279
            $values[] = $value;
280
        }
281
282
        foreach (static::resolveFromStaticMethods($reflection) as $value) {
283
            $values[] = $value;
284
        }
285
286
        foreach ($values as $index => $value) {
287
            $name = strtoupper($value);
288
289
            self::$cache[$class][$name] = [
290
                'name' => $name,
291
                'index' => static::getMappedIndex($name) ?? $index,
292
                'value' => static::getMappedValue($name) ?? $value,
293
            ];
294
        }
295
296
        foreach (array_keys(self::$cache[$class]) as $name) {
297
            self::$cache[$class][$name]['value'] = static::make($name)->getValue();
298
            self::$cache[$class][$name]['index'] = static::make($name)->getIndex();
299
        }
300
301
        $duplicatedValues = array_filter(array_count_values(static::getValues()), static function (int $count): bool {
302
            return $count > 1;
303
        });
304
305
        if (! empty($duplicatedValues)) {
306
            self::clearCache();
307
            throw new DuplicatedValueException(array_keys($duplicatedValues), static::class);
308
        }
309
310
        $duplicatedIndices = array_filter(array_count_values(static::getIndices()), static function (int $count): bool {
311
            return $count > 1;
312
        });
313
314
        if (! empty($duplicatedIndices)) {
315
            self::clearCache();
316
            throw new DuplicatedIndexException(array_keys($duplicatedIndices), static::class);
317
        }
318
319
        return self::$cache[$class];
320
    }
321
322
    protected static function resolveFromDocBlocks(ReflectionClass $reflection): array
323
    {
324
        $values = [];
325
326
        $docComment = $reflection->getDocComment();
327
328
        if (! $docComment) {
329
            return $values;
330
        }
331
332
        preg_match_all('/\@method static self ([\w]+)\(\)/', $docComment, $matches);
333
334
        foreach ($matches[1] ?? [] as $value) {
335
            $values[] = $value;
336
        }
337
338
        return $values;
339
    }
340
341
    protected static function resolveFromStaticMethods(ReflectionClass $reflection): array
342
    {
343
        $selfReflection = new ReflectionClass(self::class);
344
        $selfMethods = array_map(static function (ReflectionMethod $method): string {
345
            return $method->getName();
346
        }, $selfReflection->getMethods(ReflectionMethod::IS_STATIC | ReflectionMethod::IS_PUBLIC));
347
348
        $values = [];
349
        foreach ($reflection->getMethods(ReflectionMethod::IS_STATIC | ReflectionMethod::IS_PUBLIC) as $method) {
350
            if (
351
                $method->getDeclaringClass()->getName() === self::class
352
                || ! ($method->isPublic() && $method->isStatic())
353
                || in_array($method->getName(), $selfMethods)
354
            ) {
355
                continue;
356
            }
357
358
            $values[] = $method->getName();
359
        }
360
361
        return $values;
362
    }
363
364
    protected function resolveByStaticCall(): array
365
    {
366
        if (strpos(static::class, 'class@anonymous') !== 0) {
367
            throw new InvalidValueException(null, static::class);
368
        }
369
370
        $backtrace = debug_backtrace();
371
372
        $name = $backtrace[2]['function'];
373
374
        if (! static::isValidName($name)) {
375
            throw new InvalidValueException($name, static::class);
376
        }
377
378
        return static::resolve()[strtoupper($name)];
379
    }
380
381
    protected static function resolveByIndex(int $index): array
382
    {
383
        $name = array_combine(static::getIndices(), static::getNames())[$index];
384
        $value = array_search($index, static::toArray());
385
386
        return [$name, $index, $value];
387
    }
388
389
    protected static function resolveByString(string $string): array
390
    {
391
        if (static::isValidValue($string)) {
392
            return static::resolveByValue($string);
393
        }
394
395
        if (static::isValidName($string)) {
396
            return static::resolveByName($string);
397
        }
398
399
        throw new InvalidValueException($string, static::class);
400
    }
401
402
    protected static function resolveByValue(string $value): array
403
    {
404
        $index = static::toArray()[$value];
405
        $name = array_combine(static::getValues(), static::getNames())[$value];
406
407
        return [$name, $index, $value];
408
    }
409
410
    protected static function resolveByName(string $name): array
411
    {
412
        $name = strtoupper($name);
413
414
        ['value' => $value, 'index' => $index] = static::resolve()[$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...
415
416
        return [$name, $index, $value];
417
    }
418
419
    protected static function startsWith(string $haystack, string $needle): bool
420
    {
421
        return strlen($haystack) > 2 && strpos($haystack, $needle) === 0;
422
    }
423
424
    protected static function clearCache(): void
425
    {
426
        unset(self::$cache[static::class]);
427
    }
428
429
    protected static function getMappedIndex(string $name): ?int
430
    {
431
        if (! defined(static::class.'::MAP_INDEX')) {
432
            return null;
433
        }
434
435
        $map = [];
436
437
        foreach (constant(static::class.'::MAP_INDEX') as $key => $index) {
438
            $map[strtoupper($key)] = $index;
439
        }
440
441
        return $map[$name] ?? null;
442
    }
443
444
    protected static function getMappedValue(string $name): ?string
445
    {
446
        if (! defined(static::class.'::MAP_VALUE')) {
447
            return null;
448
        }
449
450
        $map = [];
451
452
        foreach (constant(static::class.'::MAP_VALUE') as $key => $index) {
453
            $map[strtoupper($key)] = $index;
454
        }
455
456
        return $map[$name] ?? null;
457
    }
458
}
459