Passed
Push — master ( 7bd689...ed0e19 )
by butschster
16:50 queued 18s
created

Container   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 364
Duplicated Lines 0 %

Test Coverage

Coverage 93.28%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 43
eloc 116
c 2
b 0
f 0
dl 0
loc 364
ccs 111
cts 119
cp 0.9328
rs 8.96

22 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 19 1
A __destruct() 0 3 1
A validateArguments() 0 3 1
A resolveArguments() 0 6 1
A __clone() 0 3 1
A removeBinding() 0 3 1
A getBinder() 0 5 2
A runIsolatedScope() 0 22 3
A bind() 0 3 1
A runScoped() 0 3 1
A bindInjector() 0 3 1
A has() 0 3 1
B runScope() 0 42 9
A hasInstance() 0 3 1
A bindSingleton() 0 7 2
A make() 0 5 2
A get() 0 5 2
A initServices() 0 23 3
A removeInjector() 0 3 1
A invoke() 0 5 2
A hasInjector() 0 3 1
A closeScope() 0 26 5

How to fix   Complexity   

Complex Class

Complex classes like Container often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Container, and based on these observations, apply Extract Interface, too.

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\Internal\Common\DestructorTrait;
18
use Spiral\Core\Internal\Config\StateBinder;
19
20
/**
21
 * Auto-wiring container: declarative singletons, contextual injections, parent container
22
 * delegation and ability to lazy wire.
23
 *
24
 * Container does not support setter injections, private properties, etc. Normally it will work
25
 * with classes only to be as much invisible as possible. Attention, this is hungry implementation
26
 * of container, meaning it WILL try to resolve dependency unless you specified custom lazy
27
 * factory.
28
 *
29
 * You can use injectors to delegate class resolution to external container.
30
 *
31
 * @see InjectableInterface
32
 * @see SingletonInterface
33
 *
34
 * @psalm-import-type TResolver from BinderInterface
35
 * @psalm-import-type TInvokable from InvokerInterface
36
 */
37
final class Container implements
38
    ContainerInterface,
39
    BinderInterface,
40
    FactoryInterface,
41
    ResolverInterface,
42
    InvokerInterface,
43
    ScopeInterface
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;
55
    private Internal\Scope $scope;
56
57
    /**
58
     * Container constructor.
59
     */
60 1200
    public function __construct(
61
        private Config $config = new Config(),
62
        ?string $scopeName = self::DEFAULT_ROOT_SCOPE_NAME,
63
    ) {
64 1200
        $this->initServices($this, $scopeName);
65
66
        /** @psalm-suppress RedundantPropertyInitializationCheck */
67 1200
        \assert(isset($this->state));
68
69
        // Bind himself
70 1200
        $shared = new Alias(self::class);
71 1200
        $this->state->bindings = \array_merge($this->state->bindings, [
72 1200
            self::class => new WeakReference(\WeakReference::create($this)),
73 1200
            ContainerInterface::class => $shared,
74 1200
            BinderInterface::class => $shared,
75 1200
            FactoryInterface::class => $shared,
76 1200
            ScopeInterface::class => $shared,
77 1200
            ResolverInterface::class => $shared,
78 1200
            InvokerInterface::class => $shared,
79 1200
        ]);
80
    }
81
82 638
    public function __destruct()
