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

InputParamObjectHandler::newInstance()   B

Complexity

Conditions 8
Paths 35

Size

Total Lines 49
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
eloc 23
nc 35
nop 6
dl 0
loc 49
rs 8.4444
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\InputClassParameterException;
9
use BEAR\Resource\Exception\ParameterException;
10
use InvalidArgumentException;
11
use Ray\Di\InjectorInterface;
12
use ReflectionClass;
13
use ReflectionMethod;
14
use ReflectionNamedType;
15
use ReflectionParameter;
16
use Throwable;
17
18
use function count;
19
use function is_iterable;
20
use function ltrim;
21
use function preg_replace;
22
use function sprintf;
23
use function strtolower;
24
25
final class InputParamObjectHandler
26
{
27
    /**
28
     * Creates an object instance without calling its constructor
29
     *
30
     * This method instantiates an object and directly assigns values to its properties
31
     * without invoking the constructor. This approach is used to maintain backward
32
     * compatibility for legacy code that expects objects to be created this way.
33
     *
34
     * @param array<string, mixed> $query
35
     */
36
    public function createWithoutConstructor(
37
        string $type,
38
        string $varName,
39
        array $query,
40
        bool $isDefaultAvailable,
41
        mixed $defaultValue,
42
    ): mixed {
43
        $props = $this->getPropsForClassParam($varName, $query, $isDefaultAvailable, $defaultValue);
44
45
        if (! is_iterable($props)) {
46
            if ($isDefaultAvailable) {
0 ignored issues
show
introduced by
The condition $isDefaultAvailable is always true.
Loading history...
47
                return $defaultValue;
48
            }
49
50
            throw new ParameterException("Expected array data for {$type}");
51
        }
52
53
        /** @var class-string $type */
54
        /** @psalm-suppress MixedMethodCall, InvalidStringClass */
55
        $obj = new $type();
56
        /** @psalm-suppress MixedAssignment */
57
        foreach ($props as $propName => $propValue) {
58
            $obj->{$propName} = $propValue;
59
        }
60
61
        return $obj;
62
    }
63
64
    /**
65
     * @param array<string, mixed> $query
66
     *
67
     * @return array<mixed>
68
     */
69
    public function getConstructorArgs(ReflectionMethod $constructor, array $query, InjectorInterface $injector, string $type = ''): array
70
    {
71
        /** @var list<mixed> $constructorArgs */
72
        $constructorArgs = [];
73
74
        foreach ($constructor->getParameters() as $param) {
75
            $paramName = $param->getName();
76
77
            $inputAttr = $this->getInputAttribute($param);
78
            if ($inputAttr !== null) {
79
                $paramType = $param->getType();
80
                if ($paramType instanceof ReflectionNamedType) {
81
                    $nestedInputParam = new InputParam($paramType, $param);
82
                    /** @psalm-suppress MixedArgumentTypeCoercion, MixedAssignment */
83
                    $constructorArgs[] = $nestedInputParam($paramName, $query, $injector);
84
                    continue;
85
                }
86
            }
87
88
            /** @psalm-suppress MixedArgumentTypeCoercion */
89
            $paramValue = $this->getParamValue($paramName, $query);
90
            if ($paramValue !== null) {
91
                /** @psalm-suppress MixedAssignment */
92
                $constructorArgs[] = $paramValue;
93
                continue;
94
            }
95
96
            if ($param->isDefaultValueAvailable()) {
97
                /** @psalm-suppress MixedAssignment */
98
                $constructorArgs[] = $param->getDefaultValue();
99
                continue;
100
            }
101
102
            $message = sprintf('paramName:%s: class:%s', $paramName, $type);
103
104
            throw new InputClassParameterException($message);
105
        }
106
107
        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...
108
    }
109
110
    /**
111
     * @param array<string, mixed>    $data
112
     * @param ReflectionClass<object> $refClass
113
     */
114
    public function newInstance(
115
        ReflectionMethod $constructor,
116
        array $data,
117
        InjectorInterface $injector,
118
        string $key,
119
        ReflectionClass $refClass,
120
        string $type,
121
    ): object {
122
        try {
123
            /** @var list<mixed> $constructorArgs */
124
            $constructorArgs = [];
125
126
            foreach ($constructor->getParameters() as $param) {
127
                $paramName = $param->getName();
128
129
                $inputAttr = $this->getInputAttribute($param);
130
                if ($inputAttr !== null) {
131
                    $paramType = $param->getType();
132
                    if ($paramType instanceof ReflectionNamedType) {
133
                        $nestedInputParam = new InputParam($paramType, $param);
134
                        /** @psalm-suppress MixedArgumentTypeCoercion, MixedAssignment */
135
                        $constructorArgs[] = $nestedInputParam($paramName, $data, $injector);
136
                        continue;
137
                    }
138
                }
139
140
                if (isset($data[$paramName])) {
141
                    /** @psalm-suppress MixedAssignment */
142
                    $constructorArgs[] = $data[$paramName];
143
                    continue;
144
                }
145
146
                if ($param->isDefaultValueAvailable()) {
147
                    /** @psalm-suppress MixedAssignment */
148
                    $constructorArgs[] = $param->getDefaultValue();
149
                    continue;
150
                }
151
152
                throw new ParameterException("Required parameter '{$paramName}' not found in key '{$key}' for {$type}");
153
            }
154
155
            /** @psalm-suppress MixedArgumentTypeCoercion */
156
            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

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