Passed
Push — master ( 67a020...fe0e28 )
by butschster
04:26 queued 17s
created

Container   A

Complexity

Total Complexity 39

Size/Duplication

Total Lines 338
Duplicated Lines 0 %

Test Coverage

Coverage 91.38%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 39
eloc 114
dl 0
loc 338
ccs 106
cts 116
cp 0.9138
rs 9.28
c 1
b 0
f 0

22 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 20 1
A __destruct() 0 3 1
A validateArguments() 0 3 1
A make() 0 4 1
A resolveArguments() 0 6 1
A __clone() 0 3 1
A getBinder() 0 5 2
A bind() 0 3 1
A runScoped() 0 32 4
A bindInjector() 0 3 1
A getCurrentContainer() 0 3 1
A has() 0 3 1
B runScope() 0 38 8
A hasInstance() 0 3 1
A bindSingleton() 0 3 1
A get() 0 4 1
A removeBinding() 0 3 1
A initServices() 0 23 3
A removeInjector() 0 3 1
A invoke() 0 3 1
A hasInjector() 0 3 1
A closeScope() 0 26 5
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;
57
    private Internal\Scope $scope;
58
59
    /**
60
     * Container constructor.
61
     */
62 1028
    public function __construct(
63
        private Config $config = new Config(),
64
        ?string $scopeName = self::DEFAULT_ROOT_SCOPE_NAME,
65
    ) {
66 1028
        $this->initServices($this, $scopeName);
67
68
        /** @psalm-suppress RedundantPropertyInitializationCheck */
69 1028
        \assert(isset($this->state));
70
71
        // Bind himself
72 1028
        $shared = new Alias(self::class);
73 1028
        $this->state->bindings = \array_merge($this->state->bindings, [
74 1028
            self::class => new WeakReference(\WeakReference::create($this)),
75 1028
            ContainerInterface::class => $shared,
76 1028
            BinderInterface::class => $shared,
77 1028
            FactoryInterface::class => $shared,
78 1028
            ContainerScopeInterface::class => $shared,
79 1028
            ScopeInterface::class => $shared,
80 1028
            ResolverInterface::class => $shared,
81 1028
            InvokerInterface::class => $shared,
82 1028
        ]);
83
    }
84
85 592
    public function __destruct()
86
    {
87 592
        $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 442
    public function resolveArguments(
99
        ContextFunction $reflection,
100
        array $parameters = [],
101
        bool $validate = true,
102
    ): array {
103 442
        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 541
    public function make(string $alias, array $parameters = [], string $context = null): mixed
118
    {
119
        /** @psalm-suppress TooManyArguments */
120 541
        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 802
    public function get(string|Autowire $id, string $context = null): mixed
142
    {
143
        /** @psalm-suppress TooManyArguments */
144 802
        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

144
        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

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

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