Passed
Push — master ( 05ba76...51044c )
by Sergei
11:41 queued 09:10
created

ArrayDefinition::resolve()   B

Complexity

Conditions 6
Paths 7

Size

Total Lines 39
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 19
CRAP Score 6

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
eloc 22
c 1
b 0
f 0
nc 7
nop 1
dl 0
loc 39
ccs 19
cts 19
cp 1
crap 6
rs 8.9457
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
     * Container used to resolve references.
37
     */
38
    private ?ContainerInterface $referenceContainer = null;
39
40
    /**
41
     * @psalm-param class-string $class
42
     * @psalm-param array<string, MethodOrPropertyItem> $methodsAndProperties
43
     */
44 44
    private function __construct(
45
        private string $class,
46
        private array $constructorArguments,
47
        private array $methodsAndProperties
48
    ) {
49
    }
50
51
    /**
52
     * @param ContainerInterface|null $referenceContainer Container to resolve references with.
53
     */
54 1
    public function withReferenceContainer(?ContainerInterface $referenceContainer): self
55
    {
56 1
        $new = clone $this;
57 1
        $new->referenceContainer = $referenceContainer;
58 1
        return $new;
59
    }
60
61
    /**
62
     * Create ArrayDefinition from array config.
63
     *
64
     * @psalm-param ArrayDefinitionConfig $config
65
     */
66 42
    public static function fromConfig(array $config): self
67
    {
68 42
        return new self(
69 42
            $config[self::CLASS_NAME],
70 42
            $config[self::CONSTRUCTOR] ?? [],
71 42
            self::getMethodsAndPropertiesFromConfig($config)
72
        );
73
    }
74
75
    /**
76
     * @psalm-param class-string $class
77
     * @psalm-param array<string, MethodOrPropertyItem> $methodsAndProperties
78
     */
79 2
    public static function fromPreparedData(string $class, array $constructorArguments = [], array $methodsAndProperties = []): self
80
    {
81 2
        return new self($class, $constructorArguments, $methodsAndProperties);
82
    }
83
84
    /**
85
     * @psalm-param array<string, mixed> $config
86
     *
87
     * @psalm-return array<string, MethodOrPropertyItem>
88
     */
89 42
    private static function getMethodsAndPropertiesFromConfig(array $config): array
90
    {
91 42
        $methodsAndProperties = [];
92
93
        /** @var mixed $value */
94 42
        foreach ($config as $key => $value) {
95 42
            if ($key === self::CONSTRUCTOR) {
96 12
                continue;
97
            }
98
99
            /**
100
             * @infection-ignore-all Explode limit does not affect the result.
101
             *
102
             * @see \Yiisoft\Definitions\Tests\Unit\Helpers\DefinitionValidatorTest::testIncorrectMethodName()
103
             */
104 42
            if (count($methodArray = explode('()', $key, 2)) === 2) {
105 25
                $methodsAndProperties[$key] = [self::TYPE_METHOD, $methodArray[0], $value];
106 42
            } elseif (count($propertyArray = explode('$', $key)) === 2) {
107 4
                $methodsAndProperties[$key] = [self::TYPE_PROPERTY, $propertyArray[1], $value];
108
            }
109
        }
110
111 42
        return $methodsAndProperties;
112
    }
113
114
    /**
115
     * @psalm-return class-string
116
     */
117 3
    public function getClass(): string
118
    {
119 3
        return $this->class;
120
    }
121
122 43
    public function getConstructorArguments(): array
123
    {
124 43
        return $this->constructorArguments;
125
    }
126
127
    /**
128
     * @psalm-return array<string, MethodOrPropertyItem>
129
     */
130 41
    public function getMethodsAndProperties(): array
131
    {
132 41
        return $this->methodsAndProperties;
133
    }
134
135 40
    public function resolve(ContainerInterface $container): object
136
    {
137 40
        $class = $this->class;
138
139 40
        $resolvedConstructorArguments = $this->resolveFunctionArguments(
140
            $container,
141 40
            DefinitionExtractor::fromClassName($class),
142 40
            $this->getConstructorArguments()
143
        );
144
145
        /** @psalm-suppress MixedMethodCall */
146 38
        $object = new $class(...$resolvedConstructorArguments);
147
148 38
        foreach ($this->getMethodsAndProperties() as $item) {
149
            /** @var mixed $value */
150 26
            [$type, $name, $value] = $item;
151 26
            if ($type === self::TYPE_METHOD) {
152
                /** @var array $value */
153 24
                if (method_exists($object, $name)) {
154 23
                    $resolvedMethodArguments = $this->resolveFunctionArguments(
155
                        $container,
156 23
                        DefinitionExtractor::fromFunction(new ReflectionMethod($object, $name)),
157
                        $value,
158
                    );
159
                } else {
160 1
                    $resolvedMethodArguments = $value;
161
                }
162
                /** @var mixed $setter */
163 21
                $setter = call_user_func_array([$object, $name], $resolvedMethodArguments);
164 21
                if ($setter instanceof $object) {
165
                    /** @var object $object */
166 21
                    $object = $setter;
167
                }
168 3
            } elseif ($type === self::TYPE_PROPERTY) {
169 3
                $object->$name = DefinitionResolver::resolve($container, $this->referenceContainer, $value);
170
            }
171
        }
172
173 35
        return $object;
174
    }
