Container   B
last analyzed

Complexity

Total Complexity 49

Size/Duplication

Total Lines 414
Duplicated Lines 0 %

Test Coverage

Coverage 98.28%

Importance

Changes 11
Bugs 0 Features 0
Metric Value
eloc 105
c 11
b 0
f 0
dl 0
loc 414
ccs 114
cts 116
cp 0.9828
rs 8.48
wmc 49

23 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 10 1
A factory() 0 3 1
A set() 0 3 1
A offsetExists() 0 4 1
A offsetGet() 0 4 1
A offsetSet() 0 4 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 9 1
A prepareEntry() 0 7 3
A make() 0 7 2
A buildDependencies() 0 26 5
A isAlias() 0 3 1
A getExtenders() 0 3 1
A share() 0 7 2
A extend() 0 13 3
A resolveClass() 0 5 1
B resolve() 0 31 8
A resolveArguments() 0 16 4
A resolveEntry() 0 16 3

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 array $aliases = [];
33
34
    /**
35
     * @var array<string, mixed>
36
     */
37
    private array $bindings;
38
39
    /**
40
     * @var array<string, boolean>
41
     */
42
    private array $entriesBeingResolved = [];
43
44
    /**
45
     * @var array <string, mixed>
46
     */
47
    private array $extended = [];
48
49
    /**
50
     * @var array<string, mixed>
51
     */
52
    private array $resolved = [];
53
54
    /**
55
     * @var array<string, mixed>
56
     */
57
    private array $share;
58
59
    /**
60
     * Container constructor.
61
     *
62
     * @param array<string, mixed> $config
63
     *
64
     * @throws NotFoundContainerException
65
     */
66 171
    public function __construct(array $config = [])
67
    {
68 171
        $this->bindings = $config;
69 171
        $this->share = [];
70
71 171
        $self = $this;
72 171
        $this->share(ContainerInterface::class, static function () use ($self) {
73 33
            return $self;
74
        });
75 171
        $this->alias(ContainerInterface::class, Container::class);
76
    }
77
78
    /**
79
     * @param string $entry
80
     * @param string $alias
81
     *
82
     * @throws NotFoundContainerException
83
     *
84
     * @return void
85
     */
86 171
    public function alias($entry, $alias)
87
    {
88 171
        if ($this->isAlias($entry)) {
89 3
            throw NotFoundContainerException::entryNotFound($entry);
90
        }
91
92 171
        if (!$this->has($entry)) {
93 3
            throw NotFoundContainerException::entryNotFound($entry);
94
        }
95
96 171
        $this->aliases[$alias] = $entry;
97
    }
98
99
    /**
100
     * @param string $id
101
     * @param callable|mixed $factory
102
     *
103
     * @throws NotFoundContainerException
104
     *
105
     * @return void
106
     */
107 24
    public function extend($id, $factory)
108
    {
109 24
        if (!$this->has($id)) {
110 3
            throw NotFoundContainerException::entryNotFound($id);
111
        }
112
113 21
        $factory = $this->prepareEntry($factory);
114
115 21
        if (array_key_exists($id, $this->resolved)) {
116 3
            unset($this->resolved[$id]);
117
        }
118
119 21
        $this->extended[$id][] = $factory;
120
    }
121
122
    /**
123
     * Factory binding.
124
     *
125
     * @param callable|mixed $factory
126
     *
127
     * @return void
128
     */
129 90
    public function factory(string $id, $factory)
130
    {
131 90
        $this->bindings[$id] = $this->prepareEntry($factory);
132
    }
133
134
    /**
135
     * {@inheritdoc}
136
     *
137
     * @throws ReflectionException
138
     */
139 129
    public function get(string $id)
140
    {
141 129
        return $this->resolve($id, []);
142
    }
143
144
    /**
145
     * @return ContainerInterface
146
     */
147 3
    public static function getInstance()
148
    {
149 3
        return self::$instance;
150
    }
151
152
    /**
153
     * {@inheritdoc}
154
     */
155 171
    public function has(string $id): bool
156
    {
157 171
        return array_key_exists($id, $this->bindings)
158 171
            || array_key_exists($id, $this->share)
159 171
            || array_key_exists($id, $this->aliases);
160
    }
