Completed
Push — master ( a9d323...e2a001 )
by Alex
07:37
created

Container::make()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 16
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 4

Importance

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