Test Failed
Push — 7.x ( bea7e1...2b11c5 )
by Adrien
08:51
created

SplReflectionEnumProxy::getCase()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 2
eloc 3
c 1
b 0
f 1
nc 2
nop 1
dl 0
loc 7
rs 10
1
<?php
2
3
/**
4
 * Part of SplTypes package.
5
 *
6
 * (c) Adrien Loyant <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
declare(strict_types=1);
13
14
namespace Ducks\Component\SplTypes\Reflection;
15
16
use Ducks\Component\SplTypes\SplBackedEnum;
17
use Ducks\Component\SplTypes\SplUnitEnum;
18
19
final class SplReflectionEnumProxy
20
{
21
    private \ReflectionClass $class;
22
23
    private ?bool $backed = null;
24
25
    /**
26
     * Will be used to set backingType.
27
     *
28
     * @var \ReflectionNamedType|null|false
29
     */
30
    private $backingType = null;
31
32
    /**
33
     * Array of constants class, indexed by name, as enum cases.
34
     *
35
     * @var array<string, \ReflectionClassConstant>
36
     */
37
    private array $constantCases = [];
38
39
    /**
40
     * Array of Reflection enum cases, indexed by name.
41
     *
42
     * @var array<string, SplReflectionEnumUnitCase|SplReflectionEnumBackedCase>
43
     */
44
    private array $cases = [];
45
46
    public string $name;
47
48
    public function __construct(\ReflectionClass $class)
49
    {
50
        $this->class = $class;
51
        $this->name = $class->name;
52
    }
53
54
    /**
55
     * Gets constants.
56
     *
57
     * @return array<string, mixed> An array of constants,
58
     * where the keys hold the name and the values the value of the constants.
59
     */
60
    public function getConstants(): array
61
    {
62
        return $this->class->getConstants();
63
    }
64
65
    /**
66
     * Gets a ReflectionClassConstant for a class's property
67
     *
68
     * @param string $name ? The class constant name.
69
     *
70
     * @return \ReflectionClassConstant|null
71
     */
72
    public function getReflectionConstant(string $name): ?\ReflectionClassConstant
73
    {
74
        return $this->class->getReflectionConstant($name) ?: null;
75
    }
76
77
    /**
78
     * Init internal constant cases array.
79
     *
80
     * @return void
81
     *
82
     * @codeCoverageIgnore
83
     */
84
    private function initConstantCases(): void
85
    {
86
        $constants = \array_diff_key($this->getConstants(), $this->constantCases);
87
        foreach (\array_keys($constants) as $name) {
88
            $constant = $this->getReflectionConstant($name);
89
            if ($constant instanceof \ReflectionClassConstant) {
90
                $this->addConstantCase($constant);
91
            }
92
        }
93
    }
94
95
    /**
96
     * Add a constant case to internal array
97
     *
98
     * @param \ReflectionClassConstant ...$constants
99
     *
100
     * @return void
101
     */
102
    public function addConstantCase(\ReflectionClassConstant ...$constants): void
103
    {
104
        foreach ($constants as $constant) {
105
            $name = $constant->getName();
106
            if (
107
                !isset($this->constantCases[$name])
108
                && $constant->isPublic()
109
                // Check consistency because of polyfilling or other bad overrides
110
                && $constant->getDeclaringClass()->name === $this->name
111
                // Do not use isBacked method because of infinite loop possibility
112
                // Add if not BackedEnum or Backed but valid type
113
                && (
114
                    !\is_a($this->name, SplBackedEnum::class, true)
115
                    || (
116
                        \is_a($this->name, SplBackedEnum::class, true)
117
                        && (\is_int($constant->getValue()) || \is_string($constant->getValue()))
118
                    )
119
                )
120
            ) {
121
                $this->constantCases[$name] = $constant;
122
            }
123
        }
124
    }
125
126
    /**
127
     * Return an array of class constants, indexed by name, that could be use as an enum case.
128
     *
129
     * @return array<string, \ReflectionClassConstant>
130
     */
131
    public function getConstantCases(): array
132
    {
133
        static $init = false;
134
135
        if (!$init) {
136
            $this->initConstantCases();
137
            $init = true;
138
        }
139
140
        return $this->constantCases;
141
    }
142
143
    /**
144
     * Return a class constant for a case name if exists
145
     *
146
     * @param string $name
147
     *
148
     * @return \ReflectionClassConstant
149
     *
150
     * @throws \ReflectionException If the requested constant case is not defined
151
     */
152
    public function getConstantCase(string $name): \ReflectionClassConstant
153
    {
154
        if (!$this->hasConstantCase($name)) {
155
            throw new \ReflectionException($this->name . '::' . $name . ' is not a constant case');
156
        }
157
158
        return $this->constantCases[$name];
159
    }
160
161
    /**
162
     * Checks for a constant case on an Enum
163
     *
164
     * @param string $name The case to check for.
165
     *
166
     * @return boolean
167
     */
168
    public function hasConstantCase(string $name): bool
169
    {
170
        if (isset($this->constantCases[$name])) {
171
            return true;
172
        }
173
174
        $constant = $this->getReflectionConstant($name);
175
        if ($constant instanceof \ReflectionClassConstant) {
176
            $this->addConstantCase($constant);
177
        }
178
179
        return isset($this->constantCases[$name]);
180
    }
181
182
    /**
183
     * Return the first defined constant as possible enum case.
184
     *
185
     * @return \ReflectionClassConstant|null
186
     */
187
    public function getFirstCaseConstant(): ?\ReflectionClassConstant
188
    {
189
        return \current($this->getConstantCases()) ?: null;
190
    }
