Passed
Push — master ( df5655...0452a9 )
by Alexey
04:44
created

AbstractContainer::invokeFactory()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1.125

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 2
dl 0
loc 4
ccs 2
cts 4
cp 0.5
crap 1.125
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 ReflectionClass;
7
use Venta\Container\Exception\ArgumentResolverException;
8
use Venta\Container\Exception\CircularReferenceException;
9
use Venta\Container\Exception\NotFoundException;
10
use Venta\Container\Exception\UninstantiableServiceException;
11
use Venta\Container\Exception\UnresolvableDependencyException;
12
use Venta\Contracts\Container\ArgumentResolver as ArgumentResolverContract;
13
use Venta\Contracts\Container\Container as ContainerContract;
14
use Venta\Contracts\Container\Invoker as InvokerContract;
15
16
/**
17
 * Class Container
18
 *
19
 * @package Venta\Container
20
 */
21
abstract class AbstractContainer implements ContainerContract
22
{
23
24
    /**
25
     * Array of callable definitions.
26
     *
27
     * @var Invokable[]
28
     */
29
    protected $callableDefinitions = [];
30
31
    /**
32
     * Array of class definitions.
33
     *
34
     * @var string[]
35
     */
36
    protected $classDefinitions = [];
37
38
    /**
39
     * Array of resolved instances.
40
     *
41
     * @var object[]
42
     */
43
    protected $instances = [];
44
45
    /**
46
     * Array of container service identifiers.
47
     *
48
     * @var string[]
49
     */
50
    protected $keys = [];
51
52
    /**
53
     * Array of instances identifiers marked as shared.
54
     * Such instances will be instantiated once and returned on consecutive gets.
55
     *
56
     * @var bool[]
57
     */
58
    protected $shared = [];
59
60
    /**
61
     * Array of container service callable factories.
62
     *
63
     * @var Closure[]
64
     */
65
    private $factories = [];
66
67
    /**
68
     * @var InvokerContract
69
     */
70
    private $invoker;
71
72
    /**
73
     * Array of container service identifiers currently being resolved.
74
     *
75
     * @var string[]
76
     */
77
    private $resolving = [];
78
79
    /**
80
     * Container constructor.
81
     *
82
     * @param ArgumentResolverContract|null $resolver
83
     */
84 44
    public function __construct(ArgumentResolverContract $resolver = null)
85
    {
86 44
        $this->setInvoker(new Invoker($this, $resolver ?: new ArgumentResolver($this)));
87 44
    }
88
89
    /**
90
     * @inheritDoc
91
     * @param callable|string $callable Callable to call OR class name to instantiate and invoke.
92
     */
93 12
    public function call($callable, array $arguments = [])
94
    {
95 12
        return $this->invoker->call($callable, $arguments);
96
    }
97
98
    /**
99
     * @inheritDoc
100
     */
101 36
    public function get($id, array $arguments = [])
102
    {
103
        try {
104 36
            $id = $this->normalize($id);
105 36
            $this->resolving($id);
106 36
            $object = $this->instantiateService($id, $arguments);
107
108 29
            return $object;
109 7
        } catch (ArgumentResolverException $resolveException) {
110 1
            throw new UnresolvableDependencyException($id, $this->resolving, $resolveException);
111
        } finally {
112 36
            $this->resolved($id);
113
        }
114
    }
115
116
    /**
117
     * @inheritDoc
118
     */
119 24
    public function has($id): bool
120
    {
121 24
        return $this->isResolvableService($this->normalize($id));
122
    }
123
124
    /**
125
     * @inheritDoc
126
     */
127 1
    public function isCallable($callable): bool
128
    {
129 1
        return $this->invoker->isCallable($callable);
130
    }
131
132
    /**
133
     * Create callable factory for the subject service.
134
     *
135
     * @param string $id
136
     * @param array $arguments
137
     * @return mixed
138
     * @throws NotFoundException
139
     */
140 36
    protected function instantiateService(string $id, array $arguments)
141
    {
142 36
        if (isset($this->instances[$id])) {
143 6
            return $this->instances[$id];
144
        }
145
146 32
        if (isset($this->factories[$id])) {
147 5
            return ($this->factories[$id])($arguments);
148
        }
149
150 32
        if (isset($this->callableDefinitions[$id])) {
151 12
            $this->factories[$id] = $this->createServiceFactoryFromCallable($this->callableDefinitions[$id]);
152
153 12
            return $this->invokeFactory($id, $arguments);
154
        }
155
156 25
        $class = $this->classDefinitions[$id] ?? $id;
157 25
        if ($class !== $id) {
158
            // Recursive call allows to bind contract to contract.
159 4
            return $this->saveShared($id, $this->instantiateService($class, $arguments));
160
        }
161 25
        if (!class_exists($class)) {
162 2
            throw new NotFoundException($id, $this->resolving);
163
        }
164 23
        $this->factories[$id] = $this->createServiceFactoryFromClass($class);
165
166 22
        return $this->invokeFactory($id, $arguments);
167
    }
168
169
    /**
170
     * @return InvokerContract
171
     */
172 44
    protected function invoker(): InvokerContract
173
    {
174 44
        return $this->invoker;
175
    }
176
177
    /**
178
     * Check if container can resolve the service with subject identifier.
179
     *
180
     * @param string $id
181
     * @return bool
182
     */
183 30
    protected function isResolvableService(string $id): bool
184
    {
185 30
        return isset($this->keys[$id]) || class_exists($id);
186
    }
187
188
    /**
189
     * @param string $id
190
     * @return bool
191
     */
192 30
    protected function isShared(string $id): bool
193
    {
194 30
        return isset($this->shared[$id]);
195
    }
196
197
    /**
198
     * Normalize key to use across container.
199
     *
200
     * @param  string $id
201
     * @return string
202
     */
203 38
    protected function normalize(string $id): string
204
    {
205 38
        return ltrim($id, '\\');
206
    }
207
208
    /**
209
     * @param string $id
210
     * @return void
211
     */
212 36
    protected function resolved(string $id)
213
    {
214 36
        unset($this->resolving[$id]);
215 36
    }
216
217
    /**
218
     * Detects circular references.
219
     *
220
     * @param string $id
221
     * @return void
222
     * @throws CircularReferenceException
223
     */
224 36
    protected function resolving(string $id)
225
    {
226 36
        if (isset($this->resolving[$id])) {
227 3
            throw new CircularReferenceException($id, $this->resolving);
228
        }
229
230
        // We mark service as being resolved to detect circular references through out the resolution chain.
231 36
        $this->resolving[$id] = $id;
232 36
    }
233
234
    /**
235
     * @param InvokerContract $invoker
236
     * @return void
237
     */
238 44
    protected function setInvoker(InvokerContract $invoker)
239
    {
240 44
        $this->invoker = $invoker;
241 44
    }
242
243
    /**
244
     * Forbid container cloning.
245
     *
246
     * @codeCoverageIgnore
247
     */
248
    private function __clone()
249
    {
250
    }
251
252
    /**
253
     * Create callable factory with resolved arguments from callable.
254
     *
255
     * @param Invokable $invokable
256
     * @return Closure
257
     */
258 12
    private function createServiceFactoryFromCallable(Invokable $invokable): Closure
259
    {
260
        return function (array $arguments = []) use ($invokable) {
261 12
            return $this->invoker->invoke($invokable, $arguments);
262 12
        };
263
    }
264
265
    /**
266
     * Create callable factory with resolved arguments from class name.
267
     *
268
     * @param string $class
269
     * @return Closure
270
     * @throws UninstantiableServiceException
271
     */
272 23
    private function createServiceFactoryFromClass(string $class): Closure
273
    {
274 23
        $reflection = new ReflectionClass($class);
275 23
        if (!$reflection->isInstantiable()) {
276 1
            throw new UninstantiableServiceException($class, $this->resolving);
277
        }
278 22
        $constructor = $reflection->getConstructor();
279
280 22
        if ($constructor && $constructor->getNumberOfParameters() > 0) {
281 16
            $invokable = new Invokable($constructor);
282
283
            return function (array $arguments = []) use ($invokable) {
284 16
                return $this->invoker->invoke($invokable, $arguments);
285 16
            };
286
        }
287
288 20
        return function () use ($class) {
289 20
            return new $class();
290 20
        };
291
    }
292
293
    /**
294
     * @param string $id
295
     * @param array $arguments
296
     * @return object
297
     */
298 29
    private function invokeFactory(string $id, array $arguments)
299
    {
300 29
        return $this->saveShared($id, ($this->factories[$id])($arguments));
301
    }
302
303
    /**
304
     * @param string $id
305
     * @param object $object
306
     * @return object
307
     */
308 26
    private function saveShared(string $id, $object)
309
    {
310 26
        if ($this->isShared($id)) {
311 5
            $this->instances[$id] = $object;
312
        }
313
314 26
        return $object;
315
    }
316
317
}
318