Passed
Push — 7.x ( 42e260...f51c13 )
by Adrien
09:26
created

SplReflectionEnumProxy::getBackingType()   A

Complexity

Conditions 6
Paths 5

Size

Total Lines 22
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 6
eloc 13
c 1
b 0
f 1
nc 5
nop 0
dl 0
loc 22
rs 9.2222
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\SplEnumerable;
18
use Ducks\Component\SplTypes\SplUnitEnum;
19
20
final class SplReflectionEnumProxy
21
{
22
    /**
23
     * The reflection class used in proxy
24
     *
25
     * @var \ReflectionClass
26
     *
27
     * @phpstan-var \ReflectionClass<object>
28
     */
29
    private \ReflectionClass $class;
30
31
    /**
32
     * Is enum is backed
33
     *
34
     * @var boolean|null
35
     */
36
    private ?bool $backed = null;
37
38
    /**
39
     * Will be used to set backingType.
40
     *
41
     * @var \ReflectionNamedType|null|false
42
     */
43
    private $backingType = null;
44
45
    /**
46
     * Array of constants class, indexed by name, as enum cases.
47
     *
48
     * @var \ReflectionClassConstant[]
49
     *
50
     * @phpstan-var array<string,\ReflectionClassConstant>
51
     */
52
    private array $constantCases = [];
53
54
    /**
55
     * Array of Reflection enum cases, indexed by name.
56
     *
57
     * @var (SplReflectionEnumUnitCase|SplReflectionEnumBackedCase)[]
58
     *
59
     * @phpstan-var array<string, SplReflectionEnumUnitCase|SplReflectionEnumBackedCase>
60
     */
61
    private array $cases = [];
62
63
    /**
64
     * The nameof the case beeing instanciate
65
     *
66
     * @var string|null
67
     */
68
    private ?string $running = null;
69
70
    /**
71
     * Name of the class constant.
72
     *
73
     * @var string
74
     *
75
     * @readonly
76
     *
77
     * @phpstan-var class-string
78
     *
79
     * @psalm-readonly
80
     *
81
     * @phan-read-only
82
     */
83
    public string $name;
84
85
    /**
86
     * Build a proxy SplReflectionEnum from a ReflectionClass
87
     *
88
     * @param \ReflectionClass<object> $class
89
     */
90
    public function __construct(\ReflectionClass $class)
91
    {
92
        $this->class = $class;
93
        $this->name = $class->getName();
94
    }
95
96
    /**
97
     * Gets constants.
98
     *
99
     * @return mixed[] An array of constants,
100
     * where the keys hold the name and the values the value of the constants.
101
     *
102
     * @phpstan-return array<string, mixed>
103
     */
104
    public function getConstants(): array
105
    {
106
        return $this->class->getConstants();
107
    }
108
109
    /**
110
     * Gets a ReflectionClassConstant for a class's property
111
     *
112
     * @param string $name The class constant name.
113
     *
114
     * @return \ReflectionClassConstant|null
115
     */
116
    public function getReflectionConstant(string $name): ?\ReflectionClassConstant
117
    {
118
        return $this->class->getReflectionConstant($name) ?: null;
119
    }
120
121
    /**
122
     * Init internal constant cases array.
123
     *
124
     * @return void
125
     *
126
     * @codeCoverageIgnore
127
     */
128
    private function initConstantCases(): void
129
    {
130
        $constants = \array_diff_key($this->getConstants(), $this->constantCases);
131
        foreach (\array_keys($constants) as $name) {
132
            $constant = $this->getReflectionConstant($name);
133
            if ($constant instanceof \ReflectionClassConstant) {
134
                $this->addConstantCase($constant);
135
            }
136
        }
137
    }
138
139
    /**
140
     * Add a constant case to internal array
141
     *
142
     * @param \ReflectionClassConstant ...$constants
143
     *
144
     * @return void
145
     *
146
     * @no-named-arguments
147
     */
148
    public function addConstantCase(\ReflectionClassConstant ...$constants): void
149
    {
150
        foreach ($constants as $constant) {
151
            $name = $constant->getName();
152
            if (
153
                !isset($this->constantCases[$name])
154
                && $constant->isPublic()
155
                // Check consistency because of polyfilling or other bad overrides
156
                && $constant->getDeclaringClass()->getName() === $this->name
157
                // Do not use isBacked method because of infinite loop possibility
158
                // Add if not BackedEnum or Backed but valid type
159
                && (
160
                    !\is_a($this->name, SplBackedEnum::class, true)
161
                    || (
162
                        \is_a($this->name, SplBackedEnum::class, true)
163
                        && (\is_int($constant->getValue()) || \is_string($constant->getValue()))
164
                    )
165
                )
166
            ) {
167
                $this->constantCases[$name] = $constant;
168
            }
169
        }
170
    }
171
172
    /**
173
     * Return an array of class constants, indexed by name, that could be use as an enum case.
174
     *
175
     * @return \ReflectionClassConstant[]
176
     *
177
     * @phpstan-return array<string,\ReflectionClassConstant>
178
     */
179
    public function getConstantCases(): array
180
    {
181
        static $init = false;
182
183
        if (!$init) {
184
            $this->initConstantCases();
185
            $init = true;
186
        }
187
188
        return $this->constantCases;
189
    }
190
191
    /**
192
     * Return a class constant for a case name if exists
193
     *
194
     * @param string $name
195
     *
196
     * @return \ReflectionClassConstant
197
     *
198
     * @throws \ReflectionException If the requested constant case is not defined
199
     */
200
    public function getConstantCase(string $name): \ReflectionClassConstant
201
    {
202
        if (!$this->hasConstantCase($name)) {
203
            throw new \ReflectionException($this->name . '::' . $name . ' is not a constant case');
204
        }
205
206
        return $this->constantCases[$name];
207
    }
208
209
    /**
210
     * Checks for a constant case on an Enum
211
     *
212
     * @param string $name The case to check for.
213
     *
214
     * @return boolean
215
     */
216
    public function hasConstantCase(string $name): bool
217
    {
218
        if (isset($this->constantCases[$name])) {
219
            return true;
220
        }
221
222
        $constant = $this->getReflectionConstant($name);
223
        if ($constant instanceof \ReflectionClassConstant) {
224
            $this->addConstantCase($constant);
225
        }
226
227
        return isset($this->constantCases[$name]);
228
    }
229
230
    /**
231
     * Return the first defined constant as possible enum case.
232
     *
233
     * @return \ReflectionClassConstant|null
234
     */
235
    public function getFirstCaseConstant(): ?\ReflectionClassConstant
236
    {
237
        return \current($this->getConstantCases()) ?: null;
238
    }
239
240
    /**
241
     * Init internal cases array.
242
     *
243
     * @return void
244
     *
245
     * @codeCoverageIgnore
246
     */
247
    private function initCases(): void
248
    {
249
        $cases = \array_diff_key($this->getConstantCases(), $this->cases);
250
        $this->addCase(...\array_values($cases));
251
    }
252
253
    /**
254
     * Add a case to internal array
255
     *
256
     * @param \ReflectionClassConstant ...$constants
257
     *
258
     * @return void
259
     */
260
    public function addCase(\ReflectionClassConstant ...$constants): void
261
    {
262
        foreach ($constants as $constant) {
263
            $name = $constant->getName();
264
            if (
265
                !isset($this->cases[$name])
266
                && \is_a($this->name, SplEnumerable::class, true)
267
            ) {
268
                // Check type
269
                $value = $constant->getValue();
270
271
                // Mandatory in order to prevent infinite loop
272
                $this->running = $name;
273
274
                if (!$this->isBacked() && null === $value) {
275
                    $this->cases[$name] = new SplReflectionEnumUnitCase($this->name, $name);
276
                    unset($this->running);
277
                    continue;
278
                }
279
280
                $backingType = $this->getBackingType();
281
282
                if (
283
                    $this->isBacked()
284
                    && $backingType instanceof \ReflectionNamedType
285
                    && $backingType->getName() === \gettype($value)
286
                ) {
287
                    $this->cases[$name] = new SplReflectionEnumBackedCase($this->name, $name);
288
                    unset($this->running);
289
                    continue;
290
                }
291
            }
292
        }
293
    }
294
295
    /**
296
     * Returns a list of all cases on an Enum
297
     *
298
     * @return (SplReflectionEnumUnitCase|SplReflectionEnumBackedCase)[]
299
     *
300
     * @phpstan-return array<string,SplReflectionEnumUnitCase|SplReflectionEnumBackedCase>
301
     *
302
     * @link https://www.php.net/manual/en/reflectionenum.getcases.php
303
     */
304
    public function getCases(): array
305
    {
306
        static $init = false;
307
308
        if (!$init) {
309
            $this->initCases();
310
            $init = true;
311
        }
312
313
        return $this->cases;
314
    }
315
316
    /**
317
     * Returns a specific case of an Enum
318
     *
319
     * @param string $name
320
     *
321
     * @return SplReflectionEnumUnitCase|SplReflectionEnumBackedCase
322
     *
323
     * @throws \ReflectionException If the requested case is not defined
324
     *
325
     * @link https://www.php.net/manual/en/reflectionenum.getcase.php
326
     */
327
    public function getCase(string $name): SplReflectionEnumUnitCase
328
    {
329
        if (!$this->hasCase($name)) {
330
            throw new \ReflectionException($this->name . '::' . $name . ' is not a case');
331
        }
332
333
        return $this->cases[$name];
334
    }
335
336
    /**
337
     * Checks for a case on an Enum
338
     *
339
     * @param string $name The case to check for.
340
     *
341
     * @return boolean
342
     *
343
     * @link https://www.php.net/manual/en/reflectionenum.hascase.php
344
     */
345
    public function hasCase(string $name): bool
346
    {
347
        // $this->cases could be empty
348
        if (isset($this->cases[$name]) || $this->running === $name) {
349
            return true;
350
        }
351
352
        if ($this->hasConstantCase($name)) {
353
            $constant = $this->getConstantCase($name);
354
            $this->addCase($constant);
355
        }
356
357
        return isset($this->cases[$name]);
358
    }
359
360
    /**
361
     * Determines if an Enum is a Backed Enum
362
     *
363
     * @return boolean
364
     */
365
    public function isBacked(): bool
366
    {
367
        if (null === $this->backed) {
368
            if (\is_a($this->name, SplBackedEnum::class, true)) {
369
                $this->backed = true;
370
            } elseif (\is_a($this->name, SplUnitEnum::class, true)) {
371
                $this->backed = false;
372
            } else {
373
                $constant = $this->getFirstCaseConstant();
374
                $this->backed = $constant instanceof \ReflectionClassConstant && null !== $constant->getValue();
375
            }
376
        }
377
378
        return $this->backed;
379
    }
380
381
    /**
382
     * Gets the backing type of an Enum, if any
383
     *
384
     * @return \ReflectionNamedType|null An instance of ReflectionNamedType, or null if the Enum has no backing type.
385
     *
386
     * @link https://www.php.net/manual/en/reflectionenum.getbackingtype.php
387
     */
388
    public function getBackingType(): ?\ReflectionNamedType
389
    {
390
        if (null === $this->backingType) {
391
            if ($this->isBacked()) {
392
                if ($this->class->hasProperty('value')) {
393
                    /** @var \ReflectionNamedType|false $type */
394
                    $type = $this->class->getProperty('value')->getType() ?? false;
395
                    $this->backingType = $type;
396
                } else {
397
                    $constant = $this->getFirstCaseConstant();
398
                    if ($constant instanceof \ReflectionClassConstant) {
399
                        $this->backingType = SplReflectionEnumHelper::getReflectionNamedTypeFromType(
400
                            \gettype($constant->getValue())
401
                        );
402
                    }
403
                }
404
            } else {
405
                $this->backingType = false;
406
            }
407
        }
408
409
        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...
410
    }
411
}
412