Container::hasConstructor()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 4
rs 10
cc 1
eloc 2
nc 1
nop 1
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
        $reflector = Internal\ReflectionClassFactory::create($instance);
302
303
        if (!$this->hasConstructor($reflector)) {
304
            return $this->resolveInstanceWithoutConstructor($reflector);
305
        }
306
307
        if (is_array($parameters) && empty(sizeof($parameters))) {
308
            $constructorParams = $this->getMethodParameters($reflector, '__construct');
309
310
            if (!is_null($constructorParams)) {
311
                $params = $this->resolveMethodParameters($constructorParams);
312
            }
313
        } elseif (is_array($parameters) && !empty(sizeof($parameters))) {
314
            $params = $this->resolveMethodParameters($parameters);
315
        }
316
317
        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...
318
    }
319
320
    /**
321
     * Resolve method parameters.
322
     *
323
     * @param array $params The unresolvable method.
324
     * @return array
325
     */
326
    protected function resolveMethodParameters($params = [])
327
    {
328
        if (!is_array($params)) {
329
            throw new \InvalidArgumentException(
330
                sprintf("Parameter 1 of %s must be an array.", __METHOD__)
331
            );
332
        }
333
334
        foreach ($params as $key => $value) {
335
            if ($value instanceof \ReflectionParameter) {
336
                $class = $value->getClass();
337
338
                if ($class instanceof \ReflectionClass) {
339 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...
340
                        $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...
341
                    } else {
342
                        $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...
343
                    }
344
                } else {
345
                    $params[$key] = ($value->isDefaultValueAvailable()
346
                        ? $value->getDefaultValue() : null);
347
                }
348
            } else {
349
                if (is_string($value) && class_exists($value)) {
350
                    $params[$key] = $this->circularDependencyResolver($value);
351
                } elseif ($value instanceof \Closure) {
352
                    $params[$key] = $value($this);
353
                } else {
354
                    $params[$key] = $value;
355
                }
356
            }
357
        }
358
359
        return $params;
360
    }
361
362
    /**
363
     * Recursively resolving class dependency.
364
     *
365
     * @param string $class The valid class name.
366
     * @return object
367
     */
368
    protected function circularDependencyResolver($class)
369
    {
370
        if (!is_string($class) && !class_exists($class)) {
371
            throw Internal\Exception\ReflectionExceptionFactory::invalidArgument(
372
                sprintf("Parameter 1 of %s must be a string of valid class name.", __METHOD__)
373
            );
374
        }
375
376
        $reflector = Internal\ReflectionClassFactory::create($class);
377
378
        if (!$this->hasConstructor($reflector)) {
379
            return $this->resolveInstanceWithoutConstructor($reflector);
380
        } else {
381
            $param = $this->getMethodParameters($reflector, '__construct');
382
383
            if (empty($param)) {
384
                return $reflector->newInstance();
385
            } else {
386
                foreach ($param as $key => $value) {
387
                    $class = $value->getClass();
388
389
                    if ($class instanceof \ReflectionClass) {
390 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...
391
                            $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...
392
                        } else {
393
                            $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...
394
                        }
395
                    }
396
                }
397
398
                return $reflector->newInstanceArgs($param);
399
            }
400
        }
401
    }
402
403
    /**
404
     * Get concrete implementation from given abstract.
405
     *
406
     * @param string $abstract
407
     * @return \Closure|string|null
408
     */
409
    public function getConcrete($abstract)
410
    {
411
        return (isset($this->bindings[$abstract])
412
            ? $this->bindings[$abstract]['concrete']
413
            : null);
414
    }
415
416
    /**
417
     * Get concrete implementation from abstract.
418
     *
419
     * @param string $interface The interface name.
420
     * @return object
421
     */
422
    protected function getConcreteFromInterface($interface)
423
    {
424
        if (!$this->isAbstractExists($interface)) {
425
            throw Internal\Exception\ReflectionExceptionFactory::runtime(
426
                sprintf("%s has no concrete implementation in the class binding stack.", $interface)
427
            );
428
        }
429
430
        try {
431
            return $this->getResolvedSingleton($interface);
432
        } catch (\Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
433
        }
434
435
        $concrete = $this->bindings[$interface]['concrete'];
436
437
        $object = $concrete instanceof \Closure ? $concrete($this) : $this->build($concrete);
438
439
        if ($this->isShared($interface)) {
440
            $this->markAsResolved($interface, $object, 'singleton');
441
        } else {
442
            $this->markAsResolved($interface, $object);
443
        }
444
445
        return $object;
446
    }
447
448
    /**
449
     * Determine if current reflection object has constructor.
450
     *
451
     * @param \ReflectionClass $refl The current reflection class object.
452
     * @return boolean
453
     */
454
    protected function hasConstructor(Internal\ReflectionClassFactory $refl)
455
    {
456
        return $refl->hasMethod('__construct');
457
    }
458
459
    /**
460
     * Determine if unresolvable class name has cloneable.
461
     *
462
     * @param \ReflectionClass $refl The current reflection class object.
463
     * @return boolean
464
     */
465
    protected function isCloneable(Internal\ReflectionClassFactory $refl)
466
    {
467
        return $refl->hasMethod('__clone');
468
    }
