Passed
Push — master ( bc3bac...02d6da )
by Divine Niiquaye
11:28
created

AbstractContainer::aliased()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 4
c 1
b 0
f 0
dl 0
loc 9
ccs 0
cts 5
cp 0
rs 10
cc 3
nc 3
nop 1
crap 12
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\{
23
    CircularReferenceException, ContainerResolutionException, NotFoundServiceException
24
};
25
use Rade\DI\Services\ServiceProviderInterface;
26
use Symfony\Component\Config\Definition\{ConfigurationInterface, Processor};
27
use Symfony\Contracts\Service\ResetInterface;
28
29
/**
30
 * Internal shared container.
31
 *
32
 * @method call($callback, array $args = [])
33
 *      Resolve a service definition, class string, invocable object or callable using autowiring.
34
 * @method resolveClass(string $class, array $args = []) Resolves a class string.
35
 * @method autowire(string $id, array $types) Resolve wiring classes + interfaces to service id.
36
 * @method exclude(string $type) Exclude an interface or class type from being autowired.
37
 *
38
 * @author Divine Niiquaye Ibok <[email protected]>
39
 */
40
abstract class AbstractContainer implements ContainerInterface, ResetInterface
41
{
42
    public const IGNORE_MULTIPLE_SERVICE = 0;
43
44
    public const EXCEPTION_ON_MULTIPLE_SERVICE = 1;
45
46
    /** @var array<string,mixed> For handling a global config around services */
47
    public array $parameters = [];
48
49
    /** @var array<string,mixed> A list of already loaded services (this act as a local cache) */
50
    protected static array $services;
51
52
    /** @var Services\ServiceProviderInterface[] A list of service providers */
53
    protected array $providers = [];
54
55
    protected Resolvers\Resolver $resolver;
56
57
    /** @var array<string,bool> service name => bool */
58
    protected array $loading = [];
59
60
    /** @var string[] alias => service name */
61
    protected array $aliases = [];
62
63
    /** @var array[] tag name => service name => tag value */
64
    private array $tags = [];
65
66 125
    public function __construct()
67
    {
68 125
        self::$services = [];
69 125
    }
70
71
    /**
72
     * Container can not be cloned.
73
     */
74 1
    public function __clone()
75
    {
76 1
        throw new \LogicException('Container is not cloneable');
77
    }
78
79
    /**
80
     * @throws \ReflectionException
81
     *
82
     * @return mixed
83
     */
84 22
    public function __call(string $name, array $args)
85
    {
86 22
        switch ($name) {
87 22
            case 'resolveClass':
88 4
                return $this->resolver->resolveClass($args[0], $args[1] ?? []);
89
90 20
            case 'call':
91 18
                return $this->resolver->resolve($args[0], $args[1] ?? []);
92
93 4
            case 'autowire':
94 1
                if (!$this->has($args[0])) {
95 1
                    throw $this->createNotFound($args[0]);
96
                }
97
98 1
                $this->resolver->autowire($args[0], $args[1] ?? []);
99
100 1
                break;
101
102 3
            case 'exclude':
103 2
                $this->resolver->exclude($args[0]);
104
105 2
                break;
106
107
            default:
108 1
                if (\method_exists($this, $name)) {
109 1
                    $message = \sprintf('Method call \'%s()\' is either a member of container or a protected service method.', $name);
110
                }
111
112 1
                throw new \BadMethodCallException(
113 1
                    $message ?? \sprintf('Method call %s->%s() invalid, "%2$s" doesn\'t exist.', __CLASS__, $name)
114
                );
115
        }
116
117 3
        return $this;
118
    }
119
120
    /**
121
     * {@inheritdoc}
122
     *
123
     * @param string $id              identifier of the entry to look for
124
     * @param int    $invalidBehavior The behavior when multiple services returns for $id
125
     */
126
    abstract public function get(string $id, int $invalidBehavior = /* self::EXCEPTION_ON_MULTIPLE_SERVICE */ 1);
127
128
    /**
129
     * {@inheritdoc}
130
     */
131
    abstract public function has(string $id): bool;
132
133
    /**
134
     * Returns all defined value names.
135
     *
136
     * @return string[] An array of value names
137
     */
138
    abstract public function keys(): array;
139
140
    /**
141
     * Gets the service definition or aliased entry from the container.
142
     *
143
     * @param string $id service id relying on this definition
144
     *
145
     * @throws NotFoundServiceException No entry was found for identifier
146
     *
147
     * @return Definition|RawDefinition|object
148
     */
149
    abstract public function service(string $id);
150
151
    /**
152
     * Returns the registered service provider.
153
     *
154
     * @param string $id The class name of the service provider
155
     */
156 6
    final public function provider(string $id): ?ServiceProviderInterface
157
    {
158 6
        return $this->providers[$id] ?? null;
159
    }
160
161
    /**
162
     * Registers a service provider.
163
     *
164
     * @param ServiceProviderInterface $provider A ServiceProviderInterface instance
165
     * @param array                    $config   An array of config that customizes the provider
166
     */
167 6
    final public function register(ServiceProviderInterface $provider, array $config = []): self
168
    {
169
        // If service provider depends on other providers ...
170 6
        if ($provider instanceof Services\DependedInterface) {
171 2
            foreach ($provider->dependencies() as $dependency) {
172 2
                $dependencyProvider = $this->resolver->resolveClass($dependency);
173
174 2
                if ($dependencyProvider instanceof ServiceProviderInterface) {
175 2
                    $this->register($dependencyProvider, $config[$dependency] ?? []);
176
                }
177
            }
178
        }
179
180 6
        $this->providers[$providerId = \get_class($provider)] = $provider;
181
182
        // If symfony's config is present ...
183 6
        if ($provider instanceof ConfigurationInterface) {
184 2
            $config = (new Processor())->processConfiguration($provider, [$providerId => $config[$providerId] ?? $config]);
185
        }
186
187 6
        $provider->register($this, $config[$providerId] ?? $config);
188
189 6
        return $this;
190
    }
191
192
    /**
193
     * Returns true if the given service has actually been initialized.
194
     *
195
     * @param string $id The service identifier
196
     *
197
     * @return bool true if service has already been initialized, false otherwise
198
     */
199 4
    public function initialized(string $id): bool
200
    {
201 4
        return isset(self::$services[$id]) || (isset($this->aliases[$id]) && $this->initialized($this->aliases[$id]));
202
    }
203
204
    /**
205
     * Remove an alias, service definition id, or a tagged service.
206
     */
207 8
    public function remove(string $id): void
208
    {
209 8
        unset($this->aliases[$id], $this->tags[$id]);
210 8
    }
211
212
    /**
213
     * Resets the container.
214
     */
215 4
    public function reset(): void
216
    {
217 4
        $this->resolver->reset();
218
219 4
        $this->tags = $this->aliases = [];
220 4
    }
221
222
    /**
223
     * Marks an alias id to service id.
224
     *
225
     * @param string $id        The alias id
226
     * @param string $serviceId The registered service id
227
     *
228
     * @throws ContainerResolutionException Service id is not found in container
229
     */
230 11
    public function alias(string $id, string $serviceId): void
231
    {
232 11
        if ($id === $serviceId) {
233 1
            throw new \LogicException("[$id] is aliased to itself.");
234
        }
235
236 10
        if (!$this->has($serviceId)) {
237 2
            throw new ContainerResolutionException("Service id '$serviceId' is not found in container");
238
        }
239
240 8
        $this->aliases[$id] = $this->aliases[$serviceId] ?? $serviceId;
241 8
    }
242
243
    /**
244
     * Checks if a service definition has been aliased.
245
     *
246
     * @param string $id The registered service id
247
     */
248
    public function aliased(string $id): bool
249
    {
250
        foreach ($this->aliases as $serviceId) {
251
            if ($id === $serviceId) {
252
                return true;
253
            }
254
        }
255
256
        return false;
257
    }
258
259
    /**
260
     * Assign a set of tags to service(s).
261
     *
262
     * @param string[]|string         $serviceIds
263
     * @param array<int|string,mixed> $tags
264
     */
265 13
    public function tag($serviceIds, array $tags): void
266
    {
267 13
        foreach ((array) $serviceIds as $service) {
268 13
            foreach ($tags as $tag => $attributes) {
269
                // Exchange values if $tag is an integer
270 13
                if (\is_int($tmp = $tag)) {
271 9
                    $tag = $attributes;
272 9
                    $attributes = $tmp;
273
                }
274
275 13
                $this->tags[$service][$tag] = $attributes;
276
            }
277
        }
278 13
    }
279
280
    /**
281
     * Resolve all of the bindings for a given tag.
282
     *
283
     * @return array of [service, attributes]
284
     */
285 15
    public function tagged(string $tag, bool $resolve = true): array
286
    {
287 15
        $tags = [];
288
289 15
        foreach ($this->tags as $service => $tagged) {
290 13
            if (isset($tagged[$tag])) {
291 13
                $tags[] = [$resolve ? $this->get($service) : $service, $tagged[$tag]];
292
            }
293
        }
294
295 15
        return $tags;
296
    }
297
298
    /**
299
     * Marks a definition from being interpreted as a service.
300
     *
301
     * @param mixed $definition from being evaluated
302
     */
303 21
    public function raw($definition): RawDefinition
304
    {
305 21
        return new RawDefinition($definition);
306
    }
307
308
    /**
309
     * @internal prevent service looping
310
     *
311
     * @param Definition|RawDefinition|callable $service
312
     *
313
     * @throws CircularReferenceException
314
     *
315
     * @return mixed
316
     */
317
    abstract protected function doCreate(string $id, $service);
318
319
    /**
320
     * Throw a PSR-11 not found exception.
321
     */
322 21
    protected function createNotFound(string $id, bool $throw = false): NotFoundServiceException
323
    {
324 21
        if (null !== $suggest = Helpers::getSuggestion($this->keys(), $id)) {
325 3
            $suggest = " Did you mean: \"$suggest\" ?";
326
        }
327
328 21
        $error = new NotFoundServiceException(\sprintf('Identifier "%s" is not defined.' . $suggest, $id));
329
330 21
        if ($throw) {
331 20
            throw $error;
332
        }
333
334 2
        return $error;
335
    }
336
}
337