Passed
Push — master ( 898f07...643d29 )
by Jonathan
13:05
created

Container   B

Complexity

Total Complexity 47

Size/Duplication

Total Lines 402
Duplicated Lines 0 %

Test Coverage

Coverage 92.63%

Importance

Changes 4
Bugs 0 Features 0
Metric Value
wmc 47
eloc 96
dl 0
loc 402
ccs 88
cts 95
cp 0.9263
rs 8.64
c 4
b 0
f 0

22 Methods

Rating   Name   Duplication   Size   Complexity  
A factory() 0 7 3
A set() 0 3 1
A offsetExists() 0 3 1
A resolveClass() 0 5 1
B resolve() 0 31 8
A offsetGet() 0 3 1
A resolveArguments() 0 18 4
A offsetSet() 0 3 1
A setInstance() 0 3 1
A alias() 0 11 3
A get() 0 3 1
A getInstance() 0 3 1
A has() 0 5 3
A offsetUnset() 0 8 1
A make() 0 7 2
A __construct() 0 10 1
A resolveEntry() 0 16 3
A buildDependencies() 0 17 3
A isAlias() 0 3 1
A getExtenders() 0 3 1
A share() 0 6 2
A extend() 0 13 4

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