Completed
Push — master ( d2e043...09ed12 )
by Changwan
12:25
created

Container::setAsGlobal()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 4
nc 1
nop 0
dl 0
loc 6
ccs 4
cts 4
cp 1
crap 1
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 ReflectionClass;
9
use ReflectionException;
10
use ReflectionFunctionAbstract;
11
use ReflectionMethod;
12
use ReflectionObject;
13
use ReflectionProperty;
14
use Throwable;
15
use Wandu\DI\Annotations\AutoWired;
16
use Wandu\DI\Containee\BindContainee;
17
use Wandu\DI\Containee\ClosureContainee;
18
use Wandu\DI\Containee\InstanceContainee;
19
use Wandu\DI\Exception\CannotChangeException;
20
use Wandu\DI\Exception\CannotFindParameterException;
21
use Wandu\DI\Exception\CannotResolveException;
22
use Wandu\DI\Exception\NullReferenceException;
23
use Wandu\Reflection\ReflectionCallable;
24
25
class Container implements ContainerInterface
26
{
27
    /** @var \Wandu\DI\ContainerInterface */
28
    public static $instance;
29
    
30
    /** @var \Wandu\DI\Containee\ContaineeAbstract[] */
31
    protected $containees = [];
32
    
33
    /** @var \Wandu\DI\ServiceProviderInterface[] */
34
    protected $providers = [];
35
36
    /** @var array */
37
    protected $extenders = [];
38
    
39
    /** @var array */
40
    protected $indexOfAliases = [];
41
    
42
    /** @var bool */
43
    protected $isBooted = false;
44
45 74
    public function __construct()
46
    {
47 74
        $this->instance(Container::class, $this)->freeze();
48 74
        $this->instance(ContainerInterface::class, $this)->freeze();
49 74
        $this->instance(InteropContainerInterface::class, $this)->freeze();
50 74
        $this->instance('container', $this)->freeze();
51 74
    }
52
53
    /**
54
     * @return \Wandu\DI\ContainerInterface
55
     */
56 4
    public function setAsGlobal()
57
    {
58 4
        $oldApp = static::$instance;
59 4
        static::$instance = $this;
60 4
        return $oldApp;
61
    }
62
63 6
    public function __clone()
64
    {
65
        // direct remove instance because of frozen
66
        unset(
67 6
            $this->containees[Container::class],
68 6
            $this->containees[ContainerInterface::class],
69 6
            $this->containees[InteropContainerInterface::class],
70 6
            $this->containees['container']
71
        );
72 6
        $this->instance(Container::class, $this)->freeze();
73 6
        $this->instance(ContainerInterface::class, $this)->freeze();
74 6
        $this->instance(InteropContainerInterface::class, $this)->freeze();
75 6
        $this->instance('container', $this)->freeze();
76 6
    }
77
78
    /**
79
     * {@inheritdoc}
80
     */
81
    public function __call($name, array $arguments)
82
    {
83
        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...
84
    }
85
86
    /**
87
     * {@inheritdoc}
88
     */
89 2
    public function offsetExists($name)
90
    {
91 2
        return $this->has($name) && $this->get($name) !== null;
92
    }
93
94
    /**
95
     * {@inheritdoc}
96
     */
97 17
    public function offsetGet($name)
98
    {
99 17
        return $this->get($name);
100
    }
101
102
    /**
103
     * {@inheritdoc}
104
     */
105 23
    public function offsetSet($name, $value)
106
    {
107 23
        $this->set($name, $value);
108 23
    }
109
110
    /**
111
     * {@inheritdoc}
112
     */
113 1
    public function offsetUnset($name)
114
    {
115 1
        $this->destroy($name);
116
    }
117
118
    /**
119
     * {@inheritdoc}
120
     */
121 29
    public function has($name)
122
    {
123 29
        return array_key_exists($name, $this->containees) || class_exists($name);
124
    }
125
126
    /**
127
     * {@inheritdoc}
128
     */
129 74
    public function destroy(...$names)
130
    {
131 74
        foreach ($names as $name) {
132 74
            if (array_key_exists($name, $this->containees)) {
133 4
                if ($this->containees[$name]->isFrozen()) {
134 2
                    throw new CannotChangeException($name);
135
                }
136
            }
137 74
            unset($this->containees[$name]);
138
        }
139 74
    }
140
141
    /**
142
     * {@inheritdoc}
143
     */
144 44
    public function containee($name)
145
    {
146 44
        if (!array_key_exists($name, $this->containees)) {
147 11
            throw new NullReferenceException($name);
148
        }
149 41
        return $this->containees[$name];
150
    }
151
    
152
    /**
153
     * {@inheritdoc}
154
     */
155 44
    public function get($name)
156
    {
157
        try {
158 44
            $instance = $this->containee($name)->get($this);
159 11
        } catch (NullReferenceException $e) {
160 11
            if (!class_exists($name)) {
161 1
                throw $e;
162
            }
163 10
            $instance = $this->create($name);
164 8
            $this->instance($name, $instance);
165
        }
166 42
        if ($this->containees[$name]->isWireEnabled()) {
167 3
            $this->applyWire($instance);
168
        }
169 42
        foreach ($this->getExtenders($name) as $extender) {
170 4
            $instance = $extender->__invoke($instance);
171
        }
172 42
        return $instance;
173
    }
174
175
    /**
176
     * @param string $name
177
     * @return \Closure[]
178
     */
179 42
    protected function getExtenders($name)
180
    {
181 42
        $extenders = [];
182 42
        if (isset($this->extenders[$name])) {
183 4
            $extenders = array_merge($extenders, $this->extenders[$name]);
184
        }
185
186
        // extend propagation
187 42
        if (isset($this->indexOfAliases[$name])) {
188 20
            foreach ($this->indexOfAliases[$name] as $aliasName) {
189 20
                $extenders = array_merge($extenders, $this->getExtenders($aliasName));
190
            }
191
        }
192 42
        return $extenders;
193
    }
194
195
    /**
196
     * {@inheritdoc}
197
     */
198 23
    public function set($name, $value)
199
    {
200 23
        if (!($value instanceof ContaineeInterface)) {
201 22
            $value = new InstanceContainee($value);
202
        }
203 23
        return $this->addContainee($name, $value);
204
    }
205
206
    /**
207
     * {@inheritdoc}
208
     */
209 74
    public function instance($name, $value)
210
    {
211 74
        return $this->addContainee($name, new InstanceContainee($value));
212
    }
213
214
    /**
215
     * {@inheritdoc}
216
     */
217 30
    public function closure($name, callable $handler)
218
    {
219 30
        return $this->addContainee($name, new ClosureContainee($handler));
220
    }
221
222
    /**
223
     * {@inheritdoc}
224
     */
225 25
    public function alias($name, $origin)
226
    {
227 25
        if (!array_key_exists($origin, $this->indexOfAliases)) {
228 25
            $this->indexOfAliases[$origin] = [];
229
        }
230 25
        $this->indexOfAliases[$origin][] = $name;
231 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 225 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...
232 11
            return $container->get($origin); // proxy
233 25
        })->factory(true);
