Container   F
last analyzed

Complexity

Total Complexity 77

Size/Duplication

Total Lines 465
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 167
c 1
b 0
f 0
dl 0
loc 465
rs 2.24
wmc 77

22 Methods

Rating   Name   Duplication   Size   Complexity  
B createInstance() 0 47 9
B createService() 0 34 6
A autowireArguments() 0 6 2
A parseBindMethod() 0 3 1
A getServiceMethods() 0 6 1
B make() 0 25 7
A isCreated() 0 8 2
A getService() 0 10 3
A __construct() 0 6 1
A has() 0 3 1
A getServiceType() 0 20 5
A resolveWiring() 0 11 3
A resolveMethod() 0 10 5
B addService() 0 41 8
A removeService() 0 4 1
B getByType() 0 38 8
A getParameter() 0 3 1
A runScope() 0 24 5
A hasService() 0 5 2
A hasMethodBinding() 0 3 1
A parseServiceType() 0 12 3
A get() 0 6 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
/*
6
 * This file is part of Biurad opensource projects.
7
 *
8
 * PHP version 7.2 and above required
9
 *
10
 * @author    Divine Niiquaye Ibok <[email protected]>
11
 * @copyright 2019 Biurad Group (https://biurad.com/)
12
 * @license   https://opensource.org/licenses/BSD-3-Clause License
13
 *
14
 * For the full copyright and license information, please view the LICENSE
15
 * file that was distributed with this source code.
16
 */
17
18
namespace Biurad\DependencyInjection;
19
20
use Biurad\DependencyInjection\Exceptions\ContainerResolutionException;
21
use Biurad\DependencyInjection\Exceptions\NotFoundServiceException;
22
use Closure;
23
use Nette\DI\Container as NetteContainer;
24
use Nette\DI\Helpers;
25
use Nette\DI\MissingServiceException;
26
use Nette\UnexpectedValueException;
27
use Nette\Utils\Callback;
28
use Nette\Utils\Reflection;
29
use Nette\Utils\Validators;
30
use ReflectionClass;
31
use ReflectionException;
32
use ReflectionFunction;
33
use ReflectionFunctionAbstract;
34
use ReflectionMethod;
35
use ReflectionType;
36
use Throwable;
37
38
/**
39
 * The dependency injection container default implementation.
40
 *
41
 * Auto-wiring container: declarative singletons, contextual injections, outer delegation and
42
 * ability to lazy wire.
43
 *
44
 * Container does not support setter injections, private properties and etc. Normally it will work
45
 * with classes only to be as much invisible as possible.
46
 *
47
 * @author Divine Niiquaye Ibok <[email protected]>
48
 */
