AutowireValueResolver   A
last analyzed

Complexity

Total Complexity 31

Size/Duplication

Total Lines 174
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 15
Bugs 0 Features 0
Metric Value
wmc 31
eloc 67
c 15
b 0
f 0
dl 0
loc 174
ccs 68
cts 68
cp 1
rs 9.92

6 Methods

Rating   Name   Duplication   Size   Complexity  
A resolve() 0 11 1
B autowireArgument() 0 38 9
A getDefaultValue() 0 21 6
A findByMethod() 0 19 5
A isValidType() 0 3 2
B resolveNotFoundService() 0 28 8
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of DivineNii opensource projects.
7
 *
8
 * PHP version 7.4 and above required
9
 *
10
 * @author    Divine Niiquaye Ibok <[email protected]>
11
 * @copyright 2021 DivineNii (https://divinenii.com/)
12
 * @license   https://opensource.org/licenses/BSD-3-Clause License
13
 *
14
 * For the full copyright and license information, please view the LICENSE
15
 * file that was distributed with this source code.
16
 */
17
18
namespace Rade\DI\Resolvers;
19
20
use Nette\Utils\Reflection;
21
use Rade\DI\Exceptions\{ContainerResolutionException, NotFoundServiceException};
22
use Symfony\Contracts\Service\{ServiceProviderInterface, ServiceSubscriberInterface};
23
24
/**
25
 * An advanced autowiring used for PSR-11 implementation.
26
 *
27
 * @author Divine Niiquaye Ibok <[email protected]>
28
 */
29
class AutowireValueResolver
30
{
31
    /** a unique identifier for not found parameter value */
32
    private const NONE = '\/\/:oxo:\/\/';
33
34
    /**
35
     * Resolve parameters for service definition.
36
     *
37
     * @param array<int|string,mixed> $providedParameters
38
     *
39
     * @return mixed
40
     */
41 49
    public function resolve(callable $resolver, \ReflectionParameter $parameter, array $providedParameters)
42
    {
43 49
        $paramName = $parameter->name;
44 49
        $position = $parameter->getPosition();
45
46
        try {
47 49
            return $providedParameters[$position]
48 45
                ?? $providedParameters[$paramName]
49 49
                ?? $this->autowireArgument($parameter, $resolver, $providedParameters);
50
        } finally {
51 49
            unset($providedParameters[$position], $providedParameters[$paramName]);
52
        }
53
    }
54
55
    /**
56
     * Resolves missing argument using autowiring.
57
     *
58
     * @param array<int|string,mixed> $providedParameters
59
     *
60
     * @throws ContainerResolutionException
61
     *
62
     * @return mixed
63
     */
64 45
    private function autowireArgument(\ReflectionParameter $parameter, callable $getter, array $providedParameters)
65
    {
66 45
        $types = Reflection::getParameterTypes($parameter);
67 45
        $invalid = [];
68
69 45
        foreach ($types as $typeName) {
70 42
            if ('null' === $typeName) {
71 9
                continue;
72
            }
73
74
            try {
75 42
                return $providedParameters[$typeName] ?? $getter($typeName, !$parameter->isVariadic());
76 28
            } catch (NotFoundServiceException $e) {
77 23
                $res = null;
78
79 23
                if (\in_array($typeName, ['array', 'iterable'], true)) {
80 23
                    $res = $this->findByMethod($parameter, $getter);
81
                }
82 5
            } catch (ContainerResolutionException $e) {
83 3
                $res = $this->findByMethod($parameter, $getter);
84
85 3
                if (self::NONE !== $res && 1 === \count($res)) {
0 ignored issues
show
Bug introduced by
It seems like $res can also be of type string; however, parameter $value of count() does only seem to accept Countable|array, maybe add an additional type check? ( Ignorable by Annotation )

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

85
                if (self::NONE !== $res && 1 === \count(/** @scrutinizer ignore-type */ $res)) {
Loading history...
86 1
                    return \current($res);
0 ignored issues
show
Bug introduced by
It seems like $res can also be of type string; however, parameter $array of current() does only seem to accept array|object, maybe add an additional type check? ( Ignorable by Annotation )

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

86
                    return \current(/** @scrutinizer ignore-type */ $res);
Loading history...
87
                }
88
89 3
                throw new ContainerResolutionException(\sprintf("{$e->getMessage()} (needed by %s)", Reflection::toString($parameter)));
90
            }
91
92 23
            $res = $res ?? $this->resolveNotFoundService($parameter, $getter, $typeName);
93
94 17
            if (self::NONE !== $res) {
95 2
                return $res;
96
            }
97
98 15
            $invalid[] = $typeName;
99
        }
100
101 19
        return $this->getDefaultValue($parameter, $invalid);
102
    }
103
104
    /**
105
     * Parses a methods doc comments or return default value.
106
     *
107
     * @return string|object[]
108
     */
109 8
    private function findByMethod(\ReflectionParameter $parameter, callable $getter)
110
    {
111 8
        $method = $parameter->getDeclaringFunction();
112
113 8
        if ($method instanceof \ReflectionMethod && null != $class = $method->getDeclaringClass()) {
114 8
            \preg_match(
115 8
                "#@param[ \\t]+([\\w\\\\]+?)(\\[])?[ \\t]+\\\${$parameter->name}#",
116 8
                (string) $method->getDocComment(),
117 8
                $matches
118
            );
119
120 8
            $itemType = isset($matches[1]) ? Reflection::expandClassName($matches[1], $class) : '';
121
122 8
            if ($this->isValidType($itemType)) {
123 2
                return $getter($itemType, false);
124
            }
125
        }
126
127 7
        return self::NONE;
128
    }
129
130
    /**
131
     * Resolve services which may or not exist in container.
132
     *
133
     * @return mixed
134
     */
135 20
    private function resolveNotFoundService(\ReflectionParameter $parameter, callable $getter, string $type)
136
    {
137 20
        if (ServiceProviderInterface::class === $type && null !== $class = $parameter->getDeclaringClass()) {
138 2
            if (!$class->isSubclassOf(ServiceSubscriberInterface::class)) {
139 1
                throw new ContainerResolutionException(\sprintf(
140 1
                    'Service of type %s needs parent class %s to implement %s.',
141 1
                    $type,
142 1
                    $class->getName(),
143 1
                    ServiceSubscriberInterface::class
144
                ));
145
            }
146
147 1
            return $getter($class->getName());
148
        }
149
150
        // Incase a valid class/interface is found or default value ...
151 18
        if ($this->isValidType($type) || ($parameter->isDefaultValueAvailable() || $parameter->allowsNull())) {
152 13
            return self::NONE;
153
        }
154
155 6
        $desc = Reflection::toString($parameter);
156 6
        $message = "Type '$type' needed by $desc not found. Check type hint and 'use' statements.";
157
158 6
        if (Reflection::isBuiltinType($type)) {
159 2
            $message = "Builtin Type '$type' needed by $desc is not supported for autowiring.";
160
        }
161
162 6
        throw new ContainerResolutionException($message);
163
    }
164
165
    /**
166
     * Get the parameter's default value else null.
167
     *
168
     * @param string[] $invalid
169
     *
170
     * @throws \ReflectionException
171
     *
172
     * @return mixed
173
     */
174 19
    private function getDefaultValue(\ReflectionParameter $parameter, array $invalid)
175
    {
176
        // optional + !defaultAvailable = i.e. Exception::__construct, mysqli::mysqli, ...
177 19
        if ($parameter->isOptional() && $parameter->isDefaultValueAvailable()) {
178 9
            return \PHP_VERSION_ID < 80000 ? Reflection::getParameterDefaultValue($parameter) : null;
179
        }
180
181
        // Return null if = i.e. doSomething(?$hello, $value) ...
182 13
        if ($parameter->allowsNull()) {
183 9
            return null;
184
        }
185
186 6
        $desc = Reflection::toString($parameter);
187 6
        $message = "Parameter $desc has no class type hint or default value, so its value must be specified.";
188
189 6
        if (!empty($invalid)) {
190 6
            $invalid = \implode('|', $invalid);
191 6
            $message = "Parameter $desc typehint(s) '$invalid' not found, and no default value specified.";
192
        }
193
194 6
        throw new ContainerResolutionException($message);
195
    }
196
197
    /**
198
     * @param mixed $type
199
     */
200 24
    private function isValidType($type): bool
201
    {
202 24
        return \class_exists($type) || \interface_exists($type);
203
    }
204
}
205