Passed
Pull Request — master (#85)
by Sergei
03:10
created

ArrayDefinition::fromConfig()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 19
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 2

Importance

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