Completed
Branch master (af3c1a)
by Alex
03:29 queued 01:31
created

Container::singleton()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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