Completed
Push — master ( b150a8...fa80df )
by Changwan
07:18
created

Container::getAssocArray()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3.4746

Importance

Changes 0
Metric Value
cc 3
eloc 6
nc 3
nop 1
dl 0
loc 10
ccs 5
cts 8
cp 0.625
crap 3.4746
rs 9.4285
c 0
b 0
f 0
1
<?php
2
namespace Wandu\DI;
3
4
use Closure;
5
use Interop\Container\ContainerInterface as InteropContainerInterface;
6
use ReflectionClass;
7
use ReflectionException;
8
use ReflectionFunctionAbstract;
9
use ReflectionObject;
10
use ReflectionProperty;
11
use Wandu\DI\Containee\BindContainee;
12
use Wandu\DI\Containee\ClosureContainee;
13
use Wandu\DI\Containee\InstanceContainee;
14
use Wandu\DI\Exception\CannotChangeException;
15
use Wandu\DI\Exception\CannotFindParameterException;
16
use Wandu\DI\Exception\CannotInjectException;
17
use Wandu\DI\Exception\CannotResolveException;
18
use Wandu\DI\Exception\NullReferenceException;
19
use Wandu\Reflection\ReflectionCallable;
20
21
class Container implements ContainerInterface
22
{
23
    /** @var \Wandu\DI\Containee\ContaineeAbstract[] */
24
    protected $containees = [];
25
    
26
    /** @var \Wandu\DI\ServiceProviderInterface[] */
27
    protected $providers = [];
28
29
    /** @var array */
30
    protected $extenders = [];
31
    
32
    /** @var array */
33
    protected $indexOfAliases = [];
34
    
35
    /** @var bool */
36
    protected $isBooted = false;
37
38 53
    public function __construct()
39
    {
40 53
        $this->instance(Container::class, $this)->freeze();
41 53
        $this->instance(ContainerInterface::class, $this)->freeze();
42 53
        $this->instance(InteropContainerInterface::class, $this)->freeze();
43 53
        $this->instance('container', $this)->freeze();
44 53
    }
45
46 4
    public function __clone()
47
    {
48
        // direct remove instance because of frozen
49
        unset(
50 4
            $this->containees[Container::class],
51 4
            $this->containees[ContainerInterface::class],
52 4
            $this->containees[InteropContainerInterface::class],
53 4
            $this->containees['container']
54
        );
55 4
        $this->instance(Container::class, $this)->freeze();
56 4
        $this->instance(ContainerInterface::class, $this)->freeze();
57 4
        $this->instance(InteropContainerInterface::class, $this)->freeze();
58 4
        $this->instance('container', $this)->freeze();
59 4
    }
60
61
    /**
62
     * {@inheritdoc}
63
     */
64
    public function __call($name, array $arguments)
65
    {
66
        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...
67
    }
68
69
    /**
70
     * {@inheritdoc}
71
     */
72 2
    public function offsetExists($name)
73
    {
74 2
        return $this->has($name) && $this->get($name) !== null;
75
    }
76
77
    /**
78
     * {@inheritdoc}
79
     */
80 16
    public function offsetGet($name)
81
    {
82 16
        return $this->get($name);
83
    }
84
85
    /**
86
     * {@inheritdoc}
87
     */
88 10
    public function offsetSet($name, $value)
89
    {
90 10
        $this->set($name, $value);
91 10
    }
92
93
    /**
94
     * {@inheritdoc}
95
     */
96 1
    public function offsetUnset($name)
97
    {
98 1
        $this->destroy($name);
99
    }
100
101
    /**
102
     * {@inheritdoc}
103
     */
104 11
    public function has($name)
105
    {
106 11
        return array_key_exists($name, $this->containees) || class_exists($name);
107
    }
108
109
    /**
110
     * {@inheritdoc}
111
     */
112 53
    public function destroy(...$names)
113
    {
114 53
        foreach ($names as $name) {
115 53
            if (array_key_exists($name, $this->containees)) {
116 4
                if ($this->containees[$name]->isFrozen()) {
117 2
                    throw new CannotChangeException($name);
118
                }
119 4
            }
120 53
            unset($this->containees[$name]);
121 53
        }
122 53
    }
123
124
    /**
125
     * {@inheritdoc}
126
     */
127 35
    public function containee($name)
128
    {
129 35
        if (!array_key_exists($name, $this->containees)) {
130 6
            if (class_exists($name)) {
131 4
                $this->bind($name);
132 4
            } else {
133 2
                throw new NullReferenceException($name);
134
            }
135 4
        }
136 33
        return $this->containees[$name];
137
    }
138
    
139
    /**
140
     * @param string $name
141
     * @return mixed
142
     */
143 35
    public function getRawItem($name)
144
    {
145 35
        return $this->containee($name)->get($this);
146
    }
147
    
148
    /**
149
     * {@inheritdoc}
150
     */
151 35
    public function get($name)
152
    {
153 35
        $instance = $this->getRawItem($name);
154 33
        if ($this->containees[$name]->isWireEnabled()) {
155 2
           $this->inject($instance);
156 2
        }
157
158 33
        foreach ($this->getExtenders($name) as $extender) {
159 4
            $instance = $extender->__invoke($instance);
160 33
        }
161 33
        return $instance;
162
    }
163
164
    /**
165
     * @param string $name
166
     * @return \Closure[]
167
     */
168 33
    protected function getExtenders($name)
169
    {
170 33
        $extenders = [];
171 33
        if (isset($this->extenders[$name])) {
172 4
            $extenders = array_merge($extenders, $this->extenders[$name]);
173 4
        }
174
175
        // extend propagation
176 33
        if (isset($this->indexOfAliases[$name])) {
177 10
            foreach ($this->indexOfAliases[$name] as $aliasName) {
178 10
                $extenders = array_merge($extenders, $this->getExtenders($aliasName));
179 10
            }
180 10
        }
181 33
        return $extenders;
182
    }
183
184
    /**
185
     * {@inheritdoc}
186
     */
187 10
    public function set($name, $value)
188
    {
189 10
        if (!($value instanceof ContaineeInterface)) {
190 9
            $value = new InstanceContainee($value);
191 9
        }
192 10
        return $this->addContainee($name, $value);
193
    }
194
195
    /**
196
     * {@inheritdoc}
197
     */
198 53
    public function instance($name, $value)
199
    {
200 53
        return $this->addContainee($name, new InstanceContainee($value));
201
    }
202
203
    /**
204
     * {@inheritdoc}
205
     */
206 20
    public function closure($name, callable $handler)
207
    {
208 20
        return $this->addContainee($name, new ClosureContainee($handler));
209
    }
210
211
    /**
212
     * {@inheritdoc}
213
     */
214 16
    public function alias($name, $origin)
215
    {
216 16
        if (!array_key_exists($origin, $this->indexOfAliases)) {
217 16
            $this->indexOfAliases[$origin] = [];
218 16
        }
219 16
        $this->indexOfAliases[$origin][] = $name;
220 16
        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 214 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...
221 9
            return $container->get($origin); // proxy
222 16
        })->factory(true);
