Test Failed
Pull Request — master (#870)
by Aleksei
06:40
created

Container::initServices()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 23
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

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

42
    /** @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...
43
{
44
    use DestructorTrait;
45
46
    public const DEFAULT_ROOT_SCOPE_NAME = 'root';
47
48
    private Internal\State $state;
49
    private ResolverInterface|Internal\Resolver $resolver;
50
    private FactoryInterface|Internal\Factory $factory;
51
    private ContainerInterface|Internal\Container $container;
52
    private BinderInterface|Internal\Binder $binder;
53
    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...
54
    private Internal\Scope $scope;
55
56
    /**
57 893
     * Container constructor.
58
     */
59 893
    public function __construct(
60 893
        private Config $config = new Config(),
61 893
        ?string $scopeName = self::DEFAULT_ROOT_SCOPE_NAME,
62 893
    ) {
63 893
        $this->initServices($this, $scopeName);
64 893
65
        // Bind himself
66
        /** @psalm-suppress PossiblyNullPropertyAssignment */
67
        $this->state->bindings = \array_merge($this->state->bindings, [
68
            self::class               => \WeakReference::create($this),
69 893
            ContainerInterface::class => self::class,
70 893
            BinderInterface::class    => self::class,
71 893
            FactoryInterface::class   => self::class,
72 893
            ScopeInterface::class     => self::class,
73 893
            ResolverInterface::class  => self::class,
74 893
            InvokerInterface::class   => self::class,
75 893
        ]);
76 893
    }
77 893
78
    public function __destruct()
79
    {
80 509
        $this->destruct();
81
    }
82 509
83
    /**
84
     * Container can not be cloned.
85
     */
86
    public function __clone()
87
    {
88 1
        throw new LogicException('Container is not clonable.');
89
    }
90 1
91
    public function resolveArguments(
92
        ContextFunction $reflection,
93 384
        array $parameters = [],
94
        bool $validate = true,
95
    ): array {
96
        return $this->resolver->resolveArguments($reflection, $parameters, $validate);
97
    }
98 384
99
    public function validateArguments(ContextFunction $reflection, array $arguments = []): void
100
    {
101
        $this->resolver->validateArguments($reflection, $arguments);
102
    }
103
104
    /**
105
     * @param string|null $context Related to parameter caused injection if any.
106
     */
107
    public function make(string $alias, array $parameters = [], string $context = null): mixed
108
    {
109 460
        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

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

134
        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

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

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