Passed
Push — soothe-scrutinizer ( 46267e )
by Akihito
03:08
created

ClassParam::__invoke()   A

Complexity

Conditions 6
Paths 6

Size

Total Lines 36
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 7
Bugs 0 Features 0
Metric Value
cc 6
eloc 18
c 7
b 0
f 0
nc 6
nop 3
dl 0
loc 36
rs 9.0444
1
<?php
2
3
namespace BEAR\Resource;
4
5
use BackedEnum;
6
use BEAR\Resource\Exception\ParameterEnumTypeException;
7
use BEAR\Resource\Exception\ParameterException;
8
use BEAR\Resource\Exception\ParameterInvalidEnumException;
9
use Ray\Di\InjectorInterface;
10
use ReflectionClass;
11
use ReflectionEnum;
0 ignored issues
show
Bug introduced by
The type ReflectionEnum was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
12
use ReflectionNamedType;
13
use ReflectionParameter;
14
use UnitEnum;
15
16
use function assert;
17
use function class_exists;
18
use function enum_exists;
0 ignored issues
show
introduced by
The function enum_exists was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
19
use function is_a;
20
use function is_int;
21
use function is_iterable;
22
use function is_string;
23
use function ltrim;
24
use function preg_replace;
25
use function strtolower;
26
27
use const PHP_VERSION_ID;
28
29
final class ClassParam implements ParamInterface
30
{
31
    private readonly string $type;
32
    private readonly bool $isDefaultAvailable;
33
    private readonly mixed $defaultValue; // @phpstan-ignore-line
34
35
    public function __construct(
36
        ReflectionNamedType $type,
37
        ReflectionParameter $parameter,
38
    ) {
39
        $this->type = $type->getName();
0 ignored issues
show
Bug introduced by
The property type is declared read-only in BEAR\Resource\ClassParam.
Loading history...
40
        $this->isDefaultAvailable = $parameter->isDefaultValueAvailable();
0 ignored issues
show
Bug introduced by
The property isDefaultAvailable is declared read-only in BEAR\Resource\ClassParam.
Loading history...
41
        if (! $this->isDefaultAvailable) {
42
            return;
43
        }
44
45
        $this->defaultValue = $parameter->getDefaultValue();
0 ignored issues
show
Bug introduced by
The property defaultValue is declared read-only in BEAR\Resource\ClassParam.
Loading history...
46
    }
47
48
    /**
49
     * {@inheritDoc}
50
     */
51
    public function __invoke(string $varName, array $query, InjectorInterface $injector)
52
    {
53
        try {
54
            /** @psalm-suppress MixedAssignment */
55
            $props = $this->getProps($varName, $query, $injector);
56
        } catch (ParameterException $e) {
57
            if ($this->isDefaultAvailable) {
58
                return $this->defaultValue;
59
            }
60
61
            throw $e;
62
        }
63
64
        assert(class_exists($this->type));
65
        $refClass = (new ReflectionClass($this->type));
66
67
        if ($refClass->isEnum()) {
0 ignored issues
show
Bug introduced by
The method isEnum() does not exist on ReflectionClass. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

67
        if ($refClass->/** @scrutinizer ignore-call */ isEnum()) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
68
            return $this->enum($this->type, $props, $varName);
69
        }
70
71
        assert(is_iterable($props));
72
73
        $hasConstructor = (bool) $refClass->getConstructor();
74
        if ($hasConstructor) {
75
            /** @psalm-suppress MixedMethodCall */
76
            return new $this->type(...$props);
77
        }
78
79
        /** @psalm-suppress MixedMethodCall */
80
        $obj = new $this->type();
81
        /** @psalm-suppress MixedAssignment */
82
        foreach ($props as $propName => $propValue) {
83
            $obj->{$propName} = $propValue;
84
        }
85
86
        return $obj;
87
    }
88
89
    /** @param array<string, mixed> $query */
90
    private function getProps(string $varName, array $query, InjectorInterface $injector): mixed
91
    {
92
        if (isset($query[$varName])) {
93
            return $query[$varName];
94
        }
95
96
        // try camelCase variable name
97
        $snakeName = ltrim(strtolower((string) preg_replace('/[A-Z]/', '_\0', $varName)), '_');
98
        if (isset($query[$snakeName])) {
99
            return $query[$snakeName];
100
        }
101
102
        unset($injector);
103
104
        throw new ParameterException($varName);
105
    }
106
107
    /**
108
     * @param class-string $type
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
109
     *
110
     * @psalm-suppress MixedArgument
111
     */
112
    private function enum(string $type, mixed $props, string $varName): mixed
113
    {
114
        /** @var class-string<UnitEnum> $type */
115
        $refEnum = new ReflectionEnum($type);
116
        assert(enum_exists($type));
0 ignored issues
show
Bug introduced by
The function enum_exists was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

116
        assert(/** @scrutinizer ignore-call */ enum_exists($type));
Loading history...
117
118
        if (! $refEnum->isBacked()) {
119
            throw new NotBackedEnumException($type);
120
        }
121
122
        assert(is_a($type, BackedEnum::class, true));
123
        if (! (is_int($props) || is_string($props))) {
124
            throw new ParameterEnumTypeException($varName);
125
        }
126
127
        /**  @psalm-suppress MixedAssignment */
128
        $value = $type::tryFrom($props);
129
        if ($value === null) {
130
            throw new ParameterInvalidEnumException($varName);
131
        }
132
133
        return $value;
134
    }
135
}
136