Test Failed
Pull Request — master (#870)
by Aleksei
08:56
created

Container::removeInjector()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 1
c 0
b 0
f 0
dl 0
loc 3
rs 10
ccs 0
cts 0
cp 0
cc 1
nc 1
nop 1
crap 2
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 and 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
 * @psalm-suppress PropertyNotSetInConstructor
36
 */
37
final class Container implements
38
    ContainerInterface,
39
    BinderInterface,
40
    FactoryInterface,
41
    ResolverInterface,
42
    InvokerInterface,
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 904
    /**
58
     * Container constructor.
59 904
     */
60 904
    public function __construct(
61 904
        private Config $config = new Config(),
62 904
        ?string $scopeName = self::DEFAULT_ROOT_SCOPE_NAME,
63 904
    ) {
64 904
        $this->initServices($this, $scopeName);
65
66
        // Bind himself
67
        /** @psalm-suppress PossiblyNullPropertyAssignment */
68
        $this->state->bindings = \array_merge($this->state->bindings, [
69 904
            self::class               => \WeakReference::create($this),
70 904
            ContainerInterface::class => self::class,
71 904
            BinderInterface::class    => self::class,
72 904
            FactoryInterface::class   => self::class,
73 904
            ScopeInterface::class     => self::class,
74 904
            ResolverInterface::class  => self::class,
75 904
            InvokerInterface::class   => self::class,
76 904
        ]);
77 904
    }
78
79
    public function __destruct()
80 519
    {
81
        $this->closeScope();
82 519
    }
83
84
    /**
85
     * Container can not be cloned.
86
     */
87
    public function __clone()
88 1
    {
89
        throw new LogicException('Container is not clonable.');
90 1
    }
91
92
    public function resolveArguments(
93 395
        ContextFunction $reflection,
94
        array $parameters = [],
95
        bool $validate = true,
96
    ): array {
97
        return $this->resolver->resolveArguments($reflection, $parameters, $validate);
98 395
    }
99
100
    public function validateArguments(ContextFunction $reflection, array $arguments = []): void
101
    {
102
        $this->resolver->validateArguments($reflection, $arguments);
103
    }
104
105
    /**
106
     * @param string|null $context Related to parameter caused injection if any.
107
     */
108
    public function make(string $alias, array $parameters = [], string $context = null): mixed
109 471
    {
110
        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

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

135
        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

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

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