234
    }
235
236
    /**
237
     * {@inheritdoc}
238
     */
239 13
    public function bind($name, $class = null)
240
    {
241 13
        if (isset($class)) {
242 13
            $this->alias($class, $name);
243 13
            return $this->addContainee($name, new BindContainee($class));
244
        }
245 4
        return $this->addContainee($name, new BindContainee($name));
246
    }
247
    
248
    /**
249
     * @param string $name
250
     * @param \Wandu\DI\ContaineeInterface $containee
251
     * @return \Wandu\DI\ContaineeInterface
252
     */
253 74
    public function addContainee($name, ContaineeInterface $containee)
254
    {
255 74
        $this->destroy($name);
256 74
        return $this->containees[$name] = $containee;
257
    }
258
259
    /**
260
     * {@inheritdoc}
261
     */
262 5
    public function extend($name, Closure $handler)
263
    {
264 5
        if (!array_key_exists($name, $this->extenders)) {
265 5
            $this->extenders[$name] = [];
266
        }
267 5
        $this->extenders[$name][] = $handler;
268 5
    }
269
270
    /**
271
     * {@inheritdoc}
272
     */
273 7
    public function register(ServiceProviderInterface $provider)
274
    {
275 7
        $provider->register($this);
276 7
        $this->providers[] = $provider;
277 7
    }