49
class Container extends NetteContainer implements FactoryInterface
50
{
51
    /** @var object[] service name => instance */
52
    private $instances = [];
53
54
    /** @var array circular reference detector */
55
    private $creating;
56
57
    /** @var array */
58
    private $methods;
59
60
    /**
61
     * Provide psr container interface in order to proxy get and has requests.
62
     *
63
     * @param array $params
64
     */
65
    public function __construct(array $params = [])
66
    {
67
        $this->parameters = $params;
68
        $this->methods    = $this->getServiceMethods(\get_class_methods($this));
69
70
        parent::__construct($params);
71
    }
72
73
    /**
74
     * {@inheritdoc}
75
     */
76
    public function has($id)
77
    {
78
        return $this->hasService($id);
79
    }
80
81
    /**
82
     * {@inheritdoc}
83
     *
84
     * @return object
85
     */
86
    public function get($id)
87
    {
88
        try {
89
            return $this->make($id);
90
        } catch (Throwable $e) {
91
            throw new NotFoundServiceException(\sprintf('Service [%s] is not found in container', $id), 0, $e);
92
        }
93
    }
94
95
    /**
96
     * {@inheritdoc}
97
     */
98
    public function getParameter(string $name)
99
    {
100
        return Builder::arrayGet($this->parameters, $name);
101
    }
102
103
    /**
104
     * {@inheritdoc}
105
     */
106
    public function addService(string $name, $service)
107
    {
108
        $service = null === $service ? $name : $service;
109
        $name    = $this->aliases[$name] ?? $name;
110
111
        // Create an instancer
112
        if (\is_string($service) && \class_exists($service)) {
113
            $service = $this->createInstance($service);
114
        }
115
116
        // Report exception if name already exists.
117
        if (isset($this->instances[$name])) {
118
            throw new ContainerResolutionException("Service [$name] already exists.");
119
        }
120
121
        if (!\is_object($service)) {
122
            throw new ContainerResolutionException(
123
                \sprintf("Service '%s' must be a object, %s given.", $name, \gettype($name))
124
            );
125
        }
126
127
        // Resolving the closure of the service to return it's type hint or class.
128
        $type = $this->parseServiceType($service);
129
130
        // Resolving wiring so we could call the service parent classes and interfaces.
131
        if (!$service instanceof Closure) {
132
            $this->resolveWiring($name, $type);
133
        }
134
135
        // Resolving the method calls.
136
        $this->resolveMethod($name, self::getMethodName($name), $type);
137
138
        if ($service instanceof Closure) {
139
            // Get the method binding for the given method.
140
            $this->methods[self::getMethodName($name)] = $service;
141
            $this->types[$name] = $type;
142
        } else {
143
            $this->instances[$name] = $service;
144
        }
145
146
        return $this;
147
    }
148
149
    /**
150
     * {@inheritdoc}
151
     */
152
    public function removeService(string $name): void
153
    {
154
        $name = $this->aliases[$name] ?? $name;
155
        unset($this->instances[$name]);
156
    }
157
158
    /**
159
     * {@inheritdoc}
160
     */
161
    public function getService(string $name)
162
    {
163
        if (!isset($this->instances[$name])) {
164
            if (isset($this->aliases[$name])) {
165
                return $this->getService($this->aliases[$name]);
166
            }
167
            $this->instances[$name] = $this->createService($name);
168
        }
169
170
        return $this->instances[$name];
171
    }
172
173
    /**
174
     * {@inheritdoc}
175
     */
176
    public function getServiceType(string $name): string
177
    {
178
        $method = self::getMethodName($name);
179
180
        if (isset($this->aliases[$name])) {
181
            return $this->getServiceType($this->aliases[$name]);
182
        }
183
184
        if (isset($this->types[$name])) {
185
            return $this->types[$name];
186
        }
187
188
        if ($this->hasMethodBinding($method)) {
189
            /** @var ReflectionMethod $type */
190
            $type = $this->parseBindMethod([$this, $method]);
191
192
            return $type ? $type->getName() : '';
0 ignored issues
show
introduced by
$type is of type ReflectionMethod, thus it always evaluated to true.
Loading history...
193
        }
194
195
        throw new MissingServiceException("Service '$name' not found.");
196
    }
197
198
    /**
199
     * {@inheritdoc}
200
     */
201
    public function hasService(string $name): bool
202
    {
203
        $name = $this->aliases[$name] ?? $name;
204
205
        return $this->hasMethodBinding(self::getMethodName($name)) || isset($this->instances[$name]);
206
    }
207
208
    /**
209
     * {@inheritdoc}
210
     */
211
    public function isCreated(string $name): bool
212
    {
213
        if (!$this->hasService($name)) {
214
            throw new MissingServiceException("Service '$name' not found.");
215
        }
216
        $name = $this->aliases[$name] ?? $name;
217
218
        return isset($this->instances[$name]);
219
    }
220
221
    /**
222
     * {@inheritdoc}
223
     */
224
    public function createService(string $name, array $args = [])
225
    {
226
        $name   = $this->aliases[$name] ?? $name;
227
        $method = self::getMethodName($name);
228
        $cb     = $this->methods[$method] ?? null;
229
230
        if (isset($this->creating[$name])) {
231
            throw new ContainerResolutionException(
232
                \sprintf(
233
                    'Circular reference detected for services: %s.',
234
                    \implode(', ', \array_keys($this->creating))
235
                )
236
            );
237
        }
238
239
        if ($cb === null) {
240
            throw new MissingServiceException("Service '$name' not found.");
241
        }
242
243
        try {
244
            $this->creating[$name] = true;
245
            $service = $cb instanceof Closure ? $cb(...$args) : $this->$method(...$args);
246
        } finally {
247
            unset($this->creating[$name]);
248
        }
249
250
        if (!\is_object($service)) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $service does not seem to be defined for all execution paths leading up to this point.
Loading history...
251
            throw new UnexpectedValueException(
252
                "Unable to create service '$name', value returned by " .
253
                ($cb instanceof Closure ? 'closure' : "method $method()") . ' is not object.'
254
            );
255
        }
256
257
        return $service;
258
    }
