Passed
Push — master ( 6b8c8b...f21846 )
by Sergei
04:11 queued 01:43
created

ParameterDefinition   A

Complexity

Total Complexity 37

Size/Duplication

Total Lines 233
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 98
c 2
b 0
f 0
dl 0
loc 233
ccs 96
cts 96
cp 1
rs 9.44
wmc 37

11 Methods

Rating   Name   Duplication   Size   Complexity  
A isVariadic() 0 3 1
A isOptional() 0 3 1
A getReflection() 0 3 1
A __construct() 0 3 1
A hasValue() 0 3 1
A getCallable() 0 14 2
A getType() 0 18 3
A isUnionType() 0 3 1
B resolveUnionType() 0 67 11
C resolve() 0 52 12
A resolveVariadicOrBuiltinOrNonTyped() 0 26 3
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 73
    public function __construct(
23
        private ReflectionParameter $parameter
24
    ) {
25
    }
26
27 13
    public function getReflection(): ReflectionParameter
28
    {
29 13
        return $this->parameter;
30
    }
31
32 70
    public function isVariadic(): bool
33
    {
34 70
        return $this->parameter->isVariadic();
35
    }
36
37 24
    public function isOptional(): bool
38
    {
39 24
        return $this->parameter->isOptional();
40
    }
41
42 20
    public function hasValue(): bool
43
    {
44 20
        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 === null || $this->isVariadic()) {
52 2
            return $this->resolveVariadicOrBuiltinOrNonTyped();
53
        }
54
55 49
        if ($this->isUnionType()) {
56 8
            return $this->resolveUnionType($container);
57
        }
58
59
        /** @var ReflectionNamedType|null $type */
60 41
        $type = $this->parameter->getType();
61 41
        $isBuiltin = $type !== null && $type->isBuiltin();
62
63 41
        if (!$isBuiltin) {
64
            /** @var ReflectionNamedType $type */
65 21
            $typeName = $type->getName();
66 21
            if ($typeName === 'self') {
67
                // If type name is "self", it means that called class and
68
                // $parameter->getDeclaringClass() returned instance of `ReflectionClass`.
69
                /** @psalm-suppress PossiblyNullReference */
70 1
                $typeName = $this->parameter->getDeclaringClass()->getName();
71
            }
72
73
            try {
74
                /** @var mixed */
75 21
                $result = $container->get($typeName);
76 13
            } catch (Throwable $t) {
77
                if (
78 13
                    $this->parameter->isOptional()
79
                    && (
80
                        $t instanceof CircularReferenceException
81 13
                        || !$container->has($typeName)
82
                    )
83
                ) {
84 6
                    return $this->parameter->getDefaultValue();
85
                }
86 7
                throw $t;
87
            }
88
89 8
            if (!$result instanceof $typeName) {
90 1
                $actualType = get_debug_type($result);
91 1
                throw new InvalidConfigException(
92 1
                    "Container returned incorrect type \"$actualType\" for service \"{$type->getName()}\"."
93
                );
94
            }
95 7
            return $result;
96
        }
97
98 22
        return $this->resolveVariadicOrBuiltinOrNonTyped();
99
    }
100
101
    /**
102
     * @throws NotInstantiableException
103
     *
104
     * @return mixed
105
     */
106 25
    private function resolveVariadicOrBuiltinOrNonTyped()
107
    {
108 25
        if ($this->parameter->isDefaultValueAvailable()) {
109 22
            return $this->parameter->getDefaultValue();
110
        }
111
112 3
        $type = $this->getType();
113
114 3
        if ($type === null) {
115 1
            throw new NotInstantiableException(
116 1
                sprintf(
117
                    'Can not determine value of the "%s" parameter without type when instantiating "%s". ' .
118 1
                    'Please specify argument explicitly.',
119 1
                    $this->parameter->getName(),
120 1
                    $this->getCallable(),
121
                )
122
            );
123
        }
124
125 2
        throw new NotInstantiableException(
126 2
            sprintf(
127
                'Can not determine value of the "%s" parameter of type "%s" when instantiating "%s". ' .
128 2
                'Please specify argument explicitly.',
129 2
                $this->parameter->getName(),
130
                $type,
131 2
                $this->getCallable(),
132
            )
133
        );
134
    }
