ParameterDefinition::__construct()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 0

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 0
nc 1
nop 1
dl 0
loc 3
ccs 1
cts 1
cp 1
crap 1
rs 10
c 1
b 0
f 0
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
The variable $error does not seem to be defined for all execution paths leading up to this point.
Loading history...
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