278
279
    /**
280
     * {@inheritdoc}
281
     */
282 1
    public function boot()
283
    {
284 1
        if (!$this->isBooted) {
285 1
            foreach ($this->providers as $provider) {
286 1
                $provider->boot($this);
287
            }
288 1
            $this->isBooted = true;
289
        }
290 1
        return $this;
291
    }
292
    
293
    /**
294
     * {@inheritdoc}
295
     */
296
    public function freeze($name)
297
    {
298
        $this->containees[$name]->freeze();
299
    }
300
301
    /**
302
     * {@inheritdoc}
303
     */
304 6
    public function with(array $arguments = [])
305
    {
306 6
        $new = clone $this;
307 6
        foreach ($arguments as $name => $argument) {
308 3
            if ($argument instanceof ContaineeInterface) {
309
                $new->addContainee($name, $argument);
310
            } else {
311 3
                $new->instance($name, $argument);
312
            }
313
        }
314 6
        return $new;
315
    }
316
    
317
    /**
318
     * {@inheritdoc}
319
     */
320 21
    public function create($class, array $arguments = [])
321
    {
322 21
        $reflectionClass = new ReflectionClass($class);
323 21
        $reflectionMethod = $reflectionClass->getConstructor();
324 21
        if (!$reflectionMethod) {
325 14
            return $reflectionClass->newInstance();
326
        }
327
        try {
328 11
            $parameters = $this->getParameters($reflectionMethod, $arguments);
329 6
        } catch (CannotFindParameterException $e) {
330 6
            throw new CannotResolveException($class, $e->getParameter());
331
        }
332 9
        return $reflectionClass->newInstanceArgs($parameters);
333
    }
334
335
    /**
336
     * {@inheritdoc}
337
     */
338 26
    public function call(callable $callee, array $arguments = [])
339
    {
340
        try {
341 26
            return call_user_func_array(
342
                $callee,
343 26
                $this->getParameters(new ReflectionCallable($callee), $arguments)
344
            );
345 5
        } catch (CannotFindParameterException $e) {
346 4
            throw new CannotResolveException(null, $e->getParameter());
347
        }
348
    }
349
350
    /**
351
     * {@inheritdoc}
352
     */
353 3
    public function inject($object, array $properties = [])
354
    {
355 3
        $reflectionObject = new ReflectionObject($object);
356 3
        foreach ($properties as $property => $value) {
357 3
            $this->injectProperty($reflectionObject->getProperty($property), $object, $value);
358
        }
359 3
    }
360
361
    /**
362
     * @param \ReflectionProperty $property
363
     * @param object $object
364
     * @param mixed $target
365
     */
366 3
    private function injectProperty(ReflectionProperty $property, $object, $target)
367
    {
368 3
        $property->setAccessible(true);
369 3
        $property->setValue($object, $target);
370 3
    }
371
372
    /**
373
     * @param \ReflectionFunctionAbstract $reflectionFunction
374
     * @param array $arguments
375
     * @return array
376
     */
377 33
    protected function getParameters(ReflectionFunctionAbstract $reflectionFunction, array $arguments = [])
