Passed
Push — master ( d1953e...f13ec0 )
by Sergei
02:34
created

ArrayDefinition::withReferenceContainer()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

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