Passed
Push — master ( b61ce9...2284a4 )
by Alexander
02:39
created

ArrayDefinition::isIntegerIndexed()   B

Complexity

Conditions 7
Paths 10

Size

Total Lines 25
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 7

Importance

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