83
    {
84 638
        $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 529
    public function resolveArguments(
96
        ContextFunction $reflection,
97
        array $parameters = [],
98
        bool $validate = true,
99
    ): array {
100 529
        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
     * @psalm-suppress TooManyArguments
114
     */
115 634
    public function make(string $alias, array $parameters = [], \Stringable|string|null $context = null): mixed
116
    {
117 634
        return ContainerScope::getContainer() === $this
118 422
            ? $this->factory->make($alias, $parameters, $context)
119 634
            : ContainerScope::runScope($this, fn () => $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

119
            : ContainerScope::runScope($this, fn () => $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...
120
    }
121
122
    /**
123
     * Context parameter will be passed to class injectors, which makes possible to use this method
124
     * as:
125
     *
126
     * $this->container->get(DatabaseInterface::class, 'default');
127
     *
128
     * Attention, context ignored when outer container has instance by alias.
129
     *
130
     * @template T
131
     *
132
     * @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...
133
     * @param string|null $context Call context.
134
     *
135
     * @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...
136
     *
137
     * @throws ContainerException
138
     * @throws \Throwable
139
     * @psalm-suppress TooManyArguments
140
     */
141 960
    public function get(string|Autowire $id, \Stringable|string|null $context = null): mixed
142
    {
143 960
        return ContainerScope::getContainer() === $this
144 599
            ? $this->container->get($id, $context)
145 960
            : ContainerScope::runScope($this, fn () => $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

145
            : ContainerScope::runScope($this, fn () => $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

145
            : ContainerScope::runScope($this, fn () => $this->container->get(/** @scrutinizer ignore-type */ $id, $context));
Loading history...
146
    }
147
148 553
    public function has(string $id): bool
149
    {
150 553
        return $this->container->has($id);
151
    }
152
153
    /**
154
     * Make a Binder proxy to configure bindings for a specific scope.
155
     *
156
     * @param null|string $scope Scope name.
157
     *        If {@see null}, binder for the current working scope will be returned.
158
     *        If {@see string}, the default binder for the given scope will be returned. Default bindings won't affect
159
     *        already created Container instances except the case with the root one.
160
     */
161 11
    public function getBinder(?string $scope = null): BinderInterface
162
    {
163 11
        return $scope === null
164
            ? $this->binder
165 11
            : new StateBinder($this->config->scopedBindings->getState($scope));
166
    }
167
168
    /**
169
     * @throws \Throwable
170
     */
171 650
    public function runScope(Scope|array $bindings, callable $scope): mixed
172
    {
173 650
        if (!\is_array($bindings)) {
0 ignored issues
show
introduced by
The condition is_array($bindings) is always true.
Loading history...
174 11
            return $this->runIsolatedScope($bindings, $scope);
175
        }
176
177 639
        $binds = &$this->state->bindings;
178 639
        $singletons = &$this->state->singletons;
179 639
        $cleanup = $previous = $prevSin = [];
180 639
        foreach ($bindings as $alias => $resolver) {
181
            // Store previous bindings
182 638
            if (isset($binds[$alias])) {
183 548
                $previous[$alias] = $binds[$alias];
184
            } else {
185
                // Store bindings to be removed
186 637
                $cleanup[] = $alias;
187
            }
188
            // Store previous singletons
189 638
            if (isset($singletons[$alias])) {
190 40
                $prevSin[$alias] = $singletons[$alias];
191 40
                unset($singletons[$alias]);
192
            }
193
194 638
            $this->binder->bind($alias, $resolver);
195
        }
196
197
        try {
198 639
            return ContainerScope::getContainer() !== $this
199 638
                ? ContainerScope::runScope($this, $scope)
200 625
                : $scope($this);
201
        } finally {
202
            // Remove new bindings
203 639
            foreach ($cleanup as $alias) {
204 637
                unset($binds[$alias], $singletons[$alias]);
205
            }
206
            // Restore previous bindings
207 639
            foreach ($previous as $alias => $resolver) {
208 548
                $binds[$alias] = $resolver;
209
            }
210
            // Restore singletons
211 639
            foreach ($prevSin as $alias => $instance) {
212 40
                $singletons[$alias] = $instance;
213
            }
214
        }
215
    }
216
217
    /**
218
     * Invoke given closure or function withing specific IoC scope.
219
     *
220
     * @template TReturn
221
     *
222
     * @param callable(mixed ...$params): TReturn $closure
223
     * @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...
224
     * @param null|string $name Scope name. Named scopes can have individual bindings and constrains.
225
     * @param bool $autowire If {@see false}, closure will be invoked with just only the passed Container as an
226
     *        argument. Otherwise, {@see InvokerInterface::invoke()} will be used to invoke the closure.
227
     *
228
     * @return TReturn
229
     * @throws \Throwable
230
     *
231
     * @deprecated Use {@see runScope()} with the {@see Scope} as the first argument.
232
     * @internal Used in tests only
233
     */
234 23
    public function runScoped(callable $closure, array $bindings = [], ?string $name = null, bool $autowire = true): mixed
235
    {
236 23
        return $this->runIsolatedScope(new Scope($name, $bindings, $autowire), $closure);
237
    }
238
239
    /**
240
     * Bind value resolver to container alias. Resolver can be class name (will be constructed
241
     * for each method call), function array or Closure (executed every call). Only object resolvers
242
     * supported by this method.
243
     */
244 919
    public function bind(string $alias, mixed $resolver): void
245
    {
246 919
        $this->binder->bind($alias, $resolver);
247
    }
248
249
    /**
250
     * Bind value resolver to container alias to be executed as cached. Resolver can be class name
251
     * (will be constructed only once), function array or Closure (executed only once call).
252
     *
253
     * @psalm-param TResolver $resolver
254
     * @param bool $force If the value is false, an exception will be thrown when attempting
255
     *  to bind an already constructed singleton.
256
     */
257 765
    public function bindSingleton(string $alias, string|array|callable|object $resolver, bool $force = true): void
258
    {
259 765
        if ($force) {
260 765
            $this->binder->removeBinding($alias);
261
        }
262
263 765
        $this->binder->bindSingleton($alias, $resolver);
264
    }
265
266
    /**
267
     * Check if alias points to constructed instance (singleton).
268
     */
269 14
    public function hasInstance(string $alias): bool
270
    {
271 14
        return $this->binder->hasInstance($alias);
272
    }
273
274 325
    public function removeBinding(string $alias): void
275
    {
276 325
        $this->binder->removeBinding($alias);
277
    }
278
279
    /**
280
     * @psalm-param TInvokable $target
281
     */
282 665
    public function invoke(mixed $target, array $parameters = []): mixed
283
    {
284 665
        return ContainerScope::getContainer() === $this
285 614
            ? $this->invoker->invoke($target, $parameters)
286 640
            : ContainerScope::runScope($this, fn () => $this->invoker->invoke($target, $parameters));
287
    }
288
289
    /**
290
     * Bind class or class interface to the injector source (InjectorInterface).
291
     */
292 433
    public function bindInjector(string $class, string $injector): void
293
    {
294 433
        $this->binder->bindInjector($class, $injector);
295
    }
296
297
    public function removeInjector(string $class): void
298
    {
299
        $this->binder->removeInjector($class);
300
    }
301
302 8
    public function hasInjector(string $class): bool
303
    {
304 8
        return $this->binder->hasInjector($class);
305
    }
306
307
    /**
308
     * Init internal container services.
309
     */
310 1200
    private function initServices(
311
        self $container,
312
        ?string $scopeName,
313
    ): void {
314 1200
        $isRoot = $container->config->lockRoot();
315
316
        // Get named scope or create anonymous one
317 1200
        $state = match (true) {
318 1200
            $scopeName === null => new Internal\State(),
319
            // Only root container can make default bindings directly
320 1200
            $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

320
            $isRoot => $container->config->scopedBindings->getState(/** @scrutinizer ignore-type */ $scopeName),
Loading history...
321 1200
            default => clone $container->config->scopedBindings->getState($scopeName),
322 1200
        };
323
324 1200
        $constructor = new Internal\Common\Registry($container->config, [
325 1200
            'state' => $state,
326 1200
            'scope' => new Internal\Scope($scopeName),
327 1200
        ]);
328
329
        // Create container services
330 1200
        foreach ($container->config as $property => $class) {
331 1200
            if (\property_exists($container, $property)) {
332 1200
                $container->$property = $constructor->get($property, $class);
333
            }
334
        }
335
    }
336
337
    /**
338
     * Execute finalizers and destruct the container.
339
     *
340
     * @throws FinalizersException
341
     */
342 638
    private function closeScope(): void
343
    {
344
        /** @psalm-suppress RedundantPropertyInitializationCheck */
345 638
        if (!isset($this->scope)) {
346 34
            $this->destruct();
347 34
            return;
348
        }
349
350 638
        $scopeName = $this->scope->getScopeName();
351
352
        // Run finalizers
353 638
        $errors = [];
354 638
        foreach ($this->state->finalizers as $finalizer) {
355
            try {
356 4
                $this->invoker->invoke($finalizer);
357
            } catch (\Throwable $e) {
358
                $errors[] = $e;
359
            }
360
        }
361
362
        // Destroy the container
363 638
        $this->destruct();
364
365
        // Throw collected errors
366 638
        if ($errors !== []) {
367
            throw new FinalizersException($scopeName, $errors);
368
        }
369
    }
370
371
    /**
372
     * @template TReturn
373
     *
374
     * @param callable(mixed ...$params): TReturn $closure
375
     *
376
     * @return TReturn
0 ignored issues
show
Bug introduced by
The type Spiral\Core\TReturn 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...
377
     * @throws \Throwable
378
     */
379 34
    private function runIsolatedScope(Scope $config, callable $closure): mixed
380
    {
381
        // Open scope
382 34
        $container = new self($this->config, $config->name);
383
384
        // Configure scope
385 34
        $container->scope->setParent($this, $this->scope);
386
387
        // Add specific bindings
388 34
        foreach ($config->bindings as $alias => $resolver) {
389 13
            $container->binder->bind($alias, $resolver);
390
        }
391
392 34
        return ContainerScope::runScope(
393 34
            $container,
394 34
            static function (self $container) use ($config, $closure): mixed {
395
                try {
396 34
                    return $config->autowire
397 34
                        ? $container->invoke($closure)
398 33
                        : $closure($container);
399
                } finally {
400 34
                    $container->closeScope();
401
                }
402 34
            }
403 34
        );
404
    }
405
}
406