Passed
Push — master ( dd46fb...92bcfa )
by butschster
06:23 queued 19s
created

Container::runScoped()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 1
c 0
b 0
f 0
dl 0
loc 3
ccs 2
cts 2
cp 1
rs 10
cc 1
nc 1
nop 4
crap 1
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\Internal\Common\DestructorTrait;
18
use Spiral\Core\Internal\Config\StateBinder;
19
20
/**
21
 * Auto-wiring container: declarative singletons, contextual injections, parent container
22
 * delegation and ability to lazy wire.
23
 *
24
 * Container does not support setter injections, private properties, etc. Normally it will work
25
 * with classes only to be as much invisible as possible. Attention, this is hungry implementation
26
 * of container, meaning it WILL try to resolve dependency unless you specified custom lazy
27
 * factory.
28
 *
29
 * You can use injectors to delegate class resolution to external container.
30
 *
31
 * @see InjectableInterface
32
 * @see SingletonInterface
33
 *
34
 * @psalm-import-type TResolver from BinderInterface
35
 * @psalm-import-type TInvokable from InvokerInterface
36
 */
37
final class Container implements
38
    ContainerInterface,
39
    BinderInterface,
40
    FactoryInterface,
41
    ResolverInterface,
42
    InvokerInterface,
43
    ScopeInterface
