Passed
Push — master ( 6a389c...42f43f )
by Divine Niiquaye
01:03 queued 12s
created

AbstractContainer::initialized()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 3

Importance

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