Passed
Push — master ( cee9ed...b93948 )
by Alexey
16:09
created

Container::createServiceFactoryFromClass()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 20
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 4

Importance

Changes 0
Metric Value
cc 4
eloc 11
nc 3
nop 1
dl 0
loc 20
ccs 12
cts 12
cp 1
crap 4
rs 9.2
c 0
b 0
f 0
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\Container as ContainerContract;
14
use Venta\Contracts\Container\Invoker as InvokerContract;
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
    /**
26
     * Array of callable definitions.
27
     *
28
     * @var Invokable[]
29
     */
30
    private $callableDefinitions = [];
31
32
    /**
33
     * Array of class definitions.
34
     *
35
     * @var string[]
36
     */
37
    private $classDefinitions = [];
38
39
    /**
40
     * Array of decorator definitions.
41
     *
42
     * @var callable[][]
43
     */
44
    private $decoratorDefinitions = [];
45
46
    /**
47
     * Array of container service callable factories.
48
     *
49
     * @var Closure[]
50
     */
51
    private $factories = [];
52
53
    /**
54
     * @var ObjectInflectorContract
55
     */
56
    private $inflector;
57
58
    /**
59
     * Array of resolved instances.
60
     *
61
     * @var object[]
62
     */
63
    private $instances = [];
64
65
    /**
66
     * @var InvokerContract
67
     */
68
    private $invoker;
69
70
    /**
71
     * Array of container service identifiers.
72
     *
73
     * @var string[]
74
     */
75
    private $keys = [];
76
77
    /**
78
     * Array of container service identifiers currently being resolved.
79
     *
80
     * @var string[]
81
     */
82
    private $resolving = [];
83
84
    /**
85
     * Array of instances identifiers marked as shared.
86
     * Such instances will be instantiated once and returned on consecutive gets.
87
     *
88
     * @var bool[]
89
     */
90
    private $shared = [];
91
92
    /**
93
     * Container constructor.
94
     */
95 44
    public function __construct()
96
    {
97 44
        $argumentResolver = new ArgumentResolver($this);
98 44
        $this->setInvoker(new Invoker($this, $argumentResolver));
99 44
        $this->setObjectInflector(new ObjectInflector($argumentResolver));
100 44
    }
101
102
    /**
103
     * @inheritDoc
104
     */
105 4
    public function addInflection(string $id, string $method, array $arguments = [])
106
    {
107 4
        $this->validateId($id);
108 4
        $this->inflector->addInflection($this->normalize($id), $method, $arguments);
109 3
    }
110
111
    /**
112
     * @inheritDoc
113
     */
114 7
    public function bindClass(string $id, string $class, $shared = false)
115
    {
116 7
        if (!$this->isResolvableService($class)) {
117 1
            throw new InvalidArgumentException(sprintf('Class "%s" does not exist.', $class));
118
        }
119
        $this->register($id, $shared, function ($id) use ($class) {
120 5
            $this->classDefinitions[$id] = $class;
121 6
        });
122 5
    }
123
124
    /**
125
     * @inheritDoc
126
     */
127 12
    public function bindFactory(string $id, $callable, $shared = false)
128
    {
129 12
        $reflectedCallable = new Invokable($callable);
130 12
        if (!$this->isResolvableCallable($reflectedCallable)) {
131
            throw new InvalidArgumentException('Invalid callable provided.');
132
        }
133
134 12
        $this->register(
135
            $id,
136
            $shared,
137
            function ($id) use ($reflectedCallable) {
138 12
                $this->callableDefinitions[$id] = $reflectedCallable;
139 12
        });
140 12
    }
141
142
    /**
143
     * @inheritDoc
144
     */
145 4
    public function bindInstance(string $id, $instance)
146
    {
147 4
        if (!$this->isConcrete($instance)) {
148
            throw new InvalidArgumentException('Invalid instance provided.');
149
        }
150
        $this->register($id, true, function ($id) use ($instance) {
151 4
            $this->instances[$id] = $instance;
152 4
        });
153 4
    }
154
155
    /**
156
     * @inheritDoc
157
     * @param callable|string $callable Callable to call OR class name to instantiate and invoke.
158
     */
159 12
    public function call($callable, array $arguments = [])
160
    {
161 12
        return $this->invoker->call($callable, $arguments);
162
    }
163
164
    /**
165
     * @inheritDoc
166
     */
167 3
    public function decorate($id, callable $callback)
