Passed
Push — master ( a4b794...a9d323 )
by Alex
02:08
created

Container::setIf()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 8
ccs 4
cts 4
cp 1
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 4
nc 2
nop 3
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, 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
     * @param bool           $force      Specify if a new element must be given and the dependencies must have be recalculated.
66
     *
67
     * @return mixed
68
     */
69
70 3
    public function call($function, array $parameters = [], bool $force = false)
71
    {
72 3
        $inspector = new ReflectionFunction($function);
73 3
        $dependencies = $inspector->getParameters();
74 3
        $resolvedClosureDependencies = [];
75
76 3 View Code Duplication
        foreach ($dependencies as $dependency) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
77 2
            if (isset($parameters[$dependency->name])) {
78 1
                   $resolvedClosureDependencies[] = $parameters[$dependency->name];
79 2
            } else $resolvedClosureDependencies[] = $this->resolve('', $dependency, $force);
80
        }
81
82 3
        return call_user_func_array($function, $resolvedClosureDependencies);
83
    }
84
85
    /**
86
     * Makes an element or class injecting automatically all the dependencies.
87
     *
88
     * @param string $abstract   The class name or container element name to make.
89
     * @param array  $parameters Specific parameters definition.
90
     * @param bool   $force      Specify if a new element must be given and the dependencies must have be recalculated.
91
     *
92
     * @throws ContainerException
93
     * @return object|null
94
     */
95
96 18
    public function make(string $abstract, array $parameters = [], bool $force = false)
97
    {
98 18
        if ($force === false && isset($this->collection[$abstract])) {
99 2
            return $this->get($abstract);
100
        }
101
102 18
        if (isset($this->resolving[$abstract])) {
103 3
            return $this->resolving[$abstract]($abstract, $parameters);
104
        }
105
106
        try {
107 18
            return ($this->resolving[$abstract] = $this->construct($abstract, $force))($abstract, $parameters);
108 1
        } catch (ReflectionException $e) {
109 1
            throw new ContainerException("Fail while attempt to make '$abstract'", 0, $e);
110
        }
111
    }
112
113
    /**
114
     * Construct a class and all the dependencies using the reflection library of PHP.
115
     *
116
     * @param string $abstract The class name or container element name to make.
117
     * @param bool   $force    Specify if a new element must be given and the dependencies must have be recalculated.
118
     *
119
     * @throws ReflectionException
120
     * @return Closure
121
     */
122
123 18
    protected function construct(string $abstract, bool $force) : Closure
124
    {
125 18
        $inspector = new ReflectionClass($abstract);
126
127 17
        if ($constructor = $inspector->getConstructor() and $dependencies = $constructor->getParameters()) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
Using logical operators such as and instead of && is generally not recommended.

PHP has two types of connecting operators (logical operators, and boolean operators):

  Logical Operators Boolean Operator
AND - meaning and &&
OR - meaning or ||

The difference between these is the order in which they are executed. In most cases, you would want to use a boolean operator like &&, or ||.

Let’s take a look at a few examples:

// Logical operators have lower precedence:
$f = false or true;

// is executed like this:
($f = false) or true;


// Boolean operators have higher precedence:
$f = false || true;

// is executed like this:
$f = (false || true);

Logical Operators are used for Control-Flow

One case where you explicitly want to use logical operators is for control-flow such as this:

$x === 5
    or die('$x must be 5.');

// Instead of
if ($x !== 5) {
    die('$x must be 5.');
}

Since die introduces problems of its own, f.e. it makes our code hardly testable, and prevents any kind of more sophisticated error handling; you probably do not want to use this in real-world code. Unfortunately, logical operators cannot be combined with throw at this point:

// The following is currently a parse error.
$x === 5
    or throw new RuntimeException('$x must be 5.');

These limitations lead to logical operators rarely being of use in current PHP code.

Loading history...
128
            return function (string $abstract, array $parameters) use ($inspector, $dependencies, $force) {
129 7
                $resolvedClassDependencies = [];
130
131 7 View Code Duplication
                foreach ($dependencies as $dependency) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
132 7
                    if (isset($parameters[$dependency->name])) {
133
                           $resolvedClassDependencies[] = $parameters[$dependency->name];
134 7
                    } else $resolvedClassDependencies[] = $this->resolve($abstract, $dependency, $force);
135
                }
136
137 7
                return $inspector->newInstanceArgs($resolvedClassDependencies);
138 7
            };
139
        }
140
141
        return function (string $abstract, array $parameters) {
0 ignored issues
show
Unused Code introduced by
The parameter $parameters is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

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