Passed
Push — master ( 084b59...968730 )
by Jonathan
11:11
created

Container::offsetExists()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 3
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 1
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 ReflectionParameter;
14
use Reflector;
15
16
use function array_key_exists;
17
use function is_callable;
18
19
/**
20
 * Class Container.
21
 */
22
class Container extends AutoWiringAware implements ArrayAccess, ContainerInterface
23
{
24
    /**
25
     * @var ContainerInterface
26
     */
27
    protected static $instance;
28
29
    /**
30
     * @var array<string, string>
31
     */
32
    private $aliases = [];
33
34
    /**
35
     * @var array<string, mixed>
36
     */
37
    private $bindings;
38
39
    /**
40
     * @var array<string, boolean>
41
     */
42
    private $entriesBeingResolved = [];
43
44
    /**
45
     * @var array <string, mixed>
46
     */
47
    private $extended = [];
48
49
    /**
50
     * @var array<string, mixed>
51
     */
52
    private $resolved = [];
53
54
    /**
55
     * @var array<string, mixed>
56
     */
57
    private $share;
58
59
    /**
60
     * Container constructor.
61
     *
62
     * @param array<string, mixed> $config
63
     *
64
     * @throws NotFoundContainerException
65
     */
66 171
    public function __construct(array $config = [])
67
    {
68 171
        $this->bindings = $config;
69 171
        $this->share = [];
70
71 171
        $self = $this;
72
        $this->share(ContainerInterface::class, static function () use ($self) {
73 33
            return $self;
74 171
        });
75 171
        $this->alias(ContainerInterface::class, Container::class);
76 171
    }
77
78
    /**
79
     * @param string $entry
80
     * @param string $alias
81
     *
82
     * @throws NotFoundContainerException
83
     *
84
     * @return void
85
     */
86 171
    public function alias($entry, $alias)
87
    {
88 171
        if (true === $this->isAlias($entry)) {
89 3
            throw NotFoundContainerException::entryNotFound($entry);
90
        }
91
92 171
        if (false === $this->has($entry)) {
93 3
            throw NotFoundContainerException::entryNotFound($entry);
94
        }
95
96 171
        $this->aliases[$alias] = $entry;
97 171
    }
98
99
    /**
100
     * @param string $id
101
     *
102
     * @throws NotFoundContainerException
103
     *
104
     * @return void
105
     */
106 21
    public function extend($id, callable $factory)
107
    {
108 21
        if (!$this->has($id)) {
109 3
            throw NotFoundContainerException::entryNotFound($id);
110
        }
111
112 18
        $factory = $factory instanceof Closure ? $factory : Closure::fromCallable($factory);
113
114 18
        if (true === array_key_exists($id, $this->resolved)) {
115 3
            unset($this->resolved[$id]);
116
        }
117
118 18
        $this->extended[$id][] = $factory;
119 18
    }
120
121
    /**
122
     * Factory binding.
123
     *
124
     * @param callable|mixed $factory
125
     *
126
     * @return void
127
     */
128 90
    public function factory(string $id, $factory)
129
    {
130 90
        $this->bindings[$id] = is_callable($factory) ?
131 66
            ($factory instanceof Closure ?
132 66
                $factory :
133 66
                Closure::fromCallable($factory)) :
134 30
            $factory;
135 90
    }
136
137
    /**
138
     * {@inheritdoc}
139
     *
140
     * @throws ReflectionException
141
     */
142 126
    public function get($id)
143
    {
144 126
        return $this->resolve($id, []);
145
    }
146
147
    /**
148
     * @return ContainerInterface
149
     */
150 3
    public static function getInstance()
151
    {
152 3
        return self::$instance;
153
    }
154
155
    /**
156
     * {@inheritdoc}
157
     */
158 171
    public function has($id)
159
    {
160 171
        return array_key_exists($id, $this->bindings) ||
161 171
            array_key_exists($id, $this->share) ||
162 171
            array_key_exists($id, $this->aliases);
163
    }
164
165
    /**
166
     * @param string $id
167
     *
168
     * @return bool
169
     */
170 171
    public function isAlias($id)
171
    {
172 171
        return true === array_key_exists($id, $this->aliases);
173
    }
174
175
    /**
176
     * @param string $id
177
     * @param array<string, mixed> $arguments
178
     *
179
     * @throws NotFoundContainerException
180
     * @throws ContainerException|ReflectionException
181
     *
182
     * @return mixed|object
183
     */
184 24
    public function make($id, array $arguments = [])
185
    {
186 24
        if (array_key_exists($id, $this->share)) {
187 3
            throw ContainerException::shareOnMake($id);
188
        }
189
190 21
        return $this->resolve($id, $arguments);
191
    }
192
193
    /**
194
     * @param string $offset
195
     *
196
     * @return bool
197
     */
198 6
    public function offsetExists($offset)
199
    {
200 6
        return $this->has($offset);
201
    }
202
203
    /**
204
     * @param string $offset
205
     *
206
     * @throws ReflectionException
207
     *
208
     * @return mixed
209
     */
210 6
    public function offsetGet($offset)
211
    {
212 6
        return $this->get($offset);
213
    }
214
215
    /**
216
     * @param string $offset
217
     * @param mixed $value
218
     */
219 9
    public function offsetSet($offset, $value): void
220
    {
221 9
        $this->factory($offset, $value);
222 9
    }
223
224
    /**
225
     * @param string $offset
226
     *
227
     * @return void
228
     */
229 6
    public function offsetUnset($offset)
230
    {
231
        unset(
232 6
            $this->bindings[$offset],
233 6
            $this->share[$offset],
234 6
            $this->resolved[$offset],
235 6
            $this->aliases[$offset],
236 6
            $this->extended[$offset]
237
        );
238 6
    }
239
240
    /**
241
     * Alias for Factory method.
242
     *
243
     * @param mixed $factory
244
     *
245
     * @return void
246
     */
247 45
    public function set(string $id, $factory)
248
    {
249 45
        $this->factory($id, $factory);
250 45
    }
251
252 3
    public static function setInstance(ContainerInterface $container): void
253
    {
254 3
        self::$instance = $container;
255 3
    }
256
257
    /**
258
     * Share rather resolve as factory.
259
     *
260
     * @param string $id
261
     *
262
     * @return void
263
     */
264 171
    public function share($id, Closure $factory)
265
    {
266 171
        if (true === array_key_exists($id, $this->resolved)) {
267 3
            unset($this->resolved[$id]);
268
        }
269 171
        $this->share[$id] = $factory;
270 171
    }
271
272
    /**
273
     * @param ReflectionParameter[] $params
274
     * @param array<string, mixed> $arguments
275
     *
276
     * @return array<int, string>
277
     */
278 123
    private function buildDependencies(array $params, array $arguments = [])
279
    {
280 123
        return array_map(
281
            function (ReflectionParameter $param) use ($arguments) {
282 69
                if (true === array_key_exists($param->getName(), $this->entriesBeingResolved)) {
283 3
                    throw ContainerException::circularDependency();
284
                }
285
286 69
                $this->entriesBeingResolved[$param->getName()] = true;
287
288 69
                if (true === array_key_exists($param->getName(), $arguments)) {
289 9
                    return $arguments[$param->getName()];
290
                }
291
292 66
                $type = $param->getType();
293
294 66
                if (null !== $type && true === array_key_exists($type->getName(), $arguments)) {
295 3
                    return $arguments[$type->getName()];
296
                }
297
298 63
                return $this->autoWiringArguments($param);
299 123
            },
300 41
            $params
301
        );
302
    }
303
304
    /**
305
     * Get all extenders for particular entry id.
306
     *
307
     * @return array|mixed
308
     */
309 126
    private function getExtenders(string $id)
310
    {
311 126
        return $this->extended[$id] ?? [];
312
    }
313
314
    /**
315
     * @param array<string, mixed> $arguments
316
     *
317
     * @throws NotFoundContainerException
318
     * @throws ReflectionException
319
     *
320
     * @return mixed|object
321
     */
322 135
    private function resolve(string $id, array $arguments = [])
323
    {
324 135
        if (true === array_key_exists($id, $this->resolved)) {
325 15
            return $this->resolved[$id];
326
        }
327
328 135
        if (true === array_key_exists($id, $this->aliases)) {
329 12
            return $this->resolve($this->aliases[$id], $arguments);
330
        }
331
332 135
        if ((false === $this->has($id)) && (true === class_exists($id))) {
333 48
            $get = $this->resolveClass($id, $arguments);
334
335 39
            foreach ($this->getExtenders($id) as $extend) {
336
                $get = $extend($this, $get);
337
            }
338
339 39
            return $get;
340
        }
341
342 108
        if (true === $this->has($id)) {
343 105
            $get = $this->resolveEntry($id, $arguments);
344
345 105
            foreach ($this->getExtenders($id) as $extend) {
346 15
                $get = $extend($this, $get);
347
            }
348
349 105
            return $get;
350
        }
351
352 3
        throw NotFoundContainerException::entryNotFound($id);
353
    }
354
355
    /**
356
     * @param array<string, mixed> $arguments
357
     *
358
     * @return array<int, mixed>
359
     */
360 123
    private function resolveArguments(Reflector $reflection, array $arguments = [])
361
    {
362 123
        $params = [];
363
364 123
        if ($reflection instanceof ReflectionClass) {
365 48
            if (null !== $constructor = $reflection->getConstructor()) {
366 45
                $params = $constructor->getParameters();
367
            }
368
        }
369
370 123
        if ($reflection instanceof ReflectionFunction) {
371 84
            $params = $reflection->getParameters();
372
        }
373
374 123
        $value = $this->buildDependencies($params, $arguments);
375 117
        $this->entriesBeingResolved = [];
376
377 117
        return $value;
378
    }
379
380
    /**
381
     * @param mixed $id
382
     * @param array<string, mixed> $arguments
383
     *
384
     * @throws ReflectionException
385
     *
386
     * @return object
387
     */
388 48
    private function resolveClass($id, array $arguments = [])
389
    {
390 48
        $reflection = new ReflectionClass($id);
391
392 48
        return $reflection->newInstanceArgs($this->resolveArguments($reflection, $arguments));
393
    }
394
395
    /**
396
     * @param array<string, mixed> $arguments
397
     *
398
     * @throws ReflectionException
399
     *
400
     * @return mixed
401
     */
402 105
    private function resolveEntry(string $id, array $arguments = [])
403
    {
404 105
        $get = $this->bindings[$id] ?? $this->share[$id];
405
406 105
        if ($get instanceof Closure) {
407 84
            $reflection = new ReflectionFunction($get);
408 84
            $value = $reflection->invokeArgs($this->resolveArguments($reflection, $arguments));
409
410 84
            if (true === array_key_exists($id, $this->share)) {
411 57
                $this->resolved[$id] = $value;
412
            }
413
414 84
            return $value;
415
        }
416
417 24
        return $get;
418
    }
419
}
420