Passed
Push — master ( 559900...ef8c69 )
by Alexey
10:10
created

AbstractContainer::isCallable()   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 1
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 31
    public function __construct(ArgumentResolverContract $resolver = null)
85
    {
86 31
        $this->setInvoker(new Invoker($this, $resolver ?: new ArgumentResolver($this)));
87 31
    }
88
89
    /**
90
     * @inheritDoc
91
     */
92 27
    public function get($id, array $arguments = [])
93
    {
94
        try {
95 27
            $id = $this->normalize($id);
96 27
            $this->resolving($id);
97 27
            $object = $this->instantiateService($id, $arguments);
98
99 21
            return $object;
100 6
        } catch (ArgumentResolverException $resolveException) {
101 1
            throw new UnresolvableDependencyException($id, $this->resolving, $resolveException);
102
        } finally {
103 27
            $this->resolved($id);
104
        }
105
    }
106
107
    /**
108
     * @inheritDoc
109
     */
110 16
    public function has($id): bool
111
    {
112 16
        return $this->isResolvableService($this->normalize($id));
113
    }
114
115
    /**
116
     * Create callable factory for the subject service.
117
     *
118
     * @param string $id
119
     * @param array $arguments
120
     * @return mixed
121
     * @throws NotFoundException
122
     */
123 27
    protected function instantiateService(string $id, array $arguments)
124
    {
125 27
        if (isset($this->instances[$id])) {
126 4
            return $this->instances[$id];
127
        }
128
129 25
        if (isset($this->factories[$id])) {
130 3
            return ($this->factories[$id])($arguments);
131
        }
132
133 25
        if (isset($this->callableDefinitions[$id])) {
134 12
            $this->factories[$id] = $this->createServiceFactoryFromCallable($this->callableDefinitions[$id]);
135
136 12
            return $this->invokeFactory($id, $arguments);
137
        }
138
139 18
        $class = $this->classDefinitions[$id] ?? $id;
140 18
        if ($class !== $id) {
141
            // Recursive call allows to bind contract to contract.
142 4
            return $this->saveShared($id, $this->instantiateService($class, $arguments));
143
        }
144 18
        if (!class_exists($class)) {
145 1
            throw new NotFoundException($id, $this->resolving);
146
        }
147 17
        $this->factories[$id] = $this->createServiceFactoryFromClass($class);
148
149 16
        return $this->invokeFactory($id, $arguments);
150
    }
151
152
    /**
153
     * @return InvokerContract
154
     */
155 31
    protected function invoker(): InvokerContract
156
    {
157 31
        return $this->invoker;
158
    }
159
160
    /**
161
     * Check if container can resolve the service with subject identifier.
162
     *
163
     * @param string $id
164
     * @return bool
165
     */
166 22
    protected function isResolvableService(string $id): bool
167
    {
168 22
        return isset($this->keys[$id]) || class_exists($id);
169
    }
170
171
    /**
172
     * @param string $id
173
     * @return bool
174
     */
175 22
    protected function isShared(string $id): bool
176
    {
177 22
        return isset($this->shared[$id]);
178
    }
179
180
    /**
181
     * Normalize key to use across container.
182
     *
183
     * @param  string $id
184
     * @return string
185
     */
186 31
    protected function normalize(string $id): string
187
    {
188 31
        return ltrim($id, '\\');
189
    }
190
191
    /**
192
     * @param string $id
193
     * @return void
194
     */
195 27
    protected function resolved(string $id)
196
    {
197 27
        unset($this->resolving[$id]);
198 27
    }
199
200
    /**
201
     * Detects circular references.
202
     *
203
     * @param string $id
204
     * @return void
205
     * @throws CircularReferenceException
206
     */
207 27
    protected function resolving(string $id)
208
    {
209 27
        if (isset($this->resolving[$id])) {
210 3
            throw new CircularReferenceException($id, $this->resolving);
211
        }
212
213
        // We mark service as being resolved to detect circular references through out the resolution chain.
214 27
        $this->resolving[$id] = $id;
215 27
    }
216
217
    /**
218
     * @param InvokerContract $invoker
219
     * @return void
220
     */
221 31
    protected function setInvoker(InvokerContract $invoker)
222
    {
223 31
        $this->invoker = $invoker;
224 31
    }
225
226
    /**
227
     * Forbid container cloning.
228
     *
229
     * @codeCoverageIgnore
230
     */
231
    private function __clone()
232
    {
233
    }
234
235
    /**
236
     * Create callable factory with resolved arguments from callable.
237
     *
238
     * @param Invokable $invokable
239
     * @return Closure
240
     */
241 12
    private function createServiceFactoryFromCallable(Invokable $invokable): Closure
242
    {
243
        return function (array $arguments = []) use ($invokable) {
244 12
            return $this->invoker->invoke($invokable, $arguments);
245 12
        };
246
    }
247
248
    /**
249
     * Create callable factory with resolved arguments from class name.
250
     *
251
     * @param string $class
252
     * @return Closure
253
     * @throws UninstantiableServiceException
254
     */
255 17
    private function createServiceFactoryFromClass(string $class): Closure
256
    {
257 17
        $reflection = new ReflectionClass($class);
258 17
        if (!$reflection->isInstantiable()) {
259 1
            throw new UninstantiableServiceException($class, $this->resolving);
260
        }
261 16
        $constructor = $reflection->getConstructor();
262
263 16
        if ($constructor && $constructor->getNumberOfParameters() > 0) {
264 13
            $invokable = new Invokable($constructor);
265
266
            return function (array $arguments = []) use ($invokable) {
267 13
                return $this->invoker->invoke($invokable, $arguments);
268 13
            };
269
        }
270
271 14
        return function () use ($class) {
272 14
            return new $class();
273 14
        };
274
    }
275
276
    /**
277
     * @param string $id
278
     * @param array $arguments
279
     * @return object
280
     */
281 23
    private function invokeFactory(string $id, array $arguments)
282
    {
283 23
        return $this->saveShared($id, ($this->factories[$id])($arguments));
284
    }
285
286
    /**
287
     * @param string $id
288
     * @param object $object
289
     * @return object
290
     */
291 20
    private function saveShared(string $id, $object)
292
    {
293 20
        if ($this->isShared($id)) {
294 5
            $this->instances[$id] = $object;
295
        }
296
297 20
        return $object;
298
    }
299
300
}
301