Completed
Pull Request — master (#18)
by Tom
01:50 queued 31s
created

Enum::resolveFromDocBlocks()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

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