Completed
Pull Request — master (#18)
by Tom
02:20
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
declare(strict_types=1);
4
5
namespace Spatie\Enum;
6
7
use TypeError;
8
use ReflectionClass;
9
use JsonSerializable;
10
use ReflectionMethod;
11
use BadMethodCallException;
12
use Spatie\Enum\Exceptions\InvalidIndexException;
13
use Spatie\Enum\Exceptions\InvalidValueException;
14
15
abstract class Enum implements Enumerable, JsonSerializable
16
{
17
    /** @var array[] */
18
    protected static $cache = [];
19
20
    /** @var bool[] */
21
    protected static $resolved = [];
22
23
    /** @var string */
24
    protected $value;
25
26
    /** @var int */
27
    protected $index;
28
29
    public function __construct(?string $value = null, ?int $index = null)
30
    {
31
        if(!static::isResolved()) {
32
            return;
33
        }
34
35
        if (is_null($value) && is_null($index)) {
36
            $value = $this->resolveValueFromStaticCall();
37
            $index = static::toArray()[$value];
38
        }
39
40
        if (! static::isValidValue($value)) {
41
            throw new InvalidValueException($value, static::class);
42
        }
43
44
        if (! static::isValidIndex($index)) {
45
            throw new InvalidIndexException($value, static::class);
46
        }
47
48
        $this->value = $value;
49
        $this->index = $index;
50
    }
51
52
    public function __call($name, $arguments)
53
    {
54
        if (strlen($name) > 2 && strpos($name, 'is') === 0) {
55
            return $this->isEqual(substr($name, 2));
56
        }
57
58
        if (static::isValidValue($name)) {
59
            return static::__callStatic($name, $arguments);
60
        }
61
62
        throw new BadMethodCallException(sprintf('Call to undefined method %s->%s()', static::class, $name));
63
    }
64
65
    public static function __callStatic($name, $arguments)
66
    {
67
        if (strlen($name) > 2 && strpos($name, 'is') === 0) {
68
            if (! isset($arguments[0])) {
69
                throw new \ArgumentCountError(sprintf('Calling %s::%s() in static context requires one argument', static::class, $name));
70
            }
71
72
            return static::make($arguments[0])->$name();
73
        }
74
75
        if (static::isValidName($name) || static::isValidValue($name)) {
76
            return static::make($name);
77
        }
78
79
        throw new BadMethodCallException(sprintf('Call to undefined method %s::%s()', static::class, $name));
80
    }
81
82
    public static function make($value): Enumerable
83
    {
84
        if (is_int($value)) {
85
            $index = $value;
86
87
            if (! static::isValidIndex($index)) {
88
                throw new InvalidIndexException($value, static::class);
89
            }
90
91
            return new static(array_search($index, static::toArray()), $index);
92
        } elseif (is_string($value)) {
93
            if (method_exists(static::class, $value)) {
94
                return forward_static_call(static::class.'::'.$value);
95
            }
96
97
            if (static::isValidValue($value)) {
98
                return new static($value, static::toArray()[$value]);
99
            }
100
101
            if (static::isValidName($value)) {
102
                $value = strtoupper($value);
103
                return new static(static::resolve()[$value]['value'], static::resolve()[$value]['index']);
104
            }
105
106
            throw new InvalidValueException($value, static::class);
107
        }
108
109
        throw new TypeError(sprintf('%s::make() expects string|int as argument but %s given', static::class, gettype($value)));
0 ignored issues
show
Unused Code introduced by
The call to TypeError::__construct() has too many arguments starting with sprintf('%s::make() expe...class, gettype($value)).

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...
110
    }
111
112
    public static function isValidIndex(int $index): bool
113
    {
114
        return in_array($index, static::getIndices(), true);
115
    }
116
117
    public static function isValidName(string $value): bool
118
    {
119
        return in_array(strtoupper($value), array_keys(static::resolve()), true);
120
    }
121
122
    public static function isValidValue(string $value): bool
123
    {
124
        return in_array($value, static::getValues(), true);
125
    }
126
127
    public static function getIndices(): array
128
    {
129
        return array_values(static::toArray());
130
    }
131
132
    public static function getValues(): array
133
    {
134
        return array_keys(static::toArray());
135
    }
136
137
    public static function toArray(): array
138
    {
139
        $resolved = static::resolve();
140
141
        return array_combine(array_column($resolved, 'value'), array_column($resolved, 'index'));
142
    }
143
144
    public function getValue(): string
145
    {
146
        return $this->value;
147
    }
148
149
    public function getIndex(): int
150
    {
151
        return $this->index;
152
    }
153
154
    public function isEqual($value): bool
155
    {
156
        if (is_string($value)) {
157
            $enum = static::make($value);
158
        } elseif ($value instanceof $this) {
159
            $enum = $value;
160
        }
161
162
        if (
163
            isset($enum)
164
            && $enum instanceof $this
165
            && $enum->getValue() === $this->getValue()
166
        ) {
167
            return true;
168
        }
169
170
        return false;
171
    }
172
173
    public function isAny(array $values): bool
174
    {
175
        foreach ($values as $value) {
176
            if ($this->isEqual($value)) {
177
                return true;
178
            }
179
        }
180
181
        return false;
182
    }
183
184
    public function __toString(): string
185
    {
186
        return $this->getValue();
187
    }
188
189
    public function jsonSerialize()
190
    {
191
        return $this->getValue();
192
    }
193
194
    protected static function isResolved(): bool
195
    {
196
        if(!isset(self::$resolved[static::class])) {
197
            static::resolve();
198
        }
199
200
        return self::$resolved[static::class] ?: false;
201
    }
202
203
    protected static function resolve(): array
204
    {
205
        $values = [];
206
207
        $class = static::class;
208
209
        if (isset(self::$cache[$class])) {
210
            return self::$cache[$class];
211
        }
212
213
        self::$resolved[static::class] = false;
214
        self::$cache[$class] = [];
215
216
        $reflection = new ReflectionClass(static::class);
217
218
        foreach (self::resolveFromDocBlocks($reflection) as $value) {
219
            $values[] = $value;
220
        }
221
222
        foreach (self::resolveFromStaticMethods($reflection) as $value) {
223
            $values[] = $value;
224
        }
225
226
        foreach($values as $index => $value) {
227
            self::$cache[$class][strtoupper($value)] = [
228
                'index' => $index,
229
                'value' => $value,
230
            ];
231
        }
232
233
        self::$resolved[$class] = true;
234
235
        foreach(self::$cache[$class] as $name => $enum) {
236
            self::$cache[$class][$name]['value'] = static::make($name)->getValue();
237
            self::$cache[$class][$name]['index'] = static::make($name)->getIndex();
238
        }
239
240
        return self::$cache[$class];
241
    }
242
243
    protected static function resolveFromStaticMethods(ReflectionClass $reflection): array
244
    {
245
        $values = [];
246
        foreach ($reflection->getMethods(ReflectionMethod::IS_STATIC) as $method) {
247
            if ($method->getDeclaringClass()->getName() === self::class) {
248
                continue;
249
            }
250
251
            $values[] = $method->getName();
252
        }
253
254
        return $values;
255
    }
256
257
    protected static function resolveFromDocBlocks(ReflectionClass $reflection): array
258
    {
259
        $values = [];
260
261
        $docComment = $reflection->getDocComment();
262
263
        if (! $docComment) {
264
            return $values;
265
        }
266
267
        preg_match_all('/\@method static self ([\w]+)\(\)/', $docComment, $matches);
268
269
        foreach ($matches[1] ?? [] as $value) {
270
            $values[] = $value;
271
        }
272
273
        return $values;
274
    }
275
276
    protected function resolveValueFromStaticCall(): string
277
    {
278
        $value = null;
279
280
        if (strpos(get_class($this), 'class@anonymous') === 0) {
281
            $backtrace = debug_backtrace();
282
283
            $value = $backtrace[2]['function'];
284
285
            if (static::isValidValue($value)) {
286
                return $value;
287
            }
288
        }
289
290
        throw new InvalidValueException($value, static::class);
291
    }
292
}
293