161
162
    /**
163
     * @param string $id
164
     *
165
     * @return bool
166
     */
167 171
    public function isAlias($id)
168
    {
169 171
        return array_key_exists($id, $this->aliases);
170
    }
171
172
    /**
173
     * @param string $id
174
     * @param array<string, mixed> $arguments
175
     *
176
     * @throws NotFoundContainerException
177
     * @throws ContainerException|ReflectionException
178
     *
179
     * @return mixed|object
180
     */
181 24
    public function make($id, array $arguments = [])
182
    {
183 24
        if (array_key_exists($id, $this->share)) {
184 3
            throw ContainerException::shareOnMake($id);
185
        }
186
187 21
        return $this->resolve($id, $arguments);
188
    }
189
190
    /**
191
     * @param string $offset
192
     *
193
     * @return bool
194
     */
195 6
    #[\ReturnTypeWillChange]
196
    public function offsetExists($offset)
197
    {
198 6
        return $this->has($offset);
199
    }
200
201
    /**
202
     * @param string $offset
203
     *
204
     * @throws ReflectionException
205
     *
206
     * @return mixed
207
     */
208 6
    #[\ReturnTypeWillChange]
209
    public function offsetGet($offset)
210
    {
211 6
        return $this->get($offset);
212
    }
213
214
    /**
215
     * @param string $offset
216
     * @param mixed $value
217
     */
218 9
    #[\ReturnTypeWillChange]
219
    public function offsetSet($offset, $value): void
220
    {
221 9
        $this->factory($offset, $value);
222
    }
223
224
    /**
225
     * @param string $offset
226
     *
227
     * @return void
228
     */
229 6
    #[\ReturnTypeWillChange]
230
    public function offsetUnset($offset)
231
    {
232
        unset(
233 6
            $this->bindings[$offset],
234 6
            $this->share[$offset],
235 6
            $this->resolved[$offset],
236 6
            $this->aliases[$offset],
237 6
            $this->extended[$offset]
238
        );
239
    }
240
241
    /**
242
     * Alias for Factory method.
243
     *
244
     * @param mixed $factory
245
     *
246
     * @return void
247
     */
248 45
    public function set(string $id, $factory)
249
    {
250 45
        $this->factory($id, $factory);
251
    }
252
253 3
    public static function setInstance(ContainerInterface $container): void
254
    {
255 3
        self::$instance = $container;
256
    }
257
258
    /**
259
     * Share rather resolve as factory.
260
     *
261
     * @param string $id
262
     * @param mixed $factory
263
     *
264
     * @return void
265
     */
266 171
    public function share($id, $factory)
267
    {
268 171
        if (array_key_exists($id, $this->resolved)) {
269 3
            unset($this->resolved[$id]);
270
        }
271
272 171
        $this->share[$id] = $this->prepareEntry($factory);
273
    }
274
275
    /**
276
     * @param ReflectionParameter[] $params
277
     * @param array<string, mixed> $arguments
278
     *
279
     * @return array<int, string>
280
     */
281 123
    private function buildDependencies(array $params, array $arguments = [])
