ArrayDefinition::fromConfig()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 4
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 6
ccs 5
cts 5
cp 1
crap 1
rs 10
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\ArrayDefinitionHelper;
14
use Yiisoft\Definitions\Helpers\DefinitionExtractor;
15
use Yiisoft\Definitions\Helpers\DefinitionResolver;
16
17
use function array_key_exists;
18
use function call_user_func_array;
19
use function count;
20
use function is_string;
21
22
/**
23
 * Builds an object by array config.
24
 *
25
 * @psalm-type MethodOrPropertyItem = array{0:string,1:string,2:mixed}
26
 * @psalm-type ArrayDefinitionConfig = array{class:class-string,'__construct()'?:array}&array<string, mixed>
27
 */
28
final class ArrayDefinition implements DefinitionInterface
29
{
30
    public const CLASS_NAME = 'class';
31
    public const CONSTRUCTOR = '__construct()';
32
33
    public const TYPE_PROPERTY = 'property';
34
    public const TYPE_METHOD = 'method';
35
36
    /**
37
     * Container used to resolve references.
38
     */
39
    private ?ContainerInterface $referenceContainer = null;
40
41
    /**
42
     * @psalm-param class-string $class
43
     * @psalm-param array<string, MethodOrPropertyItem> $methodsAndProperties
44
     */
45 44
    private function __construct(
46
        private string $class,
47
        private array $constructorArguments,
48
        private array $methodsAndProperties
49
    ) {
50 44
    }
51
52
    /**
53
     * @param ContainerInterface|null $referenceContainer Container to resolve references with.
54
     */
55 1
    public function withReferenceContainer(?ContainerInterface $referenceContainer): self
56
    {
57 1
        $new = clone $this;
58 1
        $new->referenceContainer = $referenceContainer;
59 1
        return $new;
60
    }
61
62
    /**
63
     * Create ArrayDefinition from array config.
64
     *
65
     * @psalm-param ArrayDefinitionConfig $config
66
     */
67 42
    public static function fromConfig(array $config): self
68
    {
69 42
        return new self(
70 42
            $config[self::CLASS_NAME],
71 42
            $config[self::CONSTRUCTOR] ?? [],
72 42
            self::getMethodsAndPropertiesFromConfig($config)
73 42
        );
74
    }
75
76
    /**
77
     * @psalm-param class-string $class
78
     * @psalm-param array<string, MethodOrPropertyItem> $methodsAndProperties
79
     */
80 2
    public static function fromPreparedData(string $class, array $constructorArguments = [], array $methodsAndProperties = []): self
81
    {
82 2
        return new self($class, $constructorArguments, $methodsAndProperties);
83
    }
84
85
    /**
86
     * @psalm-param array<string, mixed> $config
87
     *
88
     * @psalm-return array<string, MethodOrPropertyItem>
89
     */
90 42
    private static function getMethodsAndPropertiesFromConfig(array $config): array
91
    {
92 42
        $methodsAndProperties = [];
93
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 40
            $container,
141 40
            DefinitionExtractor::fromClassName($class),
142 40
            $this->getConstructorArguments()
143 40
        );
144
145
        /** @psalm-suppress MixedMethodCall */
146 38
        $object = new $class(...$resolvedConstructorArguments);
147
148 38
        foreach ($this->getMethodsAndProperties() as $item) {
149 26
            [$type, $name, $value] = $item;
150 26
            if ($type === self::TYPE_METHOD) {
151
                /** @var array $value */
152 24
                if (method_exists($object, $name)) {
153 23
                    $resolvedMethodArguments = $this->resolveFunctionArguments(
154 23
                        $container,
155 23
                        DefinitionExtractor::fromFunction(new ReflectionMethod($object, $name)),
156 23
                        $value,
157 23
                    );
158
                } else {
159 1
                    $resolvedMethodArguments = $value;
160
                }
161 21
                $setter = call_user_func_array([$object, $name], $resolvedMethodArguments);
162 21
                if ($setter instanceof $object) {
163
                    /** @var object $object */
164 21
                    $object = $setter;
165
                }
166 3
            } elseif ($type === self::TYPE_PROPERTY) {
167 3
                $object->$name = DefinitionResolver::resolve($container, $this->referenceContainer, $value);
168
            }
169
        }
170
171 35
        return $object;
172
    }
173
174
    /**
175
     * @param array<string,ParameterDefinition> $dependencies
176
     *
177
     * @psalm-return list<mixed>
178
     */
