Passed
Pull Request — master (#144)
by Dmitriy
02:22
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\Definitions\ArrayDefinition;
11
use Yiisoft\Factory\Definitions\Normalizer;
12
use Yiisoft\Factory\Exceptions\CircularReferenceException;
13
use Yiisoft\Factory\Exceptions\InvalidConfigException;
14
use Yiisoft\Factory\Exceptions\NotFoundException;
15
use Yiisoft\Factory\Exceptions\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 TAGS_META = '__tags';
33
    private const DEFINITION_META = '__definition';
34
    /**
35
     * @var array object definitions indexed by their types
36
     */
37
    private array $definitions = [];
38
    /**
39
     * @var array used to collect ids instantiated during build
40
     * to detect circular references
41
     */
42
    private array $building = [];
43
    /**
44
     * @var object[]
45
     */
46
    private array $instances = [];
47
48
    private array $tags;
49
50
    private ?CompositeContainer $rootContainer = null;
51
52
    /**
53
     * Container constructor.
54
     *
55
     * @param array $definitions Definitions to put into container.
56
     * @param ServiceProviderInterface[]|string[] $providers Service providers
57
     * to get definitions from.
58
     * @param ContainerInterface|null $rootContainer Root container to delegate
59
     * lookup to when resolving dependencies. If provided the current container
60
     * is no longer queried for dependencies.
61
     *
62
     * @throws InvalidConfigException
63
     */
64 88
    public function __construct(
65
        array $definitions = [],
66
        array $providers = [],
67
        array $tags = [],
68
        ContainerInterface $rootContainer = null
69
    ) {
70
71 88
        $this->tags = $tags;
72 88
        $this->delegateLookup($rootContainer);
73 88
        $this->setDefaultDefinitions();
74 88
        $this->setMultiple($definitions);
75 86
        $this->addProviders($providers);
76
77
        // Prevent circular reference to ContainerInterface
78 84
        $this->get(ContainerInterface::class);
79 84
    }
80
81 88
    private function setDefaultDefinitions(): void
82
    {
83 88
        $container = $this->rootContainer ?? $this;
84 88
        $this->setMultiple([
85 88
            ContainerInterface::class => $container,
86 88
            Injector::class => new Injector($container),
87
        ]);
88 88
    }
89
90
    /**
91
     * Returns a value indicating whether the container has the definition of the specified name.
92
     *
93
     * @param string $id class name, interface name or alias name
94
     *
95
     * @return bool whether the container is able to provide instance of class specified.
96
     *
97
     * @see set()
98
     */
99 30
    public function has($id): bool
100
    {
101 30
        if ($this->isTagAlias($id)) {
102 2
            $tag = substr($id, 4);
103 2
            return isset($this->tags[$tag]);
104
        }
105
106 28
        return isset($this->definitions[$id]) || class_exists($id);
107
    }
108
109
    /**
110
     * Returns an instance by either interface name or alias.
111
     *
112
     * Same instance of the class will be returned each time this method is called.
113
     *
114
     * @param string $id The interface or an alias name that was previously registered.
115
     *
116
     * @throws CircularReferenceException
117
     * @throws InvalidConfigException
118
     * @throws NotFoundException
119
     * @throws NotInstantiableException
120
     *
121
     * @return mixed|object An instance of the requested interface.
122
     *
123
     * @psalm-template T
124
     * @psalm-param string|class-string<T> $id
125
     * @psalm-return ($id is class-string ? T : mixed)
126
     */
127 85
    public function get($id)
128
    {
129 85
        if (!array_key_exists($id, $this->instances)) {
130 85
            $this->instances[$id] = $this->build($id);
131
        }
132
133 85
        return $this->instances[$id];
134
    }
135
136
    /**
137
     * Delegate service lookup to another container.
138
     *
139
     * @param ContainerInterface $container
140
     */
141 88
    protected function delegateLookup(?ContainerInterface $container): void
142
    {
143 88
        if ($container === null) {
144 88
            return;
145
        }
146 8
        if ($this->rootContainer === null) {
147 8
            $this->rootContainer = new CompositeContainer();
148 8
            $this->setDefaultDefinitions();
149
        }
150
151 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

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