Passed
Push — master ( 7c8603...03978d )
by Jonathan
11:26 queued 08:13
created

Container::resolve()   B

Complexity

Conditions 7
Paths 5

Size

Total Lines 19
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 7.049

Importance

Changes 0
Metric Value
eloc 9
dl 0
loc 19
ccs 9
cts 10
cp 0.9
rs 8.8333
c 0
b 0
f 0
cc 7
nc 5
nop 2
crap 7.049
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Gravatalonga\Container;
6
7
use ArrayAccess;
8
use Closure;
9
use Psr\Container\ContainerInterface;
10
use ReflectionClass;
11
use ReflectionException;
12
use ReflectionFunction;
13
use ReflectionNamedType;
14
use ReflectionParameter;
15
use Reflector;
16
17
use function array_key_exists;
18
19
/**
20
 * Class Container.
21
 */
22
class Container implements ArrayAccess, ContainerInterface
23
{
24
    /**
25
     * @var ContainerInterface
26
     */
27
    protected static $instance;
28
29
    /**
30
     * @var array<string, mixed>
31
     */
32
    private $bindings;
33
34
    /**
35
     * @var array<string, mixed>
36
     */
37
    private $resolved = [];
38
39
    /**
40
     * @var array<string, mixed>
41
     */
42
    private $share;
43
44
    /**
45
     * Container constructor.
46
     *
47
     * @param array<string, mixed> $config
48
     */
49 90
    public function __construct(array $config = [])
50
    {
51 90
        $this->bindings = $config;
52 90
        $this->share = [];
53 90
    }
54
55
    /**
56
     * Factory binding.
57
     *
58
     * @param string $id
59
     * @param Closure $factory
60
     *
61
     * @return void
62
     */
63 36
    public function factory($id, Closure $factory)
64
    {
65 36
        $this->bindings[$id] = $factory;
66 36
    }
67
68
    /**
69
     * {@inheritdoc}
70
     *
71
     * @throws ReflectionException
72
     */
73 63
    public function get($id)
74
    {
75 63
        return $this->resolve($id, []);
76
    }
77
78
    /**
79
     * @return ContainerInterface
80
     */
81 3
    public static function getInstance()
82
    {
83 3
        return self::$instance;
84
    }
85
86
    /**
87
     * {@inheritdoc}
88
     */
89 81
    public function has($id)
90
    {
91 81
        return array_key_exists($id, $this->bindings) || array_key_exists($id, $this->share);
92
    }
93
94
    /**
95
     * @param string $id
96
     * @param array<string, mixed> $arguments
97
     *
98
     * @throws NotFoundContainerException
99
     * @throws ContainerException|ReflectionException
100
     *
101
     * @return mixed|object
102
     */
103 15
    public function make($id, array $arguments = [])
104
    {
105 15
        if (array_key_exists($id, $this->share)) {
106 3
            throw ContainerException::shareOnMake($id);
107
        }
108
109 12
        return $this->resolve($id, $arguments);
110
    }
111
112
    /**
113
     * @param string $offset
114
     *
115
     * @return bool
116
     */
117 3
    public function offsetExists($offset)
118
    {
119 3
        return $this->has($offset);
120
    }
121
122
    /**
123
     * @param string $offset
124
     *
125
     * @throws ReflectionException
126
     *
127
     * @return mixed
128
     */
129 6
    public function offsetGet($offset)
130
    {
131 6
        return $this->get($offset);
132
    }
133
134
    /**
135
     * @param string $offset
136
     * @param mixed $value
137
     */
138 9
    public function offsetSet($offset, $value): void
139
    {
140 9
        $this->factory($offset, $value);
141 9
    }
142
143
    /**
144
     * @param string $offset
145
     *
146
     * @return void
147
     */
148 3
    public function offsetUnset($offset)
149
    {
150 3
        unset($this->bindings[$offset], $this->share[$offset]);
151 3
    }
152
153
    /**
154
     * Alias for Factory method.
155
     *
156
     * @param string $id
157
     * @param mixed $factory
158
     *
159
     * @return void
160
     */
161 27
    public function set($id, $factory)
162
    {
163 27
        if ($factory instanceof Closure) {
164 9
            $this->factory($id, $factory);
165
166 9
            return;
167
        }
168 21
        $this->bindings[$id] = $factory;
169 21
    }
170
171
    /**
172
     * @param ContainerInterface $container
173
     */
174 3
    public static function setInstance(ContainerInterface $container): void
175
    {
176 3
        self::$instance = $container;
177 3
    }
178
179
    /**
180
     * Share rather resolve as factory.
181
     *
182
     * @param string $id
183
     * @param Closure $factory
184
     *
185
     * @return void
186
     */
187 15
    public function share($id, Closure $factory)
188
    {
189 15
        $this->share[$id] = $factory;
190 15
    }
191
192
    /**
193
     * @param ReflectionParameter[] $params
194
     * @param array<string, mixed> $arguments
195
     *
196
     * @return array<int, string>
197
     */
198 63
    private function buildDependencies(array $params, array $arguments = [])
199
    {
200 63
        return array_map(
201
            function (ReflectionParameter $param) use ($arguments) {
202 39
                if (true === array_key_exists($param->getName(), $arguments)) {
203 3
                    return $arguments[$param->getName()];
204
                }
205
206
                /** @var ReflectionNamedType|null $type */
207 36
                $type = $param->getType();
208
209
                // in case we can't find type hint, we guess by variable name.
210
                // e.g.: $cache it will attempt resolve 'cache' from container.
211 36
                if (null === $type) {
212 6
                    if ($this->has($param->getName())) {
213 6
                        return $this->get($param->getName());
214
                    }
215
216
                    throw ContainerException::findType($param->getClass());
217
                }
218
219 33
                if (true === $type->isBuiltin()) {
220 12
                    if ($this->has($param->getName())) {
221 9
                        return $this->get($param->getName());
222
                    }
223
224 3
                    if ($type->allowsNull()) {
225 3
                        return null;
226
                    }
227
                }
228
229 21
                if (ContainerInterface::class === $type->getName()) {
230 18
                    return $this;
231
                }
232
233 3
                return $this->get($type->getName());
234 63
            },
235 42
            $params
236
        );
237
    }
238
239
    /**
240
     * @param string $id
241
     * @param array<string, mixed> $arguments
242
     *
243
     * @throws NotFoundContainerException
244
     * @throws ReflectionException
245
     *
246
     * @return mixed|object
247
     */
248 72
    private function resolve(string $id, array $arguments = [])
249
    {
250 72
        if (isset($this->resolved[$id])) {
251 3
            return $this->resolved[$id];
252
        }
253
254 72
        if (!$this->has($id) && !class_exists($id)) {
255 3
            throw NotFoundContainerException::entryNotFound($id);
256
        }
257
258 69
        if (!$this->has($id) && class_exists($id)) {
259 27
            return $this->resolveClass($id, $arguments);
260
        }
261
262 57
        if ($this->has($id)) {
263 57
            return $this->resolveEntry($id, $arguments);
264
        }
265
266
        throw NotFoundContainerException::entryNotFound($id);
267
    }
268
269
    /**
270
     * @param Reflector $reflection
271
     * @param array<string, mixed> $arguments
272
     *
273
     * @return array<int, mixed>
274
     */
275 63
    private function resolveArguments(Reflector $reflection, array $arguments = [])
276
    {
277 63
        $params = [];
278
279 63
        if ($reflection instanceof ReflectionClass) {
280 27
            if (!$constructor = $reflection->getConstructor()) {
281 3
                $params = [];
282
            } else {
283 27
                $params = $constructor->getParameters();
284
            }
285 39
        } elseif ($reflection instanceof ReflectionFunction) {
286 39
            if (!$params = $reflection->getParameters()) {
287 24
                $params = [];
288
            }
289
        }
290
291 63
        return $this->buildDependencies($params, $arguments);
292
    }
293
294
    /**
295
     * @param class-string|object $id
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string|object at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string|object.
Loading history...
296
     * @param array<string, mixed> $arguments
297
     *
298
     * @throws ReflectionException
299
     *
300
     * @return object
301
     */
302 27
    private function resolveClass($id, array $arguments = [])
303
    {
304 27
        $reflection = new ReflectionClass($id);
305
306 27
        return $reflection->newInstanceArgs($this->resolveArguments($reflection, $arguments));
307
    }
308
309
    /**
310
     * @param string $id
311
     * @param array<string, mixed> $arguments
312
     *
313
     * @throws ReflectionException
314
     *
315
     * @return mixed
316
     */
317 57
    private function resolveEntry(string $id, array $arguments = [])
318
    {
319 57
        $get = $this->bindings[$id] ?? $this->share[$id];
320
321 57
        if ($get instanceof Closure) {
322 39
            $reflection = new ReflectionFunction($get);
323 39
            $value = $reflection->invokeArgs($this->resolveArguments($reflection, $arguments));
324
325 39
            if (isset($this->share[$id])) {
326 6
                $this->resolved[$id] = $value;
327
            }
328
329 39
            return $value;
330
        }
331
332 21
        return $get;
333
    }
334
}
335