259
260
    /**
261
     * {@inheritdoc}
262
     */
263
    public function getByType(string $type, bool $throw = true)
264
    {
265
        $type = Helpers::normalizeClass($type);
266
267
        if (!empty($this->wiring[$type][0])) {
268
            if (\count($names = $this->wiring[$type][0]) === 1) {
269
                return $this->getService($names[0]);
270
            }
271
            \natsort($names);
272
273
            throw new MissingServiceException(
274
                "Multiple services of type $type found: " . \implode(', ', $names) . '.'
275
            );
276
        }
277
278
        if ($throw) {
279
            if (!\class_exists($type) && !\interface_exists($type)) {
280
                throw new MissingServiceException(
281
                    "Service of type '$type' not found. Check class name because it cannot be found."
282
                );
283
            }
284
285
            foreach ($this->methods as $method => $foo) {
286
                $methodType = $this->parseBindMethod([\get_class($this), $method])->getName();
287
288
                if (\is_a($methodType, $type, true)) {
289
                    throw new MissingServiceException(
290
                        "Service of type $type is not autowired or is missing in di › export › types."
291
                    );
292
                }
293
            }
294
295
            throw new MissingServiceException(
296
                "Service of type $type not found. Did you add it to configuration file?"
297
            );
298
        }
299
300
        return null;
301
    }
302
303
    /**
304
     * {@inheritdoc}
305
     */
306
    public function createInstance(string $class, array $args = [])
307
    {
308
        try {
309
            $reflector = new ReflectionClass($class);
310
        } catch (ReflectionException $e) {
311
            throw new ContainerResolutionException("Targeted class [$class] does not exist.", 0, $e);
312
        }
313
314
        // If the type is not instantiable, the developer is attempting to resolve
315
        // an abstract type such as an Interface or Abstract Class and there is
316
        // no binding registered for the abstractions so we need to bail out.
317
        if (!$reflector->isInstantiable()) {
318
            throw new ContainerResolutionException("Targeted [$class] is not instantiable");
319
        }
320
321
        $constructor = $reflector->getConstructor();
322
323
        // If there are no constructors, that means there are no dependencies then
324
        // we can just resolve the instances of the objects right away, without
325
        // resolving any other types or dependencies out of these containers.
326
        if (null === $constructor) {
327
            return $reflector->newInstance();
328
        }
329
330
        // Once we have all the constructor's parameters we can create each of the
331
        // dependency instances and then use the reflection instances to make a
332
        // new instance of this class, injecting the created dependencies in.
333
        // this will be handled in a recursive way...
334
        try {
335
            $instances = $this->autowireArguments($constructor, $args);
336
        } catch (MissingServiceException $e) {
337
            // Resolve default pararamters on class, if paramter was not given and
338
            // default paramter exists, why not let's use it.
339
            foreach ($constructor->getParameters() as $position => $parameter) {
340
                try {
341
                    if (!(isset($args[$position]) || isset($args[$parameter->name]))) {
342
                        $args[$position] = Reflection::getParameterDefaultValue($parameter);
343
                    }
344
                } catch (ReflectionException $e) {
345
                    continue;
346
                }
347
            }
348
349
            return $this->createInstance($class, $args);
350
        }
351
352
        return $reflector->newInstanceArgs($instances);
353
    }
354
355
    /**
356
     * {@inheritdoc}
357
     */
358
    public function runScope(array $bindings, callable $scope)
359
    {
360
        $cleanup = $previous = [];
361
362
        foreach ($bindings as $alias => $resolver) {
363
            if ($this->has($alias)) {
364
                $previous[$alias] = $this->get($alias);
365
366
                continue;
367
            }
368
369
            $cleanup[] = $alias;
370
            $this->addService($alias, $resolver);
371
        }
372
373
        try {
374
            return $scope(...[&$this]);
375
        } finally {
376
            foreach (\array_reverse($previous) as $alias => $resolver) {
377
                $this->instances[$alias] = $resolver;
378
            }
379
380
            foreach ($cleanup as $alias) {
381
                $this->removeService($alias);
382
            }
383
        }
384
    }
385
386
    /**
387
     * {@inheritdoc}
388
     */
389
    public function make(string $alias, ...$parameters)
