Completed
Branch master (1ac121)
by Alex
03:51 queued 01:52
created

Container::build()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 10
ccs 5
cts 5
cp 1
rs 9.4285
cc 2
eloc 5
nc 2
nop 2
crap 2
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 32 and the first side effect is on line 22.

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 Psr\Container\ContainerInterface;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Codeburner\Container\ContainerInterface.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

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