Passed
Push — master ( 20a4f6...e8677a )
by Evgeniy
01:31
created

Container::getObjectFromReflection()   B

Complexity

Conditions 11
Paths 10

Size

Total Lines 44
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 11.5605

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 26
c 1
b 0
f 0
dl 0
loc 44
ccs 20
cts 24
cp 0.8333
rs 7.3166
cc 11
nc 10
nop 1
crap 11.5605

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 29
    public function __construct(array $definitions = [])
38
    {
39 29
        $this->setMultiple($definitions);
40 29
    }
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 29
    public function setMultiple(array $definitions): void
63
    {
64 29
        foreach ($definitions as $id => $definition) {
65 2
            $this->checkIdIsStringType($id);
66 2
            $this->set($id, $definition);
67
        }
68 29
    }
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 27
    public function get($id)
79
    {
80 27
        $this->checkIdIsStringType($id);
81
82 19
        if ($this->hasInstance($id)) {
83 6
            return $this->instances[$id];
84
        }
85
86 19
        $this->instances[$id] = $this->createInstance($id);
87 15
        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 21
    public function has($id): bool
126
    {
127 21
        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 20
    private function createInstance(string $id)
139
    {
140 20
        if (!$this->has($id)) {
141 12
            if ($this->isClassName($id)) {
142 9
                return $this->createObject($id);
143
            }
144
145 3
            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 9
    private function createObject(string $className): object
167
    {
168
        try {
169 9
            $reflection = new ReflectionClass($className);
170
        } catch (ReflectionException $e) {
171
            throw new ContainerException(sprintf('Unable to create object `%s`.', $className), 0, $e);
172
        }
173
174 9
        if (in_array(FactoryInterface::class, $reflection->getInterfaceNames())) {
175
            try {
176
                /** @var FactoryInterface $factory */
177 4
                $factory = $this->getObjectFromReflection($reflection);
178 4
                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 9
        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 9
    private function getObjectFromReflection(ReflectionClass $reflection): object
199
    {
200 9
        if (($constructor = $reflection->getConstructor()) === null) {
201 2
            return $reflection->newInstance();
202
        }
203
204 9
        $arguments = [];
205
206 9
        foreach ($constructor->getParameters() as $parameter) {
207 9
            if ($type = $parameter->getType()) {
208 9
                $typeName = $type->getName();
209
210 9
                if (!$type->isBuiltin() && ($this->has($typeName) || $this->isClassName($typeName))) {
211 5
                    $arguments[] = $this->get($typeName);
212 5
                    continue;
213
                }
214
215 9
                if ($type->isBuiltin() && $typeName === 'array') {
216 3
                    $arguments[] = [];
217 3
                    continue;
218
                }
219
            }
220
221 9
            if ($parameter->isDefaultValueAvailable()) {
222
                try {
223 8
                    $arguments[] = $parameter->getDefaultValue();
224 8
                    continue;
225
                } catch (ReflectionException $e) {
226
                    throw new ContainerException(sprintf(
227
                        'Unable to create object `%s`. Unable to get default value of constructor parameter: `%s`.',
228
                        $reflection->getName(),
229
                        $parameter->getName()
230
                    ));
231
                }
232
            }
233
234 1
            throw new ContainerException(sprintf(
235
                'Unable to create object `%s`. Unable to process a constructor parameter: `%s`.',
236 1
                $reflection->getName(),
237 1
                $parameter->getName()
238
            ));
239
        }
240
241 8
        return $reflection->newInstanceArgs($arguments);
242
    }
243
244
    /**
245
     * Returns `true` if the container can return an instance for this ID, otherwise `false`.
246
     *
247
     * @param string $id
248
     * @return bool
249
     */
250 21
    private function hasInstance(string $id): bool
251
    {
252 21
        return array_key_exists($id, $this->instances);
253
    }
254
255
    /**
256
     * Returns `true` if `$className` is the class name, otherwise `false`.
257
     *
258
     * @param mixed $className
259
     * @return bool
260
     */
261 20
    private function isClassName($className): bool
262
    {
263 20
        return (is_string($className) && class_exists($className));
264
    }
265
266
    /**
267
     * @param mixed $id
268
     * @throws NotFoundException for not string types.
269
     */
270 27
    private function checkIdIsStringType($id): void
271
    {
272 27
        if (!is_string($id)) {
273 8
            throw new NotFoundException(sprintf(
274
                'Is not valid ID. Must be string type; received `%s`.',
275 8
                gettype($id)
276
            ));
277
        }
278 19
    }
279
}
280