168
    {
169 3
        $id = $this->normalize($id);
170
171
        // Check if correct id is provided.
172 3
        if (!$this->isResolvableService($id)) {
173
            throw new InvalidArgumentException('Invalid id provided.');
174
        }
175
176 3
        $this->decoratorDefinitions[$id][] = $callback;
177 3
    }
178
179
    /**
180
     * @inheritDoc
181
     */
182 35
    public function get($id, array $arguments = [])
183
    {
184 35
        $id = $this->normalize($id);
185
        // We try to resolve alias first to get a real service id.
186 35
        if (!$this->isResolvableService($id)) {
187 2
            throw new NotFoundException($id, $this->resolving);
188
        }
189
190
        // Look up service in resolved instances first.
191 33 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...
192 5
            $object = $this->decorateObject($id, $this->instances[$id]);
193
            // Delete all decorator callbacks to avoid applying them once more on another get call.
194 5
            unset($this->decoratorDefinitions[$id]);
195
196 5
            return $object;
197
        }
198
199
        // Detect circular references.
200
        // We mark service as being resolved to detect circular references through out the resolution chain.
201 29
        if (isset($this->resolving[$id])) {
202 3
            throw new CircularReferenceException($id, $this->resolving);
203
        } else {
204 29
            $this->resolving[$id] = $id;
205
        }
206
207
        try {
208
            // Instantiate service and apply inflections.
209 29
            $object = $this->instantiateService($id, $arguments);
210 25
            $this->inflector->applyInflections($object);
211 24
            $object = $this->decorateObject($id, $object);
212
213
            // Cache shared instances.
214 24 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...
215 1
                $this->instances[$id] = $object;
216
                // Remove all decorator callbacks to prevent further decorations on concrete instance.
217 1
                unset($this->decoratorDefinitions[$id]);
218
            }
219
220 24
            return $object;
221 5
        } catch (ArgumentResolverException $resolveException) {
222 1
            throw new UnresolvableDependencyException($id, $this->resolving, $resolveException);
223
        } finally {
224 29
            unset($this->resolving[$id]);
225
        }
226
    }
227
228
    /**
229
     * @inheritDoc
230
     */
231 23
    public function has($id): bool
232
    {
233 23
        return $this->isResolvableService($this->normalize($id));
234
    }
235
236
    /**
237
     * @inheritDoc
238
     */
239 1
    public function isCallable($callable): bool
240
    {
241 1
        return $this->invoker->isCallable($callable);
242
    }
243
244
    /**
245
     * @param InvokerContract $invoker
246
     * @return void
247
     */
248 44
    protected function setInvoker(InvokerContract $invoker)
249
    {
250 44
        $this->invoker = $invoker;
251 44
    }
252
253
    /**
254
     * @param ObjectInflectorContract $inflector
255
     * @return void
256
     */
257 44
    protected function setObjectInflector(ObjectInflectorContract $inflector)
258
    {
259 44
        $this->inflector = $inflector;
260 44
    }
261
262
    /**
263
     * Forbid container cloning.
264
     *
265
     * @codeCoverageIgnore
266
     */
267
    private function __clone()
268
    {
269
    }
270
271
    /**
272
     * Create callable factory with resolved arguments from callable.
273
     *
274
     * @param Invokable $invokable
275
     * @return Closure
276
     */
277 12
    private function createServiceFactoryFromCallable(Invokable $invokable): Closure
278
    {
279
        return function (array $arguments = []) use ($invokable) {
280 12
            return $this->invoker->invoke($invokable, $arguments);
281 12
        };
282
    }
283
284
    /**
285
     * Create callable factory with resolved arguments from class name.
286
     *
287
     * @param string $class
288
     * @return Closure
289
     * @throws UninstantiableServiceException
290
     */
291 22
    private function createServiceFactoryFromClass(string $class): Closure
292
    {
293 22
        $reflection = new ReflectionClass($class);
294 22
        if (!$reflection->isInstantiable()) {
295 1
            throw new UninstantiableServiceException($class, $this->resolving);
296
        }
297 21
        $constructor = $reflection->getConstructor();
298
299 21
        if ($constructor && $constructor->getNumberOfParameters() > 0) {
300 15
            $invokable = new Invokable($constructor);
301
302
            return function (array $arguments = []) use ($invokable) {
303 15
                return $this->invoker->invoke($invokable, $arguments);
304 15
            };
305
        }
306
307 19
        return function () use ($class) {
308 19
            return new $class();
309 19
        };
310
    }
