Passed
Pull Request — master (#33)
by Sergei
13:10
created

ParameterDefinition::getValueType()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 2

Importance

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