Container   B
last analyzed

Complexity

Total Complexity 40

Size/Duplication

Total Lines 460
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 2

Test Coverage

Coverage 100%

Importance

Changes 10
Bugs 0 Features 0
Metric Value
wmc 40
c 10
b 0
f 0
lcom 1
cbo 2
dl 0
loc 460
ccs 108
cts 108
cp 1
rs 8.2608

19 Methods

Rating   Name   Duplication   Size   Complexity  
A call() 0 9 1
A construct() 0 21 3
A process() 0 10 3
A resolve() 0 10 2
A generate() 0 16 3
A build() 0 10 2
A flush() 0 9 1
A get() 0 16 4
A has() 0 4 1
A make() 0 12 3
A isSingleton() 0 8 2
A isInstance() 0 8 2
A set() 0 18 4
A setIf() 0 8 2
B setTo() 0 29 2
A singleton() 0 6 1
A instance() 0 10 2
A extend() 0 8 1
A share() 0 8 1

How to fix   Complexity   

Complex Class

Complex classes like Container often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Container, and based on these observations, apply Extract Interface, too.

1
<?php
0 ignored issues
show
Coding Style Compatibility introduced by
For compatibility and reusability of your code, PSR1 recommends that a file should introduce either new symbols (like classes, functions, etc.) or have side-effects (like outputting something, or including other files), but not both at the same time. The first symbol is defined on line 31 and the first side effect is on line 21.

The PSR-1: Basic Coding Standard recommends that a file should either introduce new symbols, that is classes, functions, constants or similar, or have side effects. Side effects are anything that executes logic, like for example printing output, changing ini settings or writing to a file.

The idea behind this recommendation is that merely auto-loading a class should not change the state of an application. It also promotes a cleaner style of programming and makes your code less prone to errors, because the logic is not spread out all over the place.

To learn more about the PSR-1, please see the PHP-FIG site on the PSR-1.

Loading history...
2
3
/**
4
 * Codeburner Framework.
5
 *
6
 * @author Alex Rohleder <[email protected]>
7
 * @copyright 2015 Alex Rohleder
8
 * @license http://opensource.org/licenses/MIT
9
 */
10
11
namespace Codeburner\Container;
12
13
use Closure, Exception, ReflectionClass, ReflectionException, ReflectionFunction, ReflectionParameter;
14
use Codeburner\Container\Exceptions\{ContainerException, NotFoundException};
15
16
/**
17
 * Explicit Avoiding autoload for the container interface,
18
 * because it is aways needed.
19
 */
20
21 1
require_once __DIR__ . '/ContainerInterface.php';
22
23
/**
24
 * The container class is reponsable to construct all objects
25
 * of the project automatically, with total abstraction of dependencies.
26
 *
27
 * @author Alex Rohleder <[email protected]>
28
 * @version 1.0.0
29
 */
