Passed
Pull Request — master (#144)
by Dmitriy
02:24
created

Container::isTagAlias()   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 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
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 88
        $this->tags = $tags;
71 88
        $this->delegateLookup($rootContainer);
72 88
        $this->setDefaultDefinitions();
73 88
        $this->setMultiple($definitions);
74 86
        $this->addProviders($providers);
75
76
        // Prevent circular reference to ContainerInterface
77 84
        $this->get(ContainerInterface::class);
78 84
    }
79
80 88
    private function setDefaultDefinitions(): void
81
    {
82 88
        $container = $this->rootContainer ?? $this;
83 88
        $this->setMultiple([
84 88
            ContainerInterface::class => $container,
85 88
            Injector::class => new Injector($container),
86
        ]);
87 88
    }
88
89
    /**
90
     * Returns a value indicating whether the container has the definition of the specified name.
91
     *
92
     * @param string $id class name, interface name or alias name
93
     *
94
     * @return bool whether the container is able to provide instance of class specified.
95
     *
96
     * @see set()
97
     */
98 30
    public function has($id): bool
99
    {
100 30
        if ($this->isTagAlias($id)) {
101 2
            $tag = substr($id, 4);
102 2
            return isset($this->tags[$tag]);
103
        }
104
105 28
        return isset($this->definitions[$id]) || class_exists($id);
106
    }
107
108
    /**
109
     * Returns an instance by either interface name or alias.
110
     *
111
     * Same instance of the class will be returned each time this method is called.
112
     *
113
     * @param string $id The interface or an alias name that was previously registered.
114
     *
115
     * @throws CircularReferenceException
116
     * @throws InvalidConfigException
117
     * @throws NotFoundException
118
     * @throws NotInstantiableException
119
     *
120
     * @return mixed|object An instance of the requested interface.
121
     *
122
     * @psalm-template T
123
     * @psalm-param string|class-string<T> $id
124
     * @psalm-return ($id is class-string ? T : mixed)
125
     */
126 85
    public function get($id)
127
    {
128 85
        if (!array_key_exists($id, $this->instances)) {
129 85
            $this->instances[$id] = $this->build($id);
130
        }
131
132 85
        return $this->instances[$id];
133
    }
134
135
    /**
136
     * Delegate service lookup to another container.
137
     *
138
     * @param ContainerInterface $container
139
     */
140 88
    protected function delegateLookup(?ContainerInterface $container): void
141
    {
142 88
        if ($container === null) {
143 88
            return;
144
        }
145 8
        if ($this->rootContainer === null) {
146 8
            $this->rootContainer = new CompositeContainer();
147 8
            $this->setDefaultDefinitions();
148
        }
149
150 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

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