175
176
    /**
177
     * @param array<string,ParameterDefinition> $dependencies
178
     *
179
     * @psalm-return list<mixed>
180
     */
181 40
    private function resolveFunctionArguments(
182
        ContainerInterface $container,
183
        array $dependencies,
184
        array $arguments
185
    ): array {
186 40
        $isIntegerIndexed = $this->isIntegerIndexed($arguments);
187 39
        $dependencyIndex = 0;
188 39
        $usedArguments = [];
189 39
        $variadicKey = null;
190
191 39
        foreach ($dependencies as $key => &$value) {
192 37
            if ($value->isVariadic()) {
193 30
                $variadicKey = $key;
194
            }
195 37
            $index = $isIntegerIndexed ? $dependencyIndex : $key;
196 37
            if (array_key_exists($index, $arguments)) {
197 29
                $value = DefinitionResolver::ensureResolvable($arguments[$index]);
198
                /** @infection-ignore-all Mutation don't change behaviour. Values of `$usedArguments` not used. */
199 29
                $usedArguments[$index] = 1;
200
            }
201 37
            $dependencyIndex++;
202
        }
203 39
        unset($value);
204
205 39
        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 38
        $resolvedArguments = DefinitionResolver::resolveArray($container, $this->referenceContainer, $dependencies);
238 38
        return array_values($resolvedArguments);
239
    }
240
241
    /**
242
     * @throws InvalidConfigException
243
     */
244 40
    private function isIntegerIndexed(array $arguments): bool
245
    {
246 40
        $hasStringIndex = false;
247 40
        $hasIntegerIndex = false;
248
249 40
        foreach ($arguments as $index => $_argument) {
250 31
            if (is_string($index)) {
251 15
                $hasStringIndex = true;
252 15
                if ($hasIntegerIndex) {
253
                    /** @infection-ignore-all Mutation don't change behaviour, but degrade performance. */
254 15
                    break;
255
                }
256
            } else {
257 18
                $hasIntegerIndex = true;
258 18
                if ($hasStringIndex) {
259
                    /** @infection-ignore-all Mutation don't change behaviour, but degrade performance. */
260 1
                    break;
261
                }
262
            }
263
        }
264 40
        if ($hasIntegerIndex && $hasStringIndex) {
265 2
            throw new InvalidConfigException(
266
                'Arguments indexed both by name and by position are not allowed in the same array.'
267
            );
268
        }
269
270 39
        return $hasIntegerIndex;
271
    }
272
273
    /**
274
     * Create a new definition that is merged from this definition and another definition.
275
     *
276
     * @param ArrayDefinition $other Definition to merge with.
277
     *
278
     * @return self New definition that is merged from this definition and another definition.
279
     */
280 2
    public function merge(self $other): self
281
    {
282 2
        $new = clone $this;
283 2
        $new->class = $other->class;
284 2
        $new->constructorArguments = $this->mergeArguments($this->constructorArguments, $other->constructorArguments);
285
286 2
        $methodsAndProperties = $this->methodsAndProperties;
287 2
        foreach ($other->methodsAndProperties as $key => $item) {
288 1
            if ($item[0] === self::TYPE_PROPERTY) {
289 1
                $methodsAndProperties[$key] = $item;
290 1
            } elseif ($item[0] === self::TYPE_METHOD) {
291
                /** @psalm-suppress MixedArgument, MixedAssignment */
292 1
                $arguments = isset($methodsAndProperties[$key])
293 1
                    ? $this->mergeArguments($methodsAndProperties[$key][2], $item[2])
294 1
                    : $item[2];
295 1
                $methodsAndProperties[$key] = [$item[0], $item[1], $arguments];
296
            }
297
        }
298 2
        $new->methodsAndProperties = $methodsAndProperties;
299
300 2
        return $new;
301
    }
302
303 2
    private function mergeArguments(array $selfArguments, array $otherArguments): array
304
    {
305
        /** @var mixed $argument */
306 2
        foreach ($otherArguments as $name => $argument) {
307
            /** @var mixed */
308 1
            $selfArguments[$name] = $argument;
309
        }
310
311 2
        return $selfArguments;
312
    }
313
}
314