Passed
Branch master (b33026)
by Divine Niiquaye
159:36 queued 107:08
created

Container::getService()   B

Complexity

Conditions 7
Paths 6

Size

Total Lines 25
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 7

Importance

Changes 4
Bugs 0 Features 0
Metric Value
eloc 12
c 4
b 0
f 0
dl 0
loc 25
ccs 13
cts 13
cp 1
rs 8.8333
cc 7
nc 6
nop 2
crap 7
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of DivineNii opensource projects.
7
 *
8
 * PHP version 7.4 and above required
9
 *
10
 * @author    Divine Niiquaye Ibok <[email protected]>
11
 * @copyright 2021 DivineNii (https://divinenii.com/)
12
 * @license   https://opensource.org/licenses/BSD-3-Clause License
13
 *
14
 * For the full copyright and license information, please view the LICENSE
15
 * file that was distributed with this source code.
16
 */
17
18
namespace Rade\DI;
19
20
use Nette\Utils\{Callback, Reflection};
21
use Psr\Container\ContainerInterface;
22
use Rade\DI\{
23
    Builder\Statement,
24
    Exceptions\CircularReferenceException,
25
    Exceptions\FrozenServiceException,
26
    Exceptions\NotFoundServiceException,
27
    Exceptions\ContainerResolutionException
28
};
29
use Symfony\Contracts\Service\ResetInterface;
30
31
/**
32
 * Dependency injection container.
33
 *
34
 * @author Divine Niiquaye Ibok <[email protected]>
35
 */
36
class Container extends AbstractContainer implements \ArrayAccess
37
{
38
    protected array $types = [
39
        ContainerInterface::class => ['container'],
40
        Container::class => ['container'],
41
    ];
42
43
    /** @var array<string,string> internal cached services */
44
    protected array $methodsMap = ['container' => 'getServiceContainer'];
45
46
    /** @var array<string,mixed> service name => instance */
47
    private array $values = [];
48
49
    /** @var array<string,bool> service name => bool */
50
    private array $frozen = [];
51
52
    /** @var array<string,bool> service name => bool */
53
    private array $keys = [];
54
55
    /**
56
     * Instantiates the container.
57
     */
58 96
    public function __construct()
59
    {
60 96
        parent::__construct();
61
62
        // Incase this class it extended ...
63 96
        if (__CLASS__ !== static::class) {
0 ignored issues
show
introduced by
The condition __CLASS__ !== static::class is always false.
Loading history...
64 13
            $this->types += [static::class => ['container']];
65
        }
66
67 96
        $this->resolver = new Resolvers\Resolver($this, $this->types);
68 96
    }
69
70
    /**
71
     * Sets a new service to a unique identifier.
72
     *
73
     * @param string $offset The unique identifier for the parameter or object
74
     * @param mixed  $value  The value of the service assign to the $offset
75
     *
76
     * @throws FrozenServiceException Prevent override of a frozen service
77
     */
78 59
    public function offsetSet($offset, $value): void
79
    {
80 59
        $this->set($offset, $value, true);
81 59
    }
82
83
    /**
84
     * Gets a registered service definition.
85
     *
86
     * @param string $offset The unique identifier for the service
87
     *
88
     * @throws NotFoundServiceException If the identifier is not defined
89
     *
90
     * @return mixed The value of the service
91
     */
92 56
    public function offsetGet($offset)
93
    {
94 56
        return $this->get($offset);
95
    }
96
97
    /**
98
     * Checks if a service is set.
99
     *
100
     * @param string $offset The unique identifier for the service
101
     */
102 8
    public function offsetExists($offset): bool
103
    {
104 8
        return $this->has($offset);
105
    }
106
107
    /**
108
     * Unset a service by given offset.
109
     *
110
     * @param string $offset The unique identifier for service definition
111
     */
112 6
    public function offsetUnset($offset): void
113
    {
114 6
        $this->remove($offset);
115 6
    }
116
117
    /**
118
     * This is useful when you want to autowire a callable or class string lazily.
119
     *
120
     * @deprecated Since 1.0, use Statement class instead, will be dropped in v2
121
     *
122
     * @param callable|string $definition A class string or a callable
123
     */
124 17
    public function lazy($definition): Statement
125
    {
126 17
        return new Statement($definition);
127
    }
128
129
    /**
130
     * Marks a definition as being a factory service.
131
     *
132
     * @deprecated Since 1.0, use definition method instead, will be dropped in v2
133
     *
134
     * @param callable|object|string $callable A service definition to be used as a factory
135
     */
136 5
    public function factory($callable): Definition
137
    {
138 5
        return $this->definition($callable, Definition::FACTORY);
139
    }
140
141
    /**
142
     * Create a definition service.
143
     *
144
     * @param Definition|Statement|object|callable|string $service
145
     * @param int|null                                    $type    of Definition::FACTORY | Definition::LAZY
146
     */
147 10
    public function definition($service, int $type = null): Definition
148
    {
149 10
        $definition = new Definition($service);
150
151 10
        return null === $type ? $definition : $definition->should($type);
152
    }
153
154
    /**
155
     * Extends an object definition.
156
     *
157
     * Useful when you want to extend an existing object definition,
158
     * without necessarily loading that object.
159
     *
160
     * @param string   $id    The unique identifier for the object
161
     * @param callable $scope A service definition to extend the original
162
     *
163
     * @throws NotFoundServiceException   If the identifier is not defined
164
     * @throws FrozenServiceException     If the service is frozen
165
     * @throws CircularReferenceException If infinite loop among service is detected
166
     *
167
     * @return mixed The wrapped scope or Definition instance
168
     */
169 10
    public function extend(string $id, callable $scope)
170
    {
171 10
        if ($this->frozen[$id] ?? isset($this->methodsMap[$id])) {
172 2
            throw new FrozenServiceException($id);
173
        }
174
175 8
        $extended = $this->values[$id] ?? $this->createNotFound($id, true);
176
177 7
        if ($extended instanceof RawDefinition) {
178 1
            return $this->values[$id] = new RawDefinition($scope($extended(), $this));
179
        }
180
181 6
        if (!$extended instanceof Definition && \is_callable($extended)) {
182 4
            $extended = $this->doCreate($id, $extended);
183
        }
184
185 5
        return $this->values[$id] = $scope($extended, $this);
186
    }
187
188
    /**
189
     * {@inheritdoc}
190
     */
191 27
    public function keys(): array
192
    {
193 27
        return \array_keys($this->keys + $this->methodsMap);
194
    }
195
196
    /**
197
     * {@inheritdoc}
198
     */
199 4
    public function reset(): void
200
    {
201 4
        parent::reset();
202
203 4
        foreach ($this->values as $id => $service) {
204 2
            if (isset(self::$services[$id])) {
205 2
                $service = self::$services[$id];
206
            }
207
208 2
            if ($service instanceof ResetInterface) {
209 1
                $service->reset();
210
            }
211
212 2
            $this->remove($id);
213
        }
214 4
    }
215
216
    /**
217
     * {@inheritdoc}
218
     */
219 8
    public function remove(string $id): void
220
    {
221 8
        if (isset($this->keys[$id])) {
222 8
            unset($this->values[$id], $this->keys[$id], $this->frozen[$id], self::$services[$id]);
223
        }
224
225 8
        parent::remove($id);
226 8
    }
227
228
    /**
229
     * {@inheritdoc}
230
     */
231 11
    public function service(string $id)
232
    {
233 11
        return $this->values[$this->aliases[$id] ?? $id] ?? $this->createNotFound($id, true);
234
    }
235
236
    /**
237
     * {@inheritdoc}
238
     */
239 78
    public function get(string $id, int $invalidBehavior = /* self::EXCEPTION_ON_MULTIPLE_SERVICE */ 1)
240
    {
241
        try {
242 78
            return self::$services[$id] ?? $this->{$this->methodsMap[$id] ?? 'getService'}($id, $invalidBehavior);
243 29
        } catch (NotFoundServiceException $serviceError) {
244 19
            if (\class_exists($id)) {
245
                try {
246 8
                    return $this->resolver->resolveClass($id);
247 1
                } catch (ContainerResolutionException $e) {
248
                    // Only resolves class string and not throw it's error.
249
                }
250
            }
251
252 14
            if (isset($this->aliases[$id])) {
253 3
                return $this->get($this->aliases[$id]);
254
            }
255
256 12
            throw $serviceError;
257
        }
258
    }
259
260
    /**
261
     * {@inheritdoc}
262
     */
263 72
    public function has(string $id): bool
264
    {
265 72
        return $this->keys[$id] ?? isset($this->methodsMap[$id]) ||
266 72
            (isset($this->providers[$id]) || isset($this->aliases[$id]));
267
    }
268
269
    /**
270
     * Set a service definition.
271
     *
272
     * @param Definition|RawDefinition|Statement|\Closure|object $definition
273
     *
274
     * @throws FrozenServiceException Prevent override of a frozen service
275
     *
276
     * @return Definition|RawDefinition|object|\Closure of Definition, RawService, class object or closure
277
     */
278 97
    public function set(string $id, object $definition, bool $autowire = false)
279
    {
280 97
        if ($this->frozen[$id] ?? isset($this->methodsMap[$id])) {
281 1
            throw new FrozenServiceException($id);
282
        }
283
284
        // Incase new service definition exists in aliases.
285 97
        unset($this->aliases[$id]);
286
287 97
        if ($definition instanceof Definition) {
288 40
            $definition->attach($id, $this->resolver);
289 40
            $definition = $autowire ? $definition->autowire() : $definition;
290 63
        } elseif ($definition instanceof Statement) {
291 17
            if ($autowire) {
292 16
                $this->autowireService($id, $definition->value);
293
            }
294
295 17
            $definition = fn () => $this->resolver->resolve($definition->value, $definition->args);
296 59
        } elseif ($autowire && !$definition instanceof RawDefinition) {
297 50
            $this->autowireService($id, $definition);
298
        }
299
300 97
        $this->keys[$id] = true;
301
302 97
        return $this->values[$id] = $definition;
303
    }
304
305
    /**
306
     * @internal
307
     *
308
     * Get the mapped service container instance
309
     */
310 2
    protected function getServiceContainer(): self
311
    {
312 2
        return self::$services['container'] = $this;
313
    }
314
315
    /**
316
     * Build an entry of the container by its name.
317
     *
318
     * @throws CircularReferenceException|NotFoundServiceException
319
     *
320
     * @return mixed
321
     */
322 75
    protected function getService(string $id, int $invalidBehavior)
323
    {
324 75
        if (!isset($this->keys[$id]) && $this->resolver->has($id)) {
325 12
            return $this->resolver->get($id, self::EXCEPTION_ON_MULTIPLE_SERVICE === $invalidBehavior);
326
        }
327
328 73
        if (!\is_callable($definition = $this->values[$id] ?? $this->createNotFound($id, true))) {
329 23
            $this->frozen[$id] = true;
330
331 23
            return self::$services[$id] = $definition; // If definition is frozen, cache it ...
332
        }
333
334 54
        if ($definition instanceof Definition) {
335 13
            if (!$definition->isPublic()) {
336 1
                throw new ContainerResolutionException(
337 1
                    \sprintf('Using service definition for "%s" as private is not supported.', $id)
338
                );
339
            }
340
341 12
            if ($definition->isFactory()) {
342 5
                return $this->doCreate($id, $definition);
343
            }
344
        }
345
346 50
        return $this->values[$id] = self::$services[$id] = $this->doCreate($id, $definition, true);
347
    }
348
349
    /**
350
     * {@inheritdoc}
351
     */
352 54
    protected function doCreate(string $id, $service, bool $freeze = false)
353
    {
354
        // Checking if circular reference exists ...
355 54
        if (isset($this->loading[$id])) {
356 2
            throw new CircularReferenceException($id, [...\array_keys($this->loading), $id]);
357
        }
358 54
        $this->loading[$id] = true;
359
360
        try {
361 54
            return $this->resolver->resolve($service);
362
        } finally {
363 54
            unset($this->loading[$id]);
364
365 54
            if ($freeze) {
366 54
                $this->frozen[$id] = true; // Freeze resolved service ...
367
            }
368
        }
369
    }
370
371
    /**
372
     * @param mixed $definition
373
     */
374 53
    private function autowireService(string $id, $definition): void
375
    {
376 53
        static $types = [];
377
378 53
        if (\is_callable($definition)) {
379 28
            $types = Reflection::getReturnTypes(Callback::toReflection($definition));
380 31
        } elseif (\is_object($definition) && !$definition instanceof \stdClass) {
381 24
            $types = [\get_class($definition)];
382 18
        } elseif (\is_string($definition) && \class_exists($definition)) {
383 16
            $types = [$definition];
384
        }
385
386
        // Resolving wiring so we could call the service parent classes and interfaces.
387 53
        $this->resolver->autowire($id, $types);
388 53
    }
389
}
390