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

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