Passed
Pull Request — master (#44)
by Sergei
02:17
created

ArrayDefinition::injectArguments()   B

Complexity

Conditions 11
Paths 54

Size

Total Lines 33
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 24
CRAP Score 11

Importance

Changes 0
Metric Value
cc 11
eloc 23
nc 54
nop 2
dl 0
loc 33
ccs 24
cts 24
cp 1
crap 11
rs 7.3166
c 0
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Definitions;
6
7
use InvalidArgumentException;
8
use Psr\Container\ContainerInterface;
9
use ReflectionMethod;
10
use Yiisoft\Definitions\Contract\DefinitionInterface;
11
use Yiisoft\Definitions\Exception\InvalidConfigException;
12
use Yiisoft\Definitions\Helpers\DefinitionExtractor;
13
use Yiisoft\Definitions\Helpers\DefinitionResolver;
14
15
use function array_key_exists;
16
use function call_user_func_array;
17
use function count;
18
use function is_string;
19
20
/**
21
 * Builds an object by array config.
22
 *
23
 * @psalm-type MethodOrPropertyItem = array{0:string,1:string,2:mixed}
24
 * @psalm-type ArrayDefinitionConfig = array{class:class-string,'__construct()'?:array}&array<string, mixed>
25
 */
26
final class ArrayDefinition implements DefinitionInterface
27
{
28
    public const CLASS_NAME = 'class';
29
    public const CONSTRUCTOR = '__construct()';
30
31
    public const TYPE_PROPERTY = 'property';
32
    public const TYPE_METHOD = 'method';
33
34
    /**
35
     * @psalm-var class-string
36
     */
37
    private string $class;
38
    private array $constructorArguments;
39
    /**
40
      * Container used to resolve references.
41
      */
42
    private ?ContainerInterface $referenceContainer = null;
43
44
    /**
45
     * @psalm-var array<string, MethodOrPropertyItem>
46
     */
47
    private array $methodsAndProperties;
48
49
    /**
50
     * @psalm-param class-string $class
51
     * @psalm-param array<string, MethodOrPropertyItem> $methodsAndProperties
52
     */
53 40
    private function __construct(string $class, array $constructorArguments, array $methodsAndProperties)
54
    {
55 40
        $this->class = $class;
56 40
        $this->constructorArguments = $constructorArguments;
57 40
        $this->methodsAndProperties = $methodsAndProperties;
58
    }
59
60
    /**
61
     * @param ContainerInterface|null $referenceContainer Container to resolve references with.
62
     */
63 1
    public function withReferenceContainer(?ContainerInterface $referenceContainer): self
64
    {
65 1
        $new = clone $this;
66 1
        $new->referenceContainer = $referenceContainer;
67 1
        return $new;
68
    }
69
70
    /**
71
     * Create ArrayDefinition from array config.
72
     *
73
     * @psalm-param ArrayDefinitionConfig $config
74
     */
75 38
    public static function fromConfig(array $config): self
76
    {
77 38
        return new self(
78 38
            $config[self::CLASS_NAME],
79 38
            $config[self::CONSTRUCTOR] ?? [],
80 38
            self::getMethodsAndPropertiesFromConfig($config)
81
        );
82
    }
83
84
    /**
85
     * @psalm-param class-string $class
86
     * @psalm-param array<string, MethodOrPropertyItem> $methodsAndProperties
87
     */
88 2
    public static function fromPreparedData(string $class, array $constructorArguments = [], array $methodsAndProperties = []): self
89
    {
90 2
        return new self($class, $constructorArguments, $methodsAndProperties);
91
    }
92
93
    /**
94
     * @psalm-param array<string, mixed> $config
95
     *
96
     * @psalm-return array<string, MethodOrPropertyItem>
97
     */
98 38
    private static function getMethodsAndPropertiesFromConfig(array $config): array
99
    {
100 38
        $methodsAndProperties = [];
101
102
        /** @var mixed $value */
103 38
        foreach ($config as $key => $value) {
104 38
            if ($key === self::CONSTRUCTOR) {
105 12
                continue;
106
            }
107
108 38
            if (count($methodArray = explode('()', $key, 2)) === 2) {
109 21
                $methodsAndProperties[$key] = [self::TYPE_METHOD, $methodArray[0], $value];
110 38
            } elseif (count($propertyArray = explode('$', $key)) === 2) {
111 3
                $methodsAndProperties[$key] = [self::TYPE_PROPERTY, $propertyArray[1], $value];
112
            }
113
        }
114
115 38
        return $methodsAndProperties;
116
    }
117
118
    /**
119
     * @psalm-return class-string
120
     */
121 3
    public function getClass(): string
122
    {
123 3
        return $this->class;
124
    }
125
126 39
    public function getConstructorArguments(): array
127
    {
128 39
        return $this->constructorArguments;
129
    }
130
131
    /**
132
     * @psalm-return array<string, MethodOrPropertyItem>
133
     */
134 37
    public function getMethodsAndProperties(): array
135
    {
136 37
        return $this->methodsAndProperties;
137
    }
138
139 36
    public function resolve(ContainerInterface $container): object
140
    {
141 36
        $class = $this->class;
142
143 36
        $resolvedConstructorArguments = $this->resolveFunctionArguments(
144
            $container,
145 36
            DefinitionExtractor::fromClassName($class),
146 36
            $this->getConstructorArguments()
147
        );
148
149
        /** @psalm-suppress MixedMethodCall */
150 34
        $object = new $class(...$resolvedConstructorArguments);
151
152 34
        foreach ($this->getMethodsAndProperties() as $item) {
153
            /** @var mixed $value */
154 22
            [$type, $name, $value] = $item;
155 22
            if ($type === self::TYPE_METHOD) {
156
                /** @var array $value */
157 20
                $resolvedMethodArguments = $this->resolveFunctionArguments(
158
                    $container,
159 20
                    DefinitionExtractor::fromFunction(new ReflectionMethod($object, $name)),
160
                    $value,
161
                );
162
                /** @var mixed $setter */
163 18
                $setter = call_user_func_array([$object, $name], $resolvedMethodArguments);
164 18
                if ($setter instanceof $object) {
165
                    /** @var object $object */
166 18
                    $object = $setter;
167
                }
168 2
            } elseif ($type === self::TYPE_PROPERTY) {
169 2
                $object->$name = DefinitionResolver::resolve($container, $this->referenceContainer, $value);
170
            }
171
        }
172
173 32
        return $object;
174
    }
175
176
    /**
177
     * @param array<string,ParameterDefinition> $dependencies
178
     *
179
     * @psalm-return list<mixed>
180
     */
181 36
    private function resolveFunctionArguments(
182
        ContainerInterface $container,
183
        array $dependencies,
184
        array $arguments
185
    ): array {
186 36
        $this->injectArguments($dependencies, $arguments);
187 34
        $resolvedArguments = DefinitionResolver::resolveArray($container, $this->referenceContainer, $dependencies);
188 34
        return array_values($resolvedArguments);
189
    }
190
191
    /**
192
     * @psalm-param array<string, ParameterDefinition> $dependencies
193
     * @psalm-param-out array<array-key, Yiisoft\Definitions\ParameterDefinition|mixed> $dependencies
194
     *
195
     * @throws InvalidConfigException
196
     */
197 36
    private function injectArguments(array &$dependencies, array $arguments): void
198
    {
199 36
        $isIntegerIndexed = $this->isIntegerIndexed($arguments);
200 35
        $dependencyIndex = 0;
201 35
        $usedArguments = [];
202 35
        $variadicKey = null;
203 35
        foreach ($dependencies as $key => &$value) {
204 34
            if ($value->isVariadic()) {
205 27
                $variadicKey = $key;
206
            }
207 34
            $index = $isIntegerIndexed ? $dependencyIndex : $key;
208 34
            if (array_key_exists($index, $arguments)) {
209 26
                $value = DefinitionResolver::ensureResolvable($arguments[$index]);
210 26
                $usedArguments[$index] = 1;
211
            }
212 34
            $dependencyIndex++;
213
        }
214 35
        unset($value);
215 35
        if ($variadicKey !== null) {
216 27
            if (!$isIntegerIndexed && isset($arguments[$variadicKey])) {
217 5
                if (is_array($arguments[$variadicKey])) {
218 3
                    unset($dependencies[$variadicKey]);
219 3
                    $dependencies += $arguments[$variadicKey];
220 3
                    return;
221
                }
222
223 2
                throw new InvalidArgumentException(sprintf('Named argument for a variadic parameter should be an array, "%s" given.', gettype($arguments[$variadicKey])));
224
            }
225
226
            /** @var mixed $value */
227 22
            foreach ($arguments as $index => $value) {
228 10
                if (!isset($usedArguments[$index])) {
229 2
                    $dependencies[$index] = DefinitionResolver::ensureResolvable($value);
230
                }
231
            }
232
        }
233
    }
234
235
    /**
236
     * @throws InvalidConfigException
237
     */
238 36
    private function isIntegerIndexed(array $arguments): bool
239
    {
240 36
        $hasStringIndex = false;
241 36
        $hasIntegerIndex = false;
242
243 36
        foreach ($arguments as $index => $_argument) {
244 28
            if (is_string($index)) {
245 13
                $hasStringIndex = true;
246 13
                if ($hasIntegerIndex) {
247 13
                    break;
248
                }
249
            } else {
250 17
                $hasIntegerIndex = true;
251 17
                if ($hasStringIndex) {
252 1
                    break;
253
                }
254
            }
255
        }
256 36
        if ($hasIntegerIndex && $hasStringIndex) {
257 2
            throw new InvalidConfigException(
258
                'Arguments indexed both by name and by position are not allowed in the same array.'
259
            );
260
        }
261
262 35
        return $hasIntegerIndex;
263
    }
264
265
    /**
266
     * Create a new definition that is merged from this definition and another definition.
267
     *
268
     * @param ArrayDefinition $other Definition to merge with.
269
     *
270
     * @return self New definition that is merged from this definition and another definition.
271
     */
272 2
    public function merge(self $other): self
273
    {
274 2
        $new = clone $this;
275 2
        $new->class = $other->class;
276 2
        $new->constructorArguments = $this->mergeArguments($this->constructorArguments, $other->constructorArguments);
277
278 2
        $methodsAndProperties = $this->methodsAndProperties;
279 2
        foreach ($other->methodsAndProperties as $key => $item) {
280 1
            if ($item[0] === self::TYPE_PROPERTY) {
281 1
                $methodsAndProperties[$key] = $item;
282 1
            } elseif ($item[0] === self::TYPE_METHOD) {
283
                /** @psalm-suppress MixedArgument, MixedAssignment */
284 1
                $arguments = isset($methodsAndProperties[$key])
285 1
                    ? $this->mergeArguments($methodsAndProperties[$key][2], $item[2])
286 1
                    : $item[2];
287 1
                $methodsAndProperties[$key] = [$item[0], $item[1], $arguments];
288
            }
289
        }
290 2
        $new->methodsAndProperties = $methodsAndProperties;
291
292 2
        return $new;
293
    }
294
295 2
    private function mergeArguments(array $selfArguments, array $otherArguments): array
296
    {
297
        /** @var mixed $argument */
298 2
        foreach ($otherArguments as $name => $argument) {
299
            /** @var mixed */
300 1
            $selfArguments[$name] = $argument;
301
        }
302
303 2
        return $selfArguments;
304
    }
305
}
306