Completed
Pull Request — master (#25)
by Tom
02:02 queued 44s
created

Enum::getMappedIndex()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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