Passed
Push — master ( a99f8e...df5655 )
by Alexey
04:24
created

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