Container::initServices()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 23
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 3

Importance

Changes 0
Metric Value
eloc 12
dl 0
loc 23
ccs 14
cts 14
cp 1
rs 9.8666
c 0
b 0
f 0
cc 3
nc 3
nop 2
crap 3
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\Binder\SingletonOverloadException;
15
use Spiral\Core\Exception\Container\ContainerException;
16
use Spiral\Core\Exception\LogicException;
17
use Spiral\Core\Exception\Scope\FinalizersException;
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
    ScopeInterface
45
{
46
    use DestructorTrait;
47
48
    public const DEFAULT_ROOT_SCOPE_NAME = 'root';
49
50
    private Internal\State $state;
51
    private ResolverInterface|Internal\Resolver $resolver;
52
    private FactoryInterface|Internal\Factory $factory;
53
    private ContainerInterface|Internal\Container $container;
54
    private BinderInterface|Internal\Binder $binder;
55
    private InvokerInterface|Internal\Invoker $invoker;
56
    private Internal\Scope $scope;
57
    private Internal\Actor $actor;
58
59
    /**
60
     * Container constructor.
61
     */
62 1332
    public function __construct(
63
        private Config $config = new Config(),
64
        string|\BackedEnum|null $scopeName = self::DEFAULT_ROOT_SCOPE_NAME,
65
        private Options $options = new Options(),
66
    ) {
67 1332
        if (\is_object($scopeName)) {
68 103
            $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...
69
        }
70
71 1332
        $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

71
        $this->initServices($this, /** @scrutinizer ignore-type */ $scopeName);
Loading history...
72
73
        /** @psalm-suppress RedundantPropertyInitializationCheck */
74 1332
        \assert(isset($this->state));
75
76
        // Bind himself
77 1332
        $shared = new Alias(self::class);
78 1332
        $this->state->bindings = \array_merge($this->state->bindings, [
79 1332
            self::class => new WeakReference(\WeakReference::create($this)),
80 1332
            ContainerInterface::class => $shared,
81 1332
            BinderInterface::class => $shared,
82 1332
            FactoryInterface::class => $shared,
83 1332
            ScopeInterface::class => $shared,
84 1332
            ResolverInterface::class => $shared,
85 1332
            InvokerInterface::class => $shared,
86 1332
        ]);
87
    }
88
89 605
    public function resolveArguments(
90
        ContextFunction $reflection,
91
        array $parameters = [],
92
        bool $validate = true,
93
    ): array {
94 605
        return $this->resolver->resolveArguments($reflection, $parameters, $validate);
95
    }
96
97
    public function validateArguments(ContextFunction $reflection, array $arguments = []): void
98
    {
99
        $this->resolver->validateArguments($reflection, $arguments);
100
    }
101
102
    /**
103
     * @param \Stringable|string|null $context Related to parameter caused injection if any.
104
     *
105
     * @throws ContainerException
106
     * @throws \Throwable
107
     * @psalm-suppress TooManyArguments
108
     */
109 657
    public function make(string $alias, array $parameters = [], \Stringable|string|null $context = null): mixed
110
    {
111 657
        return ContainerScope::getContainer() === $this
112 473
            ? $this->factory->make($alias, $parameters, $context)
113 657
            : ContainerScope::runScope($this, fn(): mixed => $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

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

139
            : 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...
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

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

331
            $isRoot => $container->config->scopedBindings->getState(/** @scrutinizer ignore-type */ $scopeName),
Loading history...
332 301
            default => clone $container->config->scopedBindings->getState($scopeName),
333 1332
        };
334
335 1332
        $constructor = new Internal\Common\Registry($container->config, [
336 1332
            'state' => $state,
337 1332
            'scope' => new Internal\Scope($scopeName),
338 1332
        ], $this->options);
339
340
        // Create container services
341 1332
        foreach ($container->config as $property => $class) {
342 1332
            if (\property_exists($container, $property)) {
343 1332
                $container->$property = $constructor->get($property, $class);
344
            }
345
        }
346
    }
347
348
    /**
349
     * Execute finalizers and destruct the container.
350
     *
351
     * @throws FinalizersException
352
     */
353 609
    private function closeScope(): void
354
    {
355
        /** @psalm-suppress RedundantPropertyInitializationCheck */
356 609
        if (!isset($this->scope)) {
357 342
            $this->destruct();
358 342
            return;
359
        }
360
361 609
        $scopeName = $this->scope->getScopeName();
362
363
        // Run finalizers
364 609
        $errors = [];
365 609
        foreach ($this->state->finalizers as $finalizer) {
366
            try {
367 4
                $this->invoker->invoke($finalizer);
368
            } catch (\Throwable $e) {
369
                $errors[] = $e;
370
            }
371
        }
372
373
        // Destroy the container
374 609
        $this->destruct();
375
376
        // Throw collected errors
377 609
        if ($errors !== []) {
378
            throw new FinalizersException($scopeName, $errors);
379
        }
380
    }
381
382
    /**
383
     * @template TReturn
384
     *
385
     * @param callable(mixed ...$params): TReturn $closure
386
     *
387
     * @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...
388
     * @throws \Throwable
389
     */
390 342
    private function runIsolatedScope(Scope $config, callable $closure): mixed
391
    {
392
        // Open scope
393 342
        $container = new self($this->config, $config->name, $this->options);
394
395
        // Configure scope
396 342
        $container->scope->setParent($this, $this->scope, $this->actor);
397
398
        // Add specific bindings
399 342
        foreach ($config->bindings as $alias => $resolver) {
400 210
            $container->binder->bind($alias, $resolver);
401
        }
402
403 342
        return ContainerScope::runScope(
404 342
            $container,
405 342
            static function (self $container) use ($config, $closure): mixed {
406
                try {
407 342
                    return $config->autowire
408 342
                        ? $container->invoke($closure)
409 331
                        : $closure($container);
410
                } finally {
411 342
                    $container->closeScope();
412
                }
413 342
            },
414 342
        );
415
    }
416
}
417