AbstractContainer::alias()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 3

Importance

Changes 0
Metric Value
eloc 5
c 0
b 0
f 0
dl 0
loc 11
ccs 6
cts 6
cp 1
rs 10
cc 3
nc 3
nop 2
crap 3
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\Helpers;
21
use Psr\Container\ContainerInterface;
22
use Rade\DI\Exceptions\{CircularReferenceException, ContainerResolutionException, NotFoundServiceException};
23
use Rade\DI\Resolvers\Resolver;
24
use Rade\DI\Services\ServiceProviderInterface;
25
use Symfony\Component\Config\Definition\{ConfigurationInterface, Processor};
26
use Symfony\Contracts\Service\ResetInterface;
27
28
/**
29
 * Internal shared container.
30
 *
31
 * @method call($callback, array $args = [])             Resolve a service definition, class string, invocable object or callable using autowiring.
32
 * @method resolveClass(string $class, array $args = []) Resolves a class string.
33
 *
34
 * @author Divine Niiquaye Ibok <[email protected]>
35
 */
36
abstract class AbstractContainer implements ContainerInterface, ResetInterface
37
{
38
    public const IGNORE_MULTIPLE_SERVICE = 0;
39
40
    public const EXCEPTION_ON_MULTIPLE_SERVICE = 1;
41
42
    /** @var array<string,mixed> For handling a global config around services */
43
    public array $parameters = [];
44
45
    /** @var array<string,mixed> A list of already loaded services (this act as a local cache) */
46
    protected static array $services;
47
48
    /** @var Services\ServiceProviderInterface[] A list of service providers */
49
    protected array $providers = [];
50
51
    protected Resolvers\Resolver $resolver;
52
53
    /** @var array<string,bool> service name => bool */
54
    protected array $loading = [];
55
56
    /** @var string[] alias => service name */
57
    protected array $aliases = [];
58
59
    /** @var array[] tag name => service name => tag value */
60
    private array $tags = [];
61
62
    /** @var array<string,string[]> type => services */
63
    protected array $types = [ContainerInterface::class => ['container'], AbstractContainer::class => ['container']];
64
65
    /** @var array<string,bool> of classes excluded from autowiring */
66
    private array $excluded = [
67
        \ArrayAccess::class => true,
68
        \Countable::class => true,
69
        \IteratorAggregate::class => true,
70
        \SplDoublyLinkedList::class => true,
71
        \stdClass::class => true,
72
        \SplStack::class => true,
73
        \Stringable::class => true,
74
        \Iterator::class => true,
75
        \Traversable::class => true,
76
        \Serializable::class => true,
77
        \JsonSerializable::class => true,
78
        ServiceProviderInterface::class => true,
79
        ResetInterface::class => true,
80
        Services\ServiceLocator::class => true,
81
        Builder\Reference::class => true,
82
        Builder\Statement::class => true,
83
        RawDefinition::class => true,
84
        Definition::class => true,
85
    ];
86
87 106
    public function __construct()
88
    {
89 106
        self::$services = ['container' => $this];
90 106
        $this->resolver = new Resolver($this);
91 106
    }
92
93
    /**
94
     * Container can not be cloned.
95
     */
96 1
    public function __clone()
97
    {
98 1
        throw new \LogicException('Container is not cloneable');
99
    }
100
101
    /**
102
     * @param array<int,mixed> $args
103
     *
104
     * @throws \ReflectionException
105
     *
106
     * @return mixed
107
     */
108 22
    public function __call(string $name, array $args)
109
    {
110 22
        switch ($name) {
111 22
            case 'resolveClass':
112 6
                return $this->resolver->resolveClass($args[0], $args[1] ?? []);
113
114 18
            case 'call':
115 18
                return $this->resolver->resolve($args[0], $args[1] ?? []);
116
117
            default:
118 1
                if (\method_exists($this, $name)) {
119 1
                    $message = \sprintf('Method call \'%s()\' is either a member of container or a protected service method.', $name);
120
                }
121
122 1
                throw new \BadMethodCallException(
123 1
                    $message ?? \sprintf('Method call %s->%s() invalid, "%2$s" doesn\'t exist.', __CLASS__, $name)
124
                );
125
        }
126
    }
127
128
    /**
129
     * {@inheritdoc}
130
     *
131
     * @param string $id              identifier of the entry to look for
132
     * @param int    $invalidBehavior The behavior when multiple services returns for $id
133
     */
134
    abstract public function get(string $id, int $invalidBehavior = /* self::EXCEPTION_ON_MULTIPLE_SERVICE */ 1);
135
136
    /**
137
     * {@inheritdoc}
138
     */
139
    abstract public function has(string $id): bool;
140
141
    /**
142
     * Returns all defined value names.
143
     *
144
     * @return string[] An array of value names
145
     */
146
    abstract public function keys(): array;
147
148
    /**
149
     * Gets the service definition or aliased entry from the container.
150
     *
151
     * @param string $id service id relying on this definition
152
     *
153
     * @throws NotFoundServiceException No entry was found for identifier
154
     *
155
     * @return Definition|RawDefinition|object
156
     */
157
    abstract public function service(string $id);
158
159
    /**
160
     * Returns the registered service provider.
161
     *
162
     * @param string $id The class name of the service provider
163
     */
164 6
    final public function provider(string $id): ?ServiceProviderInterface
165
    {
166 6
        return $this->providers[$id] ?? null;
167
    }
168
169
    /**
170
     * Registers a service provider.
171
     *
172
     * @param ServiceProviderInterface $provider A ServiceProviderInterface instance
173
     * @param array<string,mixed>      $config   An array of config that customizes the provider
174
     */
175 8
    final public function register(ServiceProviderInterface $provider, array $config = []): self
176
    {
177
        // If service provider depends on other providers ...
178 8
        if ($provider instanceof Services\DependedInterface) {
179 4
            foreach ($provider->dependencies() as $name => $dependency) {
180 4
                $dependencyProvider = $this->resolver->resolveClass($dependency);
181
182 4
                if ($dependencyProvider instanceof ServiceProviderInterface) {
183 4
                    $this->register($dependencyProvider, $config[!\is_numeric($name) ? $name : $dependency] ?? []);
184
                }
185
            }
186
        }
187
188 8
        $this->providers[$providerId = \get_class($provider)] = $provider;
189
190
        // Override $providerId if method exists ...
191 8
        if (\method_exists($provider, 'getId')) {
192 4
            $providerId = $providerId::getId();
193
        }
194
195
        // If symfony's config is present ...
196 8
        if ($provider instanceof ConfigurationInterface) {
197 4
            $config = (new Processor())->processConfiguration($provider, [$providerId => $config[$providerId] ?? $config]);
198
        }
199
200 8
        $provider->register($this, $config[$providerId] ?? $config);
201
202 8
        return $this;
203
    }
204
205
    /**
206
     * Returns true if the given service has actually been initialized.
207
     *
208
     * @param string $id The service identifier
209
     *
210
     * @return bool true if service has already been initialized, false otherwise
211
     */
212 7
    public function initialized(string $id): bool
213
    {
214 7
        return isset(self::$services[$id]) || (isset($this->aliases[$id]) && $this->initialized($this->aliases[$id]));
215
    }
216
217
    /**
218
     * Remove an alias, service definition id, or a tagged service.
219
     */
220 10
    public function remove(string $id): void
221
    {
222 10
        unset(self::$services[$id], $this->aliases[$id], $this->types[$id], $this->tags[$id]);
223 10
    }
224
225
    /**
226
     * Resets the container.
227
     */
228 4
    public function reset(): void
229
    {
230 4
        $this->tags = $this->aliases = $this->types = [];
231 4
    }
232
233
    /**
234
     * Marks an alias id to service id.
235
     *
236
     * @param string $id        The alias id
237
     * @param string $serviceId The registered service id
238
     *
239
     * @throws ContainerResolutionException Service id is not found in container
240
     */
241 12
    public function alias(string $id, string $serviceId): void
242
    {
243 12
        if ($id === $serviceId) {
244 1
            throw new \LogicException("[$id] is aliased to itself.");
245
        }
246
247 11
        if (!$this->has($serviceId)) {
248 2
            throw new ContainerResolutionException("Service id '$serviceId' is not found in container");
249
        }
250
251 9
        $this->aliases[$id] = $this->aliases[$serviceId] ?? $serviceId;
252 9
    }
253
254
    /**
255
     * Checks if a service definition has been aliased.
256
     *
257
     * @param string $id The registered service id
258
     */
259 2
    public function aliased(string $id): bool
260
    {
261 2
        foreach ($this->aliases as $serviceId) {
262 2
            if ($id === $serviceId) {
263 2
                return true;
264
            }
265
        }
266
267 1
        return false;
268
    }
269
270
    /**
271
     * Add a aliased type of classes or interfaces for a service definition.
272
     *
273
     * @param string          $id    The registered service id
274
     * @param string|string[] $types The types associated with service definition
275
     */
276 123
    public function type(string $id, $types): void
277
    {
278 123
        foreach ((array) $types as $typed) {
279 98
            if ($this->excluded[$typed] ?? \in_array($id, $this->types[$typed] ?? [], true)) {
280 51
                continue;
281
            }
282
283 98
            $this->types[$typed][] = $id;
284
        }
285 123
    }
286
287
    /**
288
     * Set types for multiple services.
289
     *
290
     * @see type method
291
     *
292
     * @param string[] $types The types associated with service definition
293
     */
294 2
    public function types(array $types): void
295
    {
296 2
        foreach ($types as $id => $wiring) {
297 2
            if (\is_int($id)) {
298 1
                throw new ContainerResolutionException('Service identifier is not defined, integer found.');
299
            }
300
301 2
            $this->type($id, (array) $wiring);
302
        }
303 2
    }
304
305
    /**
306
     * If class or interface is a autowired typed value.
307
     *
308
     * @param string $id of class or interface
309
     *
310
     * @return bool|string[] If $ids is true, returns found ids else bool
311
     */
312 88
    public function typed(string $id, bool $ids = false)
313
    {
314 88
        return $ids ? $this->types[$id] ?? [] : isset($this->types[$id]);
315
    }
316
317
    /**
318
     * If id is a registered service id, return bool else if types is set on id,
319
     * resolve value and return it.
320
     *
321
     * @param string $id A class or an interface name
322
     *
323
     * @throws ContainerResolutionException|NotFoundServiceException
324
     *
325
     * @return mixed
326
     */
327 46
    public function autowired(string $id, bool $single = false)
328
    {
329 46
        if (isset($this->types[$id])) {
330 45
            if (1 === \count($autowired = $this->types[$id])) {
331 40
                return $single ? $this->get($autowired[0]) : [$this->get($autowired[0])];
332
            }
333
334 10
            if ($single) {
335 5
                \natsort($autowired);
336
337 5
                throw new ContainerResolutionException(\sprintf('Multiple services of type %s found: %s.', $id, \implode(', ', $autowired)));
338
            }
339
340 7
            return \array_map([$this, 'get'], $autowired);
341
        }
342
343 1
        throw new NotFoundServiceException("Service of type '$id' not found. Check class name because it cannot be found.");
344
    }
345
346
    /**
347
     * Add a class or interface that should be excluded from autowiring.
348
     *
349
     * @param string|string[] $types
350
     */
351 2
    public function exclude($types): void
352
    {
353 2
        foreach ((array) $types as $type) {
354 2
            $this->excluded[$type] = true;
355
        }
356 2
    }
357
358
    /**
359
     * Assign a set of tags to service(s).
360
     *
361
     * @param string[]|string         $serviceIds
362
     * @param array<int|string,mixed> $tags
363
     */
364 13
    public function tag($serviceIds, array $tags): void
365
    {
366 13
        foreach ((array) $serviceIds as $service) {
367 13
            foreach ($tags as $tag => $attributes) {
368
                // Exchange values if $tag is an integer
369 13
                if (\is_int($tmp = $tag)) {
370 9
                    $tag = $attributes;
371 9
                    $attributes = $tmp;
372
                }
373
374 13
                $this->tags[$service][$tag] = $attributes;
375
            }
376
        }
377 13
    }
378
379
    /**
380
     * Resolve all of the bindings for a given tag.
381
     *
382
     * @return object[]|string[]|mixed[]
383
     */
384 15
    public function tagged(string $tag, bool $resolve = true): array
385
    {
386 15
        $tags = [];
387
388 15
        foreach ($this->tags as $service => $tagged) {
389 13
            if (isset($tagged[$tag])) {
390 13
                $tags[] = [$resolve ? $this->get($service) : $service, $tagged[$tag]];
391
            }
392
        }
393
394 15
        return $tags;
395
    }
396
397
    /**
398
     * The resolver associated with the container.
399
     */
400 14
    public function getResolver(): Resolver
401
    {
402 14
        return $this->resolver;
403
    }
404
405
    /**
406
     * Marks a definition from being interpreted as a service.
407
     *
408
     * @param mixed $definition from being evaluated
409
     */
410 23
    public function raw($definition): RawDefinition
411
    {
412 23
        return new RawDefinition($definition);
413
    }
414
415
    /**
416
     * @internal prevent service looping
417
     *
418
     * @param Definition|RawDefinition|callable $service
419
     *
420
     * @throws CircularReferenceException
421
     *
422
     * @return mixed
423
     */
424
    abstract protected function doCreate(string $id, $service);
425
426
    /**
427
     * Throw a PSR-11 not found exception.
428
     */
429 15
    protected function createNotFound(string $id, bool $throw = false): NotFoundServiceException
430
    {
431 15
        if (null !== $suggest = Helpers::getSuggestion($this->keys(), $id)) {
432 3
            $suggest = " Did you mean: \"$suggest\" ?";
433
        }
434
435 15
        $error = new NotFoundServiceException(\sprintf('Identifier "%s" is not defined.' . $suggest, $id));
436
437 15
        if ($throw) {
438 3
            throw $error;
439
        }
440
441 14
        return $error;
442
    }
443
}
444