Passed
Push — master ( 066bd3...0139c3 )
by Alexey
03:59
created

ServiceRegistry::bindFactory()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2.0116

Importance

Changes 0
Metric Value
cc 2
eloc 6
nc 2
nop 3
dl 0
loc 11
ccs 6
cts 7
cp 0.8571
crap 2.0116
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 InvalidArgumentException;
7
use Venta\Contracts\Container\ServiceDecorator as ServiceDecoratorContract;
8
use Venta\Contracts\Container\ServiceInflector as ServiceInflectorContract;
9
use Venta\Contracts\Container\ServiceRegistry as ServiceRegistryContract;
10
11
/**
12
 * Class ServiceRegistry
13
 *
14
 * @package Venta\Container
15
 */
16
abstract class ServiceRegistry implements ServiceRegistryContract
17
{
18
19
    /**
20
     * Array of callable definitions.
21
     *
22
     * @var Invokable[]
23
     */
24
    private $callableDefinitions = [];
25
26
    /**
27
     * Array of class definitions.
28
     *
29
     * @var string[]
30
     */
31
    private $classDefinitions = [];
32
33
    /**
34
     * Array of resolved instances.
35
     *
36
     * @var object[]
37
     */
38
    private $instances = [];
39
40
    /**
41
     * Array of container service identifiers.
42
     *
43
     * @var string[]
44
     */
45
    private $keys = [];
46
47
    /**
48
     * Array of instances identifiers marked as shared.
49
     * Such instances will be instantiated once and returned on consecutive gets.
50
     *
51
     * @var bool[]
52
     */
53
    private $shared = [];
54
55
    /**
56
     * @inheritDoc
57
     */
58 4
    public function addDecorator(string $id, $decorator)
59
    {
60 4
        $id = $this->normalize($id);
61
62
        // Check if correct id is provided.
63 4
        if (!$this->isResolvableService($id)) {
64
            throw new InvalidArgumentException('Invalid id provided.');
65
        }
66
67 4
        $this->decorator()->addDecorator($id, $decorator);
68 4
    }
69
70
    /**
71
     * @inheritDoc
72
     */
73 4
    public function addInflection(string $id, string $method, array $arguments = [])
74
    {
75 4
        $this->inflector()->addInflection($id, $method, $arguments);
76 3
    }
77
78
    /**
79
     * @inheritDoc
80
     */
81 8
    public function bindClass(string $id, string $class, $shared = false)
82
    {
83 8
        if (!$this->isResolvableService($class)) {
84 1
            throw new InvalidArgumentException(sprintf('Class "%s" does not exist.', $class));
85
        }
86
        $this->register($id, $shared, function ($id) use ($class) {
87 6
            $this->classDefinitions[$id] = $class;
88 7
        });
89 6
    }
90
91
    /**
92
     * @inheritDoc
93
     */
94 12
    public function bindFactory(string $id, $callable, $shared = false)
95
    {
96 12
        $reflectedCallable = new Invokable($callable);
97 12
        if (!$this->isResolvableCallable($reflectedCallable)) {
98
            throw new InvalidArgumentException('Invalid callable provided.');
99
        }
100
101
        $this->register($id, $shared, function ($id) use ($reflectedCallable) {
102 12
            $this->callableDefinitions[$id] = $reflectedCallable;
103 12
        });
104 12
    }
105
106
    /**
107
     * @inheritDoc
108
     */
109 5
    public function bindInstance(string $id, $instance)
110
    {
111 5
        if (!$this->isConcrete($instance)) {
112
            throw new InvalidArgumentException('Invalid instance provided.');
113
        }
114 5
        $this->register($id, true, function ($id) use ($instance) {
115 5
            $this->instances[$id] = $instance;
116 5
        });
117 5
    }
118
119
    /**
120
     * @param string $id
121
     * @return null|Invokable
122
     */
123 30
    protected function callableDefinition(string $id)
124
    {
125 30
        return $this->callableDefinitions[$id] ?? null;
126
    }
127
128
    /**
129
     * @param string $id
130
     * @return null|string
131
     */
132 23
    protected function classDefinition(string $id)
133
    {
134 23
        return $this->classDefinitions[$id] ?? null;
135
    }
136
137
    /**
138
     * @return ServiceDecoratorContract
139
     */
140
    abstract protected function decorator(): ServiceDecoratorContract;
141
142
    /**
143
     * @return ServiceInflectorContract
144
     */
145
    abstract protected function inflector(): ServiceInflectorContract;
146
147
    /**
148
     * @param $id
149
     * @return null|object
150
     */
151 34
    protected function instance(string $id)
152
    {
153 34
        return $this->instances[$id] ?? null;
154
    }
155
156
    /**
157
     * Verifies that provided callable can be called by service container.
158
     *
159
     * @param Invokable $reflectedCallable
160
     * @return bool
161
     */
162 12
    protected function isResolvableCallable(Invokable $reflectedCallable): bool
163
    {
164
        // If array represents callable we need to be sure it's an object or a resolvable service id.
165 12
        $callable = $reflectedCallable->callable();
166
167 12
        return $reflectedCallable->isFunction()
168 6
               || is_object($callable[0])
169 12
               || $this->isResolvableService($callable[0]);
170
    }
171
172
    /**
173
     * Check if container can resolve the service with subject identifier.
174
     *
175
     * @param string $id
176
     * @return bool
177
     */
178 40
    protected function isResolvableService(string $id): bool
179
    {
180 40
        return isset($this->keys[$id]) || class_exists($id);
181
    }
182
183
    /**
184
     * @param string $id
185
     * @return bool
186
     */
187 25
    protected function isShared(string $id): bool
188
    {
189 25
        return isset($this->shared[$id]);
190
    }
191
192
    /**
193
     * Normalize key to use across container.
194
     *
195
     * @param  string $id
196
     * @return string
197
     */
198 38
    protected function normalize(string $id): string
199
    {
200 38
        return ltrim($id, '\\');
201
    }
202
203
    /**
204
     * Check if subject service is an object instance.
205
     *
206
     * @param mixed $service
207
     * @return bool
208
     */
209 5
    private function isConcrete($service): bool
210
    {
211 5
        return is_object($service) && !$service instanceof Closure;
212
    }
213
214
    /**
215
     * Registers binding.
216
     * After this method call binding can be resolved by container.
217
     *
218
     * @param string $id
219
     * @param bool $shared
220
     * @param Closure $registrationCallback
221
     * @return void
222
     */
223 23
    private function register(string $id, bool $shared, Closure $registrationCallback)
224
    {
225
        // Check if correct service is provided.
226 23
        $this->validateId($id);
227 22
        $id = $this->normalize($id);
228
229
        // Clean up previous bindings, if any.
230 22
        unset($this->instances[$id], $this->shared[$id], $this->keys[$id]);
231
232
        // Register service with provided callback.
233 22
        $registrationCallback($id);
234
235
        // Mark service as shared when needed.
236 22
        $this->shared[$id] = $shared ?: null;
237
238
        // Save service key to make it recognizable by container.
239 22
        $this->keys[$id] = true;
240 22
    }
241
242
    /**
243
     * Validate service identifier. Throw an Exception in case of invalid value.
244
     *
245
     * @param string $id
246
     * @return void
247
     * @throws InvalidArgumentException
248
     */
249 23
    private function validateId(string $id)
250
    {
251 23
        if (!interface_exists($id) && !class_exists($id)) {
252 1
            throw new InvalidArgumentException(
253 1
                sprintf('Invalid service id "%s". Service id must be an existing interface or class name.', $id)
254
            );
255
        }
256 22
    }
257
258
}