311
312
    /**
313
     * Applies decoration callbacks to provided instance.
314
     *
315
     * @param string $id
316
     * @param $object
317
     * @return object
318
     */
319 28
    private function decorateObject(string $id, $object)
320
    {
321 28
        if (isset($this->decoratorDefinitions[$id])) {
322 3
            foreach ($this->decoratorDefinitions[$id] as $callback) {
323 3
                $object = $this->invoker->call($callback, [$object]);
324 3
                $this->inflector->applyInflections($object);
325
            }
326
        }
327
328 28
        return $object;
329
    }
330
331
    /**
332
     * Create callable factory for the subject service.
333
     *
334
     * @param string $id
335
     * @param array $arguments
336
     * @return mixed
337
     */
338 29
    private function instantiateService(string $id, array $arguments)
339
    {
340 29
        if (isset($this->instances[$id])) {
341
            return $this->instances[$id];
342
        }
343
344 29
        if (!isset($this->factories[$id])) {
345 29
            if (isset($this->callableDefinitions[$id])) {
346 12
                $this->factories[$id] = $this->createServiceFactoryFromCallable($this->callableDefinitions[$id]);
347 22
            } elseif (isset($this->classDefinitions[$id]) && $this->classDefinitions[$id] !== $id) {
348
                // Recursive call allows to bind contract to contract.
349 3
                return $this->instantiateService($this->classDefinitions[$id], $arguments);
350
            } else {
351 22
                $this->factories[$id] = $this->createServiceFactoryFromClass($id);
352
            }
353
        }
354
355 28
        return ($this->factories[$id])($arguments);
356
    }
357
358
    /**
359
     * Check if subject service is an object instance.
360
     *
361
     * @param mixed $service
362
     * @return bool
363
     */
364 4
    private function isConcrete($service): bool
365
    {
366 4
        return is_object($service) && !$service instanceof Closure;
367
    }
368
369
    /**
370
     * Verifies that provided callable can be called by service container.
371
     *
372
     * @param Invokable $reflectedCallable
373
     * @return bool
374
     */
375 12
    private function isResolvableCallable(Invokable $reflectedCallable): bool
376
    {
377
        // If array represents callable we need to be sure it's an object or a resolvable service id.
378 12
        $callable = $reflectedCallable->callable();
379
380 12
        return $reflectedCallable->isFunction()
381 6
               || is_object($callable[0])
382 12
               || $this->isResolvableService($callable[0]);
383
    }
384
385
    /**
386
     * Check if container can resolve the service with subject identifier.
387
     *
388
     * @param string $id
389
     * @return bool
390
     */
391 39
    private function isResolvableService(string $id): bool
392
    {
393 39
        return isset($this->keys[$id]) || class_exists($id);
394
    }
395
396
    /**
397
     * Normalize key to use across container.
398
     *
399
     * @param  string $id
400
     * @return string
401
     */
402 38
    private function normalize(string $id): string
403
    {
404 38
        return ltrim($id, '\\');
405
    }
406
407
    /**
408
     * Registers binding.
409
     * After this method call binding can be resolved by container.
410
     *
411
     * @param string $id
412
     * @param bool $shared
413
     * @param Closure $registrationCallback
414
     * @return void
415
     */
416 22
    private function register(string $id, bool $shared, Closure $registrationCallback)
417
    {
418
        // Check if correct service is provided.
419 22
        $this->validateId($id);
420 21
        $id = $this->normalize($id);
421
422
        // Clean up previous bindings, if any.
423 21
        unset($this->instances[$id], $this->shared[$id], $this->keys[$id]);
424
425
        // Register service with provided callback.
426 21
        $registrationCallback($id);
427
428
        // Mark service as shared when needed.
429 21
        $this->shared[$id] = $shared ?: null;
430
431
        // Save service key to make it recognizable by container.
432 21
        $this->keys[$id] = true;
433 21
    }
434
435
    /**
436
     * Validate service identifier. Throw an Exception in case of invalid value.
437
     *
438
     * @param string $id
439
     * @return void
440
     * @throws InvalidArgumentException
441
     */
442 26
    private function validateId(string $id)
443
    {
444 26
        if (!interface_exists($id) && !class_exists($id)) {
445 1
            throw new InvalidArgumentException(sprintf(
446 1
                    'Invalid service id "%s". Service id must be an existing interface or class name.', $id
447
                )
448
            );
449
        }
450 25
    }
451
}
452