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