Passed
Pull Request — master (#85)
by Sergei
02:15
created

getMethodsAndPropertiesFromConfig()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 16
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 4

Importance

Changes 0
Metric Value
eloc 10
c 0
b 0
f 0
dl 0
loc 16
ccs 11
cts 11
cp 1
rs 9.9332
cc 4
nc 4
nop 1
crap 4
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Factory\Definition;
6
7
use Psr\Container\ContainerInterface;
8
use Yiisoft\Factory\Exception\InvalidConfigException;
9
use Yiisoft\Factory\Exception\NotInstantiableException;
10
11
use function array_key_exists;
12
use function is_string;
13
14
/**
15
 * Builds object by array config
16
 *
17
 * @psalm-type MethodOrPropertyItem = array{0:string,1:string,2:mixed}
18
 */
19
class ArrayDefinition implements DefinitionInterface
20
{
21
    public const CLASS_NAME = 'class';
22
    public const CONSTRUCTOR = '__construct()';
23
24
    public const FLAG_PROPERTY = 'property';
25
    public const FLAG_METHOD = 'method';
26
27
    /**
28
     * @psalm-var class-string
29
     */
30
    private string $class;
31
    private array $constructorArguments;
32
33
    /**
34
     * @psalm-var array<string, MethodOrPropertyItem>
35
     */
36
    private array $methodsAndProperties;
37
38
    /**
39
     * @psalm-param class-string $class
40
     * @psalm-param array<string, MethodOrPropertyItem> $methodsAndProperties
41
     */
42 44
    private function __construct(string $class, array $constructorArguments, array $methodsAndProperties)
43
    {
44 44
        $this->class = $class;
45 44
        $this->constructorArguments = $constructorArguments;
46 44
        $this->methodsAndProperties = $methodsAndProperties;
47 44
    }
48
49
    /**
50
     * @throws InvalidConfigException
51
     */
52 33
    public static function create(array $config): self
53
    {
54 33
        foreach ($config as $key => $_value) {
55 32
            if (!is_string($key)) {
56 1
                throw new InvalidConfigException('Invalid definition: keys should be string.');
57
            }
58
        }
59
60
        /** @psalm-var array<string, mixed> $config */
61
62 32
        $class = self::getClassFromConfig($config);
63 29
        $constructorArguments = self::getConstructorArgumentsFromConfig($config);
64 28
        $methodsAndProperties = self::getMethodsAndPropertiesFromConfig($config);
65
66 27
        if ($config !== []) {
67 2
            $key = array_key_first($config);
68 2
            throw new InvalidConfigException(
69 2
                sprintf(
70 2
                    'Invalid definition: key "%s" is not allowed. Did you mean "%s()" or "$%s"?',
71
                    $key,
72
                    $key,
73
                    $key
74
                )
75
            );
76
        }
77
78 25
        return new self($class, $constructorArguments, $methodsAndProperties);
79
    }
80
81
    /**
82
     * @psalm-param class-string $class
83
     * @psalm-param array<string, MethodOrPropertyItem> $methodsAndProperties
84
     */
85 20
    public static function fromPreparedData(string $class, array $constructorArguments = [], array $methodsAndProperties = []): self
86
    {
87 20
        return new self($class, $constructorArguments, $methodsAndProperties);
88
    }
89
90
    /**
91
     * @psalm-param array<string, mixed> $config
92
     *
93
     * @psalm-return class-string
94
     *
95
     * @throws InvalidConfigException
96
     */
97 32
    private static function getClassFromConfig(array &$config): string
98
    {
99 32
        if (!array_key_exists(self::CLASS_NAME, $config)) {
100 1
            throw new InvalidConfigException('Invalid definition: no class name specified.');
101
        }
102
103
        /** @var mixed */
104 31
        $class = $config[self::CLASS_NAME];
105 31
        unset($config[self::CLASS_NAME]);
106
107 31
        ArrayDefinitionValidator::validateClassName($class);
108
109
        /** @psalm-var class-string $class */
110
111 29
        return $class;
112
    }
113
114
    /**
115
     * @psalm-param array<string, mixed> $config
116
     *
117
     * @throws InvalidConfigException
118
     */
119 29
    private static function getConstructorArgumentsFromConfig(array &$config): array
120
    {
121 29
        if (!isset($config[self::CONSTRUCTOR])) {
122 20
            return [];
123
        }
124
125 9
        $arguments = $config[self::CONSTRUCTOR];
126 9
        unset($config[self::CONSTRUCTOR]);
127
128 9
        ArrayDefinitionValidator::validateConstructorArguments($arguments);
129
130 8
        return $arguments;
131
    }
132
133
    /**
134
     * @psalm-param array<string, mixed> $config
135
     *
136
     * @psalm-return array<string, MethodOrPropertyItem>
137
     *
138
     * @throws InvalidConfigException
139
     */
140 28
    private static function getMethodsAndPropertiesFromConfig(array &$config): array
141
    {
142 28
        $methodsAndProperties = [];
143
144 28
        foreach ($config as $key => $value) {
145 17
            if (substr($key, -2) === '()') {
146 13
                ArrayDefinitionValidator::validateMethodArguments($value);
147 12
                $methodsAndProperties[$key] = [self::FLAG_METHOD, $key, $value];
148 12
                unset($config[$key]);
149 6
            } elseif (strncmp($key, '$', 1) === 0) {
150 4
                $methodsAndProperties[$key] = [self::FLAG_PROPERTY, $key, $value];
151 4
                unset($config[$key]);
152
            }
153
        }
154
155 27
        return $methodsAndProperties;
156
    }
157
158
    /**
159
     * @psalm-return class-string
160
     */
161 43
    public function getClass(): string
162
    {
163 43
        return $this->class;
164
    }
165
166 40
    public function getConstructorArguments(): array
167
    {
168 40
        return $this->constructorArguments;
169
    }
170
171
    /**
172
     * @psalm-return array<string, MethodOrPropertyItem>
173
     */
174 37
    public function getMethodsAndProperties(): array
175
    {
176 37
        return $this->methodsAndProperties;
177
    }
178
179
    /**
180
     * @throws NotInstantiableException
181
     * @throws InvalidConfigException
182
     */
183 42
    public function resolve(ContainerInterface $container): object
184
    {
185 42
        return ArrayDefinitionBuilder::getInstance()->build($container, $this);
186
    }
187
188 4
    public function merge(self $other): self
189
    {
190 4
        $new = clone $this;
191 4
        $new->class = $other->class;
192 4
        $new->constructorArguments = $this->mergeArguments($this->constructorArguments, $other->constructorArguments);
193
194 4
        $methodsAndProperties = $this->methodsAndProperties;
195 4
        foreach ($other->methodsAndProperties as $key => $item) {
196 2
            if ($item[0] === self::FLAG_PROPERTY) {
197 1
                $methodsAndProperties[$key] = $item;
198 2
            } elseif ($item[0] === self::FLAG_METHOD) {
199
                /** @psalm-suppress MixedArgument */
200 2
                $methodsAndProperties[$key] = [
201 2
                    $item[0],
202 2
                    $item[1],
203 2
                    isset($methodsAndProperties[$key])
204 1
                        ? $this->mergeArguments($methodsAndProperties[$key][2], $item[2])
205 2
                        : $item[2],
206
                ];
207
            }
208
        }
209 4
        $new->methodsAndProperties = $methodsAndProperties;
210
211 4
        return $new;
212
    }
213
214 4
    private function mergeArguments(array $selfArguments, array $otherArguments): array
215
    {
216
        /** @var mixed $argument */
217 4
        foreach ($otherArguments as $name => $argument) {
218
            /** @var mixed */
219 2
            $selfArguments[$name] = $argument;
220
        }
221
222 4
        return $selfArguments;
223
    }
224
}
225