Completed
Pull Request — master (#25)
by Tom
01:36 queued 10s
created

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