Passed
Push — master ( e1417b...db188a )
by Alex
01:58
created

Container::get()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 16
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 4

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 16
ccs 8
cts 8
cp 1
rs 9.2
cc 4
eloc 9
nc 4
nop 1
crap 4
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 22
    public function make(string $abstract, array $parameters = [])
96
    {
97 22
        if (isset($this->resolving[$abstract])) {
98 4
            return $this->resolving[$abstract]($abstract, $parameters);
99
        }
100
101
        try {
102 22
            $this->resolving[$abstract] = $this->construct($abstract);
103 21
            return $this->resolving[$abstract]($abstract, $parameters);
104 2
        } catch (ReflectionException $e) {
105 1
            throw new ContainerException("Fail while attempt to make '$abstract'", 0, $e);
106
        }
107
    }
108
109
    /**
110
     * Construct a class and all the dependencies using the reflection library of PHP.
111
     *
112
     * @param string $abstract The class name or container element name to make.
113
     *
114
     * @throws ReflectionException
115
     * @return Closure
116
     */
117
118 22
    protected function construct(string $abstract) : Closure
119
    {
120 22
        $inspector = new ReflectionClass($abstract);
121
122 21
        if (($constructor = $inspector->getConstructor()) && ($dependencies = $constructor->getParameters())) {
123
124
            // if, and only if, a class has a constructor with parameters, we try to solve then
125
            // creating a resolving callback that in every call will recalculate all dependencies
126
            // for the given class, and offcourse, using a cached resolving callback if exists.
127
128
            return function (string $abstract, array $parameters) use ($inspector, $dependencies) {
129 11
                return $inspector->newInstanceArgs(
130 11
                    $this->process($abstract, $parameters, $dependencies)
131
                );
132 11
            };
133
        }
134
135
        return function (string $abstract) {
136 18
            return new $abstract;
137 18
        };
138
    }
139
140
    /**
141
     * Process all dependencies
142
     *
143
     * @param string $abstract     The class name or container element name to make
144
     * @param array  $parameters   User defined parameters that must be used instead of resolved ones
145
     * @param array  $dependencies Array of ReflectionParameter
146
     *
147
     * @throws ContainerException When a dependency cannot be solved.
148
     * @return array
149
     */
150
151 13
    protected function process(string $abstract, array $parameters, array $dependencies) : array
152
    {
153 13
        foreach ($dependencies as &$dependency) {
154 12
            if (isset($parameters[$dependency->name])) {
155 1
                   $dependency = $parameters[$dependency->name];
156 12
            } else $dependency = $this->resolve($abstract, $dependency);
157
        }
158
159 12
        return $dependencies;
160
    }
161
162
    /**
163
     * Resolve all the given class reflected dependencies.
164
     *
165
     * @param string               $abstract   The class name or container element name to resolve dependencies.
166
     * @param ReflectionParameter  $dependency The class dependency to be resolved.
167
     *
168
     * @throws ContainerException When a dependency cannot be solved.
169
     * @return Object
170
     */
171
172 11
    protected function resolve(string $abstract, ReflectionParameter $dependency)
173
    {
174 11
        $key = $abstract.$dependency->name;
175
176 11
        if (! isset($this->resolved[$key])) {
177 11
            $this->resolved[$key] = $this->generate($abstract, $dependency);
178
        }
179
180 10
        return $this->resolved[$key]($this);
181
    }
182
183
    /**
184
     * Generate the dependencies callbacks to jump some conditions in every dependency creation.
185
     *
186
     * @param string               $abstract   The class name or container element name to resolve dependencies.
187
     * @param ReflectionParameter  $dependency The class dependency to be resolved.
188
     *
189
     * @throws ContainerException When a dependency cannot be solved.
190
     * @return Closure
191
     */
192
193 11
    protected function generate(string $abstract, ReflectionParameter $dependency) : Closure
194
    {
195 11
        if ($class = $dependency->getClass()) {
196 10
            return $this->build($class->name, "{$abstract}{$class->name}");
197
        }
198
199
        try {
200 2
            $value = $dependency->getDefaultValue();
201
202
            return function () use ($value) {
203 1
                return $value;
204 1
            };
205 1
        } catch (ReflectionException $e) {
206 1
            throw new ContainerException("Cannot resolve '$dependency->name' of '$abstract'", 0, $e);
207
        }
208
    }
