Container::resolve()   D
last analyzed

Complexity

Conditions 10
Paths 100

Size

Total Lines 33
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 23
CRAP Score 10

Importance

Changes 0
Metric Value
cc 10
eloc 23
nc 100
nop 1
dl 0
loc 33
ccs 23
cts 23
cp 1
crap 10
rs 4.8196
c 0
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
namespace Wandu\DI;
3
4
use Doctrine\Common\Annotations\Reader;
5
use InvalidArgumentException;
6
use Psr\Container\ContainerExceptionInterface;
7
use Psr\Container\ContainerInterface as PsrContainerInterface;
8
use ReflectionClass;
9
use ReflectionException;
10
use ReflectionFunctionAbstract;
11
use Wandu\DI\Annotations\Alias;
12
use Wandu\DI\Annotations\Assign;
13
use Wandu\DI\Annotations\AssignValue;
14
use Wandu\DI\Annotations\Factory;
15
use Wandu\DI\Annotations\Wire;
16
use Wandu\DI\Annotations\WireValue;
17
use Wandu\DI\Exception\CannotChangeException;
18
use Wandu\DI\Exception\CannotFindParameterException;
19
use Wandu\DI\Exception\CannotResolveException;
20
use Wandu\DI\Exception\NullReferenceException;
21
use Wandu\DI\Exception\RequirePackageException;
22
use Wandu\Reflection\ReflectionCallable;
23
24
class Container implements ContainerInterface
25
{
26
    /** @var \Wandu\DI\ContainerInterface */
27
    public static $instance;
28
    
29
    /** @var \Wandu\DI\Descriptor[] */
30
    protected $descriptors = [];
31
32
    /** @var array */
33
    protected $instances = [];
34
35
    /** @var array */
36
    protected $classes = [];
37
    
38
    /** @var array */
39
    protected $closures = [];
40
    
41
    /** @var array */
42
    protected $aliases = [];
43
44
    /** @var \Wandu\DI\ServiceProviderInterface[] */
45
    protected $providers = [];
46
47
    /** @var bool */
48
    protected $isBooted = false;
49
50 92
    public function __construct(array $options = [])
51
    {
52 92
        $this->instances = [
53 92
            Container::class => $this,
54 92
            ContainerInterface::class => $this,
55 92
            PsrContainerInterface::class => $this,
56 92
            'container' => $this,
57
        ];
58 92
        $this->descriptors[Container::class]
59 92
            = $this->descriptors[ContainerInterface::class]
60 92
            = $this->descriptors[PsrContainerInterface::class]
61 92
            = $this->descriptors['container']
62 92
            = (new Descriptor())->freeze();
63 92
    }
64
65 3
    public function __clone()
66
    {
67 3
        $this->instances[Container::class] = $this;
68 3
        $this->instances[ContainerInterface::class] = $this;
69 3
        $this->instances[PsrContainerInterface::class] = $this;
70 3
        $this->instances['container'] = $this;
71 3
    }
72
73
    /**
74
     * @return \Wandu\DI\ContainerInterface
75
     */
76
    public function setAsGlobal()
77
    {
78
        $instance = static::$instance;
79
        static::$instance = $this;
80
        return $instance;
81
    }
82
83
    /**
84
     * {@inheritdoc}
85
     */
86
    public function __call($name, array $arguments)
87
    {
88
        return $this->call($this->get($name), $arguments);
0 ignored issues
show
Documentation introduced by
$this->get($name) is of type *, but the function expects a callable.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
89
    }
90
91
    /**
92
     * {@inheritdoc}
93
     */
94 3
    public function offsetExists($name)
95
    {
96 3
        return $this->has($name) && $this->get($name) !== null;
97
    }
98
99
    /**
100
     * {@inheritdoc}
101
     */
102 21
    public function offsetGet($name)
103
    {
104 21
        return $this->get($name);
105
    }
106
107
    /**
108
     * {@inheritdoc}
109
     */
110 1
    public function offsetSet($name, $value)
111
    {
112 1
        $this->instance($name, $value);
113 1
    }
114
115
    /**
116
     * {@inheritdoc}
117
     */
118 1
    public function offsetUnset($name)
119
    {
120 1
        $this->destroy($name);
121
    }
122
123
    /**
124
     * {@inheritdoc}
125
     */
126 56
    public function get($name)
127
    {
128 56
        if (isset($this->descriptors[$name])) {
129 38
            $this->descriptors[$name]->freeze();
130
        }
131 56
        return $this->resolve($name);
132
    }
133
134
    /**
135
     * {@inheritdoc}
136
     */
137
    public function assert(string $name, string $package)
138
    {
139
        try {
140
            return $this->get($name);
141
        } catch (ContainerExceptionInterface $e) {
142
            throw new RequirePackageException($name, $package);
143
        }
144
    }
145
    
146
    /**
147
     * {@inheritdoc}
148
     */
149 10
    public function has($name)
150
    {
151
        try {
152 10
            $this->resolve($name);
153 8
            return true;
154 7
        } catch (NullReferenceException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
155 2
        } catch (CannotResolveException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
156
        }
157 7
        return false;
158
    }
159
    
160
    /**
161
     * {@inheritdoc}
162
     */
163 57
    public function destroy(...$names)
164
    {
165 57
        foreach ($names as $name) {
166 57
            if (array_key_exists($name, $this->descriptors)) {
167 5
                if ($this->descriptors[$name]->frozen) {
168 2
                    throw new CannotChangeException($name);
169
                }
170
            }
171
            unset(
172 57
                $this->descriptors[$name],
173 57
                $this->instances[$name],
174 57
                $this->classes[$name],
175 57
                $this->closures[$name]
176
            );
177
        }
178 57
    }
179
180
    /**
181
     * {@inheritdoc}
182
     */
183 26
    public function instance(string $name, $value): ContainerFluent
184
    {
185 26
        $this->destroy($name);
186 26
        $this->instances[$name] = $value;
187 26
        return $this->descriptors[$name] = new Descriptor();
188
    }
189
190
    /**
191
     * @deprecated
192
     */
193 7
    public function closure(string $name, callable $handler): ContainerFluent
194
    {
195 7
        return $this->bind($name, $handler);
0 ignored issues
show
Documentation introduced by
$handler is of type callable, but the function expects a string|object<Closure>|null.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
196
    }
197
198
    /**
199
     * {@inheritdoc}
200
     */
201 44
    public function bind(string $name, $className = null): ContainerFluent
202
    {
203 44
        if (!isset($className)) {
204 18
            $this->destroy($name);
205 18
            $this->classes[$name] = $name;
206 18
            return $this->descriptors[$name] = new Descriptor();
207
        }
208 32
        if (is_string($className) && class_exists($className)) {
209 20
            $this->destroy($name, $className);
210 20
            $this->classes[$className] = $className;
211 20
            $this->alias($name, $className);
212 20
            return $this->descriptors[$className] = new Descriptor();
213 14
        } elseif (is_callable($className)) {
214 14
            $this->destroy($name);
215 14
            $this->closures[$name] = $className;
216 14
            return $this->descriptors[$name] = new Descriptor();
217
        }
218
        throw new InvalidArgumentException(
219
            sprintf('Argument 2 must be class name or Closure, "%s" given', is_object($className) ? get_class($className) : gettype($className))
220
        );
221
    }
222
223
    /**
224
     * {@inheritdoc}
225
     */
226 23
    public function alias(string $alias, string $target)
227
    {
228 23
        $this->aliases[$alias] = $target;
229 23
    }
230
231
    /**
232
     * {@inheritdoc}
233
     */
234 1
    public function descriptor(string $name): ContainerFluent
235
    {
236 1
        while (isset($this->aliases[$name])) {
237 1
            $name = $this->aliases[$name];
238
        }
239 1
        if (!array_key_exists($name, $this->descriptors)) {
240
            throw new NullReferenceException($name);
241
        }
242 1
        return $this->descriptors[$name];
243
    }
244
245
    /**
246
     * {@inheritdoc}
247
     */
248 3
    public function with(array $arguments = []): ContainerInterface
249
    {
250 3
        $new = clone $this;
251 3
        foreach ($arguments as $name => $argument) {
252 3
            $new->instance($name, $argument);
253
        }
254 3
        return $new;
255
    }
256
    
257
    /**
258
     * {@inheritdoc}
259
     */
260 35
    public function create(string $className, array $arguments = [])
261
    {
262 35
        if (!class_exists($className)) {
263
            throw new NullReferenceException($className);
264
        }
265
        try {
266 35
            if ($constructor = (new ReflectionClass($className))->getConstructor()) {
267 20
                $arguments = $this->getParameters($constructor, $arguments);
268 29
                return new $className(...$arguments);
269
            }
270 8
        } catch (CannotFindParameterException $e) {
271 8
            throw new CannotResolveException($className, $e->getParameter());
272
        }
273 23
        return new $className;
274
    }
275
    
276
    /**
277
     * {@inheritdoc}
278
     */
279 23
    public function call(callable $callee, array $arguments = [])
280
    {
281
        try {
282 23
            return call_user_func_array(
283 23
                $callee,
284 23
                $this->getParameters(new ReflectionCallable($callee), $arguments)
285
            );
286 5
        } catch (CannotFindParameterException $e) {
287 4
            throw new CannotResolveException($callee, $e->getParameter());
0 ignored issues
show
Documentation introduced by
$callee is of type callable, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
288
        }
289
    }
290
291
    /**
292
     * {@inheritdoc}
293
     */
294 7
    public function register(ServiceProviderInterface $provider)
295
    {
296 7
        $this->registerFromServiceProvider($provider);
297 7
    }
298
299
    /**
300
     * @param \Wandu\DI\ServiceProviderInterface $provider
301
     */
302 7
    public function registerFromServiceProvider(ServiceProviderInterface $provider)
303
    {
304 7
        $provider->register($this);
305 7
        $this->providers[] = $provider;
306 7
    }
307
    
308 3
    public function registerFromArray(array $provider)
309
    {
310 3
        foreach ($provider as $name => $information) {
311 3
            $descriptor = $this->bind($name, $information['class']);
312 3
            $descriptor->assignMany($information['assigns'] ?? []);
313 3
            $descriptor->wireMany($information['wires'] ?? []);
314 3
            if ($information['factory'] ?? false) {
315 3
                $descriptor->factory();
316
            }
317
        }
318 3
    }
319
    
320 2
    public function registerFromAnnotation($classes)
321
    {
322 2
        $reader = $this->get(Reader::class);
323 2
        if (is_string($classes)) $classes = [$classes];
324 2
        foreach ($classes as $class) {
325 2
            $descriptor = $this->bind($class);
326 2
            $reflClass = new ReflectionClass($class);
327 2
            foreach ($reader->getClassAnnotations($reflClass) as $annotation) {
328 2
                if ($annotation instanceof Alias) {
329 2
                    $this->alias($annotation->name, $class);
330 1
                } elseif ($annotation instanceof Factory) {
331 2
                    $descriptor->factory();
332
                }
333
            }
334 2
            if ($reflConstructor = $reflClass->getConstructor()) {
335 2
                foreach ($reader->getMethodAnnotations($reflConstructor) as $annotation) {
336 2
                    if ($annotation instanceof Assign) {
337 2
                        $descriptor->assign($annotation->name, $annotation->target);
338 2
                    } elseif ($annotation instanceof AssignValue) {
339 2
                        $descriptor->assign($annotation->name, ['value' => $annotation->value]);
340
                    }
341
                }
342
            }
343 2
            foreach ($reflClass->getProperties() as $reflProperty) {
344 2
                foreach ($reader->getPropertyAnnotations($reflProperty) as $annotation) {
345 2
                    if ($annotation instanceof Wire) {
346 2
                        $descriptor->wire($reflProperty->getName(), $annotation->target);
347 2
                    } elseif ($annotation instanceof WireValue) {
348 2
                        $descriptor->wire($reflProperty->getName(), ['value' => $annotation->value]);
349
                    }
350
                }
351
            }
352
        }
353 2
    }
354
355
    /**
356
     * {@inheritdoc}
357
     */
358 7
    public function boot()
359
    {
360 7
        if (!$this->isBooted) {
361 7
            foreach ($this->providers as $provider) {
362 5
                $provider->boot($this);
363
            }
364 7
            $this->isBooted = true;
365
        }
366 7
        return $this;
367
    }
368
369
    /**
370
     * @param string $name
371
     * @return mixed|object
372
     */
373 59
    protected function resolve($name)
374
    {
375 59
        while (isset($this->aliases[$name])) {
376 22
            $name = $this->aliases[$name];
377
        }
378 59
        if (array_key_exists($name, $this->instances)) {
379 44
            return $this->instances[$name];
380
        }
381 50
        if (!array_key_exists($name, $this->descriptors)) {
382 24
            if (!class_exists($name)) {
383 15
                throw new NullReferenceException($name);
384
            }
385 12
            $this->bind($name);
386
        }
387 42
        $descriptor = $this->descriptors[$name];
388 42
        if (array_key_exists($name, $this->classes)) {
389 31
            $instance = $this->create($this->classes[$name], $this->resolveArguments($descriptor->assigns));
390 13
        } elseif (array_key_exists($name, $this->closures)) {
391 13
            $instance = $this->call($this->closures[$name], $this->resolveArguments($descriptor->assigns));
392
        }
393 37
        foreach ($descriptor->afterHandlers as $handler) {
394 4
            $this->call($handler, [$instance]);
0 ignored issues
show
Bug introduced by
The variable $instance does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
395
        }
396 37
        foreach ($this->resolveArguments($descriptor->wires) as $propertyName => $value) {
397 9
            $refl = (new \ReflectionObject($instance))->getProperty($propertyName);
398 9
            $refl->setAccessible(true);
399 9
            $refl->setValue($instance, $value);
400
        }
401 37
        if (!$descriptor->factory) {
402 37
            $this->instances[$name] = $instance;
403
        }
404 37
        return $instance;
405
    }
406
407
    /**
408
     * @param array $arguments
409
     * @return array
410
     */
411 42
    protected function resolveArguments(array $arguments)
412
    {
413 42
        $argumentsToReturn = [];
414 42
        foreach ($arguments as $key => $value) {
415 15
            if (is_array($value)) {
416 10
                if (array_key_exists('value', $value)) {
417 10
                    $argumentsToReturn[$key] = $value['value'];
418
                }
419
            } else {
420
                try {
421 10
                    $argumentsToReturn[$key] = $this->get($value);
422 15
                } catch (NullReferenceException $e) {}
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
423
            }
424
        }
425 42
        return $argumentsToReturn;
426
    }
427
428
    /**
429
     * @param \ReflectionFunctionAbstract $reflectionFunction
430
     * @param array $arguments
431
     * @return array
432
     */
433 40
    protected function getParameters(ReflectionFunctionAbstract $reflectionFunction, array $arguments = [])
434
    {
435 40
        $parametersToReturn = static::getSeqArray($arguments);
436
437 40
        $reflectionParameters = array_slice($reflectionFunction->getParameters(), count($parametersToReturn));
438 40
        if (!count($reflectionParameters)) {
439 14
            return $parametersToReturn;
440
        }
441
        /* @var \ReflectionParameter $param */
442 31
        foreach ($reflectionParameters as $param) {
443
            /*
444
             * #1. search in arguments by parameter name
445
             * #1.1. search in arguments by class name
446
             * #2. if parameter has type hint
447
             * #2.1. search in container by class name
448
             * #3. if has default value, insert default value.
449
             * #4. exception
450
             */
451 31
            $paramName = $param->getName();
0 ignored issues
show
Bug introduced by
Consider using $param->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
452
            try {
453 31
                if (array_key_exists($paramName, $arguments)) { // #1.
454 14
                    $parametersToReturn[] = $arguments[$paramName];
455 14
                    continue;
456
                }
457 26
                $paramClass = $param->getClass();
458 26
                if ($paramClass) { // #2.
459 19
                    $paramClassName = $paramClass->getName();
0 ignored issues
show
Bug introduced by
Consider using $paramClass->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
460 19
                    if (array_key_exists($paramClassName, $arguments)) {
461 1
                        $parametersToReturn[] = $arguments[$paramClassName];
462 1
                        continue;
463
                    } else { // #2.1.
464
                        try {
465 18
                            $parametersToReturn[] = $this->get($paramClassName);
466 14
                            continue;
467 9
                        } catch (NullReferenceException $e) {}
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
468
                    }
469
                }
470 19
                if ($param->isDefaultValueAvailable()) { // #3.
471 9
                    $parametersToReturn[] = $param->getDefaultValue();
472 9
                    continue;
473
                }
474 11
                throw new CannotFindParameterException($paramName); // #4.
475 12
            } catch (ReflectionException $e) {
476 1
                throw new CannotFindParameterException($paramName);
477
            }
478
        }
479 22
        return $parametersToReturn;
480
    }
481
482
    /**
483
     * @param array $array
484
     * @return array
485
     */
486 40
    protected static function getSeqArray(array $array)
487
    {
488 40
        $arrayToReturn = [];
489 40
        foreach ($array as $key => $item) {
490 18
            if (is_int($key)) {
491 18
                $arrayToReturn[] = $item;
492
            }
493
        }
494 40
        return $arrayToReturn;
495
    } 
496
}
497