Passed
Push — master ( 9ce832...6b8c8b )
by Sergei
02:25
created

ParameterDefinition::getReflection()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 0
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
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
use function get_class;
18
use function gettype;
19
use function is_object;
20
21
/**
22
 * Parameter definition resolves an object based on information from `ReflectionParameter` instance.
23
 */
24
final class ParameterDefinition implements DefinitionInterface
25
{
26
    private ReflectionParameter $parameter;
27
28 71
    public function __construct(ReflectionParameter $parameter)
29
    {
30 71
        $this->parameter = $parameter;
31
    }
32
33 13
    public function getReflection(): ReflectionParameter
34
    {
35 13
        return $this->parameter;
36
    }
37
38 71
    public function isVariadic(): bool
39
    {
40 71
        return $this->parameter->isVariadic();
41
    }
42
43 27
    public function isOptional(): bool
44
    {
45 27
        return $this->parameter->isOptional();
46
    }
47
48 20
    public function hasValue(): bool
49
    {
50 20
        return $this->parameter->isDefaultValueAvailable();
51
    }
52
53 51
    public function resolve(ContainerInterface $container)
54
    {
55 51
        $type = $this->parameter->getType();
56
57 51
        if ($type === null || $this->isVariadic()) {
58 1
            return $this->resolveVariadicOrBuiltinOrNonTyped();
59
        }
60
61 50
        if ($this->isUnionType()) {
62 9
            return $this->resolveUnionType($container);
63
        }
64
65
        /** @var ReflectionNamedType|null $type */
66 41
        $type = $this->parameter->getType();
67 41
        $isBuiltin = $type !== null && $type->isBuiltin();
68
69 41
        if (!$isBuiltin) {
70
            /** @var ReflectionNamedType $type */
71 21
            $typeName = $type->getName();
72 21
            if ($typeName === 'self') {
73
                // If type name is "self", it means that called class and
74
                // $parameter->getDeclaringClass() returned instance of `ReflectionClass`.
75
                /** @psalm-suppress PossiblyNullReference */
76 1
                $typeName = $this->parameter
77 1
                    ->getDeclaringClass()
78 1
                    ->getName();
79
            }
80
81
            try {
82
                /** @var mixed */
83 21
                $result = $container->get($typeName);
84 13
            } catch (Throwable $t) {
85
                if (
86 13
                    $this->parameter->isOptional()
87
                    && (
88
                        $t instanceof CircularReferenceException
89 13
                        || !$container->has($typeName)
90
                    )
91
                ) {
92 6
                    return $this->parameter->getDefaultValue();
93
                }
94 7
                throw $t;
95
            }
96
97 8
            if (!$result instanceof $typeName) {
98 1
                $actualType = $this->getValueType($result);
99 1
                throw new InvalidConfigException(
100 1
                    "Container returned incorrect type \"$actualType\" for service \"{$type->getName()}\"."
101
                );
102
            }
103 7
            return $result;
104
        }
105
106 22
        return $this->resolveVariadicOrBuiltinOrNonTyped();
107
    }
108
109
    /**
110
     * @return mixed
111
     */
112 24
    private function resolveVariadicOrBuiltinOrNonTyped()
113
    {
114 24
        if ($this->parameter->isDefaultValueAvailable()) {
115 21
            return $this->parameter->getDefaultValue();
116
        }
117
118 3
        if ($this->isOptional()) {
119
            throw new NotInstantiableException(
120
                sprintf(
121
                    'Can not determine default value of parameter "%s" when instantiating "%s" ' .
122
                    'because it is PHP internal. Please specify argument explicitly.',
123
                    $this->parameter->getName(),
124
                    $this->getCallable(),
125
                )
126
            );
127
        }
128
129 3
        $type = $this->getType();
130
131 3
        if ($type === null) {
132 1
            throw new NotInstantiableException(
133 1
                sprintf(
134
                    'Can not determine value of the "%s" parameter without type when instantiating "%s". ' .
135 1
                    'Please specify argument explicitly.',
136 1
                    $this->parameter->getName(),
137 1
                    $this->getCallable(),
138
                )
139
            );
140
        }
141
142 2
        throw new NotInstantiableException(
143 2
            sprintf(
144
                'Can not determine value of the "%s" parameter of type "%s" when instantiating "%s". ' .
145 2
                'Please specify argument explicitly.',
146 2
                $this->parameter->getName(),
147
                $type,
148 2
                $this->getCallable(),
149
            )
150
        );
151
    }
152
153
    /**
154
     * Resolve union type string provided as a class name.
155
     *
156
     * @throws InvalidConfigException If an object of incorrect type was created.
157
     * @throws Throwable
158
     *
159
     * @return mixed|null Ready to use object or null if definition can
160
     * not be resolved and is marked as optional.
161
     */
162 9
    private function resolveUnionType(ContainerInterface $container)
163
    {
164
        /**
165
         * @psalm-suppress UndefinedDocblockClass This annotation is needed in PHP 7.4
166
         *
167
         * @var ReflectionUnionType $parameterType
168
         */
169 9
        $parameterType = $this->parameter->getType();
170
171
        /**
172
         * @psalm-suppress UndefinedDocblockClass This annotation is needed in PHP 7.4
173
         *
174
         * @var ReflectionNamedType[] $types
175
         */
176 9
        $types = $parameterType->getTypes();
177 9
        $class = implode('|', $types);
178
179 9
        foreach ($types as $type) {
180 9
            if (!$type->isBuiltin()) {
181 8
                $typeName = $type->getName();
182
                /**
183
                 * @psalm-suppress TypeDoesNotContainType
184
                 *
185
                 * @link https://github.com/vimeo/psalm/issues/6756
186
                 */
187 8
                if ($typeName === 'self') {
188
                    // If type name is "self", it means that called class and
189
                    // $parameter->getDeclaringClass() returned instance of `ReflectionClass`.
190
                    /** @psalm-suppress PossiblyNullReference */
191 1
                    $typeName = $this->parameter
192 1
                        ->getDeclaringClass()
193 1
                        ->getName();
194
                }
195
196
                try {
197
                    /** @var mixed */
198 8
                    $result = $container->get($typeName);
199 3
                    $resolved = true;
200 7
                } catch (Throwable $t) {
201 7
                    $error = $t;
202 7
                    $resolved = false;
203
                }
204
205 8
                if ($resolved) {
206
                    /** @var mixed $result Exist, because $resolved is true */
207 3
                    if (!$result instanceof $typeName) {
208 1
                        $actualType = $this->getValueType($result);
209 1
                        throw new InvalidConfigException(
210 1
                            "Container returned incorrect type \"$actualType\" for service \"$class\"."
211
                        );
212
                    }
213 2
                    return $result;
214
                }
215
216
                /** @var Throwable $error Exist, because $resolved is false */
217
                if (
218
                    !$error instanceof CircularReferenceException
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $error does not seem to be defined for all execution paths leading up to this point.
Loading history...
219 7
                    && $container->has($typeName)
220
                ) {
221 1
                    throw $error;
222
                }
223
            }
224
        }
225
226 5
        if ($this->parameter->isOptional()) {
227 2
            return null;
228
        }
229
230 3
        if (!isset($error)) {
231 1
            return $this->resolveVariadicOrBuiltinOrNonTyped();
232
        }
233
234 2
        throw $error;
235
    }
236
237 50
    private function isUnionType(): bool
238
    {
239
        /** @psalm-suppress UndefinedClass */
240 50
        return $this->parameter->getType() instanceof ReflectionUnionType;
241
    }
242
243 3
    private function getType(): ?string
244
    {
245 3
        $type = $this->parameter->getType();
246
247
        /** @psalm-suppress UndefinedClass, TypeDoesNotContainType */
248 3
        if ($type instanceof ReflectionUnionType) {
249
            /** @var ReflectionNamedType[] */
250 1
            $namedTypes = $type->getTypes();
251 1
            $names = array_map(
252 1
                static fn (ReflectionNamedType $t) => $t->getName(),
253
                $namedTypes
254
            );
255 1
            return implode('|', $names);
256
        }
257
258 2
        if ($type instanceof ReflectionNamedType) {
259 1
            return $type->getName();
260
        }
261
262 1
        return null;
263
    }
264
265 3
    private function getCallable(): string
266
    {
267 3
        $callable = [];
268
269 3
        $class = $this->parameter->getDeclaringClass();
270 3
        if ($class !== null) {
271 3
            $callable[] = $class->getName();
272
        }
273 3
        $callable[] = $this->parameter
274 3
                ->getDeclaringFunction()
275 3
                ->getName() .
276
            '()';
277
278 3
        return implode('::', $callable);
279
    }
280
281
    /**
282
     * Get type of the value provided.
283
     *
284
     * @param mixed $value Value to get type for.
285
     */
286 2
    private function getValueType($value): string
287
    {
288 2
        return is_object($value) ? get_class($value) : gettype($value);
289
    }
290
}
291