Building long-lasting applications requires proper management of context. In demonized applications, you can no longer treat user requests as global singletons stored across services.
This means you need to explicitly request context when processing user input. Spiral provides an elegant way to manage this using IoC (Inversion of Control) container scopes.
Scopes allow you to create isolated contexts where you can redefine services and manage their lifecycle.
To create an isolated context, use the Container::runScope()
method.
The first argument is a Scope
object that contains scope options, and the second is a function that will run inside this scope.
The result of this function is returned by runScope()
.
$result = $container->runScope(
new Scope(bindings: [
LoggerInterface::class => FileLogger::class,
]),
function () {
// Your code here
},
);
In this example, the LoggerInterface
will be resolved inside the scope as FileLogger
.
When you call $container->runScope(new Scope(...), fn() => ...)
, a new container is created with its own bindings. The existing container becomes the parent of this new container.
The new container will be used inside the provided function and will be destroyed after the function completes.
Important points:
root
.When resolving dependencies inside an isolated scope:
Spiral provides several predefined scopes:
root
— The main global scope. All other scopes are its children.http
, console
, grpc
, centrifugo
, tcp
, queue
, or temporal
.http
scope, and interceptors in http-request
.If you are sure that a service will only work within a specific dispatcher, it makes sense to use the corresponding scope.
For example, HTTP middleware should be bound at the http
scope level.
You can create your own scopes to isolate context and make only specific services available.
You can preconfigure bindings specific to named scopes using the BinderInterface::getBinder()
method.
This allows you to set default bindings for a scope.
$container->bindSingleton(Interface::class, Implementation::class);
// Configure default bindings for 'request' scope
$binder = $container->getBinder('request');
$binder->bindSingleton(Interface::class, Implementation::class);
$binder->bind(Interface::class, factory(...));
Note
Bindings in a scope do not affect existing containers of that scope (except forroot
).
When using Container::runScope()
, you can pass bindings to override defaults for a specific scope.
$container->bindSingleton(SomeInterface::class, SomeImplementation::class);
$container->runScope(
new Scope(
name: 'request',
bindings: [SomeInterface::class => AnotherImplementation::class],
),
function () {
// Your code here
}
);
Here, even if the request
scope has a default binding for SomeInterface
, this specific run uses AnotherImplementation
.
You can restrict where a dependency can be resolved using the #[Scope('name')]
attribute.
use Spiral\Boot\Environment\DebugMode;
use Spiral\Core\Attribute\Scope;
use Spiral\Core\Attribute\Singleton;
#[Singleton]
#[Scope('http')]
final readonly class DebugMiddleware implements \Psr\Http\Server\MiddlewareInterface
{
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
// ...
}
}
In this example, DebugMiddleware
can be instantiated only if the http
scope exists in the scope hierarchy.
Otherwise, an exception is thrown.
When exiting a scope, the associated container is destroyed. This means singletons created within the scope should be garbage collected, so avoid circular references.
If you need to perform cleanup actions when dependencies are resolved in a scope, use the #[Finalize('methodName')]
attribute to specify a method that will be called when the scope is destroyed.
#[Finalize('destroy')]
class MyService
{
/**
* This method will be called before the scope is destroyed in case the service was resolved in this scope.
* Arguments will be resolved using the container.
*/
public function destroy(LoggerInterface $logger): void
{
// Clean up...
}
}
Scopes are like nested containers, but there's more to them than simple delegation.
What if you want to create a stateless service in the parent scope (root
or http
)
that will handle ServerRequestInterface
objects in the http-request
scope?
With nested containers, this is impossible because ServerRequestInterface
is only available inside the http-request
scope.
Moreover, ServerRequestInterface
will be different for each request.
Spiral provides proxy objects that defer dependency resolution until it's actually needed.
Use the #[Proxy]
attribute to create proxies for interfaces:
use Psr\Http\Message\ServerRequestInterface;
use Spiral\Core\Attribute\Proxy;
use Spiral\Core\Attribute\Singleton;
#[Singleton]
final readonly class DebugService
{
public function __construct(
#[Proxy] private ServerRequestInterface $request,
) {}
public function hasDebugInfo(): bool
{
return $this->request->hasHeader('X-Debug');
}
}
Important points:
You can configure proxies for services that must only be available in specific scopes using the Binder
class.
For example, if the AuthInterface
service must only be available in the http
scope, you can use a proxy object for the root
scope:
// Configure a proxy for `AuthInterface` in the `root` scope
$rootBinder = $container->getBinder('root');
$rootBinder->bindSingleton(new \Spiral\Core\Config\Proxy(
AuthInterface::class,
singleton: true,
fallbackFactory: static fn() => throw new \LogicException(
'Unable to receive AuthInterface instance outside of `http` scope.'
),
));
// Bind `AuthInterface` in the `http` scope
$container->getBinder('http')
->bindSingleton(AuthInterface::class, Auth::class);
If a proxy is used outside the http
scope, the fallbackFactory
will be called to resolve the dependency.
If the fallbackFactory
is not provided, a RecursiveProxyException
will be thrown.