223
    }
224
225
    /**
226
     * {@inheritdoc}
227
     */
228 10
    public function bind($name, $class = null)
229
    {
230 10
        if (isset($class)) {
231 5
            $this->alias($class, $name);
232 5
            return $this->addContainee($name, new BindContainee($class));
233
        }
234 6
        return $this->addContainee($name, new BindContainee($name));
235
    }
236
    
237
    /**
238
     * @param string $name
239
     * @param \Wandu\DI\ContaineeInterface $containee
240
     * @return \Wandu\DI\ContaineeInterface
241
     */
242 53
    private function addContainee($name, ContaineeInterface $containee)
243
    {
244 53
        $this->destroy($name);
245 53
        return $this->containees[$name] = $containee;
246
    }
247
248
    /**
249
     * {@inheritdoc}
250
     */
251 5
    public function extend($name, Closure $handler)
252
    {
253 5
        if (!array_key_exists($name, $this->extenders)) {
254 5
            $this->extenders[$name] = [];
255 5
        }
256 5
        $this->extenders[$name][] = $handler;
257 5
    }
258
259
    /**
260
     * {@inheritdoc}
261
     */
262 7
    public function register(ServiceProviderInterface $provider)
263
    {
264 7
        $provider->register($this);
265 7
        $this->providers[] = $provider;
266 7
    }
267
268
    /**
269
     * {@inheritdoc}
270
     */
271 1
    public function boot()
