Passed
Push — master ( 77ecb6...dcdb12 )
by Jonathan
04:12
created

Container   A

Complexity

Total Complexity 41

Size/Duplication

Total Lines 343
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 4
Bugs 0 Features 0
Metric Value
eloc 81
c 4
b 0
f 0
dl 0
loc 343
ccs 102
cts 102
cp 1
rs 9.1199
wmc 41

19 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 10 1
A set() 0 3 1
A setInstance() 0 3 1
A factory() 0 7 3
A offsetExists() 0 3 1
A resolveClass() 0 5 1
A offsetGet() 0 3 1
A resolveArguments() 0 15 4
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 resolveEntry() 0 16 3
B buildDependencies() 0 36 7
A share() 0 3 1
A resolve() 0 19 6
A alias() 0 7 2

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 117
    public function __construct(array $config = [])
58
    {
59 117
        $this->bindings = $config;
60 117
        $this->share = [];
61
62 117
        $self = $this;
63
        $this->share(ContainerInterface::class, static function () use ($self) {
64 24
            return $self;
65 117
        });
66 117
        $this->alias(ContainerInterface::class, Container::class);
67 117
    }
68
69
    /**
70
     * @param string $entry
71
     * @param string $alias
72
     *
73
     * @throws NotFoundContainerException
74
     *
75
     * @return void
76
     */
77 117
    public function alias($entry, $alias)
78
    {
79 117
        if (false === $this->has($entry)) {
80 3
            throw NotFoundContainerException::entryNotFound($entry);
81
        }
82
83 117
        $this->aliases[$alias] = $entry;
84 117
    }
85
86
    /**
87
     * Factory binding.
88
     *
89
     * @param string $id
90
     * @param callable|mixed $factory
91
     *
92
     * @return void
93
     */
94 60
    public function factory(string $id, $factory)
95
    {
96 60
        $this->bindings[$id] = is_callable($factory) ?
97 42
            ($factory instanceof Closure ?
98 42
                $factory :
99 42
                Closure::fromCallable($factory)) :
100 24
            $factory;
101 60
    }
102
103
    /**
104
     * {@inheritdoc}
105
     *
106
     * @throws ReflectionException
107
     */
108 90
    public function get($id)
109
    {
110 90
        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 117
    public function has($id)
125
    {
126 117
        return array_key_exists($id, $this->bindings) ||
127 117
                array_key_exists($id, $this->share) ||
128 117
                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 33
    public function set(string $id, $factory)
204
    {
205 33
        $this->factory($id, $factory);
206 33
    }
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 117
    public function share($id, Closure $factory)
225
    {
226 117
        $this->share[$id] = $factory;
227 117
    }
228
229
    /**
230
     * @param ReflectionParameter[] $params
231
     * @param array<string, mixed> $arguments
232
     *
233
     * @return array<int, string>
234
     */
235 84
    private function buildDependencies(array $params, array $arguments = [])
236
    {
237 84
        return array_map(
238
            function (ReflectionParameter $param) use ($arguments) {
239 48
                if (true === array_key_exists($param->getName(), $arguments)) {
240 6
                    return $arguments[$param->getName()];
241
                }
242
243
                /** @var ReflectionNamedType|null $type */
244 45
                $type = $param->getType();
245
246
                // in case we can't find type hint, we guess by variable name.
247
                // e.g.: $cache it will attempt resolve 'cache' from container.
248 45
                if (null === $type) {
249 9
                    if (true === $this->has($param->getName())) {
250 6
                        return $this->get($param->getName());
251
                    }
252
253 3
                    throw ContainerException::findType(null);
254
                }
255
256 39
                if (true === $type->isBuiltin()) {
257 15
                    if (true === $this->has($param->getName())) {
258 9
                        return $this->get($param->getName());
259
                    }
260
261 6
                    if (true === $type->allowsNull()) {
262 3
                        return null;
263
                    }
264
265 3
                    throw ContainerException::findType($type);
266
                }
267
268 24
                return $this->get($type->getName());
269 84
            },
270 28
            $params
271
        );
272
    }
273
274
    /**
275
     * @param string $id
276
     * @param array<string, mixed> $arguments
277
     *
278
     * @throws NotFoundContainerException
279
     * @throws ReflectionException
280
     *
281
     * @return mixed|object
282
     */
283 96
    private function resolve(string $id, array $arguments = [])
284
    {
285 96
        if (true === array_key_exists($id, $this->resolved)) {
286 12
            return $this->resolved[$id];
287
        }
288
289 96
        if (true === array_key_exists($id, $this->aliases)) {
290 12
            return $this->resolve($this->aliases[$id], $arguments);
291
        }
292
293 96
        if ((false === $this->has($id)) && (true === class_exists($id))) {
294 33
            return $this->resolveClass($id, $arguments);
295
        }
296
297 78
        if (true === $this->has($id)) {
298 75
            return $this->resolveEntry($id, $arguments);
299
        }
300
301 3
        throw NotFoundContainerException::entryNotFound($id);
302
    }
303
304
    /**
305
     * @param Reflector $reflection
306
     * @param array<string, mixed> $arguments
307
     *
308
     * @return array<int, mixed>
309
     */
310 84
    private function resolveArguments(Reflector $reflection, array $arguments = [])
311
    {
312 84
        $params = [];
313
314 84
        if ($reflection instanceof ReflectionClass) {
315 33
            if (null !== $constructor = $reflection->getConstructor()) {
316 30
                $params = $constructor->getParameters();
317
            }
318
        }
319
320 84
        if ($reflection instanceof ReflectionFunction) {
321 54
            $params = $reflection->getParameters();
322
        }
323
324 84
        return $this->buildDependencies($params, $arguments);
325
    }
326
327
    /**
328
     * @param class-string|object $id
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string|object at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string|object.
Loading history...
329
     * @param array<string, mixed> $arguments
330
     *
331
     * @throws ReflectionException
332
     *
333
     * @return object
334
     */
335 33
    private function resolveClass($id, array $arguments = [])
336
    {
337 33
        $reflection = new ReflectionClass($id);
338
339 33
        return $reflection->newInstanceArgs($this->resolveArguments($reflection, $arguments));
340
    }
341
342
    /**
343
     * @param string $id
344
     * @param array<string, mixed> $arguments
345
     *
346
     * @throws ReflectionException
347
     *
348
     * @return mixed
349
     */
350 75
    private function resolveEntry(string $id, array $arguments = [])
351
    {
352 75
        $get = $this->bindings[$id] ?? $this->share[$id];
353
354 75
        if ($get instanceof Closure) {
355 54
            $reflection = new ReflectionFunction($get);
356 54
            $value = $reflection->invokeArgs($this->resolveArguments($reflection, $arguments));
357
358 54
            if (true === array_key_exists($id, $this->share)) {
359 36
                $this->resolved[$id] = $value;
360
            }
361
362 54
            return $value;
363
        }
364
365 24
        return $get;
366
    }
367
}
368