Completed
Push — master ( e83215...d2e043 )
by Changwan
03:21
created

Container::getAutoWiresFromMethod()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 12
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 3

Importance

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