Passed
Pull Request — 1.x (#321)
by Akihito
02:33
created

InputParamObjectHandler::getConstructorArgs()   B

Complexity

Conditions 7
Paths 10

Size

Total Lines 42
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 22
nc 10
nop 4
dl 0
loc 42
rs 8.6346
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace BEAR\Resource\Input;
6
7
use BEAR\Resource\Annotation\Input;
8
use BEAR\Resource\Exception\ParameterException;
9
use InvalidArgumentException;
10
use Ray\Di\InjectorInterface;
11
use ReflectionClass;
12
use ReflectionMethod;
13
use ReflectionNamedType;
14
use ReflectionParameter;
15
use Throwable;
16
use function count;
17
use function is_iterable;
18
use function ltrim;
19
use function preg_replace;
20
use function strtolower;
21
22
final class InputParamObjectHandler
23
{
24
    /** @param array<string, mixed> $query */
25
    public function createWithoutConstructor(
26
        string $type,
27
        string $varName,
28
        array $query,
29
        bool $isDefaultAvailable,
30
        mixed $defaultValue,
31
    ): mixed {
32
        $props = $this->getPropsForClassParam($varName, $query, $isDefaultAvailable, $defaultValue);
33
34
        if (! is_iterable($props)) {
35
            if ($isDefaultAvailable) {
0 ignored issues
show
introduced by
The condition $isDefaultAvailable is always true.
Loading history...
36
                return $defaultValue;
37
            }
38
39
            throw new ParameterException("Expected array data for {$type}");
40
        }
41
42
        /** @var class-string $type */
43
        /** @psalm-suppress MixedMethodCall, InvalidStringClass */
44
        $obj = new $type();
45
        /** @psalm-suppress MixedAssignment */
46
        foreach ($props as $propName => $propValue) {
47
            $obj->{$propName} = $propValue;
48
        }
49
50
        return $obj;
51
    }
52
53
    /**
54
     * @param array<string, mixed> $query
55
     *
56
     * @return array<mixed>
57
     */
58
    public function getConstructorArgs(ReflectionMethod $constructor, array $query, InjectorInterface $injector, string $type = ''): array
59
    {
60
        /** @var list<mixed> $constructorArgs */
61
        $constructorArgs = [];
62
63
        foreach ($constructor->getParameters() as $param) {
64
            $paramName = $param->getName();
65
66
            $inputAttr = $this->getInputAttribute($param);
67
            if ($inputAttr !== null) {
68
                $paramType = $param->getType();
69
                if ($paramType instanceof ReflectionNamedType) {
70
                    $nestedInputParam = new InputParam($paramType, $param);
71
                    /** @psalm-suppress MixedArgumentTypeCoercion, MixedAssignment */
72
                    $constructorArgs[] = $nestedInputParam($paramName, $query, $injector);
73
                    continue;
74
                }
75
            }
76
77
            /** @psalm-suppress MixedArgumentTypeCoercion */
78
            $paramValue = $this->getParamValue($paramName, $query);
79
            if ($paramValue !== null) {
80
                /** @psalm-suppress MixedAssignment */
81
                $constructorArgs[] = $paramValue;
82
                continue;
83
            }
84
85
            if ($param->isDefaultValueAvailable()) {
86
                /** @psalm-suppress MixedAssignment */
87
                $constructorArgs[] = $param->getDefaultValue();
88
                continue;
89
            }
90
91
            $message = "Required parameter '{$paramName}' not found";
92
            if ($type !== '') {
93
                $message .= " for {$type}";
94
            }
95
96
            throw new ParameterException($message);
97
        }
98
99
        return $constructorArgs;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $constructorArgs returns the type BEAR\Resource\Input\list which is incompatible with the type-hinted return array.
Loading history...
100
    }
101
102
    /**
103
     * @param array<string, mixed>    $data
104
     * @param ReflectionClass<object> $refClass
105
     */
106
    public function newInstance(
107
        ReflectionMethod $constructor,
108
        array $data,
109
        InjectorInterface $injector,
110
        string $key,
111
        ReflectionClass $refClass,
112
        string $type,
113
    ): object|null {
114
        try {
115
            /** @var list<mixed> $constructorArgs */
116
            $constructorArgs = [];
117
118
            foreach ($constructor->getParameters() as $param) {
119
                $paramName = $param->getName();
120
121
                $inputAttr = $this->getInputAttribute($param);
122
                if ($inputAttr !== null) {
123
                    $paramType = $param->getType();
124
                    if ($paramType instanceof ReflectionNamedType) {
125
                        $nestedInputParam = new InputParam($paramType, $param);
126
                        /** @psalm-suppress MixedArgumentTypeCoercion, MixedAssignment */
127
                        $constructorArgs[] = $nestedInputParam($paramName, $data, $injector);
128
                        continue;
129
                    }
130
                }
131
132
                if (isset($data[$paramName])) {
133
                    /** @psalm-suppress MixedAssignment */
134
                    $constructorArgs[] = $data[$paramName];
135
                    continue;
136
                }
137
138
                if ($param->isDefaultValueAvailable()) {
139
                    /** @psalm-suppress MixedAssignment */
140
                    $constructorArgs[] = $param->getDefaultValue();
141
                    continue;
142
                }
143
144
                throw new ParameterException("Required parameter '{$paramName}' not found in key '{$key}' for {$type}");
145
            }
146
147
            /** @psalm-suppress MixedArgumentTypeCoercion */
148
            return $refClass->newInstanceArgs($constructorArgs);
0 ignored issues
show
Bug introduced by
$constructorArgs of type BEAR\Resource\Input\list is incompatible with the type array expected by parameter $args of ReflectionClass::newInstanceArgs(). ( Ignorable by Annotation )

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

148
            return $refClass->newInstanceArgs(/** @scrutinizer ignore-type */ $constructorArgs);
Loading history...
149
        } catch (Throwable $e) {
150
            if ($e instanceof InvalidArgumentException) {
151
                throw $e;
152
            }
153
154
            throw new ParameterException("Failed to create {$type} from key '{$key}': " . $e->getMessage(), 0, $e);
155
        }
156
    }
157
158
    private function getInputAttribute(ReflectionParameter $param): Input|null
159
    {
160
        $attributes = $param->getAttributes(Input::class);
161
        if (count($attributes) === 0) {
162
            return null;
163
        }
164
165
        return $attributes[0]->newInstance();
166
    }
167
168
    /** @param array<string, mixed> $query */
169
    private function getParamValue(string $paramName, array $query): mixed
170
    {
171
        if (isset($query[$paramName])) {
172
            return $query[$paramName];
173
        }
174
175
        $kebabName = ltrim(strtolower((string) preg_replace('/[A-Z]/', '-\0', $paramName)), '-');
176
        if (isset($query[$kebabName])) {
177
            return $query[$kebabName];
178
        }
179
180
        return null;
181
    }
182
183
    /** @param array<string, mixed> $query */
184
    private function getPropsForClassParam(
185
        string $varName,
186
        array $query,
187
        bool $isDefaultAvailable,
188
        mixed $defaultValue,
189
    ): mixed {
190
        if (isset($query[$varName])) {
191
            return $query[$varName];
192
        }
193
194
        $snakeName = ltrim(strtolower((string) preg_replace('/[A-Z]/', '_\0', $varName)), '_');
195
        if (isset($query[$snakeName])) {
196
            return $query[$snakeName];
197
        }
198
199
        if ($isDefaultAvailable) {
200
            return $defaultValue;
201
        }
202
203
        throw new ParameterException($varName);
204
    }
205
}
206