Completed
Branch master (951571)
by Alex
01:59
created

Container::flush()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 1

Importance

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