Passed
Push — master ( e7ddb3...f8cb3b )
by Evgeniy
06:25
created

Container::getObjectFromReflection()   C

Complexity

Conditions 12
Paths 10

Size

Total Lines 44
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 12.667

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 6.9666
cc 12
nc 10
nop 1
crap 12.667

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<string, mixed> $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<string, mixed> $definitions
61
     * @psalm-suppress MixedAssignment
62
     */
63 29
    public function setMultiple(array $definitions): void
64
    {
65 29
        foreach ($definitions as $id => $definition) {
66 2
            $this->checkIdIsStringType($id);
67 2
            $this->set($id, $definition);
68
        }
69 29
    }
70
71
    /**
72
     * Gets instance by definition from the container by ID.
73
     *
74
     * @param string $id
75
     * @return mixed
76
     * @throws NotFoundException If not found definition in the container.
77
     * @throws ContainerException If unable to create instance.
78
     */
79 27
    public function get($id)
80
    {
81 27
        $this->checkIdIsStringType($id);
82
83 19
        if ($this->hasInstance($id)) {
84 6
            return $this->instances[$id];
85
        }
86
87 19
        $this->instances[$id] = $this->createInstance($id);
88 15
        return $this->instances[$id];
89
    }
90
91
    /**
92
     * Always gets a new instance by definition from the container by ID.
93
     *
94
     * @param string $id
95
     * @return mixed
96
     * @throws NotFoundException If not found definition in the container.
97
     * @throws ContainerException If unable to create instance.
98
     */
99 2
    public function getNew(string $id)
100
    {
101 2
        return $this->createInstance($id);
102
    }
103
104
    /**
105
     * Gets original definition from the container by ID.
106
     *
107
     * @param string $id
108
     * @return mixed
109
     * @throws NotFoundException If not found definition in the container.
110
     */
111 1
    public function getDefinition(string $id)
112
    {
113 1
        if ($this->has($id)) {
114 1
            return $this->definitions[$id];
115
        }
116
117
        throw new NotFoundException(sprintf('`%s` is not set in container.', $id));
118
    }
119
120
    /**
121
     * Returns 'true` if the dependency with this ID was sets, otherwise `false`.
122
     *
123
     * @param string $id
124
     * @return bool
125
     */
126 21
    public function has($id): bool
127
    {
128 21
        return array_key_exists($id, $this->definitions);
129
    }
130
131
    /**
132
     * Create instance by definition from the container by ID.
133
     *
134
     * @param string $id
135
     * @return mixed
136
     * @throws NotFoundException If not found definition in the container.
137
     * @throws ContainerException If unable to create instance.
138
     * @psalm-suppress MixedArgument
139
     */
140 20
    private function createInstance(string $id)
141
    {
142 20
        if (!$this->has($id)) {
143 12
            if ($this->isClassName($id)) {
144 9
                return $this->createObject($id);
145
            }
146
147 3
            throw new NotFoundException(sprintf('`%s` is not set in container and is not a class name.', $id));
148
        }
149
150 11
        if ($this->isClassName($this->definitions[$id])) {
151 3
            return $this->createObject($this->definitions[$id]);
152
        }
153
154 9
        if ($this->definitions[$id] instanceof Closure) {
155 7
            return $this->definitions[$id]($this);
156
        }
157
158 5
        return $this->definitions[$id];
159
    }
160
161
    /**
162
     * Create object by class name.
163
     *
164
     * @param string $className
165
     * @return object
166
     * @throws ContainerException If unable to create object.
167
     * @psalm-suppress ArgumentTypeCoercion
168
     */
169 9
    private function createObject(string $className): object
170
    {
171
        try {
172 9
            $reflection = new ReflectionClass($className);
173
        } catch (ReflectionException $e) {
174
            throw new ContainerException(sprintf('Unable to create object `%s`.', $className), 0, $e);
175
        }
176
177 9
        if (in_array(FactoryInterface::class, $reflection->getInterfaceNames())) {
178
            try {
179
                /** @var FactoryInterface $factory */
180 4
                $factory = $this->getObjectFromReflection($reflection);
181 4
                return $factory->create($this);
182
            } catch (ContainerException $e) {
183
                throw $e;
184
            } catch (Throwable $e) {
185
                throw new ContainerException(sprintf('Unable to create object `%s`.', $className), 0, $e);
186
            }
187
        }
188
189 9
        return $this->getObjectFromReflection($reflection);
190
    }
191
192
    /**
193
     * Create object from reflection.
194
     *
195
     * If the object has dependencies in the constructor, it tries to create them too.
196
     *
197
     * @param ReflectionClass $reflection
198
     * @return object
199
     * @throws ContainerException If unable to create object.
200
     * @psalm-suppress MixedAssignment
201
     */
202 9
    private function getObjectFromReflection(ReflectionClass $reflection): object
203
    {
204 9
        if (($constructor = $reflection->getConstructor()) === null) {
205 2
            return $reflection->newInstance();
206
        }
207
208 9
        $arguments = [];
209
210 9
        foreach ($constructor->getParameters() as $parameter) {
211 9
            if ($type = $parameter->getType()) {
212 9
                $typeName = $type->getName();
213
214 9
                if (!$type->isBuiltin() && ($this->has($typeName) || $this->isClassName($typeName))) {
215 5
                    $arguments[] = $this->get($typeName);
216 5
                    continue;
217
                }
218
219 9
                if ($type->isBuiltin() && $typeName === 'array' && !$parameter->isDefaultValueAvailable()) {
220 3
                    $arguments[] = [];
221 3
                    continue;
222
                }
223
            }
224
225 9
            if ($parameter->isDefaultValueAvailable()) {
226
                try {
227 8
                    $arguments[] = $parameter->getDefaultValue();
228 8
                    continue;
229
                } catch (ReflectionException $e) {
230
                    throw new ContainerException(sprintf(
231
                        'Unable to create object `%s`. Unable to get default value of constructor parameter: `%s`.',
232
                        $reflection->getName(),
233
                        $parameter->getName()
234
                    ));
235
                }
236
            }
237
238 1
            throw new ContainerException(sprintf(
239
                'Unable to create object `%s`. Unable to process a constructor parameter: `%s`.',
240 1
                $reflection->getName(),
241 1
                $parameter->getName()
242
            ));
243
        }
244
245 8
        return $reflection->newInstanceArgs($arguments);
246
    }
247
248
    /**
249
     * Returns `true` if the container can return an instance for this ID, otherwise `false`.
250
     *
251
     * @param string $id
252
     * @return bool
253
     */
254 21
    private function hasInstance(string $id): bool
255
    {
256 21
        return array_key_exists($id, $this->instances);
257
    }
258
259
    /**
260
     * Returns `true` if `$className` is the class name, otherwise `false`.
261
     *
262
     * @param mixed $className
263
     * @return bool
264
     */
265 20
    private function isClassName($className): bool
266
    {
267 20
        return (is_string($className) && class_exists($className));
268
    }
269
270
    /**
271
     * @param mixed $id
272
     * @throws NotFoundException for not string types.
273
     */
274 27
    private function checkIdIsStringType($id): void
275
    {
276 27
        if (!is_string($id)) {
277 8
            throw new NotFoundException(sprintf(
278
                'Is not valid ID. Must be string type; received `%s`.',
279 8
                gettype($id)
280
            ));
281
        }
282 19
    }
283
}
284