Passed
Push — master ( db9ed6...3e7d9b )
by Tom
02:39 queued 01:13
created

Enum::isValidValue()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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