Passed
Push — master ( 5cdbe6...617661 )
by Alexey
05:41
created

Container::inflect()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

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