272
    {
273 1
        if (!$this->isBooted) {
274 1
            foreach ($this->providers as $provider) {
275 1
                $provider->boot($this);
276 1
            }
277 1
            $this->isBooted = true;
278 1
        }
279 1
        return $this;
280
    }
281
    
282
    /**
283
     * {@inheritdoc}
284
     */
285
    public function freeze($name)
286
    {
287
        $this->containees[$name]->freeze();
288
    }
289
290
    /**
291
     * {@inheritdoc}
292
     */
293 4
    public function with(array $arguments = [])
294
    {
295 4
        $new = clone $this;
296 4
        foreach ($arguments as $name => $argument) {
297 1
            if ($argument instanceof ContaineeInterface) {
298
                $new->addContainee($name, $argument);
299
            } else {
300 1
                $new->instance($name, $argument);
301
            }
302 4
        }
303 4
        return $new;
304
    }
305
    
306
    /**
307
     * {@inheritdoc}
308
     */
309 11
    public function create($class, array $arguments = [])
310
    {
311 11
        $seqArguments = static::getSeqArray($arguments);
312 11
        $assocArguments = static::getAssocArray($arguments);
313 11
        if (count($assocArguments)) {
314
            return $this->with($assocArguments)->create($class, $seqArguments);
315
        }
316 11
        $reflectionClass = new ReflectionClass($class);
317 11
        $reflectionMethod = $reflectionClass->getConstructor();
318 11
        if ($reflectionMethod) {
319
            try {
320 1
                $parameters = $this->getParameters($reflectionMethod, $arguments);
321 1
            } catch (CannotFindParameterException $e) {
322 1
                throw new CannotResolveException($class, $e->getParameter());
323
            }
324 1
        } else {
325 10
            $parameters = [];
326
        }
327 11
        return $reflectionClass->newInstanceArgs($parameters);
328
    }
329
330
    /**
331
     * {@inheritdoc}
332
     */
333 3
    public function call(callable $callee, array $arguments = [])
334
    {
335 3
        $seqArguments = static::getSeqArray($arguments);
336 3
        $assocArguments = static::getAssocArray($arguments);
337 3
        if (count($assocArguments)) {
338
            return $this->with($assocArguments)->call($callee, $seqArguments);
339
        }
340
        try {
341 3
            return call_user_func_array(
342 3
                $callee,
343 3
                $this->getParameters(new ReflectionCallable($callee), $seqArguments)
344 3
            );
345 1
        } catch (CannotFindParameterException $e) {
346 1
            throw new CannotResolveException(null, $e->getParameter());
347
        }
348
    }
349
350
    /**
351
     * {@inheritdoc}
352
     */
353 7
    public function inject($object, array $properties = [])
354
    {
355 7
        $reflectionObject = new ReflectionObject($object);
356 7
        foreach ($properties as $property => $value) {
357 3
            $this->injectProperty($reflectionObject->getProperty($property), $object, $value);
358 6
        }
359
//        foreach ($reflectionObject->getProperties() as $property) {
0 ignored issues
show
Unused Code Comprehensibility introduced by
54% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
360
//            // if property doesn't have doc type hint,
361
//            // 1.1. search in properties by property name
362
//
363
//            // if property has doc type hint,
364
//            // 2.1. search in properties by property name ( == 1.1)
365
//            // 2.2. search in properties by class name (in doc)
366
//            // 2.3. if has doc @Autowired then search in container by class name (in doc)
367
//            //      else exception!
368
//
369
//            // 1.1, 2.1
370
//            if (array_key_exists($propertyName = $property->getName(), $properties)) {
371
//                $this->injectProperty($property, $object, $properties[$propertyName]);
372
//                continue;
373
//            }
374
//            $docComment = $property->getDocComment();
375
//            $propertyClassName = $this->getClassNameFromDocComment($docComment);
376
//            if ($propertyClassName) {
377
//                // 2.2
378
//                if (array_key_exists($propertyClassName, $properties)) {
379
//                    $this->injectProperty($property, $object, $properties[$propertyClassName]);
380
//                    continue;
381
//                }
382
//                // 2.3
383
//                if ($this->hasAutowiredFromDocComment($docComment)) {
384
//                    if ($this->has($propertyClassName)) {
385
//                        $this->injectProperty($property, $object, $this->getRawItem($propertyClassName));
386
//                        continue;
387
//                    } else {
388
//                        throw new CannotInjectException($propertyClassName, $property->getName());
389
//                    }
390
//                }
391
//            }
392
//        }
393 6
    }