469
470
    /**
471
     * Determine if unresolvable class name has serializable.
472
     *
473
     * @param \ReflectionClass $refl The current reflection class object.
474
     * @return boolean
475
     */
476
    protected function isSerializable(Internal\ReflectionClassFactory $refl)
477
    {
478
        return $refl->hasMethod('__sleep');
479
    }
480
481
    /**
482
     * Resolving class name without constructor.
483
     *
484
     * @param \ReflectionClass $refl An instance of \ReflectionClass
485
     */
486
    protected function resolveInstanceWithoutConstructor(Internal\ReflectionClassFactory $refl)
487
    {
488
        return $refl->newInstanceWithoutConstructor();
489
    }
490
491
    /**
492
     * Get method parameters.
493
     *
494
     * @param \ReflectionClass $refl An reflection class instance.
495
     * @param string $method The method name.
496
     * @return array
497
     */
498
    protected function getMethodParameters(Internal\ReflectionClassFactory $refl, $method)
499
    {
500
        return ($refl->hasMethod($method) ? $refl->getMethod($method)->getParameters() : null);
501
    }
502
503
    /**
504
     * Mark resolved class name to true.
505
     *
506
     * @param string $abstract The resolved class name.
507
     * @param object $resolvedInstance The object instance of resolved abstract.
508
     * @param mixed $flag The concrete-resolving behavior.
509
     * @return void
510
     */
511
    protected function markAsResolved($abstract, $resolvedInstance, $flag = [])
512
    {
513
        if (!is_array($flag)) {
514
            $flag = array_slice(func_get_args(), 2);
515
        }
516
517
        if ($this->isAbstractExists($abstract)) {
518
            $this->resolved[$abstract] = [
519
                'concrete' => $resolvedInstance,
520
                'resolved' => true,
521
                'flag' => join('|', $flag)
522
            ];
523
        }
524
    }
525
526
    /**
527
     * Register binding into container stack.
528
     *
529
     * @param string $abstract The unresolvable class name.
530
     * @param \Closure|string $concrete Closure or class name being bound to the class name.
531
     */
532
    public function bind($abstract, $concrete = null, $shared = false)
533
    {
534
        if (is_null($concrete)) {
535
            $concrete = $abstract;
536
        }
537
538
        if (!($concrete instanceof \Closure)) {
539
            $concrete = $this->turnIntoResolvableClosure($abstract, $concrete);
540
        }
541
542
        $this->bindings[$abstract] = compact('concrete', 'shared');
543
    }
544
545
    /**
546
     * Register shared binding into container stack.
547
     *
548
     * @param string $abstract The unresolvable abstract
549
     * @param \Closure|string|null The concrete form of supplied abstract.
550
     */
551
    public function singleton($abstract, $concrete = null)
552
    {
553
        $this->bind($abstract, $concrete, true);
554
    }
555
556
    /**
557
     * Bind service into binding container stack if supplied class name
558
     * not being bound.
559
     *
560
     * @param string $abstract The unresolvable class name.
561
     * @param \Closure|string $concrete Closure or class name begin bound to the class name.
562
     */
563
    public function bindIf($abstract, $concrete)
564
    {
565
        if (!$this->isBound($abstract)) {
566
            $this->bind($abstract, $concrete);
567
        }
568
    }
569
570
    /**
571
     * Call defined instance.
572
     *
573
     * @param string $instance The class name to invoke/call.
574
     * @param array $args The class name __invoke method argument.
575
     * @return mixed|void
576
     */
577
    public function callInstance($instance, $args = [])
578
    {
579
        $args = (is_array($args) ? $args : array_slice(func_get_args(), 1));
580
        
581
        $current = $this->make($instance);
582
583
        return call_user_func_array($current, $args);
584
    }
585
586
    /**
587
     * Determine if class name has been bound or not.
588
     *
589
     * @param string $abstract The unresolvable class name.
590
     * @return bool
591
     */
592
    public function isBound($abstract)
593
    {
594
        return $this->isAbstractExists($abstract);
595
    }
596
597
    /**
598
     * Determine if a given type is shared.
599
     *
600
     * @param string $abstract
601
     * @return bool
602
     */
603
    public function isShared($abstract)
604
    {
605
        if (!isset($this->bindings[$abstract])) {
606
            throw Internal\Exception\ReflectionExceptionFactory::invalidArgument(
607
                sprintf("Parameter 1 of %s must be valid keys in binding container stack.", __METHOD__)
608
            );
609
        }
610
611
        return ($this->bindings[$abstract]['shared'] ? true : false);
612
    }
613
614
    /**
615
     * Turn class name into resolvable closure.
616
     *
617
     * @param string $abstract The class name
618
     * @param \Closure|string $concrete Can be instance of \Closure or class name.
619
     * @return \Closure
620
     */
621
    protected function turnIntoResolvableClosure($abstract, $concrete)
622
    {
623
        return function (Container $container, $parameters = []) use ($abstract, $concrete) {
624
            return ($abstract == $concrete ? $container->resolve($abstract)
625
                : $container->resolve($concrete, $parameters));
0 ignored issues
show
Bug introduced by
It seems like $concrete defined by parameter $concrete on line 621 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...
626
        };
627
    }
628
}
629