Completed
Push — master ( af3c1a...164212 )
by Alex
01:56
created

Container::singleton()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 6
ccs 3
cts 3
cp 1
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 3
nc 1
nop 2
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 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($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 ($abstract, $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 ($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 void
203
     */
204
205 1
    public function flush()
206
    {
207 1
        $this->collection = [];
208 1
        $this->dependencies = [];
209 1
        $this->resolving = [];
210 1
        $this->resolved = [];
211 1
    }
212
213
    /**
214
     * Finds an entry of the container by its identifier and returns it.
215
     *
216
     * @param string $abstract Identifier of the entry to look for.
217
     *
218
     * @throws \Psr\Container\Exception\NotFoundException  No entry was found for this identifier.
219
     * @throws \Psr\Container\Exception\ContainerException Error while retrieving the entry.
220
     *
221
     * @return mixed Entry.
222
     */
223 6
    public function get($abstract)
224
    {
225 6
        if (! isset($this->collection[$abstract])) {
226
            throw new Exceptions\NotFoundException("Element '$abstract' not found");
227
        }
228
229 6
        if ($this->collection[$abstract] instanceof Closure) {
230
            try {
231 2
                return $this->collection[$abstract]($this);
232
            } catch (Exception $e) {
233
                throw new Exceptions\ContainerException($e->getMessage());
234
            }
235
        }
236
237 4
        return $this->collection[$abstract];
238
    }
239
240
    /**
241
     * Returns true if the container can return an entry for the given identifier.
242
     * Returns false otherwise.
243
     *
244
     * `has($abstract)` returning true does not mean that `get($abstract)` will not throw an exception.
245
     * It does however mean that `get($abstract)` will not throw a `NotFoundException`.
246
     *
247
     * @param string $abstract Identifier of the entry to look for.
248
     *
249
     * @return boolean
250
     */
251
252 2
    public function has($abstract)
253
    {
254 2
        return isset($this->collection[$abstract]);
255
    }
256
257
    /**
258
     * Verify if an element has a singleton instance.
259
     *
260
     * @param  string The class name or container element name to resolve dependencies.
261
     * @return bool
262
     */
263
264 5
    public function isSingleton(string $abstract) : bool
265
    {
266 5
        return isset($this->collection[$abstract]);
267
    }
268
269
    /**
270
     * Bind a new element to the container.
271
     *
272
     * @param string         $abstract The alias name that will be used to call the element.
273
     * @param string|closure $concrete The element class name, or an closure that makes the element.
274
     * @param bool           $shared   Define if the element will be a singleton instance.
275
     *
276
     * @return \Codeburner\Container\Container
277
     */
278
279 12
    public function set(string $abstract, $concrete, bool $shared = false) : self
280
    {
281 12
        if ($concrete instanceof Closure === false) {
282
            $concrete = function (Container $container) use ($concrete) {
283 8
                return $container->make($concrete);
284 11
            };
285
        }
286
287 12
        if ($shared === true) {
288 6
               $this->collection[$abstract] = $concrete($this);
289 6
        } else $this->collection[$abstract] = $concrete;
290
291 12
        return $this;
292
    }
293
294
    /**
295
     * Bind a new element to the container IF the element name not exists in the container.
296
     *
297
     * @param string         $abstract The alias name that will be used to call the element.
298
     * @param string|closure $concrete The element class name, or an closure that makes the element.
299
     * @param bool           $shared   Define if the element will be a singleton instance.
300
     *
301
     * @return \Codeburner\Container\Container
302
     */
303
304 1
    public function setIf(string $abstract, $concrete, bool $shared = false) : self
305
    {
306 1
        if (! isset($this->collection[$abstract])) {
307 1
            $this->set($abstract, $concrete, $shared);
308
        }
309
310 1
        return $this;
311
    }
312
313
    /**
314
     * Bind an specific instance to a class dependency.
315
     *
316
     * @param string         $class          The class full name.
317
     * @param string         $dependencyName The dependency full name.
318
     * @param string|closure $dependency     The specific object class name or a classure that makes the element.
319
     *
320
     * @return \Codeburner\Container\Container
321
     */
322
323 3
    public function setTo(string $class, string $dependencyName, $dependency) : self
324
    {
325 3
        if ($dependency instanceof Closure === false) {
326 2
            if (is_object($dependency)) {
327
                $dependency = function () use ($dependency) {
328 2
                    return $dependency;
329 2
                };
330
            } else {
331
                $dependency = function () use ($dependency) {
332
                    return $this->get($dependency);
333
                };
334
            }
335
        }
336
337 3
        $this->dependencies[$class.$dependencyName] = $dependency;
338
339 3
        return $this;
340
    }
341
342
    /**
343
     * Bind an element that will be construct only one time, and every call for the element,
344
     * the same instance will be given.
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
     *
349
     * @return \Codeburner\Container\Container
350
     */
351
352 3
    public function singleton(string $abstract, $concrete) : self
353
    {
354 3
        $this->set($abstract, $concrete, true);
355
356 3
        return $this;
357
    }
358
359
    /**
360
     * Bind an object to the container.
361
     *
362
     * @param string $abstract The alias name that will be used to call the object.
363
     * @param object $instance The object that will be inserted.
364
     *
365
     * @throws \Psr\Container\Exception\ContainerException When $instance is not an object.
366
     * @return \Codeburner\Container\Container
367
     */
368
369 1
    public function instance(string $abstract, $instance) : self
370
    {
371 1
        if (! is_object($instance)) {
372
            throw new Exceptions\ContainerException('Trying to store ' . gettype($type) . ' as object.');
373
        }
374
375 1
        $this->collection[$abstract] = $instance;
376
377 1
        return $this;
378
    }
379
380
    /**
381
     * Modify an element with a given function that receive the old element as argument.
382
     *
383
     * @param string  $abstract  The alias name that will be used to call the element.
384
     * @param closure $extension The function that receives the old element and return a new or modified one.
385
     *
386
     * @throws \Psr\Container\Exception\NotFoundException  When no element was found with $abstract key.
387
     * @return \Codeburner\Container\Container
388
     */
389
390 2
    public function extend(string $abstract, closure $extension) : self
391
    {
392 2
        if (! isset($this->collection[$abstract])) {
393
            throw new Exceptions\NotFoundException;
394
        }
395
396 2
        $object = $this->collection[$abstract];
397
398 2
        if ($object instanceof Closure) {
399 1
            $this->collection[$abstract] = function () use ($object, $extension) {
400 1
                return $extension($object($this), $this);
401 1
            };
402
        } else {
403 1
            $this->collection[$abstract] = $extension($object, $this);
404
        }
405
406 2
        return $this;
407
    }
408
409
    /**
410
     * Makes an resolvable element an singleton.
411
     *
412
     * @param  string $abstract The alias name that will be used to call the element.
413
     *
414
     * @throws \Psr\Container\Exception\NotFoundException  When no element was found with $abstract key.
415
     * @throws \Psr\Container\Exception\ContainerException When the element on $abstract key is not resolvable.
416
     *
417
     * @return \Codeburner\Container\Container
418
     */
419
420 1
    public function share(string $abstract) : self
421
    {
422 1
        if (! isset($this->collection[$abstract])) {
423
            throw new Exceptions\NotFoundException("Element '$abstract' not found");
424
        }
425
426 1
        if (! $this->collection[$abstract] instanceof Closure) {
427
            throw new Exceptions\ContainerException("'$abstract' must be a resolvable element");
428
        }
429
430 1
        $this->collection[$abstract] = $this->collection[$abstract]($this);
431
432 1
        return $this;
433
    }
434
435
}
436