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