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

Container::has()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3

Importance

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

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