Passed
Branch master (586976)
by Константин
05:57
created

Enum::validate()   B

Complexity

Conditions 9
Paths 13

Size

Total Lines 38

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 9.3188

Importance

Changes 0
Metric Value
dl 0
loc 38
ccs 16
cts 19
cp 0.8421
rs 7.7564
c 0
b 0
f 0
cc 9
nc 13
nop 1
crap 9.3188
1
<?php
2
3
namespace Grachevko\Enum;
4
5
use BadMethodCallException;
6
use function in_array;
7
use InvalidArgumentException;
8
use function is_int;
9
use LogicException;
10
use ReflectionClass;
11
use ReflectionException;
12
use ReflectionProperty;
13
use Serializable;
14
15
/**
16
 * @author Konstantin Grachev <[email protected]>
17
 */
18
abstract class Enum implements Serializable
19
{
20
    /**
21
     * @var int
22
     */
23
    private $id;
24
25
    /**
26
     * @var ReflectionClass[]
27
     */
28
    private static $reflections = [];
29
30
    /**
31
     * @var ReflectionProperty[][]
32
     */
33
    private static $properties = [];
34
35
    /**
36
     * @var Enum[][]
37
     */
38
    private static $instances = [];
39
40
    /**
41
     * @param int $id
42
     *
43
     * @throws InvalidArgumentException
44
     * @throws ReflectionException
45
     */
46 8
    private function __construct(int $id)
47
    {
48 8
        if (!in_array($id, self::getReflection()->getConstants(), true)) {
49
            throw new InvalidArgumentException(
50
                sprintf('Undefined enum "%s" of class "%s"', $id, static::class)
51
            );
52
        }
53
54 6
        $this->id = $id;
55 6
    }
56
57
    /**
58
     * @return string
59
     */
60
    final public function __toString(): string
61
    {
62
        return (string) $this->getId();
63
    }
64
65
    /**
66
     * @param string $name
67
     * @param array  $arguments
68
     *
69
     * @throws ReflectionException
70
     * @throws InvalidArgumentException
71
     * @throws BadMethodCallException
72
     *
73
     * @return bool|string
74
     */
75 5
    final public function __call(string $name, array $arguments)
76
    {
77 5
        $reflection = self::getReflection();
78
79 5
        if (0 === strpos($name, 'is') && ctype_upper($name[2])) {
80 3
            return $this->eq(static::create($reflection->getConstant(self::stringToConstant(substr($name, 2)))));
81
        }
82
83 2 View Code Duplication
        if (0 === strpos($name, 'get') && ctype_upper($name[3])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
84 2
            return $this->get(lcfirst(substr($name, 3)));
85
        }
86
87
        throw new BadMethodCallException(
88
            sprintf('Undefined method "%s" in class "%s"', $name, static::class)
89
        );
90
    }
91
92
    /**
93
     * @param string $name
94
     * @param array  $arguments
95
     *
96
     * @throws ReflectionException
97
     * @throws BadMethodCallException
98
     * @throws LogicException
99
     *
100
     * @return static
101
     */
102 14
    final public static function __callStatic(string $name, array $arguments)
103
    {
104 14
        $reflectionClass = self::getReflection();
105
106 14
        $const = self::stringToConstant($name);
107 14
        $constants = $reflectionClass->getConstants();
108
109 14
        if (array_key_exists($const, $constants)) {
110 12
            return static::create($constants[$const]);
111
        }
112
113 2 View Code Duplication
        if (0 === strpos($name, 'from') && ctype_upper($name[4])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
114 1
            return static::from(lcfirst(substr($name, 4)), $arguments[0]);
115
        }
116
117 1
        throw new BadMethodCallException(
118 1
            sprintf('Undefined method "%s" in class "%s"', $name, static::class)
119
        );
120
    }
121
122
    /**
123
     * @param int $id
124
     *
125
     * @throws ReflectionException
126
     *
127
     * @return static
128
     */
129 16
    final public static function create(int $id): self
130
    {
131 16
        return self::$instances[static::class][$id] ?? self::$instances[static::class][$id] = new static($id);
132
    }
133
134
    /**
135
     * @param string $property
136
     * @param mixed  $value
137
     *
138
     * @throws ReflectionException
139
     *
140
     * @return static
141
     */
142 1
    final public static function from(string $property, $value): self
143
    {
144 1
        return static::create(array_flip(self::$properties[static::class][$property]->getValue())[$value]);
145
    }
146
147
    /**
148
     * @return int
149
     */
150 13
    final public function getId(): int
151
    {
152 13
        return $this->id;
153
    }
154
155
    /**
156
     * @throws ReflectionException
157
     *
158
     * @return string
159
     */
160 5
    final public function getName(): string
161
    {
162 5
        if (self::getReflection()->hasProperty('name')) {
163 2
            return $this->get('name');
164
        }
165
166 4
        return strtolower(array_flip(self::getReflection()->getConstants())[$this->getId()]);
167
    }
168
169
    /**
170
     * @param string $property
171
     *
172
     * @return mixed
173
     */
174 5
    final public function get(string $property)
175
    {
176 5
        if (!array_key_exists($property, self::$properties[static::class])) {
177 1
            throw new InvalidArgumentException(
178 1
                sprintf('Property "%s" not exist at class "%s"', $property, static::class)
179
            );
180
        }
181
182 5
        return self::$properties[static::class][$property]->getValue()[$this->getId()];
183
    }
184
185
    /**
186
     * @param Enum[]|int[] $ids
187
     * @param bool         $reverse
188
     *
189
     * @throws ReflectionException
190
     *
191
     * @return static[]
192
     */
193 1
    final public static function all(array $ids = [], bool $reverse = false): array
194
    {
195
        $ids = array_map(static function ($id) {
196 1
            return $id instanceof Enum ? $id->getId() : $id;
197 1
        }, $ids);
198
199 1
        $all = array_values(self::getReflection()->getConstants());
200
201 1
        if ([] === $ids) {
202 1
            $ids = $all;
203
        } else {
204 1
            $ids = $reverse ? array_diff($all, $ids) : $ids;
205
        }
206
207
        return array_map(static function (int $id) {
208 1
            return static::create($id);
209 1
        }, $ids);
210
    }
211
212
    /**
213
     * @param Enum $enum
214
     *
215
     * @return bool
216
     */
217 5
    final public function eq(Enum $enum): bool
218
    {
219 5
        return static::class === get_class($enum) && $enum->getId() === $this->getId();
220
    }
221
222
    /**
223
     * @return string
224
     */
225 1
    final public function serialize(): string
226
    {
227 1
        return (string) $this->getId();
228
    }
229
230
    /**
231
     * @param string $serialized
232
     */
233 1
    final public function unserialize($serialized): void
234
    {
235 1
        $this->id = (int) $serialized;
236 1
    }
237
238
    /**
239
     * @param string $string
240
     *
241
     * @throws InvalidArgumentException
242
     *
243
     * @return string
244
     */
245 14
    private static function stringToConstant(string $string): string
246
    {
247 14
        $constant = preg_replace('/\B([A-Z])/', '_$1', $string);
248
249 14
        if (null === $constant) {
250
            throw new InvalidArgumentException(sprintf('preg_replace return null for string "%s"', $string));
251
        }
252
253 14
        return strtoupper($constant);
254
    }
255
256
    /**
257
     * @throws ReflectionException
258
     * @throws LogicException
259
     *
260
     * @return ReflectionClass
261
     */
262 17
    private static function getReflection(): ReflectionClass
263
    {
264 17
        $class = static::class;
265
266 17
        return self::$reflections[$class] ?? self::$reflections[$class] = self::validate(new ReflectionClass($class));
267
    }
268
269
    /**
270
     * @param ReflectionClass $reflection
271
     *
272
     * @throws LogicException
273
     *
274
     * @return ReflectionClass
275
     */
276 5
    private static function validate(ReflectionClass $reflection): ReflectionClass
277
    {
278 5
        $constants = $reflection->getConstants();
279
280 5
        if ([] === $constants) {
281 1
            throw new LogicException(sprintf('Class %s must define Constants', static::class));
282
        }
283
284 4
        foreach ($reflection->getReflectionConstants() as $reflectionConstant) {
285 4
            if (false === $reflectionConstant->isPrivate()) {
286
                throw new LogicException('All constants must be private by design.');
287
            }
288
289 4
            if (!is_int($reflectionConstant->getValue())) {
290 4
                throw new LogicException('All constants must be type of integer by design.');
291
            }
292
        }
293
294 3
        foreach ($reflection->getProperties() as $property) {
295 1
            $property->setAccessible(true);
296
297 1
            self::$properties[static::class][$property->getName()] = $property;
298
299 1
            if ($property->isPublic()) {
300
                throw new LogicException('All properties must be private or protected by design.');
301
            }
302
303 1
            if (!$property->isStatic()) {
304
                throw new LogicException('All properties must be static by design.');
305
            }
306
307 1
            if (array_values($constants) !== array_keys($property->getValue())) {
308 1
                throw new LogicException('Properties must have values for all constants by design.');
309
            }
310
        }
311
312 3
        return $reflection;
313
    }
314
}
315