Passed
Pull Request — master (#941)
by Aleksei
13:25 queued 04:54
created

Container::bindInjector()   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 2
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\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;
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...
57
    private Internal\Scope $scope;
58
59
    /**
60
     * Container constructor.
61
     */
62 1017
    public function __construct(
63
        private Config $config = new Config(),
64
        ?string $scopeName = self::DEFAULT_ROOT_SCOPE_NAME,
65
    ) {
66 1017
        $this->initServices($this, $scopeName);
67
68
        /** @psalm-suppress RedundantPropertyInitializationCheck */
69 1017
        \assert(isset($this->state));
70
71
        // Bind himself
72 1017
        $shared = new Alias(self::class);
73 1017
        $this->state->bindings = \array_merge($this->state->bindings, [
74 1017
            self::class => new WeakReference(\WeakReference::create($this)),
75 1017
            ContainerInterface::class => $shared,
76 1017
            BinderInterface::class => $shared,
77 1017
            FactoryInterface::class => $shared,
78 1017
            ContainerScopeInterface::class => $shared,
79 1017
            ScopeInterface::class => $shared,
80 1017
            ResolverInterface::class => $shared,
81 1017
            InvokerInterface::class => $shared,
82 1017
        ]);
83
    }
84
85 585
    public function __destruct()
86
    {
87 585
        $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 434
    public function resolveArguments(
99
        ContextFunction $reflection,
100
        array $parameters = [],
101
        bool $validate = true,
102
    ): array {
103 434
        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 533
    public function make(string $alias, array $parameters = [], string $context = null): mixed
118
    {
119
        /** @psalm-suppress TooManyArguments */
120 533
        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
     * @psalm-suppress InvalidArgument, InvalidCast
142
     */
143 791
    public function get(string|Autowire $id, string $context = null): mixed
144
    {
145
        /** @psalm-suppress TooManyArguments */
146 791
        return $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

146
        return $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

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

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