Passed
Push — master ( c8ae20...20f443 )
by Evgeniy
22:51 queued 12s
created

Container::createObject()   C

Complexity

Conditions 13
Paths 11

Size

Total Lines 50
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 21
CRAP Score 14.854

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 30
c 3
b 0
f 0
dl 0
loc 50
ccs 21
cts 27
cp 0.7778
rs 6.6166
cc 13
nc 11
nop 1
crap 14.854

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