Completed
Push — master ( 4a08fa...139841 )
by Alex
02:02
created

Container::make()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 4

Importance

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