135
136
    /**
137
     * Resolve union type string provided as a class name.
138
     *
139
     * @throws InvalidConfigException If an object of incorrect type was created.
140
     * @throws Throwable
141
     *
142
     * @return mixed|null Ready to use object or null if definition can
143
     * not be resolved and is marked as optional.
144
     */
145 8
    private function resolveUnionType(ContainerInterface $container)
146
    {
147
        /**
148
         * @var ReflectionUnionType $parameterType
149
         */
150 8
        $parameterType = $this->parameter->getType();
151
152
        /**
153
         * @var ReflectionNamedType[] $types
154
         */
155 8
        $types = $parameterType->getTypes();
156 8
        $class = implode('|', $types);
157
158 8
        foreach ($types as $type) {
159 8
            if (!$type->isBuiltin()) {
160 7
                $typeName = $type->getName();
161
                /**
162
                 * @psalm-suppress TypeDoesNotContainType
163
                 *
164
                 * @link https://github.com/vimeo/psalm/issues/6756
165
                 */
166 7
                if ($typeName === 'self') {
167
                    // If type name is "self", it means that called class and
168
                    // $parameter->getDeclaringClass() returned instance of `ReflectionClass`.
169
                    /** @psalm-suppress PossiblyNullReference */
170 1
                    $typeName = $this->parameter->getDeclaringClass()->getName();
171
                }
172
173
                try {
174
                    /** @var mixed */
175 7
                    $result = $container->get($typeName);
176 3
                    $resolved = true;
177 6
                } catch (Throwable $t) {
178 6
                    $error = $t;
179 6
                    $resolved = false;
180
                }
181
182 7
                if ($resolved) {
183
                    /** @var mixed $result Exist, because $resolved is true */
184 3
                    if (!$result instanceof $typeName) {
185 1
                        $actualType = get_debug_type($result);
186 1
                        throw new InvalidConfigException(
187 1
                            "Container returned incorrect type \"$actualType\" for service \"$class\"."
188
                        );
189
                    }
190 2
                    return $result;
191
                }
192
193
                /** @var Throwable $error Exist, because $resolved is false */
194
                if (
195
                    !$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...
196 6
                    && $container->has($typeName)
197
                ) {
198 1
                    throw $error;
199
                }
200
            }
201
        }
202
203 4
        if ($this->parameter->isOptional()) {
204 1
            return null;
205
        }
206
207 3
        if (!isset($error)) {
208 1
            return $this->resolveVariadicOrBuiltinOrNonTyped();
209
        }
210
211 2
        throw $error;
212
    }
213
214 49
    private function isUnionType(): bool
215
    {
216 49
        return $this->parameter->getType() instanceof ReflectionUnionType;
217
    }
218
219 3
    private function getType(): ?string
220
    {
221 3
        $type = $this->parameter->getType();
222
223 3
        if ($type instanceof ReflectionUnionType) {
224 1
            $namedTypes = $type->getTypes();
225 1
            $names = array_map(
226 1
                static fn (ReflectionNamedType $t) => $t->getName(),
227
                $namedTypes
228
            );
229 1
            return implode('|', $names);
230
        }
231
232 2
        if ($type instanceof ReflectionNamedType) {
233 1
            return $type->getName();
234
        }
235
236 1
        return null;
237
    }
238
239 3
    private function getCallable(): string
240
    {
241 3
        $callable = [];
242
243 3
        $class = $this->parameter->getDeclaringClass();
244 3
        if ($class !== null) {
245 3
            $callable[] = $class->getName();
246
        }
247 3
        $callable[] = $this->parameter
248 3
                ->getDeclaringFunction()
249 3
                ->getName() .
250
            '()';
251
252 3
        return implode('::', $callable);
253
    }
254
}
255