Passed
Push — master ( 9ce832...6b8c8b )
by Sergei
02:25
created

ArrayDefinition::resolveFunctionArguments()   C

Complexity

Conditions 12
Paths 54

Size

Total Lines 57
Code Lines 34

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 31
CRAP Score 12

Importance

Changes 0
Metric Value
cc 12
eloc 34
nc 54
nop 3
dl 0
loc 57
ccs 31
cts 31
cp 1
crap 12
rs 6.9666
c 0
b 0
f 0

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