Passed
Branch master (21730e)
by Alex
02:15
created

Container::extend()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 18
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 3.009

Importance

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