179 40
    private function resolveFunctionArguments(
180
        ContainerInterface $container,
181
        array $dependencies,
182
        array $arguments
183
    ): array {
184 40
        $isIntegerIndexed = $this->isIntegerIndexed($arguments);
185 39
        $dependencyIndex = 0;
186 39
        $usedArguments = [];
187 39
        $variadicKey = null;
188
189 39
        foreach ($dependencies as $key => &$value) {
190 37
            if ($value->isVariadic()) {
191 30
                $variadicKey = $key;
192
            }
193 37
            $index = $isIntegerIndexed ? $dependencyIndex : $key;
194 37
            if (array_key_exists($index, $arguments)) {
195 29
                $value = DefinitionResolver::ensureResolvable($arguments[$index]);
196
                /** @infection-ignore-all Mutation don't change behaviour. Values of `$usedArguments` not used. */
197 29
                $usedArguments[$index] = 1;
198
            }
199 37
            $dependencyIndex++;
200
        }
201 39
        unset($value);
202
203 39
        if ($variadicKey !== null) {
204 30
            if (!$isIntegerIndexed && isset($arguments[$variadicKey])) {
205 7
                if ($arguments[$variadicKey] instanceof ReferenceInterface) {
206 2
                    $arguments[$variadicKey] = DefinitionResolver::resolve(
207 2
                        $container,
208 2
                        $this->referenceContainer,
209 2
                        $arguments[$variadicKey]
210 2
                    );
211
                }
212
213 7
                if (is_array($arguments[$variadicKey])) {
214 4
                    unset($dependencies[$variadicKey]);
215 4
                    $dependencies += $arguments[$variadicKey];
216
                } else {
217 7
                    throw new InvalidArgumentException(
218 7
                        sprintf(
219 7
                            'Named argument for a variadic parameter should be an array, "%s" given.',
220 7
                            gettype($arguments[$variadicKey])
221 7
                        )
222 7
                    );
223
                }
224
            } else {
225 23
                foreach ($arguments as $index => $value) {
226 11
                    if (!isset($usedArguments[$index])) {
227 2
                        $dependencies[$index] = DefinitionResolver::ensureResolvable($value);
228
                    }
229
                }
230
            }
231
        }
232
233 38
        $resolvedArguments = DefinitionResolver::resolveArray($container, $this->referenceContainer, $dependencies);
234 38
        return array_values($resolvedArguments);
235
    }
236
237
    /**
238
     * @throws InvalidConfigException
239
     */
240 40
    private function isIntegerIndexed(array $arguments): bool
241
    {
242 40
        $hasStringIndex = false;
243 40
        $hasIntegerIndex = false;
244
245 40
        foreach ($arguments as $index => $_argument) {
246 31
            if (is_string($index)) {
247 15
                $hasStringIndex = true;
248 15
                if ($hasIntegerIndex) {
249
                    /** @infection-ignore-all Mutation don't change behaviour, but degrade performance. */
250 15
                    break;
251
                }
252
            } else {
253 18
                $hasIntegerIndex = true;
254 18
                if ($hasStringIndex) {
255
                    /** @infection-ignore-all Mutation don't change behaviour, but degrade performance. */
256 1
                    break;
257
                }
258
            }
259
        }
260 40
        if ($hasIntegerIndex && $hasStringIndex) {
261 2
            throw new InvalidConfigException(
262 2
                'Arguments indexed both by name and by position are not allowed in the same array.'
263 2
            );
264
        }
265
266 39
        return $hasIntegerIndex;
267
    }
268
269
    /**
270
     * Create a new definition that is merged from this definition and another definition.
271
     *
272
     * @param ArrayDefinition $other Definition to merge with.
273
     *
274
     * @return self New definition that is merged from this definition and another definition.
275
     */
276 2
    public function merge(self $other): self
277
    {
278 2
        $new = clone $this;
279 2
        $new->class = $other->class;
280 2
        $new->constructorArguments = ArrayDefinitionHelper::mergeArguments($this->constructorArguments, $other->constructorArguments);
281
282 2
        $methodsAndProperties = $this->methodsAndProperties;
283 2
        foreach ($other->methodsAndProperties as $key => $item) {
284 1
            if ($item[0] === self::TYPE_PROPERTY) {
285 1
                $methodsAndProperties[$key] = $item;
286 1
            } elseif ($item[0] === self::TYPE_METHOD) {
287
                /** @psalm-suppress MixedArgument */
288 1
                $arguments = isset($methodsAndProperties[$key])
289 1
                    ? ArrayDefinitionHelper::mergeArguments($methodsAndProperties[$key][2], $item[2])
290 1
                    : $item[2];
291 1
                $methodsAndProperties[$key] = [$item[0], $item[1], $arguments];
292
            }
293
        }
294 2
        $new->methodsAndProperties = $methodsAndProperties;
295
296 2
        return $new;
297
    }
298
}
299