390
    {
391
        try {
392
            return $this->getService($alias);
393
        } catch (MissingServiceException $e) {
394
            // Allow passing arrays or individual lists of dependencies
395
            if (isset($parameters[0]) && \is_array($parameters[0]) && \count($parameters) === 1) {
396
                $parameters = \array_shift($parameters);
397
            }
398
399
            //Automatically create instance
400
            if (Validators::isType($alias)) {
401
                try {
402
                    $instance = $this->getByType($alias);
403
                } catch (MissingServiceException $e) {
404
                    $instance = $this->createInstance($alias, $parameters);
405
                }
406
407
                $this->callInjects($instance); // Call injectors on the new class instance.
408
409
                return $instance;
410
            }
411
        }
412
413
        throw new NotFoundServiceException(\sprintf('Service [%s] is not found in container', $alias));
414
    }
415
416
    /**
417
     * {@inheritdoc}
418
     */
419
    public function hasMethodBinding($method): bool
420
    {
421
        return isset($this->methods[$method]);
422
    }
423
424
    /**
425
     * Resolve callable methods.
426
     *
427
     * @param string $abstract
428
     * @param string $concrete
429
     * @param string $type
430
     */
431
    private function resolveMethod(string $abstract, string $concrete, string $type): void
432
    {
433
        if (!$this->hasMethodBinding($concrete)) {
434
            $this->types[$abstract] = $type;
435
        }
436
437
        if (($expectedType = $this->getServiceType($abstract)) && !\is_a($type, $expectedType, true)) {
438
            throw new ContainerResolutionException(
439
                "Service '$abstract' must be instance of $expectedType, " .
440
                ($type ? "$type given." : 'add typehint to closure.')
441
            );
442
        }
443
    }
444
445
    /**
446
     * Resolve wiring classes + interfaces.
447
     *
448
     * @param string $name
449
     * @param mixed  $class
450
     */
451
    private function resolveWiring(string $name, $class): void
452
    {
453
        $all = [];
454
455
        foreach (\class_parents($class) + \class_implements($class) + [$class] as $class) {
0 ignored issues
show
introduced by
$class is overwriting one of the parameters of this function.
Loading history...
456
            $all[$class][] = $name;
457
        }
458
459
        foreach ($all as $class => $names) {
0 ignored issues
show
introduced by
$class is overwriting one of the parameters of this function.
Loading history...
460
            $this->wiring[$class] = \array_filter([
461
                \array_diff($names, $this->findByType($class) ?? [], $this->findByTag($class) ?? []),
462
            ]);
463
        }
464
    }
465
466
    /**
467
     * Get the method to be bounded.
468
     *
469
     * @param array|string $method
470
     *
471
     * @return null|ReflectionType
472
     */
473
    private function parseBindMethod($method): ?ReflectionType
474
    {
475
        return Callback::toReflection($method)->getReturnType();
476
    }
477
478
    private function autowireArguments(ReflectionFunctionAbstract $function, array $args = []): array
479
    {
480
        return Resolver::autowireArguments($function, $args, function (string $type, bool $single) {
481
            return $single
482
                ? $this->getByType($type)
483
                : \array_map([$this, 'get'], $this->findAutowired($type));
484
        });
485
    }
486
487
    /**
488
     * Get the Closure or class to be used when building a type.
489
     *
490
     * @param mixed $abstract
491
     *
492
     * @return string
493
     */
494
    private function parseServiceType($abstract): string
495
    {
496
        if ($abstract instanceof Closure) {
497
            /** @var ReflectionFunction $tmp */
498
            if ($tmp = $this->parseBindMethod($abstract)) {
0 ignored issues
show
Bug introduced by
$abstract of type Closure is incompatible with the type array|string expected by parameter $method of Biurad\DependencyInjecti...iner::parseBindMethod(). ( Ignorable by Annotation )

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

498
            if ($tmp = $this->parseBindMethod(/** @scrutinizer ignore-type */ $abstract)) {
Loading history...
499
                return $tmp->getName();
500
            }
501
502
            return '';
503
        }
504
505
        return \get_class($abstract);
506
    }
507
508
    private function getServiceMethods(?array $methods): array
509
    {
510
        return \array_flip(\array_filter(
0 ignored issues
show
Bug Best Practice introduced by
The expression return array_flip(array_...on(...) { /* ... */ })) could return the type null which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
511
            $methods,
512
            function ($s) {
513
                return \preg_match('#^createService.#', $s);
514
            }
515
        ));
516
    }
517
}
518