Passed
Push — master ( d2fe62...2ff31f )
by butschster
08:14
created

Container::scope()   A

Complexity

Conditions 4
Paths 12

Size

Total Lines 32
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 4

Importance

Changes 0
Metric Value
eloc 19
dl 0
loc 32
ccs 18
cts 18
cp 1
rs 9.6333
c 0
b 0
f 0
cc 4
nc 12
nop 4
crap 4
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\Container\Autowire;
10
use Spiral\Core\Container\InjectableInterface;
11
use Spiral\Core\Container\SingletonInterface;
12
use Spiral\Core\Exception\Container\ContainerException;
13
use Spiral\Core\Exception\LogicException;
14
use Spiral\Core\Exception\Scope\FinalizersException;
15
use Spiral\Core\Exception\Scope\ScopeContainerLeakedException;
16
use Spiral\Core\Internal\Common\DestructorTrait;
17
use Spiral\Core\Internal\Config\StateBinder;
18
19
/**
20
 * Auto-wiring container: declarative singletons, contextual injections, parent container
21
 * delegation and ability to lazy wire.
22
 *
23
 * Container does not support setter injections, private properties, etc. Normally it will work
24
 * with classes only to be as much invisible as possible. Attention, this is hungry implementation
25
 * of container, meaning it WILL try to resolve dependency unless you specified custom lazy
26
 * factory.
27
 *
28
 * You can use injectors to delegate class resolution to external container.
29
 *
30
 * @see InjectableInterface
31
 * @see SingletonInterface
32
 *
33
 * @psalm-import-type TResolver from BinderInterface
34
 * @psalm-import-type TInvokable from InvokerInterface
35
 */
36
final class Container implements
37
    ContainerInterface,
38
    BinderInterface,
39
    FactoryInterface,
40
    ResolverInterface,
41
    InvokerInterface,
42
    ContainerScopeInterface,
43
    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

43
    /** @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...
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;
0 ignored issues
show
Bug introduced by
The type Spiral\Core\Internal\Invoker 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...
55
    private Internal\Scope $scope;
56
57
    /**
58
     * Container constructor.
59
     */
60 1011
    public function __construct(
61
        private Config $config = new Config(),
62
        ?string $scopeName = self::DEFAULT_ROOT_SCOPE_NAME,
63
    ) {
64 1011
        $this->initServices($this, $scopeName);
65
66
        /** @psalm-suppress RedundantPropertyInitializationCheck */
67 1011
        \assert(isset($this->state));
68
69
        // Bind himself
70 1011
        $this->state->bindings = \array_merge($this->state->bindings, [
71 1011
            self::class => \WeakReference::create($this),
72 1011
            ContainerInterface::class => self::class,
73 1011
            BinderInterface::class => self::class,
74 1011
            FactoryInterface::class => self::class,
75 1011
            ContainerScopeInterface::class => self::class,
76 1011
            ScopeInterface::class => self::class,
77 1011
            ResolverInterface::class => self::class,
78 1011
            InvokerInterface::class => self::class,
79 1011
        ]);
80
    }
81
82 579
    public function __destruct()
83
    {
84 579
        $this->closeScope();
85
    }
86
87
    /**
88
     * Container can not be cloned.
89
     */
90 1
    public function __clone()
91
    {
92 1
        throw new LogicException('Container is not cloneable.');
93
    }
94
95 431
    public function resolveArguments(
96
        ContextFunction $reflection,
97
        array $parameters = [],
98
        bool $validate = true,
99
    ): array {
100 431
        return $this->resolver->resolveArguments($reflection, $parameters, $validate);
101
    }
102
103
    public function validateArguments(ContextFunction $reflection, array $arguments = []): void
104
    {
105
        $this->resolver->validateArguments($reflection, $arguments);
106
    }
107
108
    /**
109
     * @param string|null $context Related to parameter caused injection if any.
110
     *
111
     * @throws ContainerException
112
     * @throws \Throwable
113
     */
114 529
    public function make(string $alias, array $parameters = [], string $context = null): mixed
115
    {
116
        /** @psalm-suppress TooManyArguments */
117 529
        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

117
        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...
118
    }
119
120
    /**
121
     * Context parameter will be passed to class injectors, which makes possible to use this method
122
     * as:
123
     *
124
     * $this->container->get(DatabaseInterface::class, 'default');
125
     *
126
     * Attention, context ignored when outer container has instance by alias.
127
     *
128
     * @template T
129
     *
130
     * @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...
131
     * @param string|null $context Call context.
132
     *
133
     * @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...
134
     *
135
     * @throws ContainerException
136
     * @throws \Throwable
137
     *
138
     * @psalm-suppress InvalidArgument, InvalidCast
139
     */
140 786
    public function get(string|Autowire $id, string $context = null): mixed
141
    {
142
        /** @psalm-suppress TooManyArguments */
143 786
        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

143
        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

143
        return $this->container->get(/** @scrutinizer ignore-type */ $id, $context);
Loading history...
144
    }
145
146 474
    public function has(string $id): bool
147
    {
148 474
        return $this->container->has($id);
149
    }
150
151 3
    public function getBinder(?string $scope = null): BinderInterface
