Passed
Pull Request — 1.x (#321)
by Akihito
03:36 queued 57s
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
17
use function count;
18
use function is_iterable;
19
use function ltrim;
20
use function preg_replace;
21
use function strtolower;
22
23
final class InputParamObjectHandler
24
{
25
    /** @param array<string, mixed> $query */
26
    public function createWithoutConstructor(
27
        string $type,
28
        string $varName,
29
        array $query,
30
        bool $isDefaultAvailable,
31
        mixed $defaultValue,
32
    ): mixed {
33
        $props = $this->getPropsForClassParam($varName, $query, $isDefaultAvailable, $defaultValue);
34
35
        if (! is_iterable($props)) {
36
            if ($isDefaultAvailable) {
0 ignored issues
show
introduced by
The condition $isDefaultAvailable is always true.
Loading history...
37
                return $defaultValue;
38
            }
39
40
            throw new ParameterException("Expected array data for {$type}");
41
        }
42
43
        /** @var class-string $type */
44
        /** @psalm-suppress MixedMethodCall, InvalidStringClass */
45
        $obj = new $type();
46
        /** @psalm-suppress MixedAssignment */
47
        foreach ($props as $propName => $propValue) {
48
            $obj->{$propName} = $propValue;
49
        }
50
51
        return $obj;
52
    }
53
54
    /**
55
     * @param array<string, mixed> $query
56
     *
57
     * @return array<mixed>
58
     */
59
    public function getConstructorArgs(ReflectionMethod $constructor, array $query, InjectorInterface $injector, string $type = ''): array
60
    {
61
        /** @var list<mixed> $constructorArgs */
62
        $constructorArgs = [];
63
64
        foreach ($constructor->getParameters() as $param) {
65
            $paramName = $param->getName();
66
67
            $inputAttr = $this->getInputAttribute($param);
68
            if ($inputAttr !== null) {
69
                $paramType = $param->getType();
70
                if ($paramType instanceof ReflectionNamedType) {
71
                    $nestedInputParam = new InputParam($paramType, $param);
72
                    /** @psalm-suppress MixedArgumentTypeCoercion, MixedAssignment */
73
                    $constructorArgs[] = $nestedInputParam($paramName, $query, $injector);
74
                    continue;
75
                }
76
            }
77
78
            /** @psalm-suppress MixedArgumentTypeCoercion */
79
            $paramValue = $this->getParamValue($paramName, $query);
80
            if ($paramValue !== null) {
81
                /** @psalm-suppress MixedAssignment */
82
                $constructorArgs[] = $paramValue;
83
                continue;
84
            }
85
86
            if ($param->isDefaultValueAvailable()) {
87
                /** @psalm-suppress MixedAssignment */
88
                $constructorArgs[] = $param->getDefaultValue();
89
                continue;
90
            }
91
92
            $message = "Required parameter '{$paramName}' not found";
93
            if ($type !== '') {
94
                $message .= " for {$type}";
95
            }
96
97
            throw new ParameterException($message);
98
        }
99
100
        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...
101
    }
102
103
    /**
104
     * @param array<string, mixed>    $data
105
     * @param ReflectionClass<object> $refClass
106
     */
107
    public function newInstance(
108
        ReflectionMethod $constructor,
109
        array $data,
110
        InjectorInterface $injector,
111
        string $key,
112
        ReflectionClass $refClass,
113
        string $type,
114
    ): object|null {
115
        try {
116
            /** @var list<mixed> $constructorArgs */
117
            $constructorArgs = [];
118
119
            foreach ($constructor->getParameters() as $param) {
120
                $paramName = $param->getName();
121
122
                $inputAttr = $this->getInputAttribute($param);
123
                if ($inputAttr !== null) {
124
                    $paramType = $param->getType();
125
                    if ($paramType instanceof ReflectionNamedType) {
126
                        $nestedInputParam = new InputParam($paramType, $param);
127
                        /** @psalm-suppress MixedArgumentTypeCoercion, MixedAssignment */
128
                        $constructorArgs[] = $nestedInputParam($paramName, $data, $injector);
129
                        continue;
130
                    }
131
                }
132
133
                if (isset($data[$paramName])) {
134
                    /** @psalm-suppress MixedAssignment */
135
                    $constructorArgs[] = $data[$paramName];
136
                    continue;
137
                }
138
139
                if ($param->isDefaultValueAvailable()) {
140
                    /** @psalm-suppress MixedAssignment */
141
                    $constructorArgs[] = $param->getDefaultValue();
142
                    continue;
143
                }
144
145
                throw new ParameterException("Required parameter '{$paramName}' not found in key '{$key}' for {$type}");
146
            }
147
148
            /** @psalm-suppress MixedArgumentTypeCoercion */
149
            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

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