1 | <?php |
||
2 | |||
3 | declare(strict_types=1); |
||
4 | |||
5 | namespace Yiisoft\Definitions; |
||
6 | |||
7 | use Psr\Container\ContainerInterface; |
||
8 | use ReflectionNamedType; |
||
9 | use ReflectionParameter; |
||
10 | use ReflectionUnionType; |
||
11 | use Throwable; |
||
12 | use Yiisoft\Definitions\Contract\DefinitionInterface; |
||
13 | use Yiisoft\Definitions\Exception\CircularReferenceException; |
||
14 | use Yiisoft\Definitions\Exception\NotInstantiableException; |
||
15 | use Yiisoft\Definitions\Exception\InvalidConfigException; |
||
16 | |||
17 | /** |
||
18 | * Parameter definition resolves an object based on information from `ReflectionParameter` instance. |
||
19 | */ |
||
20 | final class ParameterDefinition implements DefinitionInterface |
||
21 | { |
||
22 | 79 | public function __construct( |
|
23 | private ReflectionParameter $parameter |
||
24 | ) { |
||
25 | 79 | } |
|
26 | |||
27 | 20 | public function getReflection(): ReflectionParameter |
|
28 | { |
||
29 | 20 | return $this->parameter; |
|
30 | } |
||
31 | |||
32 | 63 | public function isVariadic(): bool |
|
33 | { |
||
34 | 63 | return $this->parameter->isVariadic(); |
|
35 | } |
||
36 | |||
37 | 2 | public function isOptional(): bool |
|
38 | { |
||
39 | 2 | return $this->parameter->isOptional(); |
|
40 | } |
||
41 | |||
42 | 4 | public function hasValue(): bool |
|
43 | { |
||
44 | 4 | return $this->parameter->isDefaultValueAvailable(); |
|
45 | } |
||
46 | |||
47 | 51 | public function resolve(ContainerInterface $container): mixed |
|
48 | { |
||
49 | 51 | $type = $this->parameter->getType(); |
|
50 | |||
51 | 51 | if ($type instanceof ReflectionUnionType) { |
|
52 | 8 | return $this->resolveUnionType($type, $container); |
|
53 | } |
||
54 | |||
55 | 43 | if ($type === null |
|
56 | 41 | || !$type instanceof ReflectionNamedType |
|
57 | 41 | || $this->isVariadic() |
|
58 | 43 | || $type->isBuiltin() |
|
59 | ) { |
||
60 | 24 | return $this->resolveVariadicOrBuiltinOrIntersectionOrNonTyped(); |
|
61 | } |
||
62 | |||
63 | 21 | $typeName = $type->getName(); |
|
64 | /** |
||
65 | * @psalm-suppress TypeDoesNotContainType |
||
66 | * @see https://github.com/vimeo/psalm/issues/6756 |
||
67 | */ |
||
68 | 21 | if ($typeName === 'self') { |
|
69 | // If type name is "self", it means that called class and |
||
70 | // $parameter->getDeclaringClass() returned instance of `ReflectionClass`. |
||
71 | /** @psalm-suppress PossiblyNullReference */ |
||
72 | 1 | $typeName = $this->parameter->getDeclaringClass()->getName(); |
|
73 | } |
||
74 | |||
75 | try { |
||
76 | 21 | $result = $container->get($typeName); |
|
77 | 13 | } catch (Throwable $t) { |
|
78 | if ( |
||
79 | 13 | $this->parameter->isOptional() |
|
80 | && ( |
||
81 | 13 | $t instanceof CircularReferenceException |
|
82 | 13 | || !$container->has($typeName) |
|
83 | ) |
||
84 | ) { |
||
85 | 6 | return $this->parameter->getDefaultValue(); |
|
86 | } |
||
87 | 7 | throw $t; |
|
88 | } |
||
89 | |||
90 | 8 | if (!$result instanceof $typeName) { |
|
91 | 1 | $actualType = get_debug_type($result); |
|
92 | 1 | throw new InvalidConfigException( |
|
93 | 1 | "Container returned incorrect type \"$actualType\" for service \"{$type->getName()}\"." |
|
94 | 1 | ); |
|
95 | } |
||
96 | 7 | return $result; |
|
97 | } |
||
98 | |||
99 | /** |
||
100 | * @throws NotInstantiableException |
||
101 | */ |
||
102 | 25 | private function resolveVariadicOrBuiltinOrIntersectionOrNonTyped(): mixed |
|
103 | { |
||
104 | 25 | if ($this->parameter->isDefaultValueAvailable()) { |
|
105 | 22 | return $this->parameter->getDefaultValue(); |
|
106 | } |
||
107 | |||
108 | 3 | $type = $this->getType(); |
|
109 | |||
110 | 3 | if ($type === null) { |
|
111 | 1 | throw new NotInstantiableException( |
|
112 | 1 | sprintf( |
|
113 | 1 | 'Can not determine value of the "%s" parameter without type when instantiating "%s". ' . |
|
114 | 1 | 'Please specify argument explicitly.', |
|
115 | 1 | $this->parameter->getName(), |
|
116 | 1 | $this->getCallable(), |
|
117 | 1 | ) |
|
118 | 1 | ); |
|
119 | } |
||
120 | |||
121 | 2 | throw new NotInstantiableException( |
|
122 | 2 | sprintf( |
|
123 | 2 | 'Can not determine value of the "%s" parameter of type "%s" when instantiating "%s". ' . |
|
124 | 2 | 'Please specify argument explicitly.', |
|
125 | 2 | $this->parameter->getName(), |
|
126 | 2 | $type, |
|
127 | 2 | $this->getCallable(), |
|
128 | 2 | ) |
|
129 | 2 | ); |
|
130 | } |
||
131 | |||
132 | /** |
||
133 | * Resolve union type string provided as a class name. |
||
134 | * |
||
135 | * @throws InvalidConfigException If an object of incorrect type was created. |
||
136 | * @throws Throwable |
||
137 | * |
||
138 | * @return mixed Ready to use object or null if definition can not be resolved and is marked as optional. |
||
139 | */ |
||
140 | 8 | private function resolveUnionType(ReflectionUnionType $parameterType, ContainerInterface $container): mixed |
|
141 | { |
||
142 | 8 | $types = $parameterType->getTypes(); |
|
143 | 8 | $class = implode('|', $types); |
|
144 | |||
145 | 8 | foreach ($types as $type) { |
|
146 | /** |
||
147 | * @psalm-suppress DocblockTypeContradiction Need for PHP 8.0 and 8.1 only |
||
148 | */ |
||
149 | 8 | if (!$type instanceof ReflectionNamedType || $type->isBuiltin()) { |
|
150 | 2 | continue; |
|
151 | } |
||
152 | |||
153 | 7 | $typeName = $type->getName(); |
|
154 | /** |
||
155 | * @psalm-suppress TypeDoesNotContainType |
||
156 | * |
||
157 | * @link https://github.com/vimeo/psalm/issues/6756 |
||
158 | */ |
||
159 | 7 | if ($typeName === 'self') { |
|
160 | // If type name is "self", it means that called class and |
||
161 | // $parameter->getDeclaringClass() returned instance of `ReflectionClass`. |
||
162 | /** @psalm-suppress PossiblyNullReference */ |
||
163 | 1 | $typeName = $this->parameter->getDeclaringClass()->getName(); |
|
164 | } |
||
165 | |||
166 | try { |
||
167 | 7 | $result = $container->get($typeName); |
|
168 | 3 | $resolved = true; |
|
169 | 6 | } catch (Throwable $t) { |
|
170 | 6 | $error = $t; |
|
171 | 6 | $resolved = false; |
|
172 | } |
||
173 | |||
174 | 7 | if ($resolved) { |
|
175 | /** @var mixed $result Exist, because $resolved is true */ |
||
176 | 3 | if (!$result instanceof $typeName) { |
|
177 | 1 | $actualType = get_debug_type($result); |
|
178 | 1 | throw new InvalidConfigException( |
|
179 | 1 | "Container returned incorrect type \"$actualType\" for service \"$class\"." |
|
180 | 1 | ); |
|
181 | } |
||
182 | 2 | return $result; |
|
183 | } |
||
184 | |||
185 | /** @var Throwable $error Exist, because $resolved is false */ |
||
186 | if ( |
||
187 | 6 | !$error instanceof CircularReferenceException |
|
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
![]() |
|||
188 | 6 | && $container->has($typeName) |
|
189 | ) { |
||
190 | 1 | throw $error; |
|
191 | } |
||
192 | } |
||
193 | |||
194 | 4 | if ($this->parameter->isOptional()) { |
|
195 | 1 | return null; |
|
196 | } |
||
197 | |||
198 | 3 | if (!isset($error)) { |
|
199 | 1 | return $this->resolveVariadicOrBuiltinOrIntersectionOrNonTyped(); |
|
200 | } |
||
201 | |||
202 | 2 | throw $error; |
|
203 | } |
||
204 | |||
205 | 3 | private function getType(): ?string |
|
206 | { |
||
207 | 3 | $type = $this->parameter->getType(); |
|
208 | 3 | return $type === null ? null : (string) $type; |
|
209 | } |
||
210 | |||
211 | 3 | private function getCallable(): string |
|
212 | { |
||
213 | 3 | $callable = []; |
|
214 | |||
215 | 3 | $class = $this->parameter->getDeclaringClass(); |
|
216 | 3 | if ($class !== null) { |
|
217 | 3 | $callable[] = $class->getName(); |
|
218 | } |
||
219 | 3 | $callable[] = $this->parameter |
|
220 | 3 | ->getDeclaringFunction() |
|
221 | 3 | ->getName() . |
|
222 | 3 | '()'; |
|
223 | |||
224 | 3 | return implode('::', $callable); |
|
225 | } |
||
226 | } |
||
227 |