Test Setup Failed
Push — master ( cf7f1a...4fdf26 )
by
unknown
02:29
created

Container::circularDependencyResolver()   C

Complexity

Conditions 8
Paths 7

Size

Total Lines 36
Code Lines 21

Duplication

Lines 9
Ratio 25 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 9
loc 36
rs 5.3846
cc 8
eloc 21
nc 7
nop 1
1
<?php
2
3
/**
4
 * (c) Paulus Gandung Prakosa ([email protected])
5
 */
6
7
namespace DependencyInjection;
8
9
use \ArrayAccess;
10
11
class Container implements \ArrayAccess
12
{
13
    /**
14
     * @var array
15
     */
16
    private $bindings = [];
17
18
    /**
19
     * @var array
20
     */
21
    private $resolved = [];
22
23
    /**
24
     * @var array
25
     */
26
    private $aliases = [];
27
28
    /**
29
     * Resolving all dependencies in the supplied class or object instance constructor.
30
     *
31
     * @param string $instance The class name.
32
     * @param array $parameters List of needed class dependency.
33
     * @return object
34
     */
35
    public function make($instance, $parameters = [])
36
    {
37
        if ($this->isAbstractExists($instance)) {
38
            return $this->resolve($instance, $this->getConcrete($instance));
39
        }
40
41
        return $this->resolve($instance, is_array($parameters) ? $parameters
42
            : array_slice(func_get_args(), 1));
43
    }
44
45
    /**
46
     * Register a service alias.
47
     *
48
     * @param string $alias The alias name.
49
     * @param string $abstract The class name.
50
     */
51
    public function register($alias, $abstract)
52
    {
53
        if (!is_string($alias) || !is_string($abstract)) {
54
            throw new \InvalidArgumentException(
55
                sprintf("Parameter 1 and 2 of %s must be a string.", __METHOD__)
56
            );
57
        }
58
59
        if (!isset($this->aliases[$alias])) {
60
            $this->aliases[$alias] = $this->make($abstract);
61
        }
62
63
        return $this;
64
    }
65
66
    /**
67
     * Determine if registered alias were exists.
68
     *
69
     * @param string $alias The alias name.
70
     */
71
    public function isAliasExists($alias)
72
    {
73
        return isset($this->aliases[$alias]);
74
    }
75
76
    /**
77
     * Get concrete implementation from supplied alias name.
78
     *
79
     * @param string $alias The alias name.
80
     */
81
    public function get($alias)
82
    {
83
        if (!$this->isAliasExists($alias)) {
84
            throw new \InvalidArgumentException(
85
                sprintf("Parameter 1 of %s must be valid alias name.", __METHOD__)
86
            );
87
        }
88
89
        return $this->aliases[$alias];
90
    }
91
    
92
    /**
93
     * {@inheritdoc}
94
     */
95
    public function offsetExists($offset)
96
    {
97
        return $this->isBound($offset);
98
    }
99
100
    /**
101
     * {@inheritdoc}
102
     */
103
    public function offsetGet($offset)
104
    {
105
        return $this->make($offset);
106
    }
107
108
    /**
109
     * {@inheritdoc}
110
     */
111
    public function offsetSet($offset, $value)
112
    {
113
        $this->bind($offset, $value instanceof \Closure ? $value : $this->turnIntoResolvableClosure($offset, $value));
114
    }
115
116
    /**
117
     * {@inheritdoc}
118
     */
119
    public function offsetUnset($offset)
120
    {
121
        unset($this->bindings[$offset], $this->resolved[$offset]);
122
    }
123
124
    /**
125
     * Get list of unresolved class name from class binding stack.
126
     *
127
     * @return string
128
     */
129
    protected function getAbstracts()
130
    {
131
        return array_keys($this->bindings);
132
    }
133
134
    /**
135
     * Determine if unresolved class name is exists.
136
     *
137
     * @param string $abstract The unresolved class name.
138
     * @return bool
139
     */
140
    public function isAbstractExists($abstract)
141
    {
142
        return array_key_exists($abstract, $this->bindings);
143
    }
144
145
    /**
146
     * Determine if concrete dependency is exists.
147
     *
148
     * @param mixed $concrete The concrete dependency.
149
     * @return bool
150
     */
151
    public function isConcreteExists($concrete)
152
    {
153
        foreach (array_values($this->bindings) as $value) {
154
            if (in_array($concrete, $value, true)) {
155
                $isConcreteExists = true;
156
157
                break;
158
            }
159
        }
160
161
        return (isset($isConcreteExists) ? $isConcreteExists : false);
162
    }
163
164
    public function isInterface($abstract)
165
    {
166
        $reflector = Internal\ReflectionClassFactory::create($abstract);
167
168
        return $reflector->isInterface();
169
    }
170
171
    /**
172
     * Get concrete list of dependencies based on supplied class name.
173
     *
174
     * @param string $abstract The unresolved class name.
175
     * @return array
176
     */
177
    public function getConcrete($abstract)
178
    {
179
        return ($this->isAbstractExists($abstract) ? $this->bindings[$abstract] : null);
180
    }
181
182
    /**
183
     * Resolve class dependencies in the supplied class name.
184
     *
185
     * @param string $instance The class name.
186
     * @param array $parameters The needed class dependency.
187
     * @return object
188
     */
189
    protected function resolve($instance, $parameters = [])
190
    {
191
        $reflector = Internal\ReflectionClassFactory::create($instance);
192
193
        if (!$this->hasConstructor($reflector)) {
194
            $this->markAsResolved($instance);
195
            
196
            return $this->resolveInstanceWithoutConstructor($reflector);
197
        }
198
199
        if (is_array($parameters) && empty(sizeof($parameters))) {
200
            $constructorParams = $this->getMethodParameters($reflector, '__construct');
201
202
            if (!is_null($constructorParams)) {
203
                $params = $this->resolveMethodParameters($constructorParams);
204
            }
205
        } elseif (is_array($parameters) && !empty(sizeof($parameters))) {
206
            $params = $this->resolveMethodParameters($parameters);
207
        }
208
209
        $this->markAsResolved($instance);
210
211
        return $reflector->newInstanceArgs($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...
212
    }
213
214
    /**
215
     * Resolve method parameters.
216
     *
217
     * @param array $params The unresolvable method.
218
     * @return array
219
     */
220
    protected function resolveMethodParameters($params = [])
221
    {
222
        if (!is_array($params)) {
223
            throw new \InvalidArgumentException(
224
                sprintf("Parameter 1 of %s must be an array.", __METHOD__)
225
            );
226
        }
227
228
        foreach ($params as $key => $value) {
229
            if ($value instanceof \ReflectionParameter) {
230
                $class = $value->getClass();
231
232 View Code Duplication
                if ($class instanceof \ReflectionClass) {
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...
233
                    if ($class->isInterface()) {
234
                        $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...
235
236
                        $this->markAsResolved($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...
237
                    } else {
238
                        $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...
239
                    }
240
                }
241
            } else {
242
                if (is_string($value) && class_exists($value)) {
243
                    $params[$key] = $this->circularDependencyResolver($value);
244
                } elseif ($value instanceof \Closure) {
245
                    $params[$key] = ($this->isConcreteExists($value) ? $value($this) : $value);
246
                }
247
            }
248
        }
249
250
        return $params;
251
    }
252
253
    /**
254
     * Recursively resolving class dependency.
255
     *
256
     * @param string $class The valid class name.
257
     * @return object
258
     */
259
    protected function circularDependencyResolver($class)
260
    {
261
        if (!is_string($class) && !class_exists($class)) {
262
            throw Internal\Exception\ReflectionExceptionFactory::invalidArgument(
263
                sprintf("Parameter 1 of %s must be a string of valid class name.", __METHOD__)
264
            );
265
        }
266
267
        $reflector = Internal\ReflectionClassFactory::create($class);
268
269
        if (!$this->hasConstructor($reflector)) {
270
            return $this->resolveInstanceWithoutConstructor($reflector);
271
        } else {
272
            $param = $this->getMethodParameters($reflector, '__construct');
273
274
            if (empty($param)) {
275
                return $reflector->newInstance();
276
            } else {
277
                foreach ($param as $key => $value) {
278
                    $class = $value->getClass();
279
280 View Code Duplication
                    if ($class instanceof \ReflectionClass) {
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...
281
                        if ($class->isInterface()) {
282
                            $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...
283
284
                            $this->markAsResolved($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...
285
                        } else {
286
                            $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...
287
                        }
288
                    }
289
                }
290
291
                return $reflector->newInstanceArgs($param);
292
            }
293
        }
294
    }
295
296
    /**
297
     * Get concrete implementation from abstract.
298
     *
299
     * @param string $interface The interface name.
300
     * @return object
301
     */
302
    protected function getConcreteFromInterface($interface)
303
    {
304
        if (!$this->isAbstractExists($interface)) {
305
            throw Internal\Exception\ReflectionExceptionFactory::runtime(
306
                sprintf("%s has no concrete implementation in the clas binding stack.", $interface)
307
            );
308
        }
309
310
        if (sizeof($this->bindings[$interface]) > 1) {
311
            throw Internal\Exception\ReflectionExceptionFactory::logic(
312
                "An interface must only have 1 concrete implementation."
313
            );
314
        }
315
316
        $concrete = $this->bindings[$interface][0];
317
318
        return ($concrete instanceof \Closure ? $concrete($this)
319
            : (is_string($concrete) && class_exists($concrete)
320
                ? $this->resolve($concrete) : $concrete));
321
    }
322
323
    /**
324
     * Determine if current reflection object has constructor.
325
     *
326
     * @param \ReflectionClass $refl The current reflection class object.
327
     * @return boolean
328
     */
329
    protected function hasConstructor(Internal\ReflectionClassFactory $refl)
330
    {
331
        return $refl->hasMethod('__construct');
332
    }
333
334
    /**
335
     * Determine if unresolvable class name has cloneable.
336
     *
337
     * @param \ReflectionClass $refl The current reflection class object.
338
     * @return boolean
339
     */
340
    protected function isCloneable(Internal\ReflectionClassFactory $refl)
341
    {
342
        return $refl->hasMethod('__clone');
343
    }
344
345
    /**
346
     * Determine if unresolvable class name has serializable.
347
     *
348
     * @param \ReflectionClass $refl The current reflection class object.
349
     * @return boolean
350
     */
351
    protected function isSerializable(Internal\ReflectionClassFactory $refl)
352
    {
353
        return $refl->hasMethod('__sleep');
354
    }
355
356
    /**
357
     * Resolving class name without constructor.
358
     *
359
     * @param \ReflectionClass $refl An instance of \ReflectionClass
360
     */
361
    protected function resolveInstanceWithoutConstructor(Internal\ReflectionClassFactory $refl)
362
    {
363
        return $refl->newInstanceWithoutConstructor();
364
    }
365
366
    /**
367
     * Get method parameters.
368
     *
369
     * @param \ReflectionClass $refl An reflection class instance.
370
     * @param string $method The method name.
371
     * @return array
372
     */
373
    protected function getMethodParameters(Internal\ReflectionClassFactory $refl, $method)
374
    {
375
        return ($refl->hasMethod($method) ? $refl->getMethod($method)->getParameters() : null);
376
    }
377
378
    /**
379
     * Mark resolved class name to true.
380
     *
381
     * @param string $abstract The resolved class name.
382
     * @return void
383
     */
384
    protected function markAsResolved($abstract)
385
    {
386
        if ($this->isAbstractExists($abstract)) {
387
            $this->resolved[$abstract] = true;
388
389
            unset($this->bindings[$abstract]);
390
        }
391
    }
392
393
    /**
394
     * Bind service into binding container stack.
395
     *
396
     * @param string $abstract The unresolvable class name.
397
     * @param \Closure|string $concrete Closure or class name being bound to the class name.
398
     */
399
    public function bind($abstract, $concrete = null)
400
    {
401
        if (is_null($concrete)) {
402
            $concrete = $abstract;
403
        }
404
405
        if (!($concrete instanceof \Closure)) {
406
            $concrete = $this->turnIntoResolvableClosure($abstract, $concrete);
407
        }
408
409
        if (isset($this->bindings[$abstract])) {
410
            array_push($this->bindings[$abstract], $concrete);
411
        } else {
412
            $this->bindings[$abstract] = [$concrete];
413
        }
414
    }
415
416
    /**
417
     * Bind service into binding container stack if supplied class name
418
     * not being bound.
419
     *
420
     * @param string $abstract The unresolvable class name.
421
     * @param \Closure|string $concrete Closure or class name begin bound to the class name.
422
     */
423
    public function bindIf($abstract, $concrete)
424
    {
425
        if (!$this->isBound($abstract)) {
426
            $this->bind($abstract, $concrete);
427
        }
428
    }
429
430
    /**
431
     * Call defined instance.
432
     *
433
     * @param string $instance The class name to invoke/call.
434
     * @param array $args The class name __invoke method argument.
435
     * @return mixed|void
436
     */
437
    public function callInstance($instance, $args = [])
438
    {
439
        $args = (is_array($args) ? $args : array_slice(func_get_args(), 1));
440
        
441
        $current = $this->make($instance);
442
443
        $this->markAsResolved($instance);
444
445
        return call_user_func_array($current, $args);
446
    }
447
448
    /**
449
     * Determine if class name has been bound or not.
450
     *
451
     * @param string $abstract The unresolvable class name.
452
     * @return bool
453
     */
454
    public function isBound($abstract)
455
    {
456
        return $this->isAbstractExists($abstract);
457
    }
458
459
    /**
460
     * Turn class name into resolvable closure.
461
     *
462
     * @param string $abstract The class name
463
     * @param \Closure|string $concrete Can be instance of \Closure or class name.
464
     * @return \Closure
465
     */
466
    protected function turnIntoResolvableClosure($abstract, $concrete)
467
    {
468
        return function (Container $container, $parameters = []) use ($abstract, $concrete) {
469
            return ($abstract == $concrete ? $container->resolve($abstract)
470
                : $container->resolve($concrete, $parameters));
0 ignored issues
show
Bug introduced by
It seems like $concrete defined by parameter $concrete on line 466 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...
471
        };
472
    }
473
}
474