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

158
            return $refClass->newInstanceArgs(/** @scrutinizer ignore-type */ $constructorArgs);
Loading history...
159
        } catch (Throwable $e) {
160
            if ($e instanceof InvalidArgumentException) {
161
                throw $e;
162
            }
163
164
            throw new ParameterException("Failed to create {$type} from key '{$key}': " . $e->getMessage(), 0, $e);
165
        }
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