191
192
    /**
193
     * Init internal cases array.
194
     *
195
     * @return void
196
     *
197
     * @codeCoverageIgnore
198
     */
199
    private function initCases(): void
200
    {
201
        $cases = \array_diff_key($this->getConstantCases(), $this->cases);
202
        $this->addCase(...\array_values($cases));
203
    }
204
205
    /**
206
     * Add a case to internal array
207
     *
208
     * @param \ReflectionClassConstant ...$constants
209
     *
210
     * @return void
211
     */
212
    public function addCase(\ReflectionClassConstant ...$constants): void
213
    {
214
        foreach ($constants as $constant) {
215
            $name = $constant->getName();
216
            if (
217
                !isset($this->cases[$name])
218
            ) {
219
                // Check type
220
                $value = $constant->getValue();
221
222
                if (
223
                    (
224
                        // Accept nullable value for UnitEnum
225
                        null === $value && !($this->getBackingType() instanceof \ReflectionNamedType)
226
                    ) || (
227
                        // Filter acceptable value for BackedEnum
228
                        $this->getBackingType() instanceof \ReflectionNamedType
229
                        && isset($value)
230
                        && (
231
                            \is_scalar($value) && \call_user_func('is_' . $this->getBackingType(), $value)
232
                            || \is_a($value, (string) $this->getBackingType())
233
                        )
234
                    )
235
                ) {
236
                    // Mandatory in order to prevent infinite loop
237
                    $this->cases[$name] = true;
238
                    $case = $this->isBacked()
239
                        ? new SplReflectionEnumBackedCase($this->name, $name)
240
                        : new SplReflectionEnumUnitCase($this->name, $name);
241
242
                    // Now link correct class on pointer
243
                    $this->cases[$name] = $case;
244
                }
245
            }
246
        }
247
    }
248
249
    /**
250
     * Returns a list of all cases on an Enum
251
     *
252
     * @return array<int, ReflectionEnumUnitCase|ReflectionEnumBackedCase>
253
     *
254
     * @link https://www.php.net/manual/en/reflectionenum.getcases.php
255
     */
256
    public function getCases(): array
257
    {
258
        static $init = false;
259
260
        if (!$init) {
261
            $this->initCases();
262
            $init = true;
263
        }
264
265
        return $this->cases;
266
    }
267
268
    /**
269
     * Returns a specific case of an Enum
270
     *
271
     * @param string $name
272
     *
273
     * @return SplReflectionEnumUnitCase|SplReflectionEnumBackedCase
274
     *
275
     * @throws \ReflectionException If the requested case is not defined
276
     *
277
     * @link https://www.php.net/manual/en/reflectionenum.getcase.php
278
     */
279
    public function getCase(string $name): SplReflectionEnumUnitCase
280
    {
281
        if (!$this->hasCase($name)) {
282
            throw new \ReflectionException($this->name . '::' . $name . ' is not a case');
283
        }
284
285
        return $this->cases[$name];
286
    }
287
288
    /**
289
     * Checks for a case on an Enum
290
     *
291
     * @param string $name The case to check for.
292
     *
293
     * @return boolean
294
     *
295
     * @link https://www.php.net/manual/en/reflectionenum.hascase.php
296
     */
297
    public function hasCase(string $name): bool
298
    {
299
        if (isset($this->cases[$name])) {
300
            return true;
301
        }
302
303
        if ($this->hasConstantCase($name)) {
304
            $constant = $this->getConstantCase($name);
305
            $this->addCase($constant);
306
        }
307
308
        return isset($this->cases[$name]);
309
    }
310
311
    /**
312
     * Determines if an Enum is a Backed Enum
313
     *
314
     * @return boolean
315
     */
316
    public function isBacked(): bool
317
    {
318
        if (null === $this->backed) {
319
            if (\is_a($this->name, SplBackedEnum::class, true)) {
320
                $this->backed = true;
321
            } elseif (\is_a($this->name, SplUnitEnum::class, true)) {
322
                $this->backed = false;
323
            } else {
324
                $constant = $this->getFirstCaseConstant();
325
                $this->backed = null !== $constant->getValue();
326
            }
327
        }
328
329
        return $this->backed;
330
    }
331
332
    /**
333
     * Gets the backing type of an Enum, if any
334
     *
335
     * @return \ReflectionNamedType|null An instance of ReflectionNamedType, or null if the Enum has no backing type.
336
     *
337
     * @link https://www.php.net/manual/en/reflectionenum.getbackingtype.php
338
     */
339
    public function getBackingType(): ?\ReflectionNamedType
340
    {
341
        if (null === $this->backingType) {
342
            if ($this->isBacked()) {
343
                if ($this->class->hasProperty('value')) {
344
                    $this->backingType = $this->class->getProperty('value')->getType() ?? false;
345
                } else {
346
                    $constant = $this->getFirstCaseConstant();
347
                    if ($constant instanceof \ReflectionClassConstant) {
348
                        switch (\gettype($constant->getValue())) {
349
                            case 'string':
350
                                $this->backingType = SplReflectionEnumHelper::getStringReflectionNamedType();
351
                                break;
352
353
                            case 'integer':
354
                                $this->backingType = SplReflectionEnumHelper::getIntReflectionNamedType();
355
                                break;
356
357
                            default:
358
                                $this->backingType = false;
359
                                break;
360
                        }
361
                    }
362
                }
363
            } else {
364
                $this->backingType = false;
365
            }
366
        }
367
368
        return $this->backingType ?: null;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->backingType ?: null could return the type true which is incompatible with the type-hinted return ReflectionNamedType|null. Consider adding an additional type-check to rule them out.
Loading history...
369
    }
370
}
371