Passed
Pull Request — master (#17)
by Alexander
02:28
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\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
        return $this->parameter->getType()->isBuiltin();
46
    }
47
48 19
    public function hasValue(): bool
49
    {
50 19
        return $this->parameter->isDefaultValueAvailable();
51
    }
52
53 35
    public function resolve(ContainerInterface $container)
54
    {
55
56 35
        $type = $this->parameter->getType();
57
58 35
        if ($type === null || $this->isVariadic()) {
59 2
            return $this->resolveBuiltin();
60
        }
61
62 33
        if ($this->isUnionType()) {
63
            return $this->resolveUnionType($container);
64
        }
65
66 33
        if (!$this->isBuiltin()) {
67 14
            $typeName = $this->parameter->getType()->getName();
0 ignored issues
show
Bug introduced by
The method getName() does not exist on ReflectionType. It seems like you code against a sub-type of ReflectionType such as ReflectionNamedType. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

67
            $typeName = $this->parameter->getType()->/** @scrutinizer ignore-call */ getName();
Loading history...
68 14
            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
                $typeName = $this->parameter->getDeclaringClass()->getName();
73
            }
74
75
            try {
76
                /** @var mixed */
77 14
                $result = $container->get($typeName);
78 9
            } catch (Throwable $t) {
79 9
                if ($this->parameter->isOptional()) {
80 4
                    return null;
81
                }
82 5
                throw $t;
83
            }
84
85 5
            if (!$result instanceof $typeName) {
86 1
                $actualType = $this->getValueType($result);
87 1
                throw new InvalidConfigException(
88 1
                    "Container returned incorrect type \"$actualType\" for service \"{$this->parameter->getType()->getName()}\"."
89
                );
90
            }
91 4
            return $result;
92
        }
93
94 19
        return $this->resolveBuiltin();
95
    }
96
97 21
    private function resolveBuiltin()
98
    {
99 21
        if ($this->parameter->isDefaultValueAvailable()) {
100 19
            return $this->parameter->getDefaultValue();
101
        }
102
103 2
        if ($this->isOptional()) {
104 2
            throw new NotInstantiableException(
105 2
                sprintf(
106
                    'Can not determine default value of parameter "%s" when instantiating "%s" ' .
107 2
                    'because it is PHP internal. Please specify argument explicitly.',
108 2
                    $this->parameter->getName(),
109 2
                    $this->getCallable(),
110
                )
111
            );
112
        }
113
114
        throw new NotInstantiableException(
115
            sprintf(
116
                'Can not determine value of the "%s" parameter of type "%s" when instantiating "%s". ' .
117
                'Please specify argument explicitly.',
118
                $this->parameter->getName(),
119
                $this->getType(),
120
                $this->getCallable(),
121
            )
122
        );
123
    }
124
125
    /**
126
     * Resolve union type string provided as a class name.
127
     *
128
     * @throws InvalidConfigException If an object of incorrect type was created.
129
     * @throws Throwable
130
     *
131
     * @return mixed|null Ready to use object or null if definition can
132
     * not be resolved and is marked as optional.
133
     */
134
    private function resolveUnionType(ContainerInterface $container)
135
    {
136
        $types = $this->parameter->getType()->getTypes();
0 ignored issues
show
Bug introduced by
The method getTypes() does not exist on ReflectionType. It seems like you code against a sub-type of ReflectionType such as ReflectionUnionType. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

136
        $types = $this->parameter->getType()->/** @scrutinizer ignore-call */ getTypes();
Loading history...
137
        $class = implode('|', $types);
138
139
        foreach ($types as $type) {
140
            if (!$type->isBuiltin()) {
141
                $typeName = $type->getName();
142
                if ($typeName === 'self') {
143
                    // If type name is "self", it means that called class and
144
                    // $parameter->getDeclaringClass() returned instance of `ReflectionClass`.
145
                    /** @psalm-suppress PossiblyNullReference */
146
                    $typeName = $this->parameter->getDeclaringClass()->getName();
147
                }
148
                try {
149
                    /** @var mixed */
150
                    $result = $container->get($typeName);
151
                    if (!$result instanceof $typeName) {
152
                        $actualType = $this->getValueType($result);
153
                        throw new InvalidConfigException(
154
                            "Container returned incorrect type \"$actualType\" for service \"$class\"."
155
                        );
156
                    }
157
158
                    return $result;
159
                } catch (Throwable $t) {
160
                    $error = $t;
161
                }
162
            }
163
        }
164
165
        if ($this->parameter->isOptional()) {
166
            return null;
167
        }
168
169
        if (!isset($error)) {
170
            return $this->resolveBuiltin();
171
        }
172
173
        throw $error;
174
    }
175
176 33
    private function isUnionType(): bool
177
    {
178 33
        return $this->parameter->getType() instanceof ReflectionUnionType;
179
    }
180
181
    private function getType(): string
182
    {
183
        /**
184
         * @psalm-suppress UndefinedDocblockClass
185
         *
186
         * @var ReflectionNamedType|ReflectionUnionType $type Could not be `null`
187
         * because in self::resolve() checked `$this->parameter->allowsNull()`.
188
         */
189
        $type = $this->parameter->getType();
190
191
        /** @psalm-suppress UndefinedClass, TypeDoesNotContainType */
192
        if ($type instanceof ReflectionUnionType) {
193
            /** @var ReflectionNamedType[] */
194
            $namedTypes = $type->getTypes();
195
            $names = array_map(
196
                static fn (ReflectionNamedType $t) => $t->getName(),
197
                $namedTypes
198
            );
199
            return implode('|', $names);
200
        }
201
202
        /** @var ReflectionNamedType $type */
203
204
        return $type->getName();
205
    }
206
207 2
    private function getCallable(): string
208
    {
209 2
        $callable = [];
210
211 2
        $class = $this->parameter->getDeclaringClass();
212 2
        if ($class !== null) {
213 1
            $callable[] = $class->getName();
214
        }
215 2
        $callable[] = $this->parameter->getDeclaringFunction()->getName() . '()';
216
217 2
        return implode('::', $callable);
218
    }
219
220
    /**
221
     * Get type of the value provided.
222
     *
223
     * @param mixed $value Value to get type for.
224
     */
225 1
    private function getValueType($value): string
226
    {
227 1
        return is_object($value) ? get_class($value) : gettype($value);
228
    }
229
}
230