Passed
Push — master ( 617661...066bd3 )
by Alexey
09:24
created

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