44
{
45
    use DestructorTrait;
46
47
    public const DEFAULT_ROOT_SCOPE_NAME = 'root';
48
49
    private Internal\State $state;
50
    private ResolverInterface|Internal\Resolver $resolver;
51
    private FactoryInterface|Internal\Factory $factory;
52
    private ContainerInterface|Internal\Container $container;
53
    private BinderInterface|Internal\Binder $binder;
54
    private InvokerInterface|Internal\Invoker $invoker;
55
    private Internal\Scope $scope;
56
57
    /**
58
     * Container constructor.
59
     */
60 1220
    public function __construct(
61
        private Config $config = new Config(),
62
        string|\BackedEnum|null $scopeName = self::DEFAULT_ROOT_SCOPE_NAME,
63
        private Options $options = new Options(),
64
    ) {
65 1220
        if (\is_object($scopeName)) {
66 6
            $scopeName = (string) $scopeName->value;
0 ignored issues
show
Bug introduced by
Accessing value on the interface BackedEnum suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
67
        }
68
69 1220
        $this->initServices($this, $scopeName);
0 ignored issues
show
Bug introduced by
It seems like $scopeName can also be of type BackedEnum; however, parameter $scopeName of Spiral\Core\Container::initServices() does only seem to accept null|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

69
        $this->initServices($this, /** @scrutinizer ignore-type */ $scopeName);
Loading history...
70
71
        /** @psalm-suppress RedundantPropertyInitializationCheck */
72 1220
        \assert(isset($this->state));
73
74
        // Bind himself
75 1220
        $shared = new Alias(self::class);
76 1220
        $this->state->bindings = \array_merge($this->state->bindings, [
77 1220
            self::class => new WeakReference(\WeakReference::create($this)),
78 1220
            ContainerInterface::class => $shared,
79 1220
            BinderInterface::class => $shared,
80 1220
            FactoryInterface::class => $shared,
81 1220
            ScopeInterface::class => $shared,
82 1220
            ResolverInterface::class => $shared,
83 1220
            InvokerInterface::class => $shared,
84 1220
        ]);
85
    }
86
87 663
    public function __destruct()
88
    {
89 663
        $this->closeScope();
90
    }
91
92
    /**
93
     * Container can not be cloned.
94
     */
95 1
    public function __clone()
96
    {
97 1
        throw new LogicException('Container is not cloneable.');
98
    }
99
100 528
    public function resolveArguments(
101
        ContextFunction $reflection,
102
        array $parameters = [],
103
        bool $validate = true,
104
    ): array {
105 528
        return $this->resolver->resolveArguments($reflection, $parameters, $validate);
106
    }
107
108
    public function validateArguments(ContextFunction $reflection, array $arguments = []): void
109
    {
110
        $this->resolver->validateArguments($reflection, $arguments);
111
    }
112
113
    /**
114
     * @param \Stringable|string|null $context Related to parameter caused injection if any.
115
     *
116
     * @throws ContainerException
117
     * @throws \Throwable
118
     * @psalm-suppress TooManyArguments
119
     */
120 617
    public function make(string $alias, array $parameters = [], \Stringable|string|null $context = null): mixed
121
    {
122 617
        return ContainerScope::getContainer() === $this
123 420
            ? $this->factory->make($alias, $parameters, $context)
124 617
            : ContainerScope::runScope($this, fn () => $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

124
            : ContainerScope::runScope($this, fn () => $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...
125
    }
126
127
    /**
128
     * Context parameter will be passed to class injectors, which makes possible to use this method
129
     * as:
130
     *
131
     * $this->container->get(DatabaseInterface::class, 'default');
132
     *
133
     * Attention, context ignored when outer container has instance by alias.
134
     *
135
     * @template T
136
     *
137
     * @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...
138
     * @param \Stringable|string|null $context Call context.
139
     *
140
     * @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...
141
     *
142
     * @throws ContainerException
143
     * @throws \Throwable
144
     * @psalm-suppress TooManyArguments
145
     */
146 971
    public function get(string|Autowire $id, \Stringable|string|null $context = null): mixed
147
    {
148 971
        return ContainerScope::getContainer() === $this
149 608
            ? $this->container->get($id, $context)
150 970
            : ContainerScope::runScope($this, fn () => $this->container->get($id, $context));
0 ignored issues
show
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

150
            : ContainerScope::runScope($this, fn () => $this->container->get(/** @scrutinizer ignore-type */ $id, $context));
Loading history...
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

150
            : ContainerScope::runScope($this, fn () => $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...
151
    }
152
153 556
    public function has(string $id): bool
154
    {
155 556
        return $this->container->has($id);
156
    }
157
158
    /**
159
     * Make a Binder proxy to configure bindings for a specific scope.
160
     *
161
     * @param null|\BackedEnum|string $scope Scope name.
162
     *        If {@see null}, binder for the current working scope will be returned.
163
     *        If {@see string}, the default binder for the given scope will be returned. Default bindings won't affect
164
     *        already created Container instances except the case with the root one.
165
     */
166 34
    public function getBinder(string|\BackedEnum|null $scope = null): BinderInterface
167
    {
168 34
        $scope = \is_object($scope) ? (string) $scope->value : $scope;
0 ignored issues
show
Bug introduced by
Accessing value on the interface BackedEnum suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
169
170 34
        return $scope === null
171 5
            ? $this->binder
172 34
            : new StateBinder($this->config->scopedBindings->getState($scope));
173
    }
174
175
    /**
176
     * @throws \Throwable
177
     */
178 660
    public function runScope(Scope|array $bindings, callable $scope): mixed
179
    {
180 660
        if (!\is_array($bindings)) {
0 ignored issues
show
introduced by
The condition is_array($bindings) is always true.
Loading history...
181 33
            return $this->runIsolatedScope($bindings, $scope);
182
        }
183
184 638
        $binds = &$this->state->bindings;
185 638
        $singletons = &$this->state->singletons;
186 638
        $cleanup = $previous = $prevSin = [];
187 638
        foreach ($bindings as $alias => $resolver) {
188
            // Store previous bindings
189 637
            if (isset($binds[$alias])) {
190 547
                $previous[$alias] = $binds[$alias];
191
            } else {
192
                // Store bindings to be removed
193 636
                $cleanup[] = $alias;
194
            }
195
            // Store previous singletons
196 637
            if (isset($singletons[$alias])) {
197 35
                $prevSin[$alias] = $singletons[$alias];
198 35
                unset($singletons[$alias]);
199
            }
200
201 637
            $this->binder->bind($alias, $resolver);
202
        }
203
204
        try {
205 638
            return ContainerScope::getContainer() !== $this
206 637
                ? ContainerScope::runScope($this, $scope)
207 624
                : $scope($this);
208
        } finally {
209
            // Remove new bindings
210 638
            foreach ($cleanup as $alias) {
211 636
                unset($binds[$alias], $singletons[$alias]);
212
            }
213
            // Restore previous bindings
214 638
            foreach ($previous as $alias => $resolver) {
215 547
                $binds[$alias] = $resolver;
216
            }
217
            // Restore singletons
218 638
            foreach ($prevSin as $alias => $instance) {
219 35
                $singletons[$alias] = $instance;
220
            }
221
        }
222
    }
223
224
    /**
225
     * Invoke given closure or function withing specific IoC scope.
226
     *
227
     * @template TReturn
228
     *
229
     * @param callable(mixed ...$params): TReturn $closure
230
     * @param array<non-empty-string, TResolver> $bindings Custom bindings for the new scope.
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<non-empty-string, TResolver> at position 2 could not be parsed: Unknown type name 'non-empty-string' at position 2 in array<non-empty-string, TResolver>.
Loading history...
231
     * @param null|string $name Scope name. Named scopes can have individual bindings and constrains.
232
     * @param bool $autowire If {@see false}, closure will be invoked with just only the passed Container as an
233
     *        argument. Otherwise, {@see InvokerInterface::invoke()} will be used to invoke the closure.
234
     *
235
     * @return TReturn
236
     * @throws \Throwable
237
     *
238
     * @deprecated Use {@see runScope()} with the {@see Scope} as the first argument.
239
     * @internal Used in tests only
240
     */
241 26
    public function runScoped(callable $closure, array $bindings = [], ?string $name = null, bool $autowire = true): mixed
242
    {
243 26
        return $this->runIsolatedScope(new Scope($name, $bindings, $autowire), $closure);
244
    }
245
246
    /**
247
     * Bind value resolver to container alias. Resolver can be class name (will be constructed
248
     * for each method call), function array or Closure (executed every call). Only object resolvers
249
     * supported by this method.
250
     */
251 920
    public function bind(string $alias, mixed $resolver): void
252
    {
253 920
        $this->binder->bind($alias, $resolver);
254
    }
255
256
    /**
257
     * Bind value resolver to container alias to be executed as cached. Resolver can be class name
258
     * (will be constructed only once), function array or Closure (executed only once call).
259
     *
260
     * @psalm-param TResolver $resolver
261
     * @param bool $force If the value is false, an exception will be thrown when attempting
262
     *  to bind an already constructed singleton.
263
     */
264 771
    public function bindSingleton(string $alias, string|array|callable|object $resolver, bool $force = true): void
265
    {
266 771
        if ($force) {
267 771
            $this->binder->removeBinding($alias);
268
        }
269
270 771
        $this->binder->bindSingleton($alias, $resolver);
271
    }
272
273
    /**
274
     * Check if alias points to constructed instance (singleton).
275
     */
276 14
    public function hasInstance(string $alias): bool
277
    {
278 14
        return $this->binder->hasInstance($alias);
279
    }
280
281 323
    public function removeBinding(string $alias): void
282
    {
283 323
        $this->binder->removeBinding($alias);
284
    }
285
286
    /**
287
     * @psalm-param TInvokable $target
288
     */
289 680
    public function invoke(mixed $target, array $parameters = []): mixed
290
    {
291 680
        return ContainerScope::getContainer() === $this
292 627
            ? $this->invoker->invoke($target, $parameters)
293 654
            : ContainerScope::runScope($this, fn () => $this->invoker->invoke($target, $parameters));
294
    }
295
296
    /**
297
     * Bind class or class interface to the injector source (InjectorInterface).
298
     */
299 431
    public function bindInjector(string $class, string $injector): void
300
    {
301 431
        $this->binder->bindInjector($class, $injector);
302
    }
303
304
    public function removeInjector(string $class): void
305
    {
306
        $this->binder->removeInjector($class);
307
    }
308
309 8
    public function hasInjector(string $class): bool
310
    {
311 8
        return $this->binder->hasInjector($class);
312
    }
313
314
    /**
315
     * Init internal container services.
316
     */
317 1220
    private function initServices(
318
        self $container,
319
        ?string $scopeName,
320
    ): void {
321 1220
        $isRoot = $container->config->lockRoot();
322
323
        // Get named scope or create anonymous one
324 1220
        $state = match (true) {
325 1220
            $scopeName === null => new Internal\State(),
326
            // Only root container can make default bindings directly
327 1220
            $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

327
            $isRoot => $container->config->scopedBindings->getState(/** @scrutinizer ignore-type */ $scopeName),
Loading history...
328 1220
            default => clone $container->config->scopedBindings->getState($scopeName),
329 1220
        };
330
331 1220
        $constructor = new Internal\Common\Registry($container->config, [
332 1220
            'state' => $state,
333 1220
            'scope' => new Internal\Scope($scopeName),
334 1220
        ], $this->options);
335
336
        // Create container services
337 1220
        foreach ($container->config as $property => $class) {
338 1220
            if (\property_exists($container, $property)) {
339 1220
                $container->$property = $constructor->get($property, $class);
340
            }
341
        }
342
    }
343
344
    /**
345
     * Execute finalizers and destruct the container.
346
     *
347
     * @throws FinalizersException
348
     */
349 663
    private function closeScope(): void
350
    {
351
        /** @psalm-suppress RedundantPropertyInitializationCheck */
352 663
        if (!isset($this->scope)) {
353 59
            $this->destruct();
354 59
            return;
355
        }
356
357 663
        $scopeName = $this->scope->getScopeName();
358
359
        // Run finalizers
360 663
        $errors = [];
361 663
        foreach ($this->state->finalizers as $finalizer) {
362
            try {
363 4
                $this->invoker->invoke($finalizer);
364
            } catch (\Throwable $e) {
365
                $errors[] = $e;
366
            }
367
        }
368
369
        // Destroy the container
370 663
        $this->destruct();
371
372
        // Throw collected errors
373 663
        if ($errors !== []) {
374
            throw new FinalizersException($scopeName, $errors);
375
        }
376
    }
377
378
    /**
379
     * @template TReturn
380
     *
381
     * @param callable(mixed ...$params): TReturn $closure
382
     *
383
     * @return TReturn
0 ignored issues
show
Bug introduced by
The type Spiral\Core\TReturn was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
384
     * @throws \Throwable
385
     */
386 59
    private function runIsolatedScope(Scope $config, callable $closure): mixed
387
    {
388
        // Open scope
389 59
        $container = new self($this->config, $config->name, $this->options);
390
391
        // Configure scope
392 59
        $container->scope->setParent($this, $this->scope, $this->factory);
393
394
        // Add specific bindings
395 59
        foreach ($config->bindings as $alias => $resolver) {
396 24
            $container->binder->bind($alias, $resolver);
397
        }
398
399 59
        return ContainerScope::runScope(
400 59
            $container,
401 59
            static function (self $container) use ($config, $closure): mixed {
402
                try {
403 59
                    return $config->autowire
404 59
                        ? $container->invoke($closure)
405 57
                        : $closure($container);
406
                } finally {
407 59
                    $container->closeScope();
408
                }
409 59
            }
410 59
        );
411
    }
412
}
413