152
    {
153 3
        return $scope === null
154
            ? $this->binder
155 3
            : new StateBinder($this->config->scopedBindings->getState($scope));
156
    }
157
158
    /**
159
     * @throws \Throwable
160
     *
161
     * @deprecated use {@see runScoped()} instead.
162
     */
163 546
    public function runScope(array $bindings, callable $scope): mixed
164
    {
165 546
        $binds = &$this->state->bindings;
166 546
        $cleanup = $previous = [];
167 546
        foreach ($bindings as $alias => $resolver) {
168 545
            if (isset($binds[$alias])) {
169 498
                $previous[$alias] = $binds[$alias];
170
            } else {
171 545
                $cleanup[] = $alias;
172
            }
173
174 545
            $this->binder->bind($alias, $resolver);
175
        }
176
177
        try {
178 546
            return ContainerScope::getContainer() !== $this
179 545
                ? ContainerScope::runScope($this, $scope)
180 532
                : $scope($this);
181
        } finally {
182 546
            foreach ($previous as $alias => $resolver) {
183 498
                $binds[$alias] = $resolver;
184
            }
185
186 546
            foreach ($cleanup as $alias) {
187 545
                unset($binds[$alias]);
188
            }
189
        }
190
    }
191
192
    /**
193
     * Invoke given closure or function withing specific IoC scope.
194
     */
195 21
    public function runScoped(callable $closure, array $bindings = [], ?string $name = null, bool $autowire = true): mixed
196
    {
197
        // Open scope
198 21
        $container = new self($this->config, $name);
199
200
        try {
201
            // Configure scope
202 21
            $container->scope->setParent($this, $this->scope);
203
204
            // Add specific bindings
205 21
            foreach ($bindings as $alias => $resolver) {
206 11
                $container->binder->bind($alias, $resolver);
207
            }
208
209 21
            return ContainerScope::runScope(
210 21
                $container,
211 21
                static function (self $container) use ($autowire, $closure): mixed {
212
                    try {
213 21
                        return $autowire
214 21
                            ? $container->invoke($closure)
215 20
                            : $closure($container);
216
                    } finally {
217 21
                        $container->closeScope();
218
                    }
219 21
                }
220 21
            );
221
        } finally {
222
            // Check the container has not been leaked
223 21
            $link = \WeakReference::create($container);
224 21
            unset($container);
225 21
            if ($link->get() !== null) {
226 21
                throw new ScopeContainerLeakedException($name, $this->scope->getParentScopeNames());
227
            }
228
        }
229
    }
230
231
    /**
232
     * Get current scope container.
233
     *
234
     * @internal it might be removed in the future.
235
     */
236
    public function getCurrentContainer(): ContainerInterface
237
    {
238
        return ContainerScope::getContainer() ?? $this;
239
    }
240
241
    /**
242
     * Bind value resolver to container alias. Resolver can be class name (will be constructed
243
     * for each method call), function array or Closure (executed every call). Only object resolvers
244
     * supported by this method.
245
     */
246 790
    public function bind(string $alias, string|array|callable|object $resolver): void
247
    {
248 790
        $this->binder->bind($alias, $resolver);
249
    }
250
251
    /**
252
     * Bind value resolver to container alias to be executed as cached. Resolver can be class name
253
     * (will be constructed only once), function array or Closure (executed only once call).
254
     *
255
     * @psalm-param TResolver $resolver
256
     */
257 584
    public function bindSingleton(string $alias, string|array|callable|object $resolver): void
258
    {
259 584
        $this->binder->bindSingleton($alias, $resolver);
260
    }
261
262
    /**
263
     * Check if alias points to constructed instance (singleton).
264
     */
265 9
    public function hasInstance(string $alias): bool
266
    {
267 9
        return $this->binder->hasInstance($alias);
268
    }
269
270 2
    public function removeBinding(string $alias): void
271
    {
272 2
        $this->binder->removeBinding($alias);
273
    }
274
275
    /**
276
     * @psalm-param TInvokable $target
277
     */
278 563
    public function invoke(mixed $target, array $parameters = []): mixed
279
    {
280 563
        return $this->invoker->invoke($target, $parameters);
281
    }
282
283
    /**
284
     * Bind class or class interface to the injector source (InjectorInterface).
285
     *
286
     * Todo: remove suppression after {@link https://github.com/vimeo/psalm/issues/8298} fixing.
287
     * @psalm-suppress InvalidArgument,InvalidCast
288
     */
289 363
    public function bindInjector(string $class, string $injector): void
290
    {
291 363
        $this->binder->bindInjector($class, $injector);
292
    }
293
294
    public function removeInjector(string $class): void
295
    {
296
        $this->binder->removeInjector($class);
297
    }
298
299 8
    public function hasInjector(string $class): bool
300
    {
301 8
        return $this->binder->hasInjector($class);
302
    }
303
304
    /**
305
     * Init internal container services.
306
     */
307 1011
    private function initServices(
308
        self $container,
309
        ?string $scopeName,
310
    ): void {
311 1011
        $isRoot = $container->config->lockRoot();
312
313
        // Get named scope or create anonymous one
314 1011
        $state = match (true) {
315 1011
            $scopeName === null => new Internal\State(),
316
            // Only root container can make default bindings directly
317 1011
            $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

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