Completed
Push — master ( 39eeee...3177f2 )
by Tom
10s
created

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