Completed
Push — master ( 396778...5e0c6d )
by Changwan
03:27
created

Container::assert()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 10
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
cc 3
eloc 7
nc 3
nop 2
dl 0
loc 10
ccs 0
cts 7
cp 0
crap 12
rs 9.4285
c 0
b 0
f 0
1
<?php
2
namespace Wandu\DI;
3
4
use Closure;
5
use Doctrine\Common\Annotations\Reader;
6
use Exception;
7
use Interop\Container\ContainerInterface as InteropContainerInterface;
8
use Interop\Container\Exception\ContainerException;
9
use Psr\Container\ContainerExceptionInterface;
10
use Psr\Container\ContainerInterface as PsrContainerInterface;
11
use ReflectionClass;
12
use ReflectionException;
13
use ReflectionFunctionAbstract;
14
use ReflectionMethod;
15
use ReflectionObject;
16
use ReflectionProperty;
17
use Throwable;
18
use Wandu\DI\Annotations\AutoWired;
19
use Wandu\DI\Containee\BindContainee;
20
use Wandu\DI\Containee\ClosureContainee;
21
use Wandu\DI\Containee\InstanceContainee;
22
use Wandu\DI\Exception\CannotChangeException;
23
use Wandu\DI\Exception\CannotFindParameterException;
24
use Wandu\DI\Exception\CannotResolveException;
25
use Wandu\DI\Exception\NullReferenceException;
26
use Wandu\DI\Exception\RequirePackageException;
27
use Wandu\Reflection\ReflectionCallable;
28
29
class Container implements ContainerInterface
30
{
31
    /** @var \Wandu\DI\ContainerInterface */
32
    public static $instance;
33
    
34
    /** @var \Wandu\DI\Containee\ContaineeAbstract[] */
35
    protected $containees = [];
36
    
37
    /** @var \Wandu\DI\ServiceProviderInterface[] */
38
    protected $providers = [];
39
40
    /** @var array */
41
    protected $extenders = [];
42
    
43
    /** @var array */
44
    protected $indexOfAliases = [];
45
    
46
    /** @var bool */
47
    protected $isBooted = false;
48
49 77
    public function __construct()
50
    {
51 77
        $this->instance(Container::class, $this)->freeze();
52 77
        $this->instance(ContainerInterface::class, $this)->freeze();
53 77
        $this->instance(InteropContainerInterface::class, $this)->freeze();
54 77
        $this->instance(PsrContainerInterface::class, $this)->freeze();
55 77
        $this->instance('container', $this)->freeze();
56 77
    }
57
58
    /**
59
     * @return \Wandu\DI\ContainerInterface
60
     */
61 4
    public function setAsGlobal()
62
    {
63 4
        $oldApp = static::$instance;
64 4
        static::$instance = $this;
65 4
        return $oldApp;
66
    }
67
68 6
    public function __clone()
69
    {
70
        // direct remove instance because of frozen
71
        unset(
72 6
            $this->containees[Container::class],
73 6
            $this->containees[ContainerInterface::class],
74 6
            $this->containees[InteropContainerInterface::class],
75 6
            $this->containees[PsrContainerInterface::class],
76 6
            $this->containees['container']
77
        );
78 6
        $this->instance(Container::class, $this)->freeze();
79 6
        $this->instance(ContainerInterface::class, $this)->freeze();
80 6
        $this->instance(InteropContainerInterface::class, $this)->freeze();
81 6
        $this->instance(PsrContainerInterface::class, $this)->freeze();
82 6
        $this->instance('container', $this)->freeze();
83 6
    }
84
85
    /**
86
     * {@inheritdoc}
87
     */
88
    public function __call($name, array $arguments)
89
    {
90
        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...
91
    }
92
93
    /**
94
     * {@inheritdoc}
95
     */
96 2
    public function offsetExists($name)
97
    {
98 2
        return $this->has($name) && $this->get($name) !== null;
99
    }
100
101
    /**
102
     * {@inheritdoc}
103
     */
104 17
    public function offsetGet($name)
105
    {
106 17
        return $this->get($name);
107
    }
108
109
    /**
110
     * {@inheritdoc}
111
     */
112 8
    public function offsetSet($name, $value)
113
    {
114 8
        $this->set($name, $value);
115 8
    }
116
117
    /**
118
     * {@inheritdoc}
119
     */
120 1
    public function offsetUnset($name)
121
    {
122 1
        $this->destroy($name);
123
    }
124
125
    /**
126
     * {@inheritdoc}
127
     */
128 29
    public function has($name)
129
    {
130 29
        return array_key_exists($name, $this->containees) || class_exists($name);
131
    }
132
133
    /**
134
     * {@inheritdoc}
135
     */
136 77
    public function destroy(...$names)
137
    {
138 77
        foreach ($names as $name) {
139 77
            if (array_key_exists($name, $this->containees)) {
140 4
                if ($this->containees[$name]->isFrozen()) {
141 2
                    throw new CannotChangeException($name);
142
                }
143
            }
144 77
            unset($this->containees[$name]);
145
        }
146 77
    }
147
148
    /**
149
     * {@inheritdoc}
150
     */
151 44
    public function containee($name)
152
    {
153 44
        if (!array_key_exists($name, $this->containees)) {
154 11
            throw new NullReferenceException($name);
155
        }
156 41
        return $this->containees[$name];
157
    }
158
    
159
    /**
160
     * {@inheritdoc}
161
     */
162 44
    public function get($name)
163
    {
164
        try {
165 44
            $instance = $this->containee($name)->get($this);
166 11
        } catch (NullReferenceException $e) {
167 11
            if (!class_exists($name)) {
168 1
                throw $e;
169
            }
170 10
            $instance = $this->create($name);
171 8
            $this->instance($name, $instance);
172
        }
173 42
        if ($this->containees[$name]->isWireEnabled()) {
174 3
            $this->applyWire($instance);
175
        }
176 42
        foreach ($this->getExtenders($name) as $extender) {
177 4
            $instance = $extender->__invoke($instance);
178
        }
179 42
        return $instance;
180
    }
181
182
    /**
183
     * @param string $name
184
     * @return \Closure[]
185
     */
186 42
    protected function getExtenders($name)
187
    {
188 42
        $extenders = [];
189 42
        if (isset($this->extenders[$name])) {
190 4
            $extenders = array_merge($extenders, $this->extenders[$name]);
191
        }
192
193
        // extend propagation
194 42
        if (isset($this->indexOfAliases[$name])) {
195 20
            foreach ($this->indexOfAliases[$name] as $aliasName) {
196 20
                $extenders = array_merge($extenders, $this->getExtenders($aliasName));
197
            }
198
        }
199 42
        return $extenders;
200
    }
201
202
    /**
203
     * {@inheritdoc}
204
     */
205 8
    public function set($name, $value)
206
    {
207 8
        if (!($value instanceof ContaineeInterface)) {
208 7
            $value = new InstanceContainee($value);
209
        }
210 8
        return $this->addContainee($name, $value);
211
    }
212
213
    /**
214
     * {@inheritdoc}
215
     */
216 77
    public function instance($name, $value)
217
    {
218 77
        return $this->addContainee($name, new InstanceContainee($value));
219
    }
220
221
    /**
222
     * {@inheritdoc}
223
     */
224 30
    public function closure($name, callable $handler)
225
    {
226 30
        return $this->addContainee($name, new ClosureContainee($handler));
227
    }
228
229
    /**
230
     * {@inheritdoc}
231
     */
232 25
    public function alias($name, $origin)
233
    {
234 25
        if (!array_key_exists($origin, $this->indexOfAliases)) {
235 25
            $this->indexOfAliases[$origin] = [];
236
        }
237 25
        $this->indexOfAliases[$origin][] = $name;
238 25
        return $this->closure($name, function (ContainerInterface $container) use ($origin) {
0 ignored issues
show
Bug introduced by
It seems like $name defined by parameter $name on line 232 can also be of type array; however, Wandu\DI\Container::closure() does only seem to accept string, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
239 11
            return $container->get($origin); // proxy
240 25
        })->factory(true);
241
    }
242
243
    /**
244
     * {@inheritdoc}
245
     */
246 13
    public function bind($name, $class = null)
247
    {
248 13
        if (isset($class)) {
249 13
            $this->alias($class, $name);
250 13
            return $this->addContainee($name, new BindContainee($class));
251
        }
252 4
        return $this->addContainee($name, new BindContainee($name));
253
    }
254
    
255
    /**
256
     * @param string $name
257
     * @param \Wandu\DI\ContaineeInterface $containee
258
     * @return \Wandu\DI\ContaineeInterface
259
     */
260 77
    public function addContainee($name, ContaineeInterface $containee)
261
    {
262 77
        $this->destroy($name);
263 77
        return $this->containees[$name] = $containee;
264
    }
265
266
    /**
267
     * {@inheritdoc}
268
     */
269 5
    public function extend($name, Closure $handler)
270
    {
271 5
        if (!array_key_exists($name, $this->extenders)) {
272 5
            $this->extenders[$name] = [];
273
        }
274 5
        $this->extenders[$name][] = $handler;
275 5
    }
276
277
    /**
278
     * {@inheritdoc}
279
     */
280 7
    public function register(ServiceProviderInterface $provider)
281
    {
282 7
        $provider->register($this);
283 7
        $this->providers[] = $provider;
284 7
    }
285
286
    /**
287
     * {@inheritdoc}
288
     */
289 5
    public function boot()
290
    {
291 5
        if (!$this->isBooted) {
292 5
            foreach ($this->providers as $provider) {
293 5
                $provider->boot($this);
294
            }
295 5
            $this->isBooted = true;
296
        }
297 5
        return $this;
298
    }
299
    
300
    /**
301
     * {@inheritdoc}
302
     */
303
    public function freeze($name)
304
    {
305
        $this->containees[$name]->freeze();
306
    }
307
308
    /**
309
     * {@inheritdoc}
310
     */
311 6
    public function with(array $arguments = [])
312
    {
313 6
        $new = clone $this;
314 6
        foreach ($arguments as $name => $argument) {
315 3
            if ($argument instanceof ContaineeInterface) {
316
                $new->addContainee($name, $argument);
317
            } else {
318 3
                $new->instance($name, $argument);
319
            }
320
        }
321 6
        return $new;
322
    }
323
    
324
    /**
325
     * {@inheritdoc}
326
     */
327 21
    public function create($class, array $arguments = [])
328
    {
329 21
        $reflectionClass = new ReflectionClass($class);
330 21
        $reflectionMethod = $reflectionClass->getConstructor();
331 21
        if (!$reflectionMethod) {
332 14
            return $reflectionClass->newInstance();
333
        }
334
        try {
335 11
            $parameters = $this->getParameters($reflectionMethod, $arguments);
336 6
        } catch (CannotFindParameterException $e) {
337 6
            throw new CannotResolveException($class, $e->getParameter());
338
        }
339 9
        return $reflectionClass->newInstanceArgs($parameters);
340
    }
341
342
    /**
343
     * {@inheritdoc}
344
     */
345 26
    public function call(callable $callee, array $arguments = [])
346
    {
347
        try {
348 26
            return call_user_func_array(
349
                $callee,
350 26
                $this->getParameters(new ReflectionCallable($callee), $arguments)
351
            );
352 5
        } catch (CannotFindParameterException $e) {
353 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...
354
        }
355
    }
356
357
    /**
358
     * {@inheritdoc}
359
     */
360 3
    public function inject($object, array $properties = [])
361
    {
362 3
        $reflectionObject = new ReflectionObject($object);
363 3
        foreach ($properties as $property => $value) {
364 3
            $this->injectProperty($reflectionObject->getProperty($property), $object, $value);
365
        }
366 3
    }
367
368
    /**
369
     * {@inheritdoc}
370
     */
371
    public function assert($name, $package = null)
372
    {
373
        try {
374
            $this->get($name);
375
        } catch (ContainerException $e) {
376
            throw new RequirePackageException($name, $package);
377
        } catch (ContainerExceptionInterface $e) {
378
            throw new RequirePackageException($name, $package);
379
        }
380
    }
381
382
    /**
383
     * @param \ReflectionProperty $property
384
     * @param object $object
385
     * @param mixed $target
386
     */
387 3
    private function injectProperty(ReflectionProperty $property, $object, $target)
388
    {
389 3
        $property->setAccessible(true);
390 3
        $property->setValue($object, $target);
391 3
    }
392
393
    /**
394
     * @param \ReflectionFunctionAbstract $reflectionFunction
395
     * @param array $arguments
396
     * @return array
397
     * @throws \Wandu\DI\Exception\CannotFindParameterException
398
     */
399 33
    protected function getParameters(ReflectionFunctionAbstract $reflectionFunction, array $arguments = [])
400
    {
401 33
        $parametersToReturn = static::getSeqArray($arguments);
402
403 33
        $reflectionParameters = array_slice($reflectionFunction->getParameters(), count($parametersToReturn));
404 33
        if (!count($reflectionParameters)) {
405 16
            return $parametersToReturn; 
406
        }
407
        
408 25
        $autoWires = [];
409 25
        if ($reflectionFunction instanceof ReflectionMethod) {
410 11
            $declaredClassName = $reflectionFunction->getDeclaringClass()->getName();
0 ignored issues
show
introduced by
Consider using $reflectionFunction->class. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
411 11
            if (isset($this->containees[$declaredClassName]) && $this->containees[$declaredClassName]->isWireEnabled()) {
412 11
                $autoWires = $this->getAutoWiresFromMethod($reflectionFunction);
413
            }
414
        } elseif (
415 18
            $reflectionFunction instanceof ReflectionCallable &&
416 18
            $reflectionFunction->getRawReflection() instanceof ReflectionMethod
417
        ) {
418 2
            $declaredClassName = $reflectionFunction->getRawReflection()->getDeclaringClass()->getName();
0 ignored issues
show
introduced by
Consider using $reflectionFunction->getRawReflection()->class. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
419 2
            if (isset($this->containees[$declaredClassName]) && $this->containees[$declaredClassName]->isWireEnabled()) {
420 1
                $autoWires = $this->getAutoWiresFromMethod($reflectionFunction->getRawReflection());
421
            }
422
        }
423
        
424
        try {
425
            /* @var \ReflectionParameter $param */
426 25
            foreach ($reflectionParameters as $param) {
427
                /*
428
                 * #1. search in arguments by parameter name
429
                 * #2. if parameter has type hint
430
                 * #2.1. search in container by class name
431
                 * #3. if autowired enabled
432
                 * #3.1. search in container by autowired name
433
                 * #4. if has default value, insert default value.
434
                 * #5. exception
435
                 */
436 25
                $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...
437 25
                if (array_key_exists($paramName, $arguments)) { // #1.
438 4
                    $parametersToReturn[] = $arguments[$paramName];
439 4
                    continue;
440
                }
441 25
                $paramClass = $param->getClass();
442 25
                if ($paramClass) { // #2.
443 23
                    $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...
444 23
                    if ($this->has($paramClassName)) { // #2.1.
445 23
                        $parametersToReturn[] = $this->get($paramClassName);
446 22
                        continue;
447
                    }
448
                }
449 11
                if (array_key_exists($paramName, $autoWires) && $this->has($autoWires[$paramName])) {
450 2
                    $parametersToReturn[] = $this->get($autoWires[$paramName]);
451 2
                    continue;
452
                }
453 9
                if ($param->isDefaultValueAvailable()) { // #4.
454 4
                    $parametersToReturn[] = $param->getDefaultValue();
455 4
                    continue;
456
                }
457 24
                throw new CannotFindParameterException($paramName); // #5.
458
            }
459 9
        } catch (ReflectionException $e) {
460 1
            throw new CannotFindParameterException($paramName);
461
        }
462 24
        return $parametersToReturn;
463
    }
464
465
    /**
466
     * @param array $array
467
     * @return array
468
     */
469 33
    protected static function getSeqArray(array $array)
470
    {
471 33
        $arrayToReturn = [];
472 33
        foreach ($array as $key => $item) {
473 5
            if (is_int($key)) {
474 5
                $arrayToReturn[] = $item;
475
            }
476
        }
477 33
        return $arrayToReturn;
478
    }
479
    
480 3
    protected function applyWire($instance)
481
    {
482 3
        static $callStack = [];
483 3
        if (in_array($instance, $callStack)) {
484 1
            return; // return when a circular call is detected.
485
        }
486 3
        array_push($callStack, $instance);
487
        try {
488 3
            $reader = $this->get(Reader::class);
489 3
            class_exists(AutoWired::class); // pre-load for Annotation Reader
490 3
            $reflObject = new ReflectionObject($instance);
491 3
            foreach ($reflObject->getProperties() as $reflProperty) {
492
                /* @var \Wandu\DI\Annotations\AutoWired $autoWired */
493 3
                if ($autoWired = $reader->getPropertyAnnotation($reflProperty, AutoWired::class)) {
494 2
                    $this->inject($instance, [
495 3
                        $reflProperty->getName() => $this->get($autoWired->name),
496
                    ]);
497
                }
498
            }
499
        } catch (Exception $e) {
500
            array_pop($callStack);
501
            throw $e;
502
        } catch (Throwable $e) {
503
            array_pop($callStack);
504
            throw $e;
505
        }
506 3
        array_pop($callStack);
507 3
    }
508
509
    /**
510
     * @param \ReflectionMethod $reflMethod
511
     * @return array
512
     */
513 2
    protected function getAutoWiresFromMethod(ReflectionMethod $reflMethod)
514
    {
515 2
        $reader = $this->get(Reader::class);
516 2
        class_exists(AutoWired::class); // pre-load for Annotation Reader
517 2
        $autoWires = [];
518 2
        foreach ($reader->getMethodAnnotations($reflMethod) as $annotation) {
519 2
            if ($annotation instanceof AutoWired) {
520 2
                $autoWires[$annotation->to] = $annotation->name;
521
            }
522
        }
523 2
        return $autoWires;
524
    }
525
}
526