Completed
Push — master ( adf519...1e376d )
by Changwan
05:14
created

Container::applyWire()   B

Complexity

Conditions 6
Paths 22

Size

Total Lines 29
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 6.3698

Importance

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

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
433 2
                    $this->inject($instance, [
434 2
                        $reflProperty->getName() => $this->get($autoWired->name),
435 2
                    ]);
436 2
                }
437 2
            }
438 2
        } catch (Exception $e) {
439
            array_pop($callStack);
440
            throw $e;
441
        } 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...
442
            array_pop($callStack);
443
            throw $e;
444
        }
445 2
        array_pop($callStack);
446 2
    }
447
}
448