Test Setup Failed
Push — master ( 5f1e56...422ac6 )
by Jonathan
16:06
created

Container   B

Complexity

Total Complexity 50

Size/Duplication

Total Lines 411
Duplicated Lines 0 %

Test Coverage

Coverage 99.18%

Importance

Changes 6
Bugs 0 Features 0
Metric Value
wmc 50
eloc 100
c 6
b 0
f 0
dl 0
loc 411
ccs 121
cts 122
cp 0.9918
rs 8.4

23 Methods

Rating   Name   Duplication   Size   Complexity  
A alias() 0 11 3
A __construct() 0 10 1
A extend() 0 13 4
A factory() 0 3 1
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 get() 0 3 1
A getInstance() 0 3 1
A has() 0 5 3
A offsetUnset() 0 8 1
A prepareEntry() 0 7 3
A make() 0 7 2
A resolveEntry() 0 16 3
A buildDependencies() 0 26 5
A isAlias() 0 3 1
A getExtenders() 0 3 1
A share() 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 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
     */
57
    private $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
        $this->share(ContainerInterface::class, static function () use ($self) {
73 33
            return $self;
74 171
        });
75 171
        $this->alias(ContainerInterface::class, Container::class);
76 171
    }
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 (true === $this->isAlias($entry)) {
89 3
            throw NotFoundContainerException::entryNotFound($entry);
90
        }
91
92 171
        if (false === $this->has($entry)) {
93 3
            throw NotFoundContainerException::entryNotFound($entry);
94
        }
95
96 171
        $this->aliases[$alias] = $entry;
97 171
    }
98
99
    /**
100
     * @param string $id
101
     *
102
     * @throws NotFoundContainerException
103
     *
104
     * @return void
105
     */
106 21
    public function extend($id, callable $factory)
107
    {
108 21
        if (!$this->has($id)) {
109 3
            throw NotFoundContainerException::entryNotFound($id);
110
        }
111
112 18
        $factory = $factory instanceof Closure ? $factory : Closure::fromCallable($factory);
113
114 18
        if (true === array_key_exists($id, $this->resolved)) {
115 3
            unset($this->resolved[$id]);
116
        }
117
118 18
        $this->extended[$id][] = $factory;
119 18
    }
120
121
    /**
122
     * Factory binding.
123
     *
124
     * @param callable|mixed $factory
125
     *
126
     * @return void
127
     */
128 90
    public function factory(string $id, $factory)
129
    {
130 90
        $this->bindings[$id] = $this->prepareEntry($factory);
131 66
    }
132 66
133 66
    /**
134 30
     * {@inheritdoc}
135 90
     *
136
     * @throws ReflectionException
137
     */
138
    public function get($id)
139
    {
140
        return $this->resolve($id, []);
141
    }
142 126
143
    /**
144 126
     * @return ContainerInterface
145
     */
146
    public static function getInstance()
147
    {
148
        return self::$instance;
149
    }
150 3
151
    /**
152 3
     * {@inheritdoc}
153
     */
154
    public function has($id)
155
    {
156
        return array_key_exists($id, $this->bindings)
157
            || array_key_exists($id, $this->share)
158 171
            || array_key_exists($id, $this->aliases);
159
    }
160 171
161 171
    /**
162 171
     * @param string $id
163
     *
164
     * @return bool
165
     */
166
    public function isAlias($id)
167
    {
168
        return true === array_key_exists($id, $this->aliases);
169
    }
170 171
171
    /**
172 171
     * @param string $id
173
     * @param array<string, mixed> $arguments
174
     *
175
     * @throws NotFoundContainerException
176
     * @throws ContainerException|ReflectionException
177
     *
178
     * @return mixed|object
179
     */
180
    public function make($id, array $arguments = [])
181
    {
182
        if (array_key_exists($id, $this->share)) {
183
            throw ContainerException::shareOnMake($id);
184 24
        }
185
186 24
        return $this->resolve($id, $arguments);
187 3
    }
188
189
    /**
190 21
     * @param string $offset
191
     *
192
     * @return bool
193
     */
194
    public function offsetExists($offset)
195
    {
196
        return $this->has($offset);
197
    }
198 6
199
    /**
200 6
     * @param string $offset
201
     *
202
     * @throws ReflectionException
203
     *
204
     * @return mixed
205
     */
206
    public function offsetGet($offset)
207
    {
208
        return $this->get($offset);
209
    }
210 6
211
    /**
212 6
     * @param string $offset
213
     * @param mixed $value
214
     */
215
    public function offsetSet($offset, $value): void
216
    {
217
        $this->factory($offset, $value);
218
    }
219 9
220
    /**
221 9
     * @param string $offset
222 9
     *
223
     * @return void
224
     */
225
    public function offsetUnset($offset)
226
    {
227
        unset(
228
            $this->bindings[$offset],
229 6
            $this->share[$offset],
230
            $this->resolved[$offset],
231
            $this->aliases[$offset],
232 6
            $this->extended[$offset]
233 6
        );
234 6
    }
235 6
236 6
    /**
237
     * Alias for Factory method.
238 6
     *
239
     * @param mixed $factory
240
     *
241
     * @return void
242
     */
243
    public function set(string $id, $factory)
244
    {
245
        $this->factory($id, $factory);
246
    }
247 45
248
    public static function setInstance(ContainerInterface $container): void
249 45
    {
250 45
        self::$instance = $container;
251
    }
252 3
253
    /**
254 3
     * Share rather resolve as factory.
255 3
     *
256
     * @param string $id
257
     * @param mixed $factory
258
     *
259
     * @return void
260
     */
261
    public function share($id, $factory)
262
    {
263
        if (true === array_key_exists($id, $this->resolved)) {
264 171
            unset($this->resolved[$id]);
265
        }
266 171
267 3
        $this->share[$id] = $this->prepareEntry($factory);
268
    }
269 171
270 171
    /**
271
     * @param ReflectionParameter[] $params
272
     * @param array<string, mixed> $arguments
273
     *
274
     * @return array<int, string>
275
     */
276
    private function buildDependencies(array $params, array $arguments = [])
277
    {
278 123
        return array_map(
279
            function (ReflectionParameter $param) use ($arguments) {
280 123
                if (true === array_key_exists($param->getName(), $this->entriesBeingResolved)) {
281
                    throw ContainerException::circularDependency();
282 69
                }
283 3
284
                $this->entriesBeingResolved[$param->getName()] = true;
285
286 69
                if (true === array_key_exists($param->getName(), $arguments)) {
287
                    return $arguments[$param->getName()];
288 69
                }
289 9
290
                $type = $param->getType();
291
292 66
                // https://github.com/phpstan/phpstan/issues/1133
293
                // @phpstan-ignore-next-line
294 66
                if (null !== $type && true === 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

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