Passed
Push — master ( 6cfdb1...d9c2c2 )
by Sergei
02:31
created

ArrayDefinition::fromConfig()   A

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
        /** @var mixed $value */
95 42
        foreach ($config as $key => $value) {
96 42
            if ($key === self::CONSTRUCTOR) {
97 12
                continue;
98
            }
99
100
            /**
101
             * @infection-ignore-all Explode limit does not affect the result.
102
             *
103
             * @see \Yiisoft\Definitions\Tests\Unit\Helpers\DefinitionValidatorTest::testIncorrectMethodName()
104
             */
105 42
            if (count($methodArray = explode('()', $key, 2)) === 2) {
106 25
                $methodsAndProperties[$key] = [self::TYPE_METHOD, $methodArray[0], $value];
107 42
            } elseif (count($propertyArray = explode('$', $key)) === 2) {
108 4
                $methodsAndProperties[$key] = [self::TYPE_PROPERTY, $propertyArray[1], $value];
109
            }
110
        }
111
112 42
        return $methodsAndProperties;
113
    }
114
115
    /**
116
     * @psalm-return class-string
117
     */
118 3
    public function getClass(): string
119
    {
120 3
        return $this->class;
121
    }
122
123 43
    public function getConstructorArguments(): array
124
    {
125 43
        return $this->constructorArguments;
126
    }
127
128
    /**
129
     * @psalm-return array<string, MethodOrPropertyItem>
130
     */
131 41
    public function getMethodsAndProperties(): array
132
    {
133 41
        return $this->methodsAndProperties;
134
    }
135
136 40
    public function resolve(ContainerInterface $container): object
137
    {
138 40
        $class = $this->class;
139
140 40
        $resolvedConstructorArguments = $this->resolveFunctionArguments(
141 40
            $container,
142 40
            DefinitionExtractor::fromClassName($class),
143 40
            $this->getConstructorArguments()
144 40
        );
145
146
        /** @psalm-suppress MixedMethodCall */
147 38
        $object = new $class(...$resolvedConstructorArguments);
148
149 38
        foreach ($this->getMethodsAndProperties() as $item) {
150
            /** @var mixed $value */
151 26
            [$type, $name, $value] = $item;
152 26
            if ($type === self::TYPE_METHOD) {
153
                /** @var array $value */
154 24
                if (method_exists($object, $name)) {
155 23
                    $resolvedMethodArguments = $this->resolveFunctionArguments(
156 23
                        $container,
157 23
                        DefinitionExtractor::fromFunction(new ReflectionMethod($object, $name)),
158 23
                        $value,
159 23
                    );
160
                } else {
161 1
                    $resolvedMethodArguments = $value;
162
                }
163
                /** @var mixed $setter */
164 21
                $setter = call_user_func_array([$object, $name], $resolvedMethodArguments);
165 21
                if ($setter instanceof $object) {
166
                    /** @var object $object */
167 21
                    $object = $setter;
168
                }
169 3
            } elseif ($type === self::TYPE_PROPERTY) {
170 3
                $object->$name = DefinitionResolver::resolve($container, $this->referenceContainer, $value);
171
            }
172
        }
173
174 35
        return $object;
175
    }
176
177
    /**
178
     * @param array<string,ParameterDefinition> $dependencies
179
     *
180
     * @psalm-return list<mixed>
181
     */
182 40
    private function resolveFunctionArguments(
183
        ContainerInterface $container,
184
        array $dependencies,
185
        array $arguments
186
    ): array {
187 40
        $isIntegerIndexed = $this->isIntegerIndexed($arguments);
188 39
        $dependencyIndex = 0;
189 39
        $usedArguments = [];
190 39
        $variadicKey = null;
191
192 39
        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
                /** @infection-ignore-all Mutation don't change behaviour. Values of `$usedArguments` not used. */
200 29
                $usedArguments[$index] = 1;
201
            }
202 37
            $dependencyIndex++;
203
        }
204 39
        unset($value);
