Passed
Push — master ( 02d6da...980a37 )
by Divine Niiquaye
02:55
created

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