Passed
Pull Request — master (#86)
by Sergei
04:05 queued 01:35
created

ParameterDefinition   A

Complexity

Total Complexity 36

Size/Duplication

Total Lines 211
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 4
Bugs 1 Features 0
Metric Value
wmc 36
eloc 89
c 4
b 1
f 0
dl 0
loc 211
ccs 100
cts 100
cp 1
rs 9.52

10 Methods

Rating   Name   Duplication   Size   Complexity  
A isVariadic() 0 3 1
A isOptional() 0 3 1
A getReflection() 0 3 1
A getCallable() 0 14 2
A resolveVariadicOrBuiltinOrIntersectionOrNonTyped() 0 26 3
A getType() 0 4 2
C resolveUnionType() 0 68 12
A __construct() 0 3 1
C resolve() 0 51 12
A hasValue() 0 3 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Definitions;
6
7
use Psr\Container\ContainerInterface;
8
use ReflectionIntersectionType;
0 ignored issues
show
Bug introduced by
The type ReflectionIntersectionType was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

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