282
    {
283 123
        return array_map(
284 123
            function (ReflectionParameter $param) use ($arguments) {
285 69
                if (array_key_exists($param->getName(), $this->entriesBeingResolved)) {
286 3
                    throw ContainerException::circularDependency();
287
                }
288
289 69
                $this->entriesBeingResolved[$param->getName()] = true;
290
291 69
                if (array_key_exists($param->getName(), $arguments)) {
292 9
                    return $arguments[$param->getName()];
293
                }
294
295 66
                $type = $param->getType();
296
297
                // https://github.com/phpstan/phpstan/issues/1133
298
                // @phpstan-ignore-next-line
299 66
                if (null !== $type && array_key_exists($type->getName(), $arguments)) {
0 ignored issues
show
Bug introduced by
The method getName() does not exist on ReflectionType. It seems like you code against a sub-type of ReflectionType such as ReflectionNamedType. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

299
                if (null !== $type && array_key_exists($type->/** @scrutinizer ignore-call */ getName(), $arguments)) {
Loading history...
300
                    // @phpstan-ignore-next-line
301 3
                    return $arguments[$type->getName()];
302
                }
303
304 63
                return $this->autoWiringArguments($param);
305
            },
306
            $params
307
        );
308
    }
309
310
    /**
311
     * Get all extenders for particular entry id.
312
     *
313
     * @return array|mixed
314
     */
315 129
    private function getExtenders(string $id)
316
    {
317 129
        return $this->extended[$id] ?? [];
318
    }
319
320
    /**
321
     * @param mixed $factory
322
     *
323
     * @return callable|Closure
324
     */
325 171
    private function prepareEntry($factory)
326
    {
327 171
        return is_callable($factory) ?
328 171
            ($factory instanceof Closure ?
329 171
                $factory :
330 171
                Closure::fromCallable($factory)) :
331 171
            $factory;
332
    }
333
334
    /**
335
     * @param array<string, mixed> $arguments
336
     *
337
     * @throws NotFoundContainerException
338
     * @throws ReflectionException
339
     *
340
     * @return mixed|object
341
     */
342 138
    private function resolve(string $id, array $arguments = [])
343
    {
344 138
        if (array_key_exists($id, $this->resolved)) {
345 21
            return $this->resolved[$id];
346
        }
347
348 138
        if (array_key_exists($id, $this->aliases)) {
349 12
            return $this->resolve($this->aliases[$id], $arguments);
350
        }
351
352 138
        if ((!$this->has($id)) && (class_exists($id))) {
353 48
            return $this->resolveClass($id, $arguments);
354
        }
355 39
356
        if ($this->has($id)) {
357
            $get = $this->resolveEntry($id, $arguments);
358
359
            foreach ($this->getExtenders($id) as $extend) {
360 39
                if (is_callable($extend)) {
361
                    $get = $extend($this, $get);
362
                    $this->resolved[$id] = $get;
363 111
                    continue;
364 108
                }
365
                $get = $extend;
366 108
                $this->resolved[$id] = $get;
367 18
            }
368 15
369 15
            return $get;
370 15
        }
371
372 3
        throw NotFoundContainerException::entryNotFound($id);
373 3
    }
374
375
    /**
376 108
     * @param array<string, mixed> $arguments
377
     *
378
     * @return array<int, mixed>
379 3
     */
380
    private function resolveArguments(Reflector $reflection, array $arguments = [])
381
    {
382
        $params = [];
383
384
        if ($reflection instanceof ReflectionClass && null !== $constructor = $reflection->getConstructor()) {
385
            $params = $constructor->getParameters();
386
        }
387 123
388
        if ($reflection instanceof ReflectionFunction) {
389 123
            $params = $reflection->getParameters();
390
        }
391 123
392 45
        $value = $this->buildDependencies($params, $arguments);
393
        $this->entriesBeingResolved = [];
394
395 123
        return $value;
396 84
    }
397
398
    /**
399 123
     * @param mixed $id
400 117
     * @param array<string, mixed> $arguments
401
     *
402 117
     * @throws ReflectionException
403
     *
404
     * @return object
405
     */
406
    private function resolveClass($id, array $arguments = [])
407
    {
408
        $reflection = new ReflectionClass($id);
409
410
        return $reflection->newInstanceArgs($this->resolveArguments($reflection, $arguments));
411
    }
412
413 48
    /**
414
     * @param array<string, mixed> $arguments
415 48
     *
416
     * @throws ReflectionException
417 48
     *
418
     * @return mixed
419
     */
420
    private function resolveEntry(string $id, array $arguments = [])
421
    {
422
        $get = $this->bindings[$id] ?? $this->share[$id];
423
424
        if ($get instanceof Closure) {
425
            $reflection = new ReflectionFunction($get);
426
            $value = $reflection->invokeArgs($this->resolveArguments($reflection, $arguments));
427 108
428
            if (array_key_exists($id, $this->share)) {
429 108
                $this->resolved[$id] = $value;
430
            }
431 108
432 84
            return $value;
433 84
        }
434
435 84
        return $get;
436
    }
437
}