Passed
Push — master ( 19d090...749d31 )
by butschster
08:51
created

Container::runScope()   B

Complexity

Conditions 8
Paths 160

Size

Total Lines 38
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 21
CRAP Score 8

Importance

Changes 0
Metric Value
eloc 23
c 0
b 0
f 0
dl 0
loc 38
ccs 21
cts 21
cp 1
rs 7.9444
cc 8
nc 160
nop 2
crap 8
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 1045
    public function __construct(
63
        private Config $config = new Config(),
64
        ?string $scopeName = self::DEFAULT_ROOT_SCOPE_NAME,
65
    ) {
66 1045
        $this->initServices($this, $scopeName);
67
68
        /** @psalm-suppress RedundantPropertyInitializationCheck */
69 1045
        \assert(isset($this->state));
70
71
        // Bind himself
72 1045
        $shared = new Alias(self::class);
73 1045
        $this->state->bindings = \array_merge($this->state->bindings, [
74 1045
            self::class => new WeakReference(\WeakReference::create($this)),
75 1045
            ContainerInterface::class => $shared,
76 1045
            BinderInterface::class => $shared,
77 1045
            FactoryInterface::class => $shared,
78 1045
            ContainerScopeInterface::class => $shared,
79 1045
            ScopeInterface::class => $shared,
80 1045
            ResolverInterface::class => $shared,
81 1045
            InvokerInterface::class => $shared,
82 1045
        ]);
83
    }
84
85 597
    public function __destruct()
86
    {
87 597
        $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 454
    public function resolveArguments(
99
        ContextFunction $reflection,
100
        array $parameters = [],
101
        bool $validate = true,
102
    ): array {
103 454
        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 559
    public function make(string $alias, array $parameters = [], string $context = null): mixed
118
    {
119
        /** @psalm-suppress TooManyArguments */
120 559
        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 826
    public function get(string|Autowire $id, string $context = null): mixed
142
    {
143
        /** @psalm-suppress TooManyArguments */
144 826
        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 499
    public function has(string $id): bool
148
    {
149 499
        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
     * @deprecated use {@see runScoped()} instead.
163
     */
164 566
    public function runScope(array $bindings, callable $scope): mixed
165
    {
166 566
        $binds = &$this->state->bindings;
167 566
        $singletons = &$this->state->singletons;
168 566
        $cleanup = $previous = $prevSin = [];
169 566
        foreach ($bindings as $alias => $resolver) {
170
            // Store previous bindings
171 565
            if (isset($binds[$alias])) {
172 523
                $previous[$alias] = $binds[$alias];
173
            } else {
174
                // Store bindings to be removed
175 564
                $cleanup[] = $alias;
176
            }
177
            // Store previous singletons
178 565
            if (isset($singletons[$alias])) {
179 40
                $prevSin[$alias] = $singletons[$alias];
180 40
                unset($singletons[$alias]);
181
            }
182
183 565
            $this->binder->bind($alias, $resolver);
184
        }
185
186
        try {
187 566
            return ContainerScope::getContainer() !== $this
188 565
                ? ContainerScope::runScope($this, $scope)
189 552
                : $scope($this);
190
        } finally {
191
            // Remove new bindings
192 566
            foreach ($cleanup as $alias) {
193 564
                unset($binds[$alias], $singletons[$alias]);
194
            }
195
            // Restore previous bindings
196 566
            foreach ($previous as $alias => $resolver) {
197 523
                $binds[$alias] = $resolver;
198
            }
199
            // Restore singletons
200 566
            foreach ($prevSin as $alias => $instance) {
201 40
                $singletons[$alias] = $instance;
202
            }
203
        }
204
    }
205
206
    /**
207
     * Invoke given closure or function withing specific IoC scope.
208
     */
209 23
    public function runScoped(callable $closure, array $bindings = [], ?string $name = null, bool $autowire = true): mixed
210
    {
211
        // Open scope
212 23
        $container = new self($this->config, $name);
213
214
        try {
215
            // Configure scope
216 23
            $container->scope->setParent($this, $this->scope);
217
218
            // Add specific bindings
219 23
            foreach ($bindings as $alias => $resolver) {
220 12
                $container->binder->bind($alias, $resolver);
221
            }
222
223 23
            return ContainerScope::runScope(
224 23
                $container,
225 23
                static function (self $container) use ($autowire, $closure): mixed {
226
                    try {
227 23
                        return $autowire
228 23
                            ? $container->invoke($closure)
229 22
                            : $closure($container);
230
                    } finally {
231 23
                        $container->closeScope();
232
                    }
233 23
                }
234 23
            );
235
        } finally {
236
            // Check the container has not been leaked
237 23
            $link = \WeakReference::create($container);
238 23
            unset($container);
239 23
            if ($link->get() !== null) {
240 23
                throw new ScopeContainerLeakedException($name, $this->scope->getParentScopeNames());
241
            }
242
        }
243
    }
244
245
    /**
246
     * Get current scope container.
247
     *
248
     * @internal it might be removed in the future.
249
     */
250
    public function getCurrentContainer(): ContainerInterface
251
    {
252
        return ContainerScope::getContainer() ?? $this;
253
    }
254
255
    /**
256
     * Bind value resolver to container alias. Resolver can be class name (will be constructed
257
     * for each method call), function array or Closure (executed every call). Only object resolvers
258
     * supported by this method.
259
     */
260 824
    public function bind(string $alias, mixed $resolver): void
261
    {
262 824
        $this->binder->bind($alias, $resolver);
263
    }
264
265
    /**
266
     * Bind value resolver to container alias to be executed as cached. Resolver can be class name
267
     * (will be constructed only once), function array or Closure (executed only once call).
268
     *
269
     * @psalm-param TResolver $resolver
270
     * @param bool $force If the value is false, an exception will be thrown when attempting
271
     *  to bind an already constructed singleton.
272
     */
273 626
    public function bindSingleton(string $alias, string|array|callable|object $resolver, bool $force = true): void
274
    {
275 626
        if ($force) {
276 626
            $this->binder->removeBinding($alias);
277
        }
278
279 626
        $this->binder->bindSingleton($alias, $resolver);
280
    }
281
282
    /**
283
     * Check if alias points to constructed instance (singleton).
284
     */
285 9
    public function hasInstance(string $alias): bool
286
    {
287 9
        return $this->binder->hasInstance($alias);
288
    }
289
290 305
    public function removeBinding(string $alias): void
291
    {
292 305
        $this->binder->removeBinding($alias);
293
    }
294
295
    /**
296
     * @psalm-param TInvokable $target
297
     */
298 589
    public function invoke(mixed $target, array $parameters = []): mixed
299
    {
300 589
        return $this->invoker->invoke($target, $parameters);
301
    }
302
303
    /**
304
     * Bind class or class interface to the injector source (InjectorInterface).
305
     */
306 387
    public function bindInjector(string $class, string $injector): void
307
    {
308 387
        $this->binder->bindInjector($class, $injector);
309
    }
310
311
    public function removeInjector(string $class): void
312
    {
313
        $this->binder->removeInjector($class);
314
    }
315
316 8
    public function hasInjector(string $class): bool
317
    {
318 8
        return $this->binder->hasInjector($class);
319
    }
320
321
    /**
322
     * Init internal container services.
323
     */
324 1045
    private function initServices(
325
        self $container,
326
        ?string $scopeName,
327
    ): void {
328 1045
        $isRoot = $container->config->lockRoot();
329
330
        // Get named scope or create anonymous one
331 1045
        $state = match (true) {
332 1045
            $scopeName === null => new Internal\State(),
333
            // Only root container can make default bindings directly
334 1045
            $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

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