Passed
Push — master ( 240d06...3e81fd )
by Jonathan
03:51
created

Container   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 350
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 4
Bugs 0 Features 0
Metric Value
wmc 43
eloc 85
c 4
b 0
f 0
dl 0
loc 350
ccs 106
cts 106
cp 1
rs 8.96

19 Methods

Rating   Name   Duplication   Size   Complexity  
A resolveClass() 0 5 1
A resolveArguments() 0 15 4
A __construct() 0 10 1
A resolveEntry() 0 16 3
A set() 0 3 1
A setInstance() 0 3 1
A factory() 0 7 3
A offsetExists() 0 3 1
A offsetGet() 0 3 1
A offsetSet() 0 3 1
A getInstance() 0 3 1
A has() 0 5 3
A get() 0 3 1
A offsetUnset() 0 7 1
A make() 0 7 2
A alias() 0 7 2
A share() 0 6 2
A resolve() 0 19 6
B buildDependencies() 0 40 8

How to fix   Complexity   

Complex Class

Complex classes like Container often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Container, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Gravatalonga\Container;
6
7
use ArrayAccess;
8
use Closure;
9
use Psr\Container\ContainerInterface;
10
use ReflectionClass;
11
use ReflectionException;
12
use ReflectionFunction;
13
use ReflectionNamedType;
14
use ReflectionParameter;
15
use Reflector;
16
17
use function array_key_exists;
18
use function is_callable;
19
20
/**
21
 * Class Container.
22
 */
