Completed
Pull Request — master (#25)
by Tom
01:17
created

Enum::getMappedIndex()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 18
rs 9.6666
c 0
b 0
f 0
cc 4
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 array[] */
23
    protected static $maps = [];
24
25
    /** @var int */
26
    protected $index;
27
28
    /** @var string */
29
    protected $value;
30
31
    /** @var string */
32
    protected $name;
33
34
    /**
35
     * This construct is not part of the public API and COULD change in a minor release.
36
     * You SHOULD NOT use it by your own - instead you SHOULD use the make() method.
37
     *
38
     * @internal
39
     * @see \Spatie\Enum\Enum::make()
40
     *
41
     * @param string|null $name
42
     * @param string|null $value
43
     * @param int|null $index
44
     */
45
    public function __construct(?string $name = null, ?string $value = null, ?int $index = null)
46
    {
47
        if (is_null($name) && is_null($value) && is_null($index)) {
48
            ['name' => $name, 'value' => $value, 'index' => $index] = $this->resolveByStaticCall();
49
        }
50
51
        if (is_null($name) || ! static::isValidName($name)) {
52
            throw new InvalidNameException($name, static::class);
53
        }
54
55
        if (is_null($value) || ! static::isValidValue($value)) {
56
            throw new InvalidValueException($value, static::class);
57
        }
58
59
        if (is_null($index) || ! static::isValidIndex($index)) {
60
            throw new InvalidIndexException($index, static::class);
61
        }
62
63
        $this->name = $name;
64
        $this->value = $value;
65
        $this->index = $index;
66
    }
67
68
    public function __call($name, $arguments)
69
    {
70
        if (static::startsWith($name, 'is')) {
71
            return $this->isEqual(substr($name, 2));
72
        }
73
74
        throw new BadMethodCallException('Call to undefined method '.static::class.'->'.$name.'()');
75
    }
76
77
    public static function __callStatic($name, $arguments)
78
    {
79
        if (static::startsWith($name, 'is')) {
80
            if (! isset($arguments[0])) {
81
                throw new ArgumentCountError('Calling '.static::class.'::'.$name.'() in static context requires one argument');
82
            }
83
84
            return static::make($arguments[0])->$name();
85
        }
86
87
        if (static::isValidName($name) || static::isValidValue($name)) {
88
            return static::make($name);
89
        }
90
91
        throw new BadMethodCallException('Call to undefined method '.static::class.'::'.$name.'()');
92
    }
93
94
    public function __toString(): string
95
    {
96
        return $this->getValue();
97
    }
98
99
    public function getIndex(): int
100
    {
101
        return $this->index;
102
    }
103
104
    public static function getIndices(): array
105
    {
106
        return array_column(static::resolve(), 'index');
107
    }
108
109
    public function getValue(): string
110
    {
111
        return $this->value;
112
    }
113
114
    public static function getValues(): array
115
    {
116
        return array_column(static::resolve(), 'value');
117
    }
118
119
    public function getName(): string
120
    {
121
        return $this->name;
122
    }
123
124
    public static function getNames(): array
125
    {
126
        return array_keys(static::resolve());
127
    }
128
129
    public function isAny(array $values): bool
130
    {
131
        foreach ($values as $value) {
132
            if ($this->isEqual($value)) {
133
                return true;
134
            }
135
        }
136
137
        return false;
138
    }
139
140
    public function isEqual($value): bool
141
    {
142
        if (is_int($value) || is_string($value)) {
143
            $value = static::make($value);
144
        }
145
146
        if ($value instanceof $this) {
147
            return $value->getValue() === $this->getValue();
148
        }
149
150
        return false;
151
    }
152
153
    public function jsonSerialize()
154
    {
155
        return $this->getValue();
156
    }
157
158
    /**
159
     * @param int|string $value
160
     *
161
     * @return static
162
     */
163
    public static function make($value): Enumerable
164
    {
165
        if (! is_int($value) && ! is_string($value)) {
166
            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...
167
        }
168
169
        $name = null;
170
        $index = null;
171
172
        if (is_int($value)) {
173
            if (! static::isValidIndex($value)) {
174
                throw new InvalidIndexException($value, static::class);
175
            }
176
177
            [$name, $index, $value] = static::resolveByIndex($value);
178
        } elseif (is_string($value)) {
179
            [$name, $index, $value] = static::resolveByString($value);
180
        }
181
182
        if (is_string($name) && method_exists(static::class, $name)) {
183
            return forward_static_call(static::class.'::'.$name);
184
        }
185
186
        return new static($name, $value, $index);
187
    }
188
189
    public static function toArray(): array
190
    {
191
        $resolved = static::resolve();
192
193
        return array_combine(array_column($resolved, 'value'), array_column($resolved, 'index'));
194
    }
195
196
    protected static function isValidIndex(int $index): bool
197
    {
198
        return in_array($index, static::getIndices(), true);
199
    }
200
201
    protected static function isValidName(string $value): bool
202
    {
203
        return in_array(strtoupper($value), static::getNames(), true);
204
    }
205
206
    protected static function isValidValue(string $value): bool
207
    {
208
        return in_array($value, static::getValues(), true);
209
    }
210
211
    protected static function resolve(): array
212
    {
213
        $values = [];
214
215
        $class = static::class;
216
217
        if (isset(self::$cache[$class])) {
218
            return self::$cache[$class];
219
        }
220
221
        self::$cache[$class] = [];
222
223
        $reflection = new ReflectionClass(static::class);
224
225
        foreach (self::resolveFromDocBlocks($reflection) as $value) {
226
            $values[] = $value;
227
        }
228
229
        foreach (self::resolveFromStaticMethods($reflection) as $value) {
230
            $values[] = $value;
231
        }
232
233
        foreach ($values as $index => $value) {
234
            $name = strtoupper($value);
235
236
            self::$cache[$class][$name] = [
237
                'name' => $name,
238
                'index' => static::getMappedIndex($name) ?? $index,
239
                'value' => static::getMappedValue($name) ?? $value,
240
            ];
241
        }
242
243
        foreach (self::$cache[$class] as $name => $enum) {
244
            self::$cache[$class][$name]['value'] = static::make($name)->getValue();
245
            self::$cache[$class][$name]['index'] = static::make($name)->getIndex();
246
        }
247
248
        $duplicatedValues = array_filter(array_count_values(static::getValues()), function (int $count) {
249
            return $count > 1;
250
        });
251
252
        if (! empty($duplicatedValues)) {
253
            self::clearCache();
254
            throw new DuplicatedValueException(array_keys($duplicatedValues), static::class);
255
        }
256
257
        $duplicatedIndices = array_filter(array_count_values(static::getIndices()), function (int $count) {
258
            return $count > 1;
259
        });
260
261
        if (! empty($duplicatedIndices)) {
262
            self::clearCache();
263
            throw new DuplicatedIndexException(array_keys($duplicatedIndices), static::class);
264
        }
265
266
        return self::$cache[$class];
267
    }
268
269
    protected static function resolveFromDocBlocks(ReflectionClass $reflection): array
270
    {
271
        $values = [];
272
273
        $docComment = $reflection->getDocComment();
274
275
        if (! $docComment) {
276
            return $values;
277
        }
278
279
        preg_match_all('/\@method static self ([\w]+)\(\)/', $docComment, $matches);
280
281
        foreach ($matches[1] ?? [] as $value) {
282
            $values[] = $value;
283
        }
284
285
        return $values;
286
    }
287
288
    protected static function resolveFromStaticMethods(ReflectionClass $reflection): array
289
    {
290
        $values = [];
291
        foreach ($reflection->getMethods(ReflectionMethod::IS_STATIC) as $method) {
292
            if ($method->getDeclaringClass()->getName() === self::class) {
293
                continue;
294
            }
295
296
            $values[] = $method->getName();
297
        }
298
299
        return $values;
300
    }
301
302
    protected function resolveByStaticCall(): array
303
    {
304
        if (strpos(get_class($this), 'class@anonymous') !== 0) {
305
            throw new InvalidValueException(null, static::class);
306
        }
307
308
        $backtrace = debug_backtrace();
309
310
        $name = $backtrace[2]['function'];
311
312
        if (! static::isValidName($name)) {
313
            throw new InvalidValueException($name, static::class);
314
        }
315
316
        return static::resolve()[strtoupper($name)];
317
    }
318
319
    protected static function resolveByIndex(int $index): array
320
    {
321
        $name = array_combine(static::getIndices(), static::getNames())[$index];
322
        $value = array_search($index, static::toArray());
323
324
        return [$name, $index, $value];
325
    }
326
327
    protected static function resolveByString(string $string): array
328
    {
329
        if (static::isValidValue($string)) {
330
            return static::resolveByValue($string);
331
        }
332
333
        if (static::isValidName($string)) {
334
            return static::resolveByName($string);
335
        }
336
337
        throw new InvalidValueException($string, static::class);
338
    }
339
340
    protected static function resolveByValue(string $value): array
341
    {
342
        $index = static::toArray()[$value];
343
        $name = array_combine(static::getValues(), static::getNames())[$value];
344
345
        return [$name, $index, $value];
346
    }
347
348
    protected static function resolveByName(string $name): array
349
    {
350
        ['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...
351
352
        return [$name, $index, $value];
353
    }
354
355
    protected static function startsWith(string $haystack, string $needle)
356
    {
357
        return strlen($haystack) > 2 && strpos($haystack, $needle) === 0;
358
    }
359
360
    protected static function clearCache()
361
    {
362
        unset(self::$cache[static::class]);
363
    }
364
365
    protected static function getMappedIndex(string $name): ?int
366
    {
367
        if (! defined(static::class.'::MAP_INDEX')) {
368
            return null;
369
        }
370
371
        if (! isset(self::$maps[static::class]['index'])) {
372
            $map = [];
373
374
            foreach (constant(static::class.'::MAP_INDEX') as $key => $index) {
375
                $map[strtoupper($key)] = $index;
376
            }
377
378
            self::$maps[static::class]['index'] = $map;
379
        }
380
381
        return self::$maps[static::class]['index'][$name] ?? null;
382
    }
383
384
    protected static function getMappedValue(string $name): ?string
385
    {
386
        if (! defined(static::class.'::MAP_VALUE')) {
387
            return null;
388
        }
389
390
        if (! isset(self::$maps[static::class]['value'])) {
391
            $map = [];
392
393
            foreach (constant(static::class.'::MAP_VALUE') as $key => $index) {
394
                $map[strtoupper($key)] = $index;
395
            }
396
397
            self::$maps[static::class]['value'] = $map;
398
        }
399
400
        return self::$maps[static::class]['value'][$name] ?? null;
401
    }
402
}
403