Passed
Push — master ( 080e24...8d1589 )
by Divine Niiquaye
20:01
created

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