23
class Container implements ArrayAccess, ContainerInterface
24
{
25
    /**
26
     * @var ContainerInterface
27
     */
28
    protected static $instance;
29
30
    /**
31
     * @var array<string, string>
32
     */
33
    private $aliases = [];
34
35
    /**
36
     * @var array<string, mixed>
37
     */
38
    private $bindings;
39
40
    /**
41
     * @var array<string, mixed>
42
     */
43
    private $resolved = [];
44
45
    /**
46
     * @var array<string, mixed>
47
     */
48
    private $share;
49
50
    /**
51
     * Container constructor.
52
     *
53
     * @param array<string, mixed> $config
54
     *
55
     * @throws NotFoundContainerException
56
     */
57 132
    public function __construct(array $config = [])
58
    {
59 132
        $this->bindings = $config;
60 132
        $this->share = [];
61
62 132
        $self = $this;
63
        $this->share(ContainerInterface::class, static function () use ($self) {
64 24
            return $self;
65 132
        });
66 132
        $this->alias(ContainerInterface::class, Container::class);
67 132
    }
68
69
    /**
70
     * @param string $entry
71
     * @param string $alias
72
     *
73
     * @throws NotFoundContainerException
74
     *
75
     * @return void
76
     */
77 132
    public function alias($entry, $alias)
78
    {
79 132
        if (false === $this->has($entry)) {
80 3
            throw NotFoundContainerException::entryNotFound($entry);
81
        }
82
83 132
        $this->aliases[$alias] = $entry;
84 132
    }
85
86
    /**
87
     * Factory binding.
88
     *
89
     * @param string $id
90
     * @param callable|mixed $factory
91
     *
92
     * @return void
93
     */
94 63
    public function factory(string $id, $factory)
95
    {
96 63
        $this->bindings[$id] = is_callable($factory) ?
97 45
            ($factory instanceof Closure ?
98 45
                $factory :
99 45
                Closure::fromCallable($factory)) :
100 24
            $factory;
101 63
    }
102
103
    /**
104
     * {@inheritdoc}
105
     *
106
     * @throws ReflectionException
107
     */
108 105
    public function get($id)
109
    {
110 105
        return $this->resolve($id, []);
111
    }
112
113
    /**
114
     * @return ContainerInterface
115
     */
116 3
    public static function getInstance()
117
    {
118 3
        return self::$instance;
119
    }
120
121
    /**
122
     * {@inheritdoc}
123
     */
124 132
    public function has($id)
125
    {
126 132
        return array_key_exists($id, $this->bindings) ||
127 132
            array_key_exists($id, $this->share) ||
128 132
            array_key_exists($id, $this->aliases);
129
    }
130
131
    /**
132
     * @param string $id
133
     * @param array<string, mixed> $arguments
134
     *
135
     * @throws NotFoundContainerException
136
     * @throws ContainerException|ReflectionException
137
     *
138
     * @return mixed|object
139
     */
140 18
    public function make($id, array $arguments = [])
141
    {
142 18
        if (array_key_exists($id, $this->share)) {
143 3
            throw ContainerException::shareOnMake($id);
144
        }
145
146 15
        return $this->resolve($id, $arguments);
147
    }
148
149
    /**
150
     * @param string $offset
151
     *
152
     * @return bool
153
     */
154 6
    public function offsetExists($offset)
155
    {
156 6
        return $this->has($offset);
157
    }
158
159
    /**
160
     * @param string $offset
161
     *
162
     * @throws ReflectionException
163
     *
164
     * @return mixed
165
     */
166 6
    public function offsetGet($offset)
167
    {
168 6
        return $this->get($offset);
169
    }
170
171
    /**
172
     * @param string $offset
173
     * @param mixed $value
174
     */
175 9
    public function offsetSet($offset, $value): void
176
    {
177 9
        $this->factory($offset, $value);
178 9
    }
179
180
    /**
181
     * @param string $offset
182
     *
183
     * @return void
184
     */
185 3
    public function offsetUnset($offset)
186
    {
187
        unset(
188 3
            $this->bindings[$offset],
189 3
            $this->share[$offset],
190 3
            $this->resolved[$offset],
191 3
            $this->aliases[$offset]
192
        );
193 3
    }
194
195
    /**
196
     * Alias for Factory method.
197
     *
198
     * @param string $id
199
     * @param mixed $factory
200
     *
201
     * @return void
202
     */
203 36
    public function set(string $id, $factory)
204
    {
205 36
        $this->factory($id, $factory);
206 36
    }
207
208
    /**
209
     * @param ContainerInterface $container
210
     */
211 3
    public static function setInstance(ContainerInterface $container): void
212
    {
213 3
        self::$instance = $container;
214 3
    }
215
216
    /**
217
     * Share rather resolve as factory.
218
     *
219
     * @param string $id
220
     * @param Closure $factory
221
     *
222
     * @return void
223
     */
224 132
    public function share($id, Closure $factory)
225
    {
226 132
        if (true === array_key_exists($id, $this->resolved)) {
227 3
            unset($this->resolved[$id]);
228
        }
229 132
        $this->share[$id] = $factory;
230 132
    }
231
232
    /**
233
     * @param ReflectionParameter[] $params
234
     * @param array<string, mixed> $arguments
235
     *
236
     * @return array<int, string>
237
     */
238 99
    private function buildDependencies(array $params, array $arguments = [])
239
    {
240 99
        return array_map(
241
            function (ReflectionParameter $param) use ($arguments) {
242 57
                if (true === array_key_exists($param->getName(), $arguments)) {
243 6
                    return $arguments[$param->getName()];
244
                }
245
246
                /** @var ReflectionNamedType|null $type */
247 54
                $type = $param->getType();
248
249
                // in case we can't find type hint, we guess by variable name.
250
                // e.g.: $cache it will attempt resolve 'cache' from container.
251 54
                if (null === $type) {
252 18
                    if (true === $this->has($param->getName())) {
253 9
                        return $this->get($param->getName());
254
                    }
255
256 9
                    if (false === $param->isOptional()) {
257 3
                        throw ContainerException::findType(null);
258
                    }
259
260 6
                    return $param->getDefaultValue();
261
                }
262
263 39
                if (true === $type->isBuiltin()) {
264 15
                    if (true === $this->has($param->getName())) {
265 9
                        return $this->get($param->getName());
266
                    }
267
268 6
                    if (true === $type->allowsNull()) {
269 3
                        return null;
270
                    }
271
272 3
                    throw ContainerException::findType($type);
273
                }
274
275 24
                return $this->get($type->getName());
276 99
            },
277 33
            $params
278
        );
279
    }
280
281
    /**
282
     * @param string $id
283
     * @param array<string, mixed> $arguments
284
     *
285
     * @throws NotFoundContainerException
286
     * @throws ReflectionException
287
     *
288
     * @return mixed|object
289
     */
290 111
    private function resolve(string $id, array $arguments = [])
291
    {
292 111
        if (true === array_key_exists($id, $this->resolved)) {
293 12
            return $this->resolved[$id];
294
        }
295
296 111
        if (true === array_key_exists($id, $this->aliases)) {
297 12
            return $this->resolve($this->aliases[$id], $arguments);
298
        }
299
300 111
        if ((false === $this->has($id)) && (true === class_exists($id))) {
301 42
            return $this->resolveClass($id, $arguments);
302
        }
303
304 87
        if (true === $this->has($id)) {
305 84
            return $this->resolveEntry($id, $arguments);
306
        }
307
308 3
        throw NotFoundContainerException::entryNotFound($id);
309
    }
310
311
    /**
312
     * @param Reflector $reflection
313
     * @param array<string, mixed> $arguments
314
     *
315
     * @return array<int, mixed>
316
     */
317 99
    private function resolveArguments(Reflector $reflection, array $arguments = [])
318
    {
319 99
        $params = [];
320
321 99
        if ($reflection instanceof ReflectionClass) {
322 42
            if (null !== $constructor = $reflection->getConstructor()) {
323 39
                $params = $constructor->getParameters();
324
            }
325
        }
326
327 99
        if ($reflection instanceof ReflectionFunction) {
328 63
            $params = $reflection->getParameters();
329
        }
330
331 99
        return $this->buildDependencies($params, $arguments);
332
    }
333
334
    /**
335
     * @param mixed $id
336
     * @param array<string, mixed> $arguments
337
     *
338
     * @throws ReflectionException
339
     *
340
     * @return object
341
     */
342 42
    private function resolveClass($id, array $arguments = [])
343
    {
344 42
        $reflection = new ReflectionClass($id);
345
346 42
        return $reflection->newInstanceArgs($this->resolveArguments($reflection, $arguments));
347
    }
348
349
    /**
350
     * @param string $id
351
     * @param array<string, mixed> $arguments
352
     *
353
     * @throws ReflectionException
354
     *
355
     * @return mixed
356
     */
357 84
    private function resolveEntry(string $id, array $arguments = [])
358
    {
359 84
        $get = $this->bindings[$id] ?? $this->share[$id];
360
361 84
        if ($get instanceof Closure) {
362 63
            $reflection = new ReflectionFunction($get);
363 63
            $value = $reflection->invokeArgs($this->resolveArguments($reflection, $arguments));
364
365 63
            if (true === array_key_exists($id, $this->share)) {
366 42
                $this->resolved[$id] = $value;
367
            }
368
369 63
            return $value;
370
        }
371
372 24
        return $get;
373
    }
374
}
375