394
395
    /**
396
     * @param \ReflectionProperty $property
397
     * @param object $object
398
     * @param mixed $target
399
     */
400 2
    protected function injectProperty(ReflectionProperty $property, $object, $target)
401
    {
402 2
        $property->setAccessible(true);
403 2
        $property->setValue($object, $target);
404 2
    }
405
406
    /**
407
     * @param string $comment
408
     * @return string
409
     */
410
    protected function getClassNameFromDocComment($comment)
411
    {
412
        $varPosition = strpos($comment, '@var');
413
        if ($varPosition === false) {
414
            return null;
415
        }
416
        preg_match('/^([a-zA-Z0-9\\\\]+)/', ltrim(substr($comment, $varPosition + 4)), $matches);
417
        $className =  $matches[0];
418
        if ($className[0] === '\\') {
419
            $className = substr($className, 1);
420
        }
421
        return $className;
422
    }
423
424
    /**
425
     * @param string $comment
426
     * @return bool
427
     */
428
    protected function hasAutowiredFromDocComment($comment)
429
    {
430
        return strpos($comment, '@autowired') !== false ||
431
            strpos($comment, '@Autowired') !== false ||
432
            strpos($comment, '@AUTOWIRED') !== false;
433
    }
434
435
    /**
436
     * @param \ReflectionFunctionAbstract $reflectionFunction
437
     * @param array $arguments
438
     * @return array
439
     */
440 3
    protected function getParameters(ReflectionFunctionAbstract $reflectionFunction, array $arguments)
441
    {
442 3
        $parametersToReturn = [];
443 3
        $parameters = $arguments;
444 3
        foreach ($reflectionFunction->getParameters() as $param) {
445
            // if parameter doesn't have type hint,
446
            // 1.2. insert remain arguments
447
            // 1.3. if has default value, insert default value.
448
            // 1.4. exception
449
450
            // if parameter has type hint,
451
            // 2.2. search in arguments by class name
452
            // 2.3. search in container by class name
453
            // 2.4. if has default value, insert default vlue. ( == 1.3)
454
            // 2.5. exception ( == 1.4)a
455
456 1
            $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...
457
            try {
458 1
                $paramClass = $param->getClass();
459 1
                if ($paramClass) { // 2.*
460
                    // 2.2
461 1
                    $paramClassName = $paramClass->getName();
462 1
                    if (isset($arguments[$paramClassName])) {
463
                        $parametersToReturn[] = $arguments[$paramClassName];
464
                        continue;
465
                    }
466
                    // 2.3
467 1
                    if ($this->has($paramClassName)) {
468 1
                        $parametersToReturn[] = $this->get($paramClassName);
469 1
                        continue;
470
                    }
471 1
                } else { // 1.*
472
                    // 1.2
473 1
                    if (count($parameters)) {
474
                        $parametersToReturn[] = array_shift($parameters);
475
                        continue;
476
                    }
477
                }
478
                // 1.3, 2.4
479 1
                if ($param->isDefaultValueAvailable()) {
480 1
                    $parametersToReturn[] = $param->getDefaultValue();
481 1
                    continue;
482
                }
483
                // 1.4, 2.5
484 1
                throw new CannotFindParameterException($paramName);
485 1
            } catch (ReflectionException $e) {
486
                throw new CannotFindParameterException($paramName);
487
            }
488 3
        }
489 3
        return array_merge($parametersToReturn, $parameters);
490
    }
491
492
    /**
493
     * @param array $array
494
     * @return array
495
     */
496 13
    protected static function getSeqArray(array $array)
497
    {
498 13
        $arrayToReturn = [];
499 13
        foreach ($array as $key => $item) {
500
            if (is_int($key)) {
501
                $arrayToReturn[] = $item;
502
            }
503 13
        }
504 13
        return $arrayToReturn;
505
    }
506
507
    /**
508
     * @param array $array
509
     * @return array
510
     */
511 13
    protected static function getAssocArray(array $array)
512
    {
513 13
        $arrayToReturn = [];
514 13
        foreach ($array as $key => $item) {
515
            if (!is_int($key)) {
516
                $arrayToReturn[$key] = $item;
517
            }
518 13
        }
519 13
        return $arrayToReturn;
520
    }
521
}
522