Test Failed
Push — refactoring ( 592638 )
by Sergei
08:01
created

Container::parseDefinition()   A

Complexity

Conditions 6
Paths 9

Size

Total Lines 29
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 18
c 0
b 0
f 0
nc 9
nop 2
dl 0
loc 29
rs 9.0444
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 in_array;
26
use function is_array;
27
use function is_callable;
28
use function is_object;
29
use function is_string;
30
31
/**
32
 * Container implements a [dependency injection](http://en.wikipedia.org/wiki/Dependency_injection) container.
33
 */
34
final class Container extends AbstractContainerConfigurator implements ContainerInterface
35
{
36
    private const DEFINITION_META = 'definition';
37
38
    private const META_TAGS = 'tags';
39
    private const META_RESET = 'reset';
40
    private const ALLOWED_META = ['tags', 'reset'];
41
42
    /**
43
     * @var array object definitions indexed by their types
44
     */
45
    private array $definitions = [];
46
    /**
47
     * @var array used to collect ids instantiated during build
48
     * to detect circular references
49
     */
50
    private array $building = [];
51
    /**
52
     * @var object[]
53
     */
54
    private array $instances = [];
55
56
    private array $tags;
57
58
    private array $resetters = [];
59
60
    private ?CompositeContainer $rootContainer = null;
61
62
    /**
63
     * Container constructor.
64
     *
65
     * @param array $definitions Definitions to put into container.
66
     * @param ServiceProviderInterface[]|string[] $providers Service providers
67
     * to get definitions from.
68
     * @param ContainerInterface|null $rootContainer Root container to delegate
69
     * lookup to when resolving dependencies. If provided the current container
70
     * is no longer queried for dependencies.
71
     *
72
     * @throws InvalidConfigException
73
     */
74
    public function __construct(
75
        array $definitions = [],
76
        array $providers = [],
77
        array $tags = [],
78
        ContainerInterface $rootContainer = null
79
    ) {
80
        $this->tags = $tags;
81
        $this->delegateLookup($rootContainer);
82
        $this->setDefaultDefinitions();
83
        $this->setMultiple($definitions);
84
        $this->addProviders($providers);
85
86
        // Prevent circular reference to ContainerInterface
87
        $this->get(ContainerInterface::class);
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
    public function has($id): bool
100
    {
101
        if ($this->isTagAlias($id)) {
102
            $tag = substr($id, 4);
103
            return isset($this->tags[$tag]);
104
        }
105
106
        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
    public function get($id)
128
    {
129
        if ($id === StateResetter::class && !isset($this->definitions[$id])) {
130
            $resetters = [];
131
            foreach ($this->resetters as $serviceId => $callback) {
132
                if (isset($this->instances[$serviceId])) {
133
                    $resetters[] = $callback->bindTo($this->instances[$serviceId], get_class($this->instances[$serviceId]));
134
                }
135
            }
136
            return new StateResetter($resetters, $this);
137
        }
138
139
        if (!array_key_exists($id, $this->instances)) {
140
            $this->instances[$id] = $this->build($id);
141
        }
142
143
        return $this->instances[$id];
144
    }
145
146
    /**
147
     * Delegate service lookup to another container.
148
     *
149
     * @param ContainerInterface $container
150
     */
151
    protected function delegateLookup(?ContainerInterface $container): void
152
    {
153
        if ($container === null) {
154
            return;
155
        }
156
        if ($this->rootContainer === null) {
157
            $this->rootContainer = new CompositeContainer();
158
            $this->setDefaultDefinitions();
159
        }
160
161
        $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

161
        $this->rootContainer->/** @scrutinizer ignore-call */ 
162
                              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...
162
    }
163
164
    /**
165
     * Sets a definition to the container. Definition may be defined multiple ways.
166
     *
167
     * @param string $id
168
     * @param mixed $definition
169
     *
170
     * @throws InvalidConfigException
171
     *
172
     * @see `Normalizer::normalize()`
173
     */
174
    protected function set(string $id, $definition): void
175
    {
176
        [$definition, $meta] = self::parseDefinition($id, $definition);
0 ignored issues
show
Bug Best Practice introduced by
The method Yiisoft\Di\Container::parseDefinition() is not static, but was called statically. ( Ignorable by Annotation )

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

176
        /** @scrutinizer ignore-call */ 
177
        [$definition, $meta] = self::parseDefinition($id, $definition);
Loading history...
177
178
        Normalizer::validate($definition);
179
        if (isset($meta[self::META_TAGS])) {
180
            $this->validateTags($meta[self::META_TAGS]);
181
            $this->setTags($id, $meta[self::META_TAGS]);
182
        }
183
        if (isset($meta[self::META_RESET])) {
184
            $this->setResetter($id, $meta[self::META_RESET]);
185
        }
186
187
        unset($this->instances[$id]);
188
        $this->definitions[$id] = $definition;
189
    }
190
191
    /**
192
     * Sets multiple definitions at once.
193
     *
194
     * @param array $config definitions indexed by their ids
195
     *
196
     * @throws InvalidConfigException
197
     */
198
    protected function setMultiple(array $config): void
199
    {
200
        foreach ($config as $id => $definition) {
201
            if (!is_string($id)) {
202
                throw new InvalidConfigException(sprintf('Key must be a string. %s given.', $this->getVariableType($id)));
203
            }
204
            $this->set($id, $definition);
205
        }
206
    }
207
208
    /**
209
     * @throws InvalidConfigException
210
     */
211
    private function parseDefinition(string $id, $definition): array
212
    {
213
        if (!is_array($definition)) {
214
            return [$definition, []];
215
        }
216
217
        if (
218
            !array_key_exists(ArrayDefinition::CLASS_NAME, $definition) &&
219
            !is_callable($definition, true)
220
        ) {
221
            $definition[ArrayDefinition::CLASS_NAME] = $id;
222
        }
223
224
        $meta = [];
225
        if (isset($definition[self::DEFINITION_META])) {
226
            $newDefinition = $definition[self::DEFINITION_META];
227
            unset($definition[self::DEFINITION_META]);
228
            $meta = array_filter($definition, static function ($key) {
229
                return in_array($key, self::ALLOWED_META, true);
230
            }, ARRAY_FILTER_USE_KEY);
231
            $definition = $newDefinition;
232
        }
233
234
        if (is_callable($definition, true)) {
235
            return [$definition, $meta];
236
        }
237
238
        $definition = new ArrayDefinition($definition, self::ALLOWED_META);
0 ignored issues
show
Bug introduced by
self::ALLOWED_META of type array is incompatible with the type boolean expected by parameter $checkDefinition of Yiisoft\Factory\Definiti...finition::__construct(). ( Ignorable by Annotation )

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

238
        $definition = new ArrayDefinition($definition, /** @scrutinizer ignore-type */ self::ALLOWED_META);
Loading history...
239
        return [$definition, $definition->getMeta()];
240
    }
241
242
    private function setDefaultDefinitions(): void
243
    {
244
        $container = $this->rootContainer ?? $this;
245
        $this->setMultiple([
246
            ContainerInterface::class => $container,
247
            Injector::class => new Injector($container),
248
        ]);
249
    }
250
251
    private function validateTags(array $tags): void
252
    {
253
        foreach ($tags as $tag) {
254
            if (!is_string($tag)) {
255
                throw new InvalidConfigException('Invalid tag. Expected a string, got ' . var_export($tag, true) . '.');
256
            }
257
        }
258
    }
259
260
    private function setTags(string $id, array $tags): void
261
    {
262
        foreach ($tags as $tag) {
263
            if (!isset($this->tags[$tag]) || !in_array($id, $this->tags[$tag], true)) {
264
                $this->tags[$tag][] = $id;
265
            }
266
        }
267
    }
268
269
    private function setResetter(string $id, Closure $resetter): void
270
    {
271
        $this->resetters[$id] = $resetter;
272
    }
273
274
    /**
275
     * Creates new instance by either interface name or alias.
276
     *
277
     * @param string $id The interface or an alias name that was previously registered.
278
     *
279
     * @throws CircularReferenceException
280
     * @throws InvalidConfigException
281
     * @throws NotFoundException
282
     *
283
     * @return mixed|object New built instance of the specified class.
284
     *
285
     * @internal
286
     */
287
    private function build(string $id)
288
    {
289
        if ($this->isTagAlias($id)) {
290
            return $this->getTaggedServices($id);
291
        }
292
293
        if (isset($this->building[$id])) {
294
            if ($id === ContainerInterface::class) {
295
                return $this;
296
            }
297
            throw new CircularReferenceException(sprintf(
298
                'Circular reference to "%s" detected while building: %s.',
299
                $id,
300
                implode(',', array_keys($this->building))
301
            ));
302
        }
303
304
        $this->building[$id] = 1;
305
        try {
306
            $object = $this->buildInternal($id);
307
        } finally {
308
            unset($this->building[$id]);
309
        }
310
311
        return $object;
312
    }
313
314
    private function isTagAlias(string $id): bool
315
    {
316
        return strpos($id, 'tag@') === 0;
317
    }
318
319
    private function getTaggedServices(string $tagAlias): array
320
    {
321
        $tag = substr($tagAlias, 4);
322
        $services = [];
323
        if (isset($this->tags[$tag])) {
324
            foreach ($this->tags[$tag] as $service) {
325
                $services[] = $this->get($service);
326
            }
327
        }
328
329
        return $services;
330
    }
331
332
    /**
333
     * @param mixed $definition
334
     */
335
    private function processDefinition($definition): void
336
    {
337
        if ($definition instanceof DeferredServiceProviderInterface) {
338
            $definition->register($this);
339
        }
340
    }
341
342
    /**
343
     * @param string $id
344
     *
345
     * @throws InvalidConfigException
346
     * @throws NotFoundException
347
     *
348
     * @return mixed|object
349
     */
350
    private function buildInternal(string $id)
351
    {
352
        if (!isset($this->definitions[$id])) {
353
            return $this->buildPrimitive($id);
354
        }
355
        $this->processDefinition($this->definitions[$id]);
356
        $definition = Normalizer::normalize($this->definitions[$id], $id, [], false);
357
358
        return $definition->resolve($this->rootContainer ?? $this);
359
    }
360
361
    /**
362
     * @param string $class
363
     *
364
     * @throws InvalidConfigException
365
     * @throws NotFoundException
366
     *
367
     * @return mixed|object
368
     */
369
    private function buildPrimitive(string $class)
370
    {
371
        if (class_exists($class)) {
372
            $definition = new ArrayDefinition([ArrayDefinition::CLASS_NAME => $class]);
373
374
            return $definition->resolve($this->rootContainer ?? $this);
375
        }
376
377
        throw new NotFoundException($class);
378
    }
379
380
    private function addProviders(array $providers): void
381
    {
382
        foreach ($providers as $provider) {
383
            $this->addProvider($provider);
384
        }
385
    }
386
387
    /**
388
     * Adds service provider to the container. Unless service provider is deferred
389
     * it would be immediately registered.
390
     *
391
     * @param mixed $providerDefinition
392
     *
393
     * @throws InvalidConfigException
394
     * @throws NotInstantiableException
395
     *
396
     * @see ServiceProviderInterface
397
     * @see DeferredServiceProviderInterface
398
     */
399
    private function addProvider($providerDefinition): void
400
    {
401
        $provider = $this->buildProvider($providerDefinition);
402
403
        if ($provider instanceof DeferredServiceProviderInterface) {
404
            foreach ($provider->provides() as $id) {
405
                $this->definitions[$id] = $provider;
406
            }
407
        } else {
408
            $provider->register($this);
409
        }
410
    }
411
412
    /**
413
     * Builds service provider by definition.
414
     *
415
     * @param mixed $providerDefinition class name or definition of provider.
416
     *
417
     * @throws InvalidConfigException
418
     *
419
     * @return ServiceProviderInterface instance of service provider;
420
     */
421
    private function buildProvider($providerDefinition): ServiceProviderInterface
422
    {
423
        $provider = Normalizer::normalize($providerDefinition)->resolve($this);
424
        assert($provider instanceof ServiceProviderInterface, new InvalidConfigException(
425
            sprintf(
426
                'Service provider should be an instance of %s. %s given.',
427
                ServiceProviderInterface::class,
428
                $this->getVariableType($provider)
429
            )
430
        ));
431
432
        return $provider;
433
    }
434
435
    /**
436
     * @param mixed $variable
437
     */
438
    private function getVariableType($variable): string
439
    {
440
        if (is_object($variable)) {
441
            return get_class($variable);
442
        }
443
444
        return gettype($variable);
445
    }
446
}
447