209
210
    /**
211
     * Create a build closure for a given class
212
     *
213
     * @param string $classname The class that need to be build
214
     * @param string $entry     Cache entry to search
215
     *
216
     * @return Closure
217
     */
218
219 10
    protected function build(string $classname, string $entry) : Closure
220
    {
221 10
        if (isset($this->dependencies[$entry])) {
222 3
            return $this->dependencies[$entry];
223
        }
224
225
        return function () use ($classname) {
226 7
            return $this->make($classname);
227 7
        };
228
    }
229
230
    /**
231
     * Reset the container, removing all the elements, cache and options.
232
     *
233
     * @return ContainerInterface
234
     */
235
236 1
    public function flush() : ContainerInterface
237
    {
238 1
        $this->collection = [];
239 1
        $this->dependencies = [];
240 1
        $this->resolving = [];
241 1
        $this->resolved = [];
242
243 1
        return $this;
244
    }
245
246
    /**
247
     * Finds an entry of the container by its identifier and returns it.
248
     *
249
     * @param string $abstract Identifier of the entry to look for.
250
     *
251
     * @throws NotFoundException  No entry was found for this identifier.
252
     * @throws ContainerException Error while retrieving the entry.
253
     *
254
     * @return mixed Entry.
255
     */
256 8
    public function get($abstract)
257
    {
258 8
        if (! isset($this->collection[$abstract])) {
259 1
            throw new NotFoundException("Element '$abstract' not found");
260
        }
261
262 7
        if ($this->collection[$abstract] instanceof Closure) {
263
            try {
264 4
                return $this->collection[$abstract]($this);
265 1
            } catch (Exception $e) {
266 1
                throw new ContainerException("An exception was thrown while attempt to make $abstract", 0, $e);
267
            }
268
        }
269
270 3
        return $this->collection[$abstract];
271
    }
272
273
    /**
274
     * Returns true if the container can return an entry for the given identifier.
275
     * Returns false otherwise.
276
     *
277
     * `has($abstract)` returning true does not mean that `get($abstract)` will not throw an exception.
278
     * It does however mean that `get($abstract)` will not throw a `NotFoundException`.
279
     *
280
     * @param string $abstract Identifier of the entry to look for.
281
     *
282
     * @return boolean
283
     */
284
285 2
    public function has($abstract)
286
    {
287 2
        return isset($this->collection[$abstract]);
288
    }
289
290
    /**
291
     * Verify if an element has a singleton instance.
292
     *
293
     * @param  string The class name or container element name to resolve dependencies.
294
     * @return bool
295
     */
296
297 5
    public function isSingleton(string $abstract) : bool
298
    {
299 5
        return isset($this->collection[$abstract]) && $this->collection[$abstract] instanceof Closure === false;
300
    }
301
302
    /**
303
     * Verify if an element is a instance of something.
304
     *
305
     * @param  string The class name or container element name to resolve dependencies.
306
     * @return bool
307
     */
308 1
    public function isInstance(string $abstract) : bool
309
    {
310 1
        return isset($this->collection[$abstract]) && is_object($this->collection[$abstract]);
311
    }
312
313
    /**
314
     * Bind a new element to the container.
315
     *
316
     * @param string                $abstract The alias name that will be used to call the element.
317
     * @param string|closure|object $concrete The element class name, or an closure that makes the element, or the object itself.
318
     * @param bool                  $shared   Define if the element will be a singleton instance.
319
     *
320
     * @return ContainerInterface
321
     */
322
323 13
    public function set(string $abstract, $concrete, bool $shared = false) : ContainerInterface
324
    {
325 13
        if (is_object($concrete)) {
326 2
            return $this->instance($abstract, $concrete);
327
        }
328
329 11
        if ($concrete instanceof Closure === false) {
330
            $concrete = function (Container $container) use ($concrete) {
331 8
                return $container->make($concrete);
332 11
            };
333
        }
334
335 11
        if ($shared === true) {
336 5
               $this->collection[$abstract] = $concrete($this);
337 6
        } else $this->collection[$abstract] = $concrete;
338
339 11
        return $this;
340
    }
341
342
    /**
343
     * Bind a new element to the container IF the element name not exists in the container.
344
     *
345
     * @param string         $abstract The alias name that will be used to call the element.
346
     * @param string|closure $concrete The element class name, or an closure that makes the element.
347
     * @param bool           $shared   Define if the element will be a singleton instance.
348
     *
349
     * @return ContainerInterface
350
     */