378
    {
379 33
        $parametersToReturn = static::getSeqArray($arguments);
380
381 33
        $reflectionParameters = array_slice($reflectionFunction->getParameters(), count($parametersToReturn));
382 33
        if (!count($reflectionParameters)) {
383 16
            return $parametersToReturn; 
384
        }
385
        
386 25
        $autoWires = [];
387 25
        if ($reflectionFunction instanceof ReflectionMethod) {
388 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...
389 11
            if (isset($this->containees[$declaredClassName]) && $this->containees[$declaredClassName]->isWireEnabled()) {
390 11
                $autoWires = $this->getAutoWiresFromMethod($reflectionFunction);
391
            }
392
        } elseif (
393 18
            $reflectionFunction instanceof ReflectionCallable &&
394 18
            $reflectionFunction->getRawReflection() instanceof ReflectionMethod
395
        ) {
396 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...
397 2
            if (isset($this->containees[$declaredClassName]) && $this->containees[$declaredClassName]->isWireEnabled()) {
398 1
                $autoWires = $this->getAutoWiresFromMethod($reflectionFunction->getRawReflection());
399
            }
400
        }
401
        
402
        try {
403
            /* @var \ReflectionParameter $param */
404 25
            foreach ($reflectionParameters as $param) {
405
                /*
406
                 * #1. search in arguments by parameter name
407
                 * #2. if parameter has type hint
408
                 * #2.1. search in container by class name
409
                 * #3. if autowired enabled
410
                 * #3.1. search in container by autowired name
411
                 * #4. if has default value, insert default value.
412
                 * #5. exception
413
                 */
414 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...
415 25
                if (array_key_exists($paramName, $arguments)) { // #1.
416 4
                    $parametersToReturn[] = $arguments[$paramName];
417 4
                    continue;
418
                }
419 25
                $paramClass = $param->getClass();
420 25
                if ($paramClass) { // #2.
421 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...
422 23
                    if ($this->has($paramClassName)) { // #2.1.
423 23
                        $parametersToReturn[] = $this->get($paramClassName);
424 22
                        continue;
425
                    }
426
                }
427 11
                if (array_key_exists($paramName, $autoWires) && $this->has($autoWires[$paramName])) {
428 2
                    $parametersToReturn[] = $this->get($autoWires[$paramName]);
429 2
                    continue;
430
                }
431 9
                if ($param->isDefaultValueAvailable()) { // #4.
432 4
                    $parametersToReturn[] = $param->getDefaultValue();
433 4
                    continue;
434
                }
435 24
                throw new CannotFindParameterException($paramName); // #5.
436
            }
437 9
        } catch (ReflectionException $e) {
438 1
            throw new CannotFindParameterException($paramName);
439
        }
440 24
        return $parametersToReturn;
441
    }
442
443
    /**
444
     * @param array $array
445
     * @return array
446
     */
447 33
    protected static function getSeqArray(array $array)
448
    {
449 33
        $arrayToReturn = [];
450 33
        foreach ($array as $key => $item) {
451 5
            if (is_int($key)) {
452 5
                $arrayToReturn[] = $item;
453
            }
454
        }
455 33
        return $arrayToReturn;
456
    }
457
    
458 3
    protected function applyWire($instance)
459
    {
460 3
        static $callStack = [];
461 3
        if (in_array($instance, $callStack)) {
462 1
            return; // return when a circular call is detected.
463
        }
464 3
        array_push($callStack, $instance);
465
        try {
466 3
            $reader = $this->get(Reader::class);
467 3
            class_exists(AutoWired::class); // pre-load for Annotation Reader
468 3
            $reflObject = new ReflectionObject($instance);
469 3
            foreach ($reflObject->getProperties() as $reflProperty) {
470
                /* @var \Wandu\DI\Annotations\AutoWired $autoWired */
471 3
                if ($autoWired = $reader->getPropertyAnnotation($reflProperty, AutoWired::class)) {
472 2
                    $this->inject($instance, [
473 3
                        $reflProperty->getName() => $this->get($autoWired->name),
474
                    ]);
475
                }
476
            }
477
        } catch (Exception $e) {
478
            array_pop($callStack);
479
            throw $e;
480
        } catch (Throwable $e) {
481
            array_pop($callStack);
482
            throw $e;
483
        }
484 3
        array_pop($callStack);
485 3
    }
486
487
    /**
488
     * @param \ReflectionMethod $reflMethod
489
     * @return array
490
     */
491 2
    protected function getAutoWiresFromMethod(ReflectionMethod $reflMethod)
492
    {
493 2
        $reader = $this->get(Reader::class);
494 2
        class_exists(AutoWired::class); // pre-load for Annotation Reader
495 2
        $autoWires = [];
496 2
        foreach ($reader->getMethodAnnotations($reflMethod) as $annotation) {
497 2
            if ($annotation instanceof AutoWired) {
498 2
                $autoWires[$annotation->to] = $annotation->name;
499
            }
500
        }
501 2
        return $autoWires;
502
    }
503
}
504