Passed
Pull Request — master (#1038)
by Aleksei
11:08
created

Container   A

Complexity

Total Complexity 39

Size/Duplication

Total Lines 333
Duplicated Lines 0 %

Test Coverage

Coverage 91.23%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 39
eloc 110
c 2
b 0
f 0
dl 0
loc 333
ccs 104
cts 114
cp 0.9123
rs 9.28

22 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 20 1
A __destruct() 0 3 1
A validateArguments() 0 3 1
A make() 0 4 1
A resolveArguments() 0 6 1
A __clone() 0 3 1
A getBinder() 0 5 2
A has() 0 3 1
A get() 0 4 1
A bind() 0 3 1
A runScoped() 0 22 3
A bindInjector() 0 3 1
A getCurrentContainer() 0 3 1
B runScope() 0 38 8
A hasInstance() 0 3 1
A bindSingleton() 0 7 2
A removeBinding() 0 3 1
A initServices() 0 23 3
A removeInjector() 0 3 1
A invoke() 0 3 1
A hasInjector() 0 3 1
A closeScope() 0 26 5
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Spiral\Core;
6
7
use Psr\Container\ContainerInterface;
8
use ReflectionFunctionAbstract as ContextFunction;
9
use Spiral\Core\Config\Alias;
10
use Spiral\Core\Config\WeakReference;
11
use Spiral\Core\Container\Autowire;
12
use Spiral\Core\Container\InjectableInterface;
13
use Spiral\Core\Container\SingletonInterface;
14
use Spiral\Core\Exception\Container\ContainerException;
15
use Spiral\Core\Exception\LogicException;
16
use Spiral\Core\Exception\Scope\FinalizersException;
17
use Spiral\Core\Exception\Scope\ScopeContainerLeakedException;
18
use Spiral\Core\Internal\Common\DestructorTrait;
19
use Spiral\Core\Internal\Config\StateBinder;
20
21
/**
22
 * Auto-wiring container: declarative singletons, contextual injections, parent container
23
 * delegation and ability to lazy wire.
24
 *
25
 * Container does not support setter injections, private properties, etc. Normally it will work
26
 * with classes only to be as much invisible as possible. Attention, this is hungry implementation
27
 * of container, meaning it WILL try to resolve dependency unless you specified custom lazy
28
 * factory.
29
 *
30
 * You can use injectors to delegate class resolution to external container.
31
 *
32
 * @see InjectableInterface
33
 * @see SingletonInterface
34
 *
35
 * @psalm-import-type TResolver from BinderInterface
36
 * @psalm-import-type TInvokable from InvokerInterface
37
 */
38
final class Container implements
39
    ContainerInterface,
40
    BinderInterface,
41
    FactoryInterface,
42
    ResolverInterface,
43
    InvokerInterface,
44
    ContainerScopeInterface,
45
    ScopeInterface
0 ignored issues
show
Deprecated Code introduced by
The interface Spiral\Core\ScopeInterface has been deprecated. ( Ignorable by Annotation )

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

45
    /** @scrutinizer ignore-deprecated */ ScopeInterface

This interface has been deprecated. The supplier of the interface has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the interface will be removed and what other interface to use instead.

Loading history...
46
{
47
    use DestructorTrait;
48
49
    public const DEFAULT_ROOT_SCOPE_NAME = 'root';
50
51
    private Internal\State $state;
52
    private ResolverInterface|Internal\Resolver $resolver;
53
    private FactoryInterface|Internal\Factory $factory;
54
    private ContainerInterface|Internal\Container $container;
55
    private BinderInterface|Internal\Binder $binder;
56
    private InvokerInterface|Internal\Invoker $invoker;
57
    private Internal\Scope $scope;
58
59
    /**
60
     * Container constructor.
61
     */
62 1170
    public function __construct(
63
        private Config $config = new Config(),
64
        ?string $scopeName = self::DEFAULT_ROOT_SCOPE_NAME,
65
    ) {
66 1170
        $this->initServices($this, $scopeName);
67
68
        /** @psalm-suppress RedundantPropertyInitializationCheck */
69 1170
        \assert(isset($this->state));
70
71
        // Bind himself
72 1170
        $shared = new Alias(self::class);
73 1170
        $this->state->bindings = \array_merge($this->state->bindings, [
74 1170
            self::class => new WeakReference(\WeakReference::create($this)),
75 1170
            ContainerInterface::class => $shared,
76 1170
            BinderInterface::class => $shared,
77 1170
            FactoryInterface::class => $shared,
78 1170
            ContainerScopeInterface::class => $shared,
79 1170
            ScopeInterface::class => $shared,
80 1170
            ResolverInterface::class => $shared,
81 1170
            InvokerInterface::class => $shared,
82 1170
        ]);
83
    }
84
85 614
    public function __destruct()
86
    {
87 614
        $this->closeScope();
88
    }
89
90
    /**
91
     * Container can not be cloned.
92
     */
93 1
    public function __clone()
94
    {
95 1
        throw new LogicException('Container is not cloneable.');
96
    }
97
98 526
    public function resolveArguments(
99
        ContextFunction $reflection,
100
        array $parameters = [],
101
        bool $validate = true,
102
    ): array {
103 526
        return $this->resolver->resolveArguments($reflection, $parameters, $validate);
104
    }
105
106
    public function validateArguments(ContextFunction $reflection, array $arguments = []): void
107
    {
108
        $this->resolver->validateArguments($reflection, $arguments);
109
    }
110
111
    /**
112
     * @param string|null $context Related to parameter caused injection if any.
113
     *
114
     * @throws ContainerException
115
     * @throws \Throwable
116
     */
117 621
    public function make(string $alias, array $parameters = [], string $context = null): mixed
118
    {
119
        /** @psalm-suppress TooManyArguments */
120 621
        return $this->factory->make($alias, $parameters, $context);
0 ignored issues
show
Unused Code introduced by
The call to Spiral\Core\FactoryInterface::make() has too many arguments starting with $context. ( Ignorable by Annotation )

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

120
        return $this->factory->/** @scrutinizer ignore-call */ make($alias, $parameters, $context);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
121
    }
122
123
    /**
124
     * Context parameter will be passed to class injectors, which makes possible to use this method
125
     * as:
126
     *
127
     * $this->container->get(DatabaseInterface::class, 'default');
128
     *
129
     * Attention, context ignored when outer container has instance by alias.
130
     *
131
     * @template T
132
     *
133
     * @param class-string<T>|string|Autowire $id
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string<T>|string|Autowire at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string<T>|string|Autowire.
Loading history...
134
     * @param string|null $context Call context.
135
     *
136
     * @return ($id is class-string ? T : mixed)
0 ignored issues
show
Documentation Bug introduced by
The doc comment ($id at position 1 could not be parsed: Unknown type name '$id' at position 1 in ($id.
Loading history...
137
     *
138
     * @throws ContainerException
139
     * @throws \Throwable
140
     */
141 941
    public function get(string|Autowire $id, string $context = null): mixed
142
    {
143
        /** @psalm-suppress TooManyArguments */
144 941
        return $this->container->get($id, $context);
0 ignored issues
show
Unused Code introduced by
The call to Psr\Container\ContainerInterface::get() has too many arguments starting with $context. ( Ignorable by Annotation )

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

144
        return $this->container->/** @scrutinizer ignore-call */ get($id, $context);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
Bug introduced by
It seems like $id can also be of type Spiral\Core\Container\Autowire; however, parameter $id of Psr\Container\ContainerInterface::get() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

144
        return $this->container->get(/** @scrutinizer ignore-type */ $id, $context);
Loading history...
145
    }
146
147 550
    public function has(string $id): bool
148
    {
149 550
        return $this->container->has($id);
150
    }
151
152 3
    public function getBinder(?string $scope = null): BinderInterface
153
    {
154 3
        return $scope === null
155
            ? $this->binder
156 3
            : new StateBinder($this->config->scopedBindings->getState($scope));
157
    }
158
159
    /**
160
     * @throws \Throwable
161
     */
162 636
    public function runScope(array $bindings, callable $scope): mixed
163
    {
164 636
        $binds = &$this->state->bindings;
165 636
        $singletons = &$this->state->singletons;
166 636
        $cleanup = $previous = $prevSin = [];
167 636
        foreach ($bindings as $alias => $resolver) {
168
            // Store previous bindings
169 635
            if (isset($binds[$alias])) {
170 545
                $previous[$alias] = $binds[$alias];
171
            } else {
172
                // Store bindings to be removed
173 634
                $cleanup[] = $alias;
174
            }
175
            // Store previous singletons
176 635
            if (isset($singletons[$alias])) {
177 40
                $prevSin[$alias] = $singletons[$alias];
178 40
                unset($singletons[$alias]);
179
            }
180
181 635
            $this->binder->bind($alias, $resolver);
182
        }
183
184
        try {
185 636
            return ContainerScope::getContainer() !== $this
186 635
                ? ContainerScope::runScope($this, $scope)
187 622
                : $scope($this);
188
        } finally {
189
            // Remove new bindings
190 636
            foreach ($cleanup as $alias) {
191 634
                unset($binds[$alias], $singletons[$alias]);
192
            }
193
            // Restore previous bindings
194 636
            foreach ($previous as $alias => $resolver) {
195 545
                $binds[$alias] = $resolver;
196
            }
197
            // Restore singletons
198 636
            foreach ($prevSin as $alias => $instance) {
199 40
                $singletons[$alias] = $instance;
200
            }
201
        }
202
    }
203
204
    /**
205
     * Invoke given closure or function withing specific IoC scope.
206
     */
207 25
    public function runScoped(callable $closure, array $bindings = [], ?string $name = null, bool $autowire = true): mixed
208
    {
209
        // Open scope
210 25
        $container = new self($this->config, $name);
211
212
        // Configure scope
213 25
        $container->scope->setParent($this, $this->scope);
214
215
        // Add specific bindings
216 25
        foreach ($bindings as $alias => $resolver) {
217 12
            $container->binder->bind($alias, $resolver);
218
        }
219
220 25
        return ContainerScope::runScope(
221 25
            $container,
222 25
            static function (self $container) use ($autowire, $closure): mixed {
223
                try {
224 25
                    return $autowire
225 25
                        ? $container->invoke($closure)
226 24
                        : $closure($container);
227
                } finally {
228 25
                    $container->closeScope();
229
                }
230 25
            }
231 25
        );
232
    }
233
234
    /**
235
     * Get current scope container.
236
     *
237
     * @internal it might be removed in the future.
238
     */
239
    public function getCurrentContainer(): ContainerInterface
240
    {
241
        return ContainerScope::getContainer() ?? $this;
242
    }
243
244
    /**
245
     * Bind value resolver to container alias. Resolver can be class name (will be constructed
246
     * for each method call), function array or Closure (executed every call). Only object resolvers
247
     * supported by this method.
248
     */
249 908
    public function bind(string $alias, mixed $resolver): void
250
    {
251 908
        $this->binder->bind($alias, $resolver);
252
    }
253
254
    /**
255
     * Bind value resolver to container alias to be executed as cached. Resolver can be class name
256
     * (will be constructed only once), function array or Closure (executed only once call).
257
     *
258
     * @psalm-param TResolver $resolver
259
     * @param bool $force If the value is false, an exception will be thrown when attempting
260
     *  to bind an already constructed singleton.
261
     */
262 748
    public function bindSingleton(string $alias, string|array|callable|object $resolver, bool $force = true): void
263
    {
264 748
        if ($force) {
265 748
            $this->binder->removeBinding($alias);
266
        }
267
268 748
        $this->binder->bindSingleton($alias, $resolver);
269
    }
270
271
    /**
272
     * Check if alias points to constructed instance (singleton).
273
     */
274 14
    public function hasInstance(string $alias): bool
275
    {
276 14
        return $this->binder->hasInstance($alias);
277
    }
278
279 322
    public function removeBinding(string $alias): void
280
    {
281 322
        $this->binder->removeBinding($alias);
282
    }
283
284
    /**
285
     * @psalm-param TInvokable $target
286
     */
287 641
    public function invoke(mixed $target, array $parameters = []): mixed
288
    {
289 641
        return $this->invoker->invoke($target, $parameters);
290
    }
291
292
    /**
293
     * Bind class or class interface to the injector source (InjectorInterface).
294
     */
295 429
    public function bindInjector(string $class, string $injector): void
296
    {
297 429
        $this->binder->bindInjector($class, $injector);
298
    }
299
300
    public function removeInjector(string $class): void
301
    {
302
        $this->binder->removeInjector($class);
303
    }
304
305 8
    public function hasInjector(string $class): bool
306
    {
307 8
        return $this->binder->hasInjector($class);
308
    }
309
310
    /**
311
     * Init internal container services.
312
     */
313 1170
    private function initServices(
314
        self $container,
315
        ?string $scopeName,
316
    ): void {
317 1170
        $isRoot = $container->config->lockRoot();
318
319
        // Get named scope or create anonymous one
320 1170
        $state = match (true) {
321 1170
            $scopeName === null => new Internal\State(),
322
            // Only root container can make default bindings directly
323 1170
            $isRoot => $container->config->scopedBindings->getState($scopeName),
0 ignored issues
show
Bug introduced by
It seems like $scopeName can also be of type null; however, parameter $scope of Spiral\Core\Internal\Con...tateStorage::getState() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

323
            $isRoot => $container->config->scopedBindings->getState(/** @scrutinizer ignore-type */ $scopeName),
Loading history...
324 1170
            default => clone $container->config->scopedBindings->getState($scopeName),
325 1170
        };
326
327 1170
        $constructor = new Internal\Common\Registry($container->config, [
328 1170
            'state' => $state,
329 1170
            'scope' => new Internal\Scope($scopeName),
330 1170
        ]);
331
332
        // Create container services
333 1170
        foreach ($container->config as $property => $class) {
334 1170
            if (\property_exists($container, $property)) {
335 1170
                $container->$property = $constructor->get($property, $class);
336
            }
337
        }
338
    }
339
340
    /**
341
     * Execute finalizers and destruct the container.
342
     *
343
     * @throws FinalizersException
344
     */
345 614
    private function closeScope(): void
346
    {
347
        /** @psalm-suppress RedundantPropertyInitializationCheck */
348 614
        if (!isset($this->scope)) {
349 25
            $this->destruct();
350 25
            return;
351
        }
352
353 614
        $scopeName = $this->scope->getScopeName();
354
355
        // Run finalizers
356 614
        $errors = [];
357 614
        foreach ($this->state->finalizers as $finalizer) {
358
            try {
359 4
                $this->invoker->invoke($finalizer);
360
            } catch (\Throwable $e) {
361
                $errors[] = $e;
362
            }
363
        }
364
365
        // Destroy the container
366 614
        $this->destruct();
367
368
        // Throw collected errors
369 614
        if ($errors !== []) {
370
            throw new FinalizersException($scopeName, $errors);
371
        }
372
    }
373
}
374