Passed
Pull Request — master (#27)
by Sergei
02:24
created

getMethodsAndPropertiesFromConfig()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 18
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 5

Importance

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