Passed
Push — master ( 76d43e...ace620 )
by Alexander
02:16
created

ParameterDefinition::isBuiltin()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

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