Passed
Push — master ( 76d43e...ace620 )
by Alexander
02:16
created

ArrayDefinition   A

Complexity

Total Complexity 42

Size/Duplication

Total Lines 258
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 2
Bugs 1 Features 0
Metric Value
eloc 100
dl 0
loc 258
ccs 107
cts 107
cp 1
rs 9.0399
c 2
b 1
f 0
wmc 42

13 Methods

Rating   Name   Duplication   Size   Complexity  
A getConstructorArguments() 0 3 1
A merge() 0 21 5
A getClass() 0 3 1
B injectArguments() 0 33 11
A fromConfig() 0 6 1
A __construct() 0 5 1
A fromPreparedData() 0 3 1
B isIntegerIndexed() 0 25 7
A resolve() 0 32 5
A getMethodsAndPropertiesFromConfig() 0 18 5
A getMethodsAndProperties() 0 3 1
A mergeArguments() 0 9 2
A setReferenceContainer() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like ArrayDefinition often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ArrayDefinition, and based on these observations, apply Extract Interface, too.

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