Passed
Pull Request — master (#211)
by Dmitriy
02:59
created

Container::getTaggedServices()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 6
c 0
b 0
f 0
nc 2
nop 1
dl 0
loc 11
ccs 7
cts 7
cp 1
crap 3
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Di;
6
7
use Psr\Container\ContainerInterface;
8
use Yiisoft\Di\Contracts\DeferredServiceProviderInterface;
9
use Yiisoft\Di\Contracts\ServiceProviderInterface;
10
use Yiisoft\Factory\Definition\ArrayDefinition;
11
use Yiisoft\Factory\Definition\Normalizer;
12
use Yiisoft\Factory\Exception\CircularReferenceException;
13
use Yiisoft\Factory\Exception\InvalidConfigException;
14
use Yiisoft\Factory\Exception\NotFoundException;
15
use Yiisoft\Factory\Exception\NotInstantiableException;
16
use Yiisoft\Injector\Injector;
17
18
use function array_key_exists;
19
use function array_keys;
20
use function assert;
21
use function class_exists;
22
use function get_class;
23
use function implode;
24
use function is_object;
25
use function is_string;
26
27
/**
28
 * Container implements a [dependency injection](http://en.wikipedia.org/wiki/Dependency_injection) container.
29
 */
30
final class Container extends AbstractContainerConfigurator implements ContainerInterface
31
{
32
    private const META_TAGS = 'tags';
33
    private const META_RESET = 'reset';
34
    private const ALLOWED_META = ['tags', 'reset'];
35
36
    /**
37
     * @var array object definitions indexed by their types
38
     */
39
    private array $definitions = [];
40
    /**
41
     * @var array used to collect ids instantiated during build
42
     * to detect circular references
43
     */
44
    private array $building = [];
45
    /**
46
     * @var object[]
47
     */
48
    private array $instances = [];
49
50
    private array $tags;
51
52
    private array $resetters = [];
53
54
    private ?CompositeContainer $rootContainer = null;
55
56
    /**
57
     * Container constructor.
58
     *
59
     * @param array $definitions Definitions to put into container.
60
     * @param ServiceProviderInterface[]|string[] $providers Service providers
61
     * to get definitions from.
62
     * @param ContainerInterface|null $rootContainer Root container to delegate
63
     * lookup to when resolving dependencies. If provided the current container
64
     * is no longer queried for dependencies.
65
     *
66
     * @throws InvalidConfigException
67
     */
68 91
    public function __construct(
69
        array $definitions = [],
70
        array $providers = [],
71
        array $tags = [],
72
        ContainerInterface $rootContainer = null
73
    ) {
74 91
        $this->tags = $tags;
75 91
        $this->delegateLookup($rootContainer);
76 91
        $this->setDefaultDefinitions();
77 91
        $this->setMultiple($definitions);
78 88
        $this->addProviders($providers);
79
80
        // Prevent circular reference to ContainerInterface
81 86
        $this->get(ContainerInterface::class);
82 86
    }
83
84
    /**
85
     * Returns a value indicating whether the container has the definition of the specified name.
86
     *
87
     * @param string $id class name, interface name or alias name
88
     *
89
     * @return bool whether the container is able to provide instance of class specified.
90
     *
91
     * @see set()
92
     */
93 30
    public function has($id): bool
94
    {
95 30
        if ($this->isTagAlias($id)) {
96 2
            $tag = substr($id, 4);
97 2
            return isset($this->tags[$tag]);
98
        }
99
100 28
        return isset($this->definitions[$id]) || class_exists($id);
101
    }
102
103
    /**
104
     * Returns an instance by either interface name or alias.
105
     *
106
     * Same instance of the class will be returned each time this method is called.
107
     *
108
     * @param string $id The interface or an alias name that was previously registered.
109
     *
110
     * @throws CircularReferenceException
111
     * @throws InvalidConfigException
112
     * @throws NotFoundException
113
     * @throws NotInstantiableException
114
     *
115
     * @return mixed|object An instance of the requested interface.
116
     *
117
     * @psalm-template T
118
     * @psalm-param string|class-string<T> $id
119
     * @psalm-return ($id is class-string ? T : mixed)
120
     */
121 87
    public function get($id)
122
    {
123 87
        if ($id === StateResetter::class) {
124 2
            $resetters = [];
125 2
            foreach ($this->resetters as $id => $resetter) {
0 ignored issues
show
introduced by
$id is overwriting one of the parameters of this function.
Loading history...
126 2
                if (isset($this->instances[$id])) {
127 2
                    $resetters[] = $resetter->bindTo($this->instances[$id], get_class($this->instances[$id]));
128
                }
129
            }
130 2
            return new StateResetter($resetters);
131
        }
132
133 87
        if (!array_key_exists($id, $this->instances)) {
134 87
            $this->instances[$id] = $this->build($id);
135
        }
136
137 87
        return $this->instances[$id];
138
    }
139
140
    /**
141
     * Delegate service lookup to another container.
142
     *
143
     * @param ContainerInterface $container
144
     */
145 91
    protected function delegateLookup(?ContainerInterface $container): void
146
    {
147 91
        if ($container === null) {
148 91
            return;
149
        }
150 8
        if ($this->rootContainer === null) {
151 8
            $this->rootContainer = new CompositeContainer();
152 8
            $this->setDefaultDefinitions();
153
        }
154
155 8
        $this->rootContainer->attach($container);
0 ignored issues
show
Bug introduced by
The method attach() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

155
        $this->rootContainer->/** @scrutinizer ignore-call */ 
156
                              attach($container);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
156 8
    }
157
158
    /**
159
     * Sets a definition to the container. Definition may be defined multiple ways.
160
     *
161
     * @param string $id
162
     * @param mixed $definition
163
     *
164
     * @throws InvalidConfigException
165
     *
166
     * @see `Normalizer::normalize()`
167
     */
168 91
    protected function set(string $id, $definition): void
169
    {
170 91
        [$definition, $meta] = Normalizer::parse($definition, self::ALLOWED_META);
171
172 91
        Normalizer::validate($definition);
173 91
        if (isset($meta[self::META_TAGS])) {
174 8
            $this->validateTags($meta[self::META_TAGS]);
175 8
            $this->setTags($id, $meta[self::META_TAGS]);
176
        }
177 91
        if (isset($meta[self::META_RESET])) {
178 3
            $this->setResetter($id, $meta[self::META_RESET]);
179
        }
180
181 91
        unset($this->instances[$id]);
182 91
        $this->definitions[$id] = $definition;
183 91
    }
184
185
    /**
186
     * Sets multiple definitions at once.
187
     *
188
     * @param array $config definitions indexed by their ids
189
     *
190
     * @throws InvalidConfigException
191
     */
192 91
    protected function setMultiple(array $config): void
193
    {
194 91
        foreach ($config as $id => $definition) {
195 91
            if (!is_string($id)) {
196 1
                throw new InvalidConfigException(sprintf('Key must be a string. %s given.', $this->getVariableType($id)));
197
            }
198 91
            $this->set($id, $definition);
199
        }
200 91
    }
201
202 91
    private function setDefaultDefinitions(): void
203
    {
204 91
        $container = $this->rootContainer ?? $this;
205 91
        $this->setMultiple([
206 91
            ContainerInterface::class => $container,
207 91
            Injector::class => new Injector($container),
208
        ]);
209 91
    }
210
211 8
    private function validateTags(array $tags): void
212
    {
213 8
        foreach ($tags as $tag) {
214 8
            if (!is_string($tag)) {
215
                throw new InvalidConfigException('Invalid tag. Expected a string, got ' . var_export($tag, true) . '.');
216
            }
217
        }
218 8
    }
219
220 8
    private function setTags(string $id, array $tags): void
221
    {
222 8
        foreach ($tags as $tag) {
223 8
            if (!isset($this->tags[$tag]) || !in_array($id, $this->tags[$tag], true)) {
224 8
                $this->tags[$tag][] = $id;
225
            }
226
        }
227 8
    }
228
229 2
    private function setResetter(string $id, \Closure $resetter): void
230
    {
231 2
        $this->resetters[$id] = $resetter;
232 2
    }
233
234
    /**
235
     * Creates new instance by either interface name or alias.
236
     *
237
     * @param string $id The interface or an alias name that was previously registered.
238
     *
239
     * @throws CircularReferenceException
240
     * @throws InvalidConfigException
241
     * @throws NotFoundException
242
     *
243
     * @return mixed|object New built instance of the specified class.
244
     *
245
     * @internal
246
     */
247 87
    private function build(string $id)
248
    {
249 87
        if ($this->isTagAlias($id)) {
250 9
            return $this->getTaggedServices($id);
251
        }
252
253 87
        if (isset($this->building[$id])) {
254 9
            if ($id === ContainerInterface::class) {
255 2
                return $this;
256
            }
257 7
            throw new CircularReferenceException(sprintf(
258 7
                'Circular reference to "%s" detected while building: %s.',
259
                $id,
260 7
                implode(',', array_keys($this->building))
261
            ));
262
        }
263
264 87
        $this->building[$id] = 1;
265
        try {
266 87
            $object = $this->buildInternal($id);
267 87
        } finally {
268 87
            unset($this->building[$id]);
269
        }
270
271 87
        return $object;
272
    }
273
274 87
    private function isTagAlias(string $id): bool
275
    {
276 87
        return strpos($id, 'tag@') === 0;
277
    }
278
279 9
    private function getTaggedServices(string $tagAlias): array
280
    {
281 9
        $tag = substr($tagAlias, 4);
282 9
        $services = [];
283 9
        if (isset($this->tags[$tag])) {
284 8
            foreach ($this->tags[$tag] as $service) {
285 8
                $services[] = $this->get($service);
286
            }
287
        }
288
289 9
        return $services;
290
    }
291
292
    /**
293
     * @param mixed $definition
294
     */
295 87
    private function processDefinition($definition): void
296
    {
297 87
        if ($definition instanceof DeferredServiceProviderInterface) {
298 1
            $definition->register($this);
299
        }
300 87
    }
301
302
    /**
303
     * @param string $id
304
     *
305
     * @throws InvalidConfigException
306
     * @throws NotFoundException
307
     *
308
     * @return mixed|object
309
     */
310 87
    private function buildInternal(string $id)
311
    {
312 87
        if (!isset($this->definitions[$id])) {
313 51
            return $this->buildPrimitive($id);
314
        }
315 87
        $this->processDefinition($this->definitions[$id]);
316 87
        $definition = Normalizer::normalize($this->definitions[$id], $id, [], false);
317
318 87
        return $definition->resolve($this->rootContainer ?? $this);
319
    }
320
321
    /**
322
     * @param string $class
323
     *
324
     * @throws InvalidConfigException
325
     * @throws NotFoundException
326
     *
327
     * @return mixed|object
328
     */
329 51
    private function buildPrimitive(string $class)
330
    {
331 51
        if (class_exists($class)) {
332 49
            $definition = new ArrayDefinition([ArrayDefinition::CLASS_NAME => $class], false);
333
334 49
            return $definition->resolve($this->rootContainer ?? $this);
335
        }
336
337 4
        throw new NotFoundException($class);
338
    }
339
340 88
    private function addProviders(array $providers): void
341
    {
342 88
        foreach ($providers as $provider) {
343 6
            $this->addProvider($provider);
344
        }
345 86
    }
346
347
    /**
348
     * Adds service provider to the container. Unless service provider is deferred
349
     * it would be immediately registered.
350
     *
351
     * @param mixed $providerDefinition
352
     *
353
     * @throws InvalidConfigException
354
     * @throws NotInstantiableException
355
     *
356
     * @see ServiceProviderInterface
357
     * @see DeferredServiceProviderInterface
358
     */
359 6
    private function addProvider($providerDefinition): void
360
    {
361 6
        $provider = $this->buildProvider($providerDefinition);
362
363 5
        if ($provider instanceof DeferredServiceProviderInterface) {
364 1
            foreach ($provider->provides() as $id) {
365 1
                $this->definitions[$id] = $provider;
366
            }
367
        } else {
368 4
            $provider->register($this);
369
        }
370 4
    }
371
372
    /**
373
     * Builds service provider by definition.
374
     *
375
     * @param mixed $providerDefinition class name or definition of provider.
376
     *
377
     * @throws InvalidConfigException
378
     *
379
     * @return ServiceProviderInterface instance of service provider;
380
     */
381 6
    private function buildProvider($providerDefinition): ServiceProviderInterface
382
    {
383 6
        $provider = Normalizer::normalize($providerDefinition)->resolve($this);
384 5
        assert($provider instanceof ServiceProviderInterface, new InvalidConfigException(
385 5
            sprintf(
386 5
                'Service provider should be an instance of %s. %s given.',
387 5
                ServiceProviderInterface::class,
388 5
                $this->getVariableType($provider)
389
            )
390
        ));
391
392 5
        return $provider;
393
    }
394
395
    /**
396
     * @param mixed $variable
397
     */
398 6
    private function getVariableType($variable): string
399
    {
400 6
        if (is_object($variable)) {
401 5
            return get_class($variable);
402
        }
403
404 1
        return gettype($variable);
405
    }
406
}
407