Passed
Pull Request — master (#936)
by Aleksei
19:34
created

Container::make()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 3
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\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
    public function getCurrentContainer(): ContainerInterface
232
    {
233
        return ContainerScope::getContainer() ?? $this;
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 790
    public function bind(string $alias, string|array|callable|object $resolver): void
242
    {
243 790
        $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 584
    public function bindSingleton(string $alias, string|array|callable|object $resolver): void
253
    {
254 584
        $this->binder->bindSingleton($alias, $resolver);
255
    }
256
257
    /**
258
     * Check if alias points to constructed instance (singleton).
259
     */
260 9
    public function hasInstance(string $alias): bool
261
    {
262 9
        return $this->binder->hasInstance($alias);
263
    }
264
265 2
    public function removeBinding(string $alias): void
266
    {
267 2
        $this->binder->removeBinding($alias);
268
    }
269
270
    /**
271
     * @psalm-param TInvokable $target
272
     */
273 563
    public function invoke(mixed $target, array $parameters = []): mixed
274
    {
275 563
        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 363
    public function bindInjector(string $class, string $injector): void
285
    {
286 363
        $this->binder->bindInjector($class, $injector);
287
    }
288
289
    public function removeInjector(string $class): void
290
    {
291
        $this->binder->removeInjector($class);
292
    }
293
294 8
    public function hasInjector(string $class): bool
295
    {
296 8
        return $this->binder->hasInjector($class);
297
    }
298
299
    /**
300
     * Init internal container services.
301
     */
302 1011
    private function initServices(
303
        self $container,
304
        ?string $scopeName,
305
    ): void {
306 1011
        $isRoot = $container->config->lockRoot();
307
308
        // Get named scope or create anonymous one
309 1011
        $state = match (true) {
310 1011
            $scopeName === null => new Internal\State(),
311
            // Only root container can make default bindings directly
312 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

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