Passed
Push — master ( b1d671...613e8b )
by Evgeniy
01:22
created

Container::setMultiple()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 3
nc 2
nop 1
dl 0
loc 5
ccs 4
cts 4
cp 1
crap 2
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Devanych\Di;
6
7
use Closure;
8
use Devanych\Di\Exception\NotFoundException;
9
use Devanych\Di\Exception\ContainerException;
10
use Psr\Container\ContainerInterface;
11
use ReflectionClass;
12
use ReflectionException;
13
use Throwable;
14
15
use function array_key_exists;
16
use function class_exists;
17
use function gettype;
18
use function in_array;
19
use function is_string;
20
use function sprintf;
21
22
final class Container implements ContainerInterface
23
{
24
    /**
25
     * @var array<string, mixed>
26
     */
27
    private array $definitions = [];
28
29
    /**
30
     * @var array<string, mixed>
31
     */
32
    private array $instances = [];
33
34
    /**
35
     * @param array $definitions
36
     */
37 26
    public function __construct(array $definitions = [])
38
    {
39 26
        $this->setMultiple($definitions);
40 26
    }
41
42
    /**
43
     * Sets definition to the container.
44
     *
45
     * @param string $id
46
     * @param mixed $definition
47
     */
48 12
    public function set(string $id, $definition): void
49
    {
50 12
        if ($this->hasInstance($id)) {
51 1
            unset($this->instances[$id]);
52
        }
53
54 12
        $this->definitions[$id] = $definition;
55 12
    }
56
57
    /**
58
     * Sets multiple definitions at once.
59
     *
60
     * @param array $definitions
61
     */
62 26
    public function setMultiple(array $definitions): void
63
    {
64 26
        foreach ($definitions as $id => $definition) {
65 2
            $this->checkIdIsStringType($id);
66 2
            $this->set($id, $definition);
67
        }
68 26
    }
69
70
    /**
71
     * Gets instance by definition from the container by ID.
72
     *
73
     * @param string $id
74
     * @return mixed
75
     * @throws NotFoundException If not found definition in the container.
76
     * @throws ContainerException If unable to create instance.
77
     */
78 24
    public function get($id)
79
    {
80 24
        $this->checkIdIsStringType($id);
81
82 16
        if ($this->hasInstance($id)) {
83 6
            return $this->instances[$id];
84
        }
85
86 16
        $this->instances[$id] = $this->createInstance($id);
87 13
        return $this->instances[$id];
88
    }
89
90
    /**
91
     * Always gets a new instance by definition from the container by ID.
92
     *
93
     * @param string $id
94
     * @return mixed
95
     * @throws NotFoundException If not found definition in the container.
96
     * @throws ContainerException If unable to create instance.
97
     */
98 2
    public function getNew(string $id)
99
    {
100 2
        return $this->createInstance($id);
101
    }
102
103
    /**
104
     * Gets original definition from the container by ID.
105
     *
106
     * @param string $id
107
     * @return mixed
108
     * @throws NotFoundException If not found definition in the container.
109
     */
110 1
    public function getDefinition(string $id)
111
    {
112 1
        if ($this->has($id)) {
113 1
            return $this->definitions[$id];
114
        }
115
116
        throw new NotFoundException(sprintf('`%s` is not set in container.', $id));
117
    }
118
119
    /**
120
     * Returns 'true` if the dependency with this ID was sets, otherwise `false`.
121
     *
122
     * @param string $id
123
     * @return bool
124
     */
125 18
    public function has($id): bool
126
    {
127 18
        return array_key_exists($id, $this->definitions);
128
    }
129
130
    /**
131
     * Create instance by definition from the container by ID.
132
     *
133
     * @param string $id
134
     * @return mixed
135
     * @throws NotFoundException If not found definition in the container.
136
     * @throws ContainerException If unable to create instance.
137
     */
138 17
    private function createInstance(string $id)
139
    {
140 17
        if (!$this->has($id)) {
141 9
            if ($this->isClassName($id)) {
142 7
                return $this->createObject($id);
143
            }
144
145 2
            throw new NotFoundException(sprintf('`%s` is not set in container and is not a class name.', $id));
146
        }
147
148 11
        if ($this->isClassName($this->definitions[$id])) {
149 3
            return $this->createObject($this->definitions[$id]);
150
        }
151
152 9
        if ($this->definitions[$id] instanceof Closure) {
153 7
            return $this->definitions[$id]($this);
154
        }
155
156 5
        return $this->definitions[$id];
157
    }
158
159
    /**
160
     * Create object by class name.
161
     *
162
     * @param string $className
163
     * @return object
164
     * @throws ContainerException If unable to create object.
165
     */
166 7
    private function createObject(string $className): object
167
    {
168
        try {
169 7
            $reflection = new ReflectionClass($className);
170
        } catch (ReflectionException $e) {
171
            throw new ContainerException(sprintf('Unable to create object `%s`.', $className), 0, $e);
172
        }
173
174 7
        if (in_array(FactoryInterface::class, $reflection->getInterfaceNames())) {
175
            try {
176
                /** @var FactoryInterface $factory */
177 3
                $factory = $this->getObjectFromReflection($reflection);
178 3
                return $factory->create($this);
179
            } catch (ContainerException $e) {
180
                throw $e;
181
            } catch (Throwable $e) {
182
                throw new ContainerException(sprintf('Unable to create object `%s`.', $className), 0, $e);
183
            }
184
        }
185
186 7
        return $this->getObjectFromReflection($reflection);
187
    }
188
189
    /**
190
     * Create object from reflection.
191
     *
192
     * If the object has dependencies in the constructor, it tries to create them too.
193
     *
194
     * @param ReflectionClass $reflection
195
     * @return object
196
     * @throws ContainerException If unable to create object.
197
     */
198 7
    private function getObjectFromReflection(ReflectionClass $reflection): object
199
    {
200 7
        if (($constructor = $reflection->getConstructor()) === null) {
201 2
            return $reflection->newInstance();
202
        }
203
204 7
        $arguments = [];
205
206 7
        foreach ($constructor->getParameters() as $parameter) {
207 7
            $type = $parameter->getType();
208
209 7
            if ($type && !$type->isBuiltin()) {
210 3
                $arguments[] = $this->get($type->getName());
211 3
                continue;
212
            }
213
214 7
            if ($type && $type->isBuiltin() && $type->getName() === 'array') {
215 1
                $arguments[] = [];
216 1
                continue;
217
            }
218
219 7
            if ($parameter->isDefaultValueAvailable()) {
220
                try {
221 6
                    $arguments[] = $parameter->getDefaultValue();
222 6
                    continue;
223
                } catch (ReflectionException $e) {
224
                    throw new ContainerException(sprintf(
225
                        'Unable to create object `%s`. Unable to get default value of constructor parameter: `%s`.',
226
                        $reflection->getName(),
227
                        $parameter->getName()
228
                    ));
229
                }
230
            }
231
232 1
            throw new ContainerException(sprintf(
233
                'Unable to create object `%s`. Unable to process a constructor parameter: `%s`.',
234 1
                $reflection->getName(),
235 1
                $parameter->getName()
236
            ));
237
        }
238
239 6
        return $reflection->newInstanceArgs($arguments);
240
    }
241
242
    /**
243
     * Returns `true` if the container can return an instance for this ID, otherwise `false`.
244
     *
245
     * @param string $id
246
     * @return bool
247
     */
248 18
    private function hasInstance(string $id): bool
249
    {
250 18
        return array_key_exists($id, $this->instances);
251
    }
252
253
    /**
254
     * Returns `true` if `$className` is the class name, otherwise `false`.
255
     *
256
     * @param mixed $className
257
     * @return bool
258
     */
259 17
    private function isClassName($className): bool
260
    {
261 17
        return (is_string($className) && class_exists($className));
262
    }
263
264
    /**
265
     * @param mixed $id
266
     * @throws NotFoundException for not string types.
267
     */
268 24
    private function checkIdIsStringType($id): void
269
    {
270 24
        if (!is_string($id)) {
271 8
            throw new NotFoundException(sprintf(
272
                'Is not valid ID. Must be string type; received `%s`.',
273 8
                gettype($id)
274
            ));
275
        }
276 16
    }
277
}
278