Passed
Push — master ( e77146...4128b2 )
by Alexey
03:21
created

createServiceFactoryFromCallable()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1.064

Importance

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