Completed
Pull Request — master (#32)
by
unknown
02:08 queued 11s
created

Enum::resolveFromStaticMethods()   B

Complexity

Conditions 6
Paths 3

Size

Total Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

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