Passed
Pull Request — master (#36)
by
unknown
12:25
created

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