Passed
Push — master ( d44e7d...fd48aa )
by Alexey
07:53
created

Container::normalizeCallable()   C

Complexity

Conditions 11
Paths 11

Size

Total Lines 35
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 11.0245

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 35
rs 5.2653
ccs 16
cts 17
cp 0.9412
cc 11
eloc 17
nc 11
nop 1
crap 11.0245

1 Method

Rating   Name   Duplication   Size   Complexity  
A Container::register() 0 18 2

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php declare(strict_types = 1);
2
3
namespace Venta\Container;
4
5
use Closure;
6
use InvalidArgumentException;
7
use ReflectionClass;
8
use Venta\Container\Exception\ArgumentResolverException;
9
use Venta\Container\Exception\CircularReferenceException;
10
use Venta\Container\Exception\NotFoundException;
11
use Venta\Container\Exception\UninstantiableServiceException;
12
use Venta\Container\Exception\UnresolvableDependencyException;
13
use Venta\Contracts\Container\ArgumentResolver as ArgumentResolverContract;
14
use Venta\Contracts\Container\Container as ContainerContract;
15
use Venta\Contracts\Container\ObjectInflector as ObjectInflectorContract;
16
17
/**
18
 * Class Container
19
 *
20
 * @package Venta\Container
21
 */
22
class Container implements ContainerContract
23
{
24
    /**
25
     * @var ArgumentResolverContract
26
     */
27
    private $argumentResolver;
28
29
    /**
30
     * Array of callable definitions.
31
     *
32
     * @var ReflectedCallable[]
33
     */
34
    private $callableDefinitions = [];
35
36
    /**
37
     * Array of class definitions.
38
     *
39
     * @var string[]
40
     */
41
    private $classDefinitions = [];
42
43
    /**
44
     * Array of decorator definitions.
45
     *
46
     * @var callable[][]
47
     */
48
    private $decoratorDefinitions = [];
49
50
    /**
51
     * Array of container service callable factories.
52
     *
53
     * @var Closure[]
54
     */
55
    private $factories = [];
56
57
    /**
58
     * Array of resolved instances.
59
     *
60
     * @var object[]
61
     */
62
    private $instances = [];
63
64
    /**
65
     * Array of container service identifiers.
66
     *
67
     * @var string[]
68
     */
69
    private $keys = [];
70
71
    /**
72
     * @var ObjectInflectorContract
73
     */
74
    private $objectInflector;
75
76
    /**
77
     * Array of container service identifiers currently being resolved.
78
     *
79
     * @var string[]
80
     */
81
    private $resolving = [];
82
83
    /**
84
     * Array of instances identifiers marked as shared.
85
     * Such instances will be instantiated once and returned on consecutive gets.
86
     *
87
     * @var bool[]
88
     */
89
    private $shared = [];
90
91
    /**
92
     * Container constructor.
93
     */
94 44
    public function __construct()
95
    {
96 44
        $this->setArgumentResolver(new ArgumentResolver($this))
97 44
             ->setObjectInflector(new ObjectInflector($this->argumentResolver));
98 44
    }
99
100
    /**
101
     * @inheritDoc
102
     */
103 4
    public function addInflection(string $id, string $method, array $arguments = [])
104
    {
105 4
        $this->validateId($id);
106 4
        $this->objectInflector->addInflection($this->normalize($id), $method, $arguments);
107 3
    }
108
109
    /**
110
     * @inheritDoc
111
     */
112 7
    public function bindClass(string $id, string $class, $shared = false)
113
    {
114 7
        if (!$this->isResolvableService($class)) {
115 1
            throw new InvalidArgumentException(sprintf('Class "%s" does not exist.', $class));
116
        }
117
        $this->register($id, $shared, function ($id) use ($class) {
118 5
            $this->classDefinitions[$id] = $class;
119 6
        });
120 5
    }
121
122
    /**
123
     * @inheritDoc
124
     */
125 13
    public function bindFactory(string $id, $callable, $shared = false)
126
    {
127 13
        $reflectedCallable = new ReflectedCallable($callable);
128 13
        if (!$this->isResolvableCallable($reflectedCallable)) {
129
            throw new InvalidArgumentException('Invalid callable provided.');
130
        }
131
132 13
        $this->register(
133
            $id,
134
            $shared,
135
            function ($id) use ($reflectedCallable) {
136 13
                $this->callableDefinitions[$id] = $reflectedCallable;
137 13
        });
138 13
    }
139
140
    /**
141
     * @inheritDoc
142
     */
143 4
    public function bindInstance(string $id, $instance)
144
    {
145 4
        if (!$this->isConcrete($instance)) {
146
            throw new InvalidArgumentException('Invalid instance provided.');
147
        }
148
        $this->register($id, true, function ($id) use ($instance) {
149 4
            $this->instances[$id] = $instance;
150 4
        });
151 4
    }
152
153
    /**
154
     * @inheritDoc
155
     * @param callable|string $callable Callable to call OR class name to instantiate and invoke.
156
     */
157 15
    public function call($callable, array $arguments = [])
158
    {
159 15
        $reflectedCallable = new ReflectedCallable($callable);
160 13
        $reflection = $reflectedCallable->reflection();
161 13
        $arguments = $this->argumentResolver->resolve($reflection, $arguments);
162
163 13
        if ($reflectedCallable->isFunction()) {
164
            // We have Closure or "functionName" string.
165 5
            $callable = $reflectedCallable->callable();
166
167 5
            return $callable(...$arguments);
168
        }
169
170 8
        list($object, $method) = $reflectedCallable->callable();
171 8
        if ($reflection->isStatic()) {
172 1
            return $object::$method(...$arguments);
173
        }
174
175 7
        if (is_string($object)) {
176 5
            $object = $this->get($object);
177
        }
178
179 6
        return $object->$method(...$arguments);
180
    }
181
182
    /**
183
     * @inheritDoc
184
     */
185 3
    public function decorate($id, callable $callback)
186
    {
187 3
        $id = $this->normalize($id);
188
189
        // Check if correct id is provided.
190 3
        if (!$this->isResolvableService($id)) {
191
            throw new InvalidArgumentException('Invalid id provided.');
192
        }
193
194 3
        $this->decoratorDefinitions[$id][] = $callback;
195 3
    }
196
197
    /**
198
     * @inheritDoc
199
     */
200 36
    public function get($id, array $arguments = [])
201
    {
202 36
        $id = $this->normalize($id);
203
        // We try to resolve alias first to get a real service id.
204 36
        if (!$this->isResolvableService($id)) {
205 2
            throw new NotFoundException($id, $this->resolving);
206
        }
207
208
        // Look up service in resolved instances first.
209 34 View Code Duplication
        if (isset($this->instances[$id])) {
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...
210 5
            $object = $this->decorateObject($id, $this->instances[$id]);
211
            // Delete all decorator callbacks to avoid applying them once more on another get call.
212 5
            unset($this->decoratorDefinitions[$id]);
213
214 5
            return $object;
215
        }
216
217
        // Detect circular references.
218
        // We mark service as being resolved to detect circular references through out the resolution chain.
219 30
        if (isset($this->resolving[$id])) {
220 3
            throw new CircularReferenceException($id, $this->resolving);
221
        } else {
222 30
            $this->resolving[$id] = $id;
223
        }
224
225
        try {
226
            // Instantiate service and apply inflections.
227 30
            $object = $this->instantiateService($id, $arguments);
228 26
            $this->objectInflector->applyInflections($object);
229 25
            $object = $this->decorateObject($id, $object);
230
231
            // Cache shared instances.
232 25 View Code Duplication
            if (isset($this->shared[$id])) {
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...
233 1
                $this->instances[$id] = $object;
234
                // Remove all decorator callbacks to prevent further decorations on concrete instance.
235 1
                unset($this->decoratorDefinitions[$id]);
236
            }
237
238 25
            return $object;
239 5
        } catch (ArgumentResolverException $resolveException) {
240 1
            throw new UnresolvableDependencyException($id, $this->resolving, $resolveException);
241
        } finally {
242 30
            unset($this->resolving[$id]);
243
        }
244
    }
245
246
    /**
247
     * @inheritDoc
248
     */
249 22
    public function has($id): bool
250
    {
251 22
        return $this->isResolvableService($this->normalize($id));
252
    }
253
254
    /**
255
     * @inheritDoc
256
     */
257 1
    public function isCallable($callable): bool
258
    {
259
        try {
260 1
            return $this->isResolvableCallable(new ReflectedCallable($callable));
0 ignored issues
show
Documentation introduced by
$callable is of type *, but the function expects a callable.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
261
        } catch (InvalidArgumentException $e) {
262
            return false;
263
        }
264
    }
265
266
    /**
267
     * @param ArgumentResolverContract $argumentResolver
268
     * @return Container
269
     */
270 45
    protected function setArgumentResolver(ArgumentResolverContract $argumentResolver): Container
271
    {
272 45
        $this->argumentResolver = $argumentResolver;
273
274 45
        return $this;
275
    }
276
277
    /**
278
     * @param ObjectInflectorContract $objectInflector
279
     * @return Container
280
     */
281 45
    protected function setObjectInflector(ObjectInflectorContract $objectInflector): Container
282
    {
283 45
        $this->objectInflector = $objectInflector;
284
285 45
        return $this;
286
    }
287
288
    /**
289
     * Forbid container cloning.
290
     *
291
     * @codeCoverageIgnore
292
     */
293
    private function __clone()
294
    {
295
    }
296
297
    /**
298
     * Create callable factory with resolved arguments from callable.
299
     *
300
     * @param ReflectedCallable $reflectedCallable
301
     * @return Closure
302
     */
303 13
    private function createServiceFactoryFromCallable(ReflectedCallable $reflectedCallable): Closure
304
    {
305 13
        $reflection = $reflectedCallable->reflection();
306
        // Wrap reflected function arguments with closure.
307 13
        $resolve = $this->argumentResolver->createCallback($reflection);
308
309 13
        if ($reflectedCallable->isFunction()) {
310
            // We have Closure or "functionName" string.
311 7
            $callable = $reflectedCallable->callable();
312
313
            return function (array $arguments = []) use ($callable, $resolve) {
314 7
                return $callable(...$resolve($arguments));
315 7
            };
316
        }
317 6
        list($object, $method) = $reflectedCallable->callable();
318
319 6
        if (!$reflection->isStatic() && is_string($object)) {
320 2
            $object = $this->get($object);
321
        }
322
323
        // Wrap with Closure to save reflection resolve results.
324
        return function (array $arguments = []) use ($object, $method, $resolve) {
325 6
            return ([$object, $method])(...$resolve($arguments));
326 6
        };
327
    }
328
329
    /**
330
     * Create callable factory with resolved arguments from class name.
331
     *
332
     * @param string $class
333
     * @return Closure
334
     * @throws UninstantiableServiceException
335
     */
336 22
    private function createServiceFactoryFromClass(string $class): Closure
337
    {
338 22
        $reflection = new ReflectionClass($class);
339 22
        if (!$reflection->isInstantiable()) {
340 1
            throw new UninstantiableServiceException($class, $this->resolving);
341
        }
342 21
        $constructor = $reflection->getConstructor();
343 21
        $resolve = ($constructor && $constructor->getNumberOfParameters())
344 15
            ? $this->argumentResolver->createCallback($constructor)
345 21
            : null;
346
347 21
        return function (array $arguments = []) use ($class, $resolve) {
348 21
            $object = $resolve ? new $class(...$resolve($arguments)) : new $class();
349
350 19
            return $object;
351 21
        };
352
    }
353
354
    /**
355
     * Applies decoration callbacks to provided instance.
356
     *
357
     * @param string $id
358
     * @param $object
359
     * @return object
360
     */
361 29
    private function decorateObject(string $id, $object)
362
    {
363 29
        if (isset($this->decoratorDefinitions[$id])) {
364 3
            foreach ($this->decoratorDefinitions[$id] as $callback) {
365 3
                $object = $this->call($callback, [$object]);
366 3
                $this->objectInflector->applyInflections($object);
367
            }
368
        }
369
370 29
        return $object;
371
    }
372
373
    /**
374
     * Create callable factory for the subject service.
375
     *
376
     * @param string $id
377
     * @param array $arguments
378
     * @return mixed
379
     */
380 30
    private function instantiateService(string $id, array $arguments)
381
    {
382 30
        if (isset($this->instances[$id])) {
383
            return $this->instances[$id];
384
        }
385
386 30
        if (!isset($this->factories[$id])) {
387 30
            if (isset($this->callableDefinitions[$id])) {
388 13
                $this->factories[$id] = $this->createServiceFactoryFromCallable($this->callableDefinitions[$id]);
389 22
            } elseif (isset($this->classDefinitions[$id]) && $this->classDefinitions[$id] !== $id) {
390
                // Recursive call allows to bind contract to contract.
391 3
                return $this->instantiateService($this->classDefinitions[$id], $arguments);
392
            } else {
393 22
                $this->factories[$id] = $this->createServiceFactoryFromClass($id);
394
            }
395
        }
396
397 29
        return ($this->factories[$id])($arguments);
398
    }
399
400
    /**
401
     * Check if subject service is an object instance.
402
     *
403
     * @param mixed $service
404
     * @return bool
405
     */
406 4
    private function isConcrete($service): bool
407
    {
408 4
        return is_object($service) && !$service instanceof Closure;
409
    }
410
411
    /**
412
     * Verifies that provided callable can be called by service container.
413
     *
414
     * @param ReflectedCallable $reflectedCallable
415
     * @return bool
416
     */
417 14
    private function isResolvableCallable(ReflectedCallable $reflectedCallable): bool
418
    {
419
        // If array represents callable we need to be sure it's an object or a resolvable service id.
420 14
        $callable = $reflectedCallable->callable();
421
422 14
        return !is_array($callable)
423 7
               || is_object($callable[0])
424 14
               || $this->isResolvableService($callable[0]);
425
    }
426
427
    /**
428
     * Check if container can resolve the service with subject identifier.
429
     *
430
     * @param string $id
431
     * @return bool
432
     */
433 40
    private function isResolvableService(string $id): bool
434
    {
435 40
        return isset($this->keys[$id]) || class_exists($id);
436
    }
437
438
    /**
439
     * Normalize key to use across container.
440
     *
441
     * @param  string $id
442
     * @return string
443
     */
444 38
    private function normalize(string $id): string
445
    {
446 38
        return ltrim($id, '\\');
447
    }
448
449
    /**
450
     * Registers binding.
451
     * After this method call binding can be resolved by container.
452
     *
453
     * @param string $id
454
     * @param bool $shared
455
     * @param Closure $registrationCallback
456
     * @return void
457
     */
458 23
    private function register(string $id, bool $shared, Closure $registrationCallback)
459
    {
460
        // Check if correct service is provided.
461 23
        $this->validateId($id);
462 22
        $id = $this->normalize($id);
463
464
        // Clean up previous bindings, if any.
465 22
        unset($this->instances[$id], $this->shared[$id], $this->keys[$id]);
466
467
        // Register service with provided callback.
468 22
        $registrationCallback($id);
469
470
        // Mark service as shared when needed.
471 22
        $this->shared[$id] = $shared ?: null;
472
473
        // Save service key to make it recognizable by container.
474 22
        $this->keys[$id] = true;
475 22
    }
476
477
    /**
478
     * Validate service identifier. Throw an Exception in case of invalid value.
479
     *
480
     * @param string $id
481
     * @return void
482
     * @throws InvalidArgumentException
483
     */
484 27
    private function validateId(string $id)
485
    {
486 27
        if (!interface_exists($id) && !class_exists($id)) {
487 1
            throw new InvalidArgumentException(sprintf(
488 1
                    'Invalid service id "%s". Service id must be an existing interface or class name.', $id
489
                )
490
            );
491
        }
492 26
    }
493
}
494