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

Container::addProviders()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2

Importance

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

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