Completed
Push — master ( 5d7e0f...d5e6f7 )
by
unknown
02:39
created

Container::make()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 5
rs 9.4285
cc 2
eloc 3
nc 1
nop 2
1
<?php
2
3
/**
4
 * (c) Paulus Gandung Prakosa ([email protected])
5
 */
6
7
namespace DependencyInjection;
8
9
use Psr\Container\ContainerInterface;
10
use DependencyInjection\Exception\ContainerException;
11
use DependencyInjection\Exception\NotFoundException;
12
13
class Container implements \ArrayAccess, ContainerInterface
14
{
15
    /**
16
     * @var array
17
     */
18
    private $bindings = [];
19
20
    /**
21
     * @var array
22
     */
23
    private $resolved = [];
24
25
    /**
26
     * @var array
27
     */
28
    private $aliases = [];
29
30
    /**
31
     * Resolving all dependencies in the supplied class or object instance constructor.
32
     *
33
     * @param string $instance The class name.
34
     * @param array $parameters List of needed class dependency.
35
     * @return object
36
     */
37
    public function make($instance, $parameters = [])
38
    {
39
        return $this->resolve($instance, is_array($parameters) ? $parameters
40
            : array_slice(func_get_args(), 1));
41
    }
42
43
    /**
44
     * Register a service alias.
45
     *
46
     * @param string $alias The alias name.
47
     * @param string $abstract The class name.
48
     */
49
    public function register($alias, $abstract)
50
    {
51
        if (!is_string($alias) || !is_string($abstract)) {
52
            throw new \InvalidArgumentException(
53
                sprintf("Parameter 1 and 2 of %s must be a string.", __METHOD__)
54
            );
55
        }
56
57
        if (!isset($this->aliases[$alias])) {
58
            $this->aliases[$alias] = $this->make($abstract);
59
        }
60
61
        return $this;
62
    }
63
64
    /**
65
     * Determine if registered alias were exists.
66
     *
67
     * @param string $alias The alias name.
68
     */
69
    public function isAliasExists($alias)
70
    {
71
        return isset($this->aliases[$alias]);
72
    }
73
74
    /**
75
     * Finds an entry of the container by its identifier and returns it.
76
     *
77
     * @param string $id Identifier of the entry to look for.
78
     *
79
     * @throws NotFoundExceptionInterface  No entry was found for **this** identifier.
80
     * @throws ContainerExceptionInterface Error while retrieving the entry.
81
     *
82
     * @return mixed Entry.
83
     */
84
    public function get($id)
85
    {
86
        if (!$this->isAliasExists($id)) {
87
            throw new NotFoundException(
88
                sprintf("Identifier %s was not found in our service container stack.", $id)
89
            );
90
        }
91
92
        return $this->aliases[$id];
93
    }
94
    
95
    /**
96
     * Returns true if the container can return an entry for the given identifier.
97
     * Returns false otherwise.
98
     *
99
     * `has($id)` returning true does not mean that `get($id)` will not throw an exception.
100
     * It does however mean that `get($id)` will not throw a `NotFoundExceptionInterface`.
101
     *
102
     * @param string $id Identifier of the entry to look for.
103
     *
104
     * @return bool
105
     */
106
    public function has($id)
107
    {
108
        return $this->isAliasExists($id);
109
    }
110
111
    /**
112
     * {@inheritdoc}
113
     */
114
    public function offsetExists($offset)
115
    {
116
        return $this->isBound($offset);
117
    }
118
119
    /**
120
     * {@inheritdoc}
121
     */
122
    public function offsetGet($offset)
123
    {
124
        return $this->make($offset);
125
    }
126
127
    /**
128
     * {@inheritdoc}
129
     */
130
    public function offsetSet($offset, $value)
131
    {
132
        $this->bind($offset, $value instanceof \Closure ? $value : $this->turnIntoResolvableClosure($offset, $value));
133
    }
134
135
    /**
136
     * {@inheritdoc}
137
     */
138
    public function offsetUnset($offset)
139
    {
140
        unset($this->bindings[$offset], $this->resolved[$offset]);
141
    }
142
143
    /**
144
     * Determine if defined abstract class name were in resolved concrete stack and it was a
145
     * singleton.
146
     *
147
     * @param string $abstract The resolved abstract class name.
148
     */
149
    public function hasResolvedSingleton($abstract)
150
    {
151
        $flag = $this->getResolvedConcreteFlag($abstract);
152
153
        return in_array('singleton', $flag, true);
154
    }
155
156
    /**
157
     * Get singleton resolved concrete from defined abstract class name.
158
     *
159
     * @param string $abstract The resolved abstract class name.
160
     */
161
    public function getResolvedSingleton($abstract)
162
    {
163
        return ($this->hasResolvedSingleton($abstract)
164
            ? $this->resolved[$abstract]['concrete']
165
            : null);
166
    }
167
168
    /**
169
     * Determine if defined abstract class name were in resolved concrete stack.
170
     *
171
     * @param string $abstract The resolved abstract class name.
172
     */
173
    public function hasResolvedConcrete($abstract)
174
    {
175
        return isset($this->resolved[$abstract]);
176
    }
177
178
    /**
179
     * Get flag of resolved concrete behavior on abstract class name.
180
     *
181
     * @param string $abstract The resolved abstract class name.
182
     */
183
    public function getResolvedConcreteFlag($abstract)
184
    {
185
        if (!$this->hasResolvedConcrete($abstract)) {
186
            throw Internal\Exception\ReflectionExceptionFactory::invalidArgument(
187
                sprintf(
188
                    "Parameter 1 of %s must be an abstract class name which exists in resolved concrete stack.",
189
                     __METHOD__
190
                )
191
            );
192
        }
193
194
        return explode('|', $this->resolved[$abstract]['flag']);
195
    }
196
197
    /**
198
     * Get resolved concrete from defined abstract class name.
199
     *
200
     * @param string $abstract The resolved abstract class name.
201
     */
202
    public function getResolvedConcrete($abstract)
203
    {
204
        return ($this->hasResolvedConcrete($abstract)
205
            ? $this->resolved[$abstract]['concrete']
206
            : null);
207
    }
208
209
    /**
210
     * Get list of unresolved class name from class binding stack.
211
     *
212
     * @return string
213
     */
214
    protected function getAbstracts()
215
    {
216
        return array_keys($this->bindings);
217
    }
218
219
    /**
220
     * Determine if unresolved class name is exists.
221
     *
222
     * @param string $abstract The unresolved class name.
223
     * @return bool
224
     */
225
    public function isAbstractExists($abstract)
226
    {
227
        return array_key_exists($abstract, $this->bindings);
228
    }
229
230
    /**
231
     * Determine if unresolved abstract is an interface.
232
     *
233
     * @param string $abstract The unresolved abstract name.
234
     */
235
    public function isInterface($abstract)
236
    {
237
        $reflector = Internal\ReflectionClassFactory::create($abstract);
238
239
        return $reflector->isInterface();
240
    }
241
242
    /**
243
     * Get concrete list of dependencies based on supplied class name.
244
     *
245
     * @param string $abstract The unresolved class name.
246
     * @return array
247
     */
248
    public function getAbstractDependencies($abstract)
249
    {
250
        return ($this->isAbstractExists($abstract) ? $this->bindings[$abstract] : null);
251
    }
252
253
    /**
254
     * Resolve class dependencies in the supplied class name.
255
     *
256
     * @param string $instance The class name.
257
     * @param array $parameters The needed class dependency.
258
     * @return object
259
     */
260
    protected function resolve($instance, $parameters = [])
261
    {
262
        // If the current abstract is an interface,
263
        // just return the concrete implementation to the callee.
264
        if ($this->isInterface($instance)) {
265
            return $this->getConcreteFromInterface($instance);
266
        }
267
268
        // If the current abstract type being managed as a singleton,
269
        // just return it to the caller instead of reinstantiating it.
270
        try {
271
            return $this->getResolvedSingleton($instance);
272
        } catch (\Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
273
        }
274
        
275
        $concrete = $this->getConcrete($instance);
276
277
        if (!is_null($concrete)) {
278
            $object = $this->build($instance,
279
                $concrete instanceof \Closure ? $concrete($this) : $concrete);
280
281
            if ($this->isShared($instance)) {
282
                $this->markAsResolved($instance, $object, 'singleton');
283
            } else {
284
                $this->markAsResolved($instance, $object);
285
            }
286
        } else {
287
            $object = $this->build($instance, $parameters);
288
        }
289
290
        return $object;
291
    }
292
293
    protected function build($instance, $parameters = [])
294
    {
295
        $parameters = (is_null($parameters)
296
            ? []
297
            : (is_array($parameters)
298
                ? $parameters
299
                : array_slice(func_get_args(), 1)));
300
301
        var_dump($parameters);
0 ignored issues
show
Security Debugging Code introduced by
var_dump($parameters); looks like debug code. Are you sure you do not want to remove it? This might expose sensitive data.
Loading history...
302
303
        $reflector = Internal\ReflectionClassFactory::create($instance);
304
305
        if (!$this->hasConstructor($reflector)) {
306
            return $this->resolveInstanceWithoutConstructor($reflector);
307
        }
308
309
        if (is_array($parameters) && empty(sizeof($parameters))) {
310
            $constructorParams = $this->getMethodParameters($reflector, '__construct');
311
312
            if (!is_null($constructorParams)) {
313
                $params = $this->resolveMethodParameters($constructorParams);
314
            }
315
        } elseif (is_array($parameters) && !empty(sizeof($parameters))) {
316
            $params = $this->resolveMethodParameters($parameters);
317
        }
318
319
        return $reflector->newInstanceArgs(!empty($parameters) ? $parameters : $params);
0 ignored issues
show
Bug introduced by
The variable $params does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
320
    }
321
322
    /**
323
     * Resolve method parameters.
324
     *
325
     * @param array $params The unresolvable method.
326
     * @return array
327
     */
328
    protected function resolveMethodParameters($params = [])
329
    {
330
        if (!is_array($params)) {
331
            throw new \InvalidArgumentException(
332
                sprintf("Parameter 1 of %s must be an array.", __METHOD__)
333
            );
334
        }
335
336
        foreach ($params as $key => $value) {
337
            if ($value instanceof \ReflectionParameter) {
338
                $class = $value->getClass();
339
340
                if ($class instanceof \ReflectionClass) {
341 View Code Duplication
                    if ($class->isInterface()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
342
                        $params[$key] = $this->getConcreteFromInterface($class->getName());
0 ignored issues
show
Bug introduced by
Consider using $class->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
343
                    } else {
344
                        $params[$key] = $this->circularDependencyResolver($class->getName());
0 ignored issues
show
Bug introduced by
Consider using $class->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
345
                    }
346
                } else {
347
                    $params[$key] = ($value->isDefaultValueAvailable()
348
                        ? $value->getDefaultValue() : null);
349
                }
350
            } else {
351
                if (is_string($value) && class_exists($value)) {
352
                    $params[$key] = $this->circularDependencyResolver($value);
353
                } elseif ($value instanceof \Closure) {
354
                    $params[$key] = $value($this);
355
                } else {
356
                    $params[$key] = $value;
357
                }
358
            }
359
        }
360
361
        return $params;
362
    }
363
364
    /**
365
     * Recursively resolving class dependency.
366
     *
367
     * @param string $class The valid class name.
368
     * @return object
369
     */
370
    protected function circularDependencyResolver($class)
371
    {
372
        if (!is_string($class) && !class_exists($class)) {
373
            throw Internal\Exception\ReflectionExceptionFactory::invalidArgument(
374
                sprintf("Parameter 1 of %s must be a string of valid class name.", __METHOD__)
375
            );
376
        }
377
378
        $reflector = Internal\ReflectionClassFactory::create($class);
379
380
        if (!$this->hasConstructor($reflector)) {
381
            return $this->resolveInstanceWithoutConstructor($reflector);
382
        } else {
383
            $param = $this->getMethodParameters($reflector, '__construct');
384
385
            if (empty($param)) {
386
                return $reflector->newInstance();
387
            } else {
388
                foreach ($param as $key => $value) {
389
                    $class = $value->getClass();
390
391
                    if ($class instanceof \ReflectionClass) {
392 View Code Duplication
                        if ($class->isInterface()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
393
                            $param[$key] = $this->getConcreteFromInterface($class->getName());
0 ignored issues
show
Bug introduced by
Consider using $class->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
394
                        } else {
395
                            $param[$key] = $this->circularDependencyResolver($class->getName());
0 ignored issues
show
Bug introduced by
Consider using $class->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
396
                        }
397
                    }
398
                }
399
400
                return $reflector->newInstanceArgs($param);
401
            }
402
        }
403
    }
404
405
    /**
406
     * Get concrete implementation from given abstract.
407
     *
408
     * @param string $abstract
409
     * @return \Closure|string|null
410
     */
411
    public function getConcrete($abstract)
412
    {
413
        return (isset($this->bindings[$abstract])
414
            ? $this->bindings[$abstract]['concrete']
415
            : null);
416
    }
417
418
    /**
419
     * Get concrete implementation from abstract.
420
     *
421
     * @param string $interface The interface name.
422
     * @return object
423
     */
424
    protected function getConcreteFromInterface($interface)
425
    {
426
        if (!$this->isAbstractExists($interface)) {
427
            throw Internal\Exception\ReflectionExceptionFactory::runtime(
428
                sprintf("%s has no concrete implementation in the class binding stack.", $interface)
429
            );
430
        }
431
432
        try {
433
            return $this->getResolvedSingleton($interface);
434
        } catch (\Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
435
        }
436
437
        $concrete = $this->bindings[$interface]['concrete'];
438
439
        $object = $concrete instanceof \Closure ? $concrete($this) : $this->build($concrete);
440
441
        if ($this->isShared($interface)) {
442
            $this->markAsResolved($interface, $object, 'singleton');
443
        } else {
444
            $this->markAsResolved($interface, $object);
445
        }
446
447
        return $object;
448
    }
449
450
    /**
451
     * Determine if current reflection object has constructor.
452
     *
453
     * @param \ReflectionClass $refl The current reflection class object.
454
     * @return boolean
455
     */
456
    protected function hasConstructor(Internal\ReflectionClassFactory $refl)
457
    {
458
        return $refl->hasMethod('__construct');
459
    }
460
461
    /**
462
     * Determine if unresolvable class name has cloneable.
463
     *
464
     * @param \ReflectionClass $refl The current reflection class object.
465
     * @return boolean
466
     */
467
    protected function isCloneable(Internal\ReflectionClassFactory $refl)
468
    {
469
        return $refl->hasMethod('__clone');
470
    }
471
472
    /**
473
     * Determine if unresolvable class name has serializable.
474
     *
475
     * @param \ReflectionClass $refl The current reflection class object.
476
     * @return boolean
477
     */
478
    protected function isSerializable(Internal\ReflectionClassFactory $refl)
479
    {
480
        return $refl->hasMethod('__sleep');
481
    }
482
483
    /**
484
     * Resolving class name without constructor.
485
     *
486
     * @param \ReflectionClass $refl An instance of \ReflectionClass
487
     */
488
    protected function resolveInstanceWithoutConstructor(Internal\ReflectionClassFactory $refl)
489
    {
490
        return $refl->newInstanceWithoutConstructor();
491
    }
492
493
    /**
494
     * Get method parameters.
495
     *
496
     * @param \ReflectionClass $refl An reflection class instance.
497
     * @param string $method The method name.
498
     * @return array
499
     */
500
    protected function getMethodParameters(Internal\ReflectionClassFactory $refl, $method)
501
    {
502
        return ($refl->hasMethod($method) ? $refl->getMethod($method)->getParameters() : null);
503
    }
504
505
    /**
506
     * Mark resolved class name to true.
507
     *
508
     * @param string $abstract The resolved class name.
509
     * @param object $resolvedInstance The object instance of resolved abstract.
510
     * @param mixed $flag The concrete-resolving behavior.
511
     * @return void
512
     */
513
    protected function markAsResolved($abstract, $resolvedInstance, $flag = [])
514
    {
515
        if (!is_array($flag)) {
516
            $flag = array_slice(func_get_args(), 2);
517
        }
518
519
        if ($this->isAbstractExists($abstract)) {
520
            $this->resolved[$abstract] = [
521
                'concrete' => $resolvedInstance,
522
                'resolved' => true,
523
                'flag' => join('|', $flag)
524
            ];
525
        }
526
    }
527
528
    /**
529
     * Register binding into container stack.
530
     *
531
     * @param string $abstract The unresolvable class name.
532
     * @param \Closure|string $concrete Closure or class name being bound to the class name.
533
     */
534
    public function bind($abstract, $concrete = null, $shared = false)
535
    {
536
        if (is_null($concrete)) {
537
            $concrete = $abstract;
538
        }
539
540
        if (!($concrete instanceof \Closure)) {
541
            $concrete = $this->turnIntoResolvableClosure($abstract, $concrete);
542
        }
543
544
        $this->bindings[$abstract] = compact('concrete', 'shared');
545
    }
546
547
    /**
548
     * Register shared binding into container stack.
549
     *
550
     * @param string $abstract The unresolvable abstract
551
     * @param \Closure|string|null The concrete form of supplied abstract.
552
     */
553
    public function singleton($abstract, $concrete = null)
554
    {
555
        $this->bind($abstract, $concrete, true);
556
    }
557
558
    /**
559
     * Bind service into binding container stack if supplied class name
560
     * not being bound.
561
     *
562
     * @param string $abstract The unresolvable class name.
563
     * @param \Closure|string $concrete Closure or class name begin bound to the class name.
564
     */
565
    public function bindIf($abstract, $concrete)
566
    {
567
        if (!$this->isBound($abstract)) {
568
            $this->bind($abstract, $concrete);
569
        }
570
    }
571
572
    /**
573
     * Call defined instance.
574
     *
575
     * @param string $instance The class name to invoke/call.
576
     * @param array $args The class name __invoke method argument.
577
     * @return mixed|void
578
     */
579
    public function callInstance($instance, $args = [])
580
    {
581
        $args = (is_array($args) ? $args : array_slice(func_get_args(), 1));
582
        
583
        $current = $this->make($instance);
584
585
        var_dump($current);
0 ignored issues
show
Security Debugging Code introduced by
var_dump($current); looks like debug code. Are you sure you do not want to remove it? This might expose sensitive data.
Loading history...
586
587
        return call_user_func_array($current, $args);
588
    }
589
590
    /**
591
     * Determine if class name has been bound or not.
592
     *
593
     * @param string $abstract The unresolvable class name.
594
     * @return bool
595
     */
596
    public function isBound($abstract)
597
    {
598
        return $this->isAbstractExists($abstract);
599
    }
600
601
    /**
602
     * Determine if a given type is shared.
603
     *
604
     * @param string $abstract
605
     * @return bool
606
     */
607
    public function isShared($abstract)
608
    {
609
        if (!isset($this->bindings[$abstract])) {
610
            throw Internal\Exception\ReflectionExceptionFactory::invalidArgument(
611
                sprintf("Parameter 1 of %s must be valid keys in binding container stack.", __METHOD__)
612
            );
613
        }
614
615
        return ($this->bindings[$abstract]['shared'] ? true : false);
616
    }
617
618
    /**
619
     * Turn class name into resolvable closure.
620
     *
621
     * @param string $abstract The class name
622
     * @param \Closure|string $concrete Can be instance of \Closure or class name.
623
     * @return \Closure
624
     */
625
    protected function turnIntoResolvableClosure($abstract, $concrete)
626
    {
627
        return function (Container $container, $parameters = []) use ($abstract, $concrete) {
628
            return ($abstract == $concrete ? $container->resolve($abstract)
629
                : $container->resolve($concrete, $parameters));
0 ignored issues
show
Bug introduced by
It seems like $concrete defined by parameter $concrete on line 625 can also be of type object<Closure>; however, DependencyInjection\Container::resolve() 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...
630
        };
631
    }
632
}
633