30
31
class Container implements ContainerInterface
32
{
33
34
    /**
35
     * Holds all resolved or resolvable instances into the container.
36
     *
37
     * @var array
38
     */
39
40
    protected $collection;
41
42
    /**
43
     * Class specific defined dependencies.
44
     *
45
     * @var array
46
     */
47
48
    protected $dependencies;
49
50
    /**
51
     * Cache of classes inspector and resolver.
52
     *
53
     * @var array
54
     */
55
56
    protected $resolving;
57
58
    /**
59
     * Cache of classes dependencies in callbacks ready for resolution.
60
     *
61
     * @var array
62
     */
63
64
    protected $resolved;
65
66
    /**
67
     * Call a user function injecting the dependencies.
68
     *
69
     * @param string|Closure $function   The function or the user function name.
70
     * @param array          $parameters The predefined dependencies.
71
     *
72
     * @return mixed
73
     */
74
75 3
    public function call($function, array $parameters = [])
76
    {
77 3
        $inspector = new ReflectionFunction($function);
78
79 3
        $dependencies = $inspector->getParameters();
80 3
        $dependencies = $this->process('', $parameters, $dependencies);
81
82 3
        return call_user_func_array($function, $dependencies);
83
    }
84
85
    /**
86
     * Makes an element or class injecting automatically all the dependencies.
87
     *
88
     * @param string $abstract   The class name or container element name to make.
89
     * @param array  $parameters Specific parameters definition.
90
     *
91
     * @throws ContainerException
92
     * @return object|null
93
     */
94
95 21
    public function make(string $abstract, array $parameters = [])
96
    {
97
        try {
98 21
            if (! isset($this->resolving[$abstract])) {
99 21
                $this->resolving[$abstract] = $this->construct($abstract);
100
            }
101
102 20
            return $this->resolving[$abstract]($abstract, $parameters);
103 2
        } catch (ReflectionException $e) {
104 1
            throw new ContainerException("Fail while attempt to make '$abstract'", 0, $e);
105
        }
106
    }
107
108
    /**
109
     * Construct a class and all the dependencies using the reflection library of PHP.
110
     *
111
     * @param string $abstract The class name or container element name to make.
112
     *
113
     * @throws ReflectionException
114
     * @return Closure
115
     */
116
117 21
    protected function construct(string $abstract) : Closure
118
    {
119 21
        $inspector = new ReflectionClass($abstract);
120
121 20
        if (($constructor = $inspector->getConstructor()) && ($dependencies = $constructor->getParameters())) {
122
123
            // if, and only if, a class has a constructor with parameters, we try to solve then
124
            // creating a resolving callback that in every call will recalculate all dependencies
125
            // for the given class, and offcourse, using a cached resolving callback if exists.
126
127
            return function (string $abstract, array $parameters) use ($inspector, $dependencies) {
128 10
                return $inspector->newInstanceArgs(
129 10
                    $this->process($abstract, $parameters, $dependencies)
130
                );
131 10
            };
132
        }
133
134
        return function (string $abstract) {
135 17
            return new $abstract;
136 17
        };
137
    }
138
139
    /**
140
     * Process all dependencies
141
     *
142
     * @param string $abstract     The class name or container element name to make
143
     * @param array  $parameters   User defined parameters that must be used instead of resolved ones
144
     * @param array  $dependencies Array of ReflectionParameter
145
     *
146
     * @throws ContainerException When a dependency cannot be solved.
147
     * @return array
148
     */
149
150 12
    protected function process(string $abstract, array $parameters, array $dependencies) : array
151
    {
152 12
        foreach ($dependencies as &$dependency) {
153 11
            if (isset($parameters[$dependency->name])) {
154 1
                   $dependency = $parameters[$dependency->name];
155 11
            } else $dependency = $this->resolve($abstract, $dependency);
156
        }
157
158 11
        return $dependencies;
159
    }
160
161
    /**
162
     * Resolve all the given class reflected dependencies.
163
     *
164
     * @param string               $abstract   The class name or container element name to resolve dependencies.
165
     * @param ReflectionParameter  $dependency The class dependency to be resolved.
166
     *
167
     * @throws ContainerException When a dependency cannot be solved.
168
     * @return Object
169
     */
170
171 10
    protected function resolve(string $abstract, ReflectionParameter $dependency)
172
    {
173 10
        $key = $abstract.$dependency->name;
174
175 10
        if (! isset($this->resolved[$key])) {
176 10
            $this->resolved[$key] = $this->generate($abstract, $dependency);
177
        }
178
179 9
        return $this->resolved[$key]($this);
180
    }
181
182
    /**
183
     * Generate the dependencies callbacks to jump some conditions in every dependency creation.
184
     *
185
     * @param string               $abstract   The class name or container element name to resolve dependencies.
186
     * @param ReflectionParameter  $dependency The class dependency to be resolved.
187
     *
188
     * @throws ContainerException When a dependency cannot be solved.
189
     * @return Closure
190
     */
191
192 10
    protected function generate(string $abstract, ReflectionParameter $dependency) : Closure
193
    {
194 10
        if ($class = $dependency->getClass()) {
195 9
            return $this->build($class->name, "{$abstract}{$class->name}");
196
        }
197
198
        try {
199 2
            $value = $dependency->getDefaultValue();
200
201
            return function () use ($value) {
202 1
                return $value;
203 1
            };
204 1
        } catch (ReflectionException $e) {
205 1
            throw new ContainerException("Cannot resolve '$dependency->name' of '$abstract'", 0, $e);
206
        }
207
    }
208
209
    /**
210
     * Create a build closure for a given class
211
     *
212
     * @param string $classname The class that need to be build
213
     * @param string $entry     Cache entry to search
214
     *
215
     * @return Closure
216
     */
217
218 9
    protected function build(string $classname, string $entry) : Closure
219
    {
220 9
        if (isset($this->dependencies[$entry])) {
221 3
            return $this->dependencies[$entry];
222
        }
223
224
        return function () use ($classname) {
225 6
            return $this->make($classname);
226 6
        };
227
    }
228
229
    /**
230
     * Reset the container, removing all the elements, cache and options.
231
     *
232
     * @return ContainerInterface
233
     */
234
235 1
    public function flush() : ContainerInterface
236
    {
237 1
        $this->collection = [];
238 1
        $this->dependencies = [];
239 1
        $this->resolving = [];
240 1
        $this->resolved = [];
241
242 1
        return $this;
243
    }
244
245
    /**
246
     * Finds an entry of the container by its identifier and returns it.
247
     *
248
     * @param string $abstract Identifier of the entry to look for.
249
     *
250
     * @throws NotFoundException  No entry was found for this identifier.
251
     * @throws ContainerException Error while retrieving the entry.
252
     *
253
     * @return mixed Entry.
254
     */
255 11
    public function get($abstract)
256
    {
257 11
        if (! isset($this->collection[$abstract])) {
258 3
            throw new NotFoundException("Element '$abstract' not found");
259
        }
260
261 8
        if ($this->collection[$abstract] instanceof Closure) {
262
            try {
263 5
                return $this->collection[$abstract]($this);
264 1
            } catch (Exception $e) {
265 1
                throw new ContainerException("An exception was thrown while attempt to make $abstract", 0, $e);
266
            }
267
        }
268
269 4
        return $this->collection[$abstract];
270
    }
271
272
    /**
273
     * Returns true if the container can return an entry for the given identifier.
274
     * Returns false otherwise.
275
     *
276
     * `has($abstract)` returning true does not mean that `get($abstract)` will not throw an exception.
277
     * It does however mean that `get($abstract)` will not throw a `NotFoundException`.
278
     *
279
     * @param string $abstract Identifier of the entry to look for.
280
     *
281
     * @return boolean
282
     */
283
284 10
    public function has($abstract)
285
    {
286 10
        return isset($this->collection[$abstract]);
287
    }
288
289
    /**
290
     * Verify if an element has a singleton instance.
291
     *
292
     * @param  string The class name or container element name to resolve dependencies.
293
     *
294
     * @throws NotFoundException When $abstract does not exists
295
     * @return bool
296
     */
297
298 6
    public function isSingleton(string $abstract) : bool
299
    {
300 6
        if (! $this->has($abstract)) {
301 1
            throw new NotFoundException("Element '$abstract' not found");
302
        }
303
304 5
        return $this->collection[$abstract] instanceof Closure === false;
305
    }
306
307
    /**
308
     * Verify if an element is a instance of something.
309
     *
310
     * @param  string The class name or container element name to resolve dependencies.
311
     *
312
     * @throws NotFoundException When $abstract does not exists
313
     * @return bool
314
     */
315
316 2
    public function isInstance(string $abstract) : bool
317
    {
318 2
        if (! $this->has($abstract)) {
319 1
            throw new NotFoundException("Element '$abstract' not found");
320
        }
321
322 1
        return is_object($this->collection[$abstract]);
323
    }
324
325
    /**
326
     * Bind a new element to the container.
327
     *
328
     * @param string                $abstract The alias name that will be used to call the element.
329
     * @param string|closure|object $concrete The element class name, or an closure that makes the element, or the object itself.
330
     * @param bool                  $shared   Define if the element will be a singleton instance.
331
     *
332
     * @return ContainerInterface
333
     */
334
335 15
    public function set(string $abstract, $concrete, bool $shared = false) : ContainerInterface
336
    {
337 15
        if (is_object($concrete)) {
338 4
            return $this->instance($abstract, $concrete);
339
        }
340
341 11
        if ($concrete instanceof Closure === false) {
342
            $concrete = function (Container $container) use ($concrete) {
343 8
                return $container->make($concrete);
344 11
            };
345
        }
346
347 11
        if ($shared === true) {
348 5
               $this->collection[$abstract] = $concrete($this);
349 6
        } else $this->collection[$abstract] = $concrete;
350
351 11
        return $this;
352
    }
353
354
    /**
355
     * Bind a new element to the container IF the element name not exists in the container.
356
     *
357
     * @param string         $abstract The alias name that will be used to call the element.
358
     * @param string|closure $concrete The element class name, or an closure that makes the element.
359
     * @param bool           $shared   Define if the element will be a singleton instance.
360
     *
361
     * @return ContainerInterface
362
     */
363
364 1
    public function setIf(string $abstract, $concrete, bool $shared = false) : ContainerInterface
365
    {
366 1
        if (! $this->has($abstract)) {
367 1
            $this->set($abstract, $concrete, $shared);
368
        }
369
370 1
        return $this;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this; (Codeburner\Container\Container) is incompatible with the return type declared by the interface Codeburner\Container\ContainerInterface::setIf of type self.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
371
    }
372
373
    /**
374
     * Bind an specific instance to a class dependency.
375
     *
376
     * @param string         $class          The class full name.
377
     * @param string         $dependencyName The dependency full name.
378
     * @param string|closure $dependency     The specific object class name or a classure that makes the element.
379
     *
380
     * @return ContainerInterface
381
     */
382
383 3
    public function setTo(string $class, string $dependencyName, $dependency) : ContainerInterface
384
    {
385 3
        $key = "$class$dependencyName";
386
387 3
        if ($dependency instanceof Closure === false) {
388
389
            // let's use temporarily the set method to resolve
390
            // the $dependency if it is not a closure ready to use.
391
392 2
            $this->set($key, $dependency);
393 2
            $resolved = $this->collection[$key];
394
395
            // now we already have a resolved version of $dependency
396
            // we just need to ensure the dependencies type, a closure.
397
398 2
            $dependency = function () use ($resolved) {
399 2
                return $resolved;
400 2
            };
401
402
            // we have used the set method to resolve the $dependency
403
            // now that we have done all the process let's clear the memory.
404
405 2
            unset($resolved, $this->collection[$key]);
406
        }
407
408 3
        $this->dependencies[$key] = $dependency;
409
410 3
        return $this;
411
    }
412
413
    /**
414
     * Bind an element that will be construct only one time, and every call for the element,
415
     * the same instance will be given.
416
     *
417
     * @param string         $abstract The alias name that will be used to call the element.
418
     * @param string|closure $concrete The element class name, or an closure that makes the element.
419
     *
420
     * @return ContainerInterface
421
     */
422
423 3
    public function singleton(string $abstract, $concrete) : ContainerInterface
424
    {
425 3
        $this->set($abstract, $concrete, true);
426
427 3
        return $this;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this; (Codeburner\Container\Container) is incompatible with the return type declared by the interface Codeburner\Container\ContainerInterface::singleton of type self.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
428
    }
429
430
    /**
431
     * Bind an object to the container.
432
     *
433
     * @param string $abstract The alias name that will be used to call the object.
434
     * @param object $instance The object that will be inserted.
435
     *
436
     * @throws ContainerException When $instance is not an object.
437
     * @return ContainerInterface
438
     */
439
440 7
    public function instance(string $abstract, $instance) : ContainerInterface
441
    {
442 7
        if (! is_object($instance)) {
443 1
            throw new ContainerException('Trying to store ' . gettype($instance) . ' as object.');
444
        }
445
446 6
        $this->collection[$abstract] = $instance;
447
448 6
        return $this;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this; (Codeburner\Container\Container) is incompatible with the return type declared by the interface Codeburner\Container\ContainerInterface::instance of type self.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
449
    }
450
451
    /**
452
     * Modify an element with a given function that receive the old element as argument.
453
     *
454
     * @param string  $abstract  The alias name that will be used to call the element.
455
     * @param closure $extension The function that receives the old element and return a new or modified one.
456
     *
457
     * @throws NotFoundException  When no element was found with $abstract key.
458
     * @return ContainerInterface
459
     */
460
461 3
    public function extend(string $abstract, closure $extension) : ContainerInterface
462
    {
463 3
        $object = $this->get($abstract);
464
465 2
        $this->collection[$abstract] = $extension($object, $this);
466
467 2
        return $this;
468
    }
469
470
    /**
471
     * Makes an resolvable element an singleton.
472
     *
473
     * @param  string $abstract The alias name that will be used to call the element.
474
     *
475
     * @throws NotFoundException  When no element was found with $abstract key.
476
     * @throws ContainerException When the element on $abstract key is not resolvable.
477
     *
478
     * @return ContainerInterface
479
     */
480
481 2
    public function share(string $abstract) : ContainerInterface
482
    {
483 2
        $object = $this->get($abstract);
484
485 1
        $this->collection[$abstract] = $object;
486
487 1
        return $this;
488
    }
489
490
}
491