Issues (3)

src/Enum.php (1 issue)

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Zlikavac32\Enum;
6
7
use ArrayIterator;
8
use InvalidArgumentException;
9
use Iterator;
10
use JsonSerializable;
11
use LogicException;
12
use ReflectionClass;
13
use Serializable;
14
use Throwable;
15
use function array_values;
16
use function count;
17
use function get_class;
18
use function get_parent_class;
19
use function sprintf;
20
21
abstract class Enum implements Serializable, JsonSerializable
22
{
23
    /**
24
     * @var Enum[][]
25
     */
26
    private static array $existingEnums = [];
27
    /**
28
     * @var bool[]
29
     */
30
    private static array $enumConstructionContext = [];
31
    private int $ordinal;
32
    private string $name;
33
    private bool $correctlyInitialized = false;
34
35
    public function __construct()
36
    {
37
        $this->assertValidConstructionContext();
38
        $this->correctlyInitialized = true;
39
    }
40
41
    private function assertValidConstructionContext(): void
42
    {
43
        if (isset(self::$enumConstructionContext[get_parent_class($this)])) {
44
            return ;
45
        }
46
47
        throw new LogicException(
48
            sprintf(
49
                'It seems you tried to manually create enum outside of enumerate() method for enum %s',
50
                get_class($this)
51
            )
52
        );
53
    }
54
55
    private function assertCorrectlyInitialized(): void
56
    {
57
        if ($this->correctlyInitialized) {
58
            return ;
59
        }
60
61
        throw new LogicException(
62
            sprintf(
63
                'It seems that enum is not correctly initialized. Did you forget to call parent::__construct() in enum %s?',
64
                get_class($this)
65
            )
66
        );
67
    }
68
69
    final public function ordinal(): int
70
    {
71
        $this->assertCorrectlyInitialized();
72
73
        return $this->ordinal;
74
    }
75
76
    final public function name(): string
77
    {
78
        $this->assertCorrectlyInitialized();
79
80
        return $this->name;
81
    }
82
83
    final public function isAnyOf(Enum ...$enums): bool
84
    {
85
        $this->assertCorrectlyInitialized();
86
87
        foreach ($enums as $enum) {
88
            if ($this === $enum) {
89
                return true;
90
            }
91
        }
92
93
        return false;
94
    }
95
96
    final public function __clone()
97
    {
98
        throw new LogicException('Cloning enum element is not allowed');
99
    }
100
101
    /**
102
     * @throws Throwable
103
     */
104
    final public static function __set_state()
105
    {
106
        throw self::createNoSerializeUnserializeException();
107
    }
108
109
    /**
110
     * @throws Throwable
111
     */
112
    final public function __wakeup()
113
    {
114
        throw self::createNoSerializeUnserializeException();
115
    }
116
117
    /**
118
     * @throws Throwable
119
     */
120
    final public function __sleep()
121
    {
122
        throw self::createNoSerializeUnserializeException();
123
    }
124
125
    /**
126
     * @throws Throwable
127
     */
128
    final public function serialize()
129
    {
130
        throw self::createNoSerializeUnserializeException();
131
    }
132
133
    /**
134
     * @param string $serialized
135
     *
136
     * @throws Throwable
137
     */
138
    final public function unserialize($serialized)
139
    {
140
        throw self::createNoSerializeUnserializeException();
141
    }
142
143
    private static function createNoSerializeUnserializeException(): Throwable
144
    {
145
        return new LogicException('Serialization/deserialization of enum element is not allowed');
146
    }
147
148
    public function __toString(): string
149
    {
150
        return $this->name();
151
    }
152
153
    public function jsonSerialize()
154
    {
155
        return $this->name();
156
    }
157
158
    /**
159
     * @return Iterator|static[] Enum items in order they are defined
160
     */
161
    final public static function iterator(): Iterator
162
    {
163
        return new ArrayIterator(self::values());
164
    }
165
166
    /**
167
     * @return static[] Enum items in order they are defined
168
     */
169
    final public static function values(): array
170
    {
171
        return array_values(self::retrieveCurrentContextEnumerations());
172
    }
173
174
    /**
175
     * @return bool
176
     */
177
    final public static function contains(string $name): bool
178
    {
179
        return isset(self::retrieveCurrentContextEnumerations()[$name]);
180
    }
181
182
    /**
183
     * @todo: make final as is stated in docs
184
     *
185
     * @return static
186
     */
187
    public static function valueOf(string $name): Enum
188
    {
189
        return self::__callStatic($name, []);
190
    }
191
192
    /**
193
     * @param $name
194
     * @param $arguments
195
     *
196
     * @return static
197
     */
198
    final public static function __callStatic($name, $arguments): Enum
199
    {
200
        if (count($arguments) > 0) {
201
            throw new InvalidArgumentException(
202
                sprintf('No argument must be provided when calling %s::%s', static::class, $name)
203
            );
204
        }
205
206
        $objects = self::retrieveCurrentContextEnumerations();
207
208
        if (isset($objects[$name])) {
209
            return $objects[$name];
210
        }
211
212
        throw new EnumNotFoundException($name, static::class);
213
    }
214
215
    protected static function enumerate(): array
216
    {
217
        return [];
218
    }
219
220
    /**
221
     * @return Enum[]
222
     */
223
    private static function retrieveCurrentContextEnumerations(): array
224
    {
225
        $class = static::class;
226
227
        self::ensureEnumsAreDiscoveredForClass($class);
228
229
        return self::$existingEnums[$class];
230
    }
231
232
    private static function ensureEnumsAreDiscoveredForClass(string $class)
233
    {
234
        if (isset(self::$existingEnums[$class])) {
235
            return ;
236
        }
237
238
        try {
239
            self::$enumConstructionContext[$class] = true;
240
241
            self::$existingEnums[$class] = self::discoverEnumerationObjectsForClass($class);
242
        } finally {
243
            unset(self::$enumConstructionContext[$class]);
244
        }
245
    }
246
247
    private static function discoverEnumerationObjectsForClass(string $class)
248
    {
249
        assertEnumClassAdheresConstraints($class);
250
251
        $enumNames = self::resolveMethodsFromDocblock($class);
252
253
        /* @var Enum[] $enumObjects */
254
        $enumObjects = static::enumerate();
255
256
        $objects = self::normalizeElementsArray($class, $enumNames, $enumObjects);
257
258
        self::populateEnumObjectProperties($objects);
259
260
        return $objects;
261
    }
262
263
    private static function resolveMethodsFromDocblock(string $class): array
264
    {
265
        $docBlock = (new ReflectionClass($class))->getDocComment();
266
267
        if (!$docBlock) {
268
            throw new LogicException(
269
                sprintf(
270
                    'You must provide PHPDoc for static methods in your enum class %s',
271
                    static::class
272
                )
273
            );
274
        }
275
276
        $regex = '/@method\s+static\s+[^\s]+\s+(\w+)\s*(?:\(\))?\s*$/m';
277
278
        if (!preg_match_all($regex, $docBlock, $matches, PREG_SET_ORDER)) {
279
            throw new LogicException(sprintf('Enum %s must define at least one element', $class));
280
        }
281
282
        $enumNames = [];
283
284
        foreach ($matches as $match) {
285
            assertValidNamePattern($match[1]);
286
287
            $enumNames[] = $match[1];
288
        }
289
290
        return $enumNames;
291
    }
292
293
    private static function normalizeElementsArray(string $class, array $enumNames, array $enumObjects): array
294
    {
295
        if (count($enumObjects) === 0) {
296
            return self::createDynamicEnumElementObjects($class, $enumNames);
297
        }
298
299
        assertValidEnumCollection($class, $enumObjects, $class);
300
301
        $enumeratedEnumNames = array_keys($enumObjects);
302
303
        $extraEnumeratedKeys = array_diff($enumeratedEnumNames, $enumNames);
304
305
        if (count($extraEnumeratedKeys) > 0) {
306
            throw new LogicException(sprintf('Enum %s enumerates [%s] which are not found in PHPDoc', $class, implode(', ', $extraEnumeratedKeys)));
307
        }
308
309
        $missingKeysInEnumeration = array_diff($enumNames, $enumeratedEnumNames);
310
311
        if (count($missingKeysInEnumeration) > 0) {
312
            throw new LogicException(sprintf('Enum %s does not enumerate [%s] which are found in PHPDoc', $class, implode(', ', $missingKeysInEnumeration)));
313
        }
314
315
        return $enumObjects;
316
    }
317
318
    private static function populateEnumObjectProperties(array $objects): void
319
    {
320
        $ordinal = 0;
321
322
        foreach ($objects as $elementName => $object) {
323
            $object->ordinal = $ordinal++;
324
            $object->name = $elementName;
325
        }
326
    }
327
328
    private static function createDynamicEnumElementObjects(string $class, array $enumNames): array
329
    {
330
        $defineClasses = '';
331
        $createObjects = '';
332
333
        $usedNames = [];
334
335
        foreach ($enumNames as $enumName) {
336
            assertElementNameIsString($class, $enumName);
337
            assertValidNamePattern($enumName);
338
339
            if (isset($usedNames[$enumName])) {
340
                throw new LogicException(sprintf('Duplicate element %s exists in enum %s', $enumName, $class));
341
            }
342
343
            $proxyClassName = 'Proxy_' . sha1($enumName . $class);
344
345
            $defineClasses .= sprintf('class %s extends \\%s {};', $proxyClassName, $class);
346
            $createObjects .= sprintf('"%s" => new %s(),', $enumName, $proxyClassName);
347
348
            $usedNames[$enumName] = true;
349
        }
350
351
        $evalString = sprintf('namespace Zlikavac32\\Dynamic\\Proxy; %s return [%s];', $defineClasses, $createObjects);
352
353
        return eval($evalString);
0 ignored issues
show
The use of eval() is discouraged.
Loading history...
354
    }
355
}
356