Completed
Pull Request — master (#21)
by Tom
01:15
created

Enum::resolveByValue()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 7
rs 10
c 0
b 0
f 0
cc 1
nc 1
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\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
        if (is_int($index) && is_string($value)) {
149
            return new static($value, $index);
150
        }
151
152
        throw new InvalidValueException($value, static::class);
153
    }
154
155
    public static function toArray(): array
156
    {
157
        $resolved = static::resolve();
158
159
        return array_combine(array_column($resolved, 'value'), array_column($resolved, 'index'));
160
    }
161
162
    protected static function isValidIndex(int $index): bool
163
    {
164
        return in_array($index, static::getIndices(), true);
165
    }
166
167
    protected static function isValidName(string $value): bool
168
    {
169
        return in_array(strtoupper($value), array_keys(static::resolve()), true);
170
    }
171
172
    protected static function isValidValue(string $value): bool
173
    {
174
        return in_array($value, static::getValues(), true);
175
    }
176
177
    protected static function resolve(): array
178
    {
179
        $values = [];
180
181
        $class = static::class;
182
183
        if (isset(self::$cache[$class])) {
184
            return self::$cache[$class];
185
        }
186
187
        self::$cache[$class] = [];
188
189
        $reflection = new ReflectionClass(static::class);
190
191
        foreach (self::resolveFromDocBlocks($reflection) as $value) {
192
            $values[] = $value;
193
        }
194
195
        foreach (self::resolveFromStaticMethods($reflection) as $value) {
196
            $values[] = $value;
197
        }
198
199
        foreach ($values as $index => $value) {
200
            self::$cache[$class][strtoupper($value)] = [
201
                'index' => $index,
202
                'value' => $value,
203
            ];
204
        }
205
206
        foreach (self::$cache[$class] as $name => $enum) {
207
            self::$cache[$class][$name]['value'] = static::make($name)->getValue();
208
            self::$cache[$class][$name]['index'] = static::make($name)->getIndex();
209
        }
210
211
        $duplicatedValues = array_filter(array_count_values(static::getValues()), function (int $count) {
212
            return $count > 1;
213
        });
214
215
        if (! empty($duplicatedValues)) {
216
            self::clearCache();
217
            throw new DuplicatedValueException(array_keys($duplicatedValues), static::class);
218
        }
219
220
        $duplicatedIndices = array_filter(array_count_values(static::getIndices()), function (int $count) {
221
            return $count > 1;
222
        });
223
224
        if (! empty($duplicatedIndices)) {
225
            self::clearCache();
226
            throw new DuplicatedIndexException(array_keys($duplicatedIndices), static::class);
227
        }
228
229
        return self::$cache[$class];
230
    }
231
232
    protected static function resolveFromDocBlocks(ReflectionClass $reflection): array
233
    {
234
        $values = [];
235
236
        $docComment = $reflection->getDocComment();
237
238
        if (! $docComment) {
239
            return $values;
240
        }
241
242
        preg_match_all('/\@method static self ([\w]+)\(\)/', $docComment, $matches);
243
244
        foreach ($matches[1] ?? [] as $value) {
245
            $values[] = $value;
246
        }
247
248
        return $values;
249
    }
250
251
    protected static function resolveFromStaticMethods(ReflectionClass $reflection): array
252
    {
253
        $values = [];
254
        foreach ($reflection->getMethods(ReflectionMethod::IS_STATIC) as $method) {
255
            if ($method->getDeclaringClass()->getName() === self::class) {
256
                continue;
257
            }
258
259
            $values[] = $method->getName();
260
        }
261
262
        return $values;
263
    }
264
265
    protected function resolveByStaticCall(): array
266
    {
267
        if (strpos(get_class($this), 'class@anonymous') !== 0) {
268
            throw new InvalidValueException(null, static::class);
269
        }
270
271
        $backtrace = debug_backtrace();
272
273
        $name = $backtrace[2]['function'];
274
275
        if (! static::isValidName($name)) {
276
            throw new InvalidValueException($name, static::class);
277
        }
278
279
        return static::resolve()[strtoupper($name)];
280
    }
281
282
    protected static function resolveByIndex(int $index): array
283
    {
284
        $name = array_combine(static::getIndices(), array_keys(static::resolve()))[$index];
285
        $value = array_search($index, static::toArray());
286
287
        return [$name, $index, $value];
288
    }
289
290
    protected static function resolveByString(string $string): array
291
    {
292
        if (static::isValidValue($string)) {
293
            return static::resolveByValue($string);
294
        }
295
296
        if (static::isValidName($string)) {
297
            return static::resolveByName($string);
298
        }
299
300
        throw new InvalidValueException($string, static::class);
301
    }
302
303
    protected static function resolveByValue(string $value): array
304
    {
305
        $index = static::toArray()[$value];
306
        $name = array_combine(static::getValues(), array_keys(static::resolve()))[$value];
307
308
        return [$name, $index, $value];
309
    }
310
311
    protected static function resolveByName(string $name): array
312
    {
313
        ['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...
314
315
        return [$name, $index, $value];
316
    }
317
318
    protected static function startsWith(string $haystack, string $needle)
319
    {
320
        return strlen($haystack) > 2 && strpos($haystack, $needle) === 0;
321
    }
322
323
    protected static function clearCache()
324
    {
325
        unset(self::$cache[static::class]);
326
    }
327
}
328