351
352 1
    public function setIf(string $abstract, $concrete, bool $shared = false) : ContainerInterface
353
    {
354 1
        if (! isset($this->collection[$abstract])) {
355 1
            $this->set($abstract, $concrete, $shared);
356
        }
357
358 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...
359
    }
360
361
    /**
362
     * Bind an specific instance to a class dependency.
363
     *
364
     * @param string         $class          The class full name.
365
     * @param string         $dependencyName The dependency full name.
366
     * @param string|closure $dependency     The specific object class name or a classure that makes the element.
367
     *
368
     * @return ContainerInterface
369
     */
370
371 3
    public function setTo(string $class, string $dependencyName, $dependency) : ContainerInterface
372
    {
373 3
        if ($dependency instanceof Closure === false) {
374 2
            if (is_object($dependency)) {
375
                $dependency = function () use ($dependency) {
376 2
                    return $dependency;
377 2
                };
378
            } else {
379
                $dependency = function () use ($dependency) {
380
                    return $this->get($dependency);
381
                };
382
            }
383
        }
384
385 3
        $this->dependencies[$class.$dependencyName] = $dependency;
386
387 3
        return $this;
388
    }
389
390
    /**
391
     * Bind an element that will be construct only one time, and every call for the element,
392
     * the same instance will be given.
393
     *
394
     * @param string         $abstract The alias name that will be used to call the element.
395
     * @param string|closure $concrete The element class name, or an closure that makes the element.
396
     *
397
     * @return ContainerInterface
398
     */
399
400 3
    public function singleton(string $abstract, $concrete) : ContainerInterface
401
    {
402 3
        $this->set($abstract, $concrete, true);
403
404 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...
405
    }
406
407
    /**
408
     * Bind an object to the container.
409
     *
410
     * @param string $abstract The alias name that will be used to call the object.
411
     * @param object $instance The object that will be inserted.
412
     *
413
     * @throws ContainerException When $instance is not an object.
414
     * @return ContainerInterface
415
     */
416
417 6
    public function instance(string $abstract, $instance) : ContainerInterface
418
    {
419 6
        if (! is_object($instance)) {
420 1
            throw new ContainerException('Trying to store ' . gettype($instance) . ' as object.');
421
        }
422
423 5
        $this->collection[$abstract] = $instance;
424
425 5
        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...
426
    }
427
428
    /**
429
     * Modify an element with a given function that receive the old element as argument.
430
     *
431
     * @param string  $abstract  The alias name that will be used to call the element.
432
     * @param closure $extension The function that receives the old element and return a new or modified one.
433
     *
434
     * @throws NotFoundException  When no element was found with $abstract key.
435
     * @return ContainerInterface
436
     */
437
438 3
    public function extend(string $abstract, closure $extension) : ContainerInterface
439
    {
440 3
        if (! isset($this->collection[$abstract])) {
441 1
            throw new NotFoundException($abstract);
442
        }
443
444 2
        $object = $this->collection[$abstract];
445
446 2
        if ($object instanceof Closure) {
447 1
            $this->collection[$abstract] = function () use ($object, $extension) {
448 1
                return $extension($object($this), $this);
449 1
            };
450
        } else {
451 1
            $this->collection[$abstract] = $extension($object, $this);
452
        }
453
454 2
        return $this;
455
    }
456
457
    /**
458
     * Makes an resolvable element an singleton.
459
     *
460
     * @param  string $abstract The alias name that will be used to call the element.
461
     *
462
     * @throws NotFoundException  When no element was found with $abstract key.
463
     * @throws ContainerException When the element on $abstract key is not resolvable.
464
     *
465
     * @return ContainerInterface
466
     */
467
468 3
    public function share(string $abstract) : ContainerInterface
469
    {
470 3
        if (! isset($this->collection[$abstract])) {
471 1
            throw new NotFoundException("Element '$abstract' not found");
472
        }
473
474 2
        if (! $this->collection[$abstract] instanceof Closure) {
475 1
            throw new ContainerException("'$abstract' must be a resolvable element");
476
        }
477
478 1
        $this->collection[$abstract] = $this->collection[$abstract]($this);
479
480 1
        return $this;
481
    }
482
483
}
484