205
206 39
        if ($variadicKey !== null) {
207 30
            if (!$isIntegerIndexed && isset($arguments[$variadicKey])) {
208 7
                if ($arguments[$variadicKey] instanceof ReferenceInterface) {
209
                    /** @var mixed */
210 2
                    $arguments[$variadicKey] = DefinitionResolver::resolve(
211 2
                        $container,
212 2
                        $this->referenceContainer,
213 2
                        $arguments[$variadicKey]
214 2
                    );
215
                }
216
217 7
                if (is_array($arguments[$variadicKey])) {
218 4
                    unset($dependencies[$variadicKey]);
219 4
                    $dependencies += $arguments[$variadicKey];
220
                } else {
221 7
                    throw new InvalidArgumentException(
222 7
                        sprintf(
223 7
                            'Named argument for a variadic parameter should be an array, "%s" given.',
224 7
                            gettype($arguments[$variadicKey])
225 7
                        )
226 7
                    );
227
                }
228
            } else {
229
                /** @var mixed $value */
230 23
                foreach ($arguments as $index => $value) {
231 11
                    if (!isset($usedArguments[$index])) {
232 2
                        $dependencies[$index] = DefinitionResolver::ensureResolvable($value);
233
                    }
234
                }
235
            }
236
        }
237
238 38
        $resolvedArguments = DefinitionResolver::resolveArray($container, $this->referenceContainer, $dependencies);
239 38
        return array_values($resolvedArguments);
240
    }
241
242
    /**
243
     * @throws InvalidConfigException
244
     */
245 40
    private function isIntegerIndexed(array $arguments): bool
246
    {
247 40
        $hasStringIndex = false;
248 40
        $hasIntegerIndex = false;
249
250 40
        foreach ($arguments as $index => $_argument) {
251 31
            if (is_string($index)) {
252 15
                $hasStringIndex = true;
253 15
                if ($hasIntegerIndex) {
254
                    /** @infection-ignore-all Mutation don't change behaviour, but degrade performance. */
255 15
                    break;
256
                }
257
            } else {
258 18
                $hasIntegerIndex = true;
259 18
                if ($hasStringIndex) {
260
                    /** @infection-ignore-all Mutation don't change behaviour, but degrade performance. */
261 1
                    break;
262
                }
263
            }
264
        }
265 40
        if ($hasIntegerIndex && $hasStringIndex) {
266 2
            throw new InvalidConfigException(
267 2
                'Arguments indexed both by name and by position are not allowed in the same array.'
268 2
            );
269
        }
270
271 39
        return $hasIntegerIndex;
272
    }
273
274
    /**
275
     * Create a new definition that is merged from this definition and another definition.
276
     *
277
     * @param ArrayDefinition $other Definition to merge with.
278
     *
279
     * @return self New definition that is merged from this definition and another definition.
280
     */
281 2
    public function merge(self $other): self
282
    {
283 2
        $new = clone $this;
284 2
        $new->class = $other->class;
285 2
        $new->constructorArguments = ArrayDefinitionHelper::mergeArguments($this->constructorArguments, $other->constructorArguments);
286
287 2
        $methodsAndProperties = $this->methodsAndProperties;
288 2
        foreach ($other->methodsAndProperties as $key => $item) {
289 1
            if ($item[0] === self::TYPE_PROPERTY) {
290 1
                $methodsAndProperties[$key] = $item;
291 1
            } elseif ($item[0] === self::TYPE_METHOD) {
292
                /** @psalm-suppress MixedArgument, MixedAssignment */
293 1
                $arguments = isset($methodsAndProperties[$key])
294 1
                    ? ArrayDefinitionHelper::mergeArguments($methodsAndProperties[$key][2], $item[2])
295 1
                    : $item[2];
296 1
                $methodsAndProperties[$key] = [$item[0], $item[1], $arguments];
297
            }
298
        }
299 2
        $new->methodsAndProperties = $methodsAndProperties;
300
301 2
        return $new;
302
    }
303
}
304