One of the key features of Spiral is its support for interceptors, which can be used to add functionality to the application without modifying the core code of the application. This can help to keep your codebase more modular and maintainable.
Benefits of using interceptors:
You can use interceptors with various components such as:
Interceptors implement the Spiral\Interceptors\InterceptorInterface
:
namespace Spiral\Interceptors;
use Spiral\Interceptors\Context\CallContextInterface;
interface InterceptorInterface
{
public function intercept(CallContextInterface $context, HandlerInterface $handler): mixed;
}
The interface provides a flexible way to intercept calls to any target, whether it's a method, function, or custom handler.
The CallContextInterface
contains all the information about the intercepted call:
namespace Spiral\Interceptors\Context;
interface CallContextInterface extends AttributedInterface
{
public function getTarget(): TargetInterface;
public function getArguments(): array;
public function withTarget(TargetInterface $target): static;
public function withArguments(array $arguments): static;
// Methods from AttributedInterface:
public function getAttributes(): array;
public function getAttribute(string $name, mixed $default = null): mixed;
public function withAttribute(string $name, mixed $value): static;
public function withoutAttribute(string $name): static;
}
Note
CallContextInterface
is immutable, so calls towithTarget()
andwithArguments()
return a new instance with the updated values.
The TargetInterface
defines the target whose call you want to intercept. It can represent a method, function, closure, or even a path string for RPC or message queue endpoints.
namespace Spiral\Interceptors\Context;
interface TargetInterface extends \Stringable
{
public function getPath(): array;
public function withPath(array $path, ?string $delimiter = null): static;
public function getReflection(): ?\ReflectionFunctionAbstract;
public function getObject(): ?object;
public function getCallable(): callable|array|null;
}
The static factory methods in the Target
class make it easy to create different types of targets:
use Spiral\Interceptors\Context\Target;
// From a method reflection
$target = Target::fromReflectionMethod(new \ReflectionMethod(UserController::class, 'show'), UserController::class);
// From a function reflection
$target = Target::fromReflectionFunction(new \ReflectionFunction('array_map'));
// From a closure
$target = Target::fromClosure(fn() => 'Hello, World!');
// From a path string (for RPC endpoints or message queue handlers)
$target = Target::fromPathString('user.show');
// From a controller-action pair
$target = Target::fromPair(UserController::class, 'show');
Let's create a simple interceptor that logs the execution time of a call:
namespace App\Interceptor;
use Psr\Log\LoggerInterface;
use Spiral\Interceptors\Context\CallContextInterface;
use Spiral\Interceptors\HandlerInterface;
use Spiral\Interceptors\InterceptorInterface;
class ExecutionTimeInterceptor implements InterceptorInterface
{
public function __construct(
private readonly LoggerInterface $logger
) {
}
public function intercept(CallContextInterface $context, HandlerInterface $handler): mixed
{
$target = $context->getTarget();
$startTime = \microtime(true);
try {
return $handler->handle($context);
} finally {
$executionTime = \microtime(true) - $startTime;
$this->logger->debug(
'Target executed',
[
'target' => (string)$target,
'execution_time' => $executionTime,
]
);
}
}
}
This interceptor:
To use interceptors, you need to build an interceptor pipeline using the PipelineBuilderInterface
:
use Spiral\Interceptors\PipelineBuilder;
use Spiral\Interceptors\Context\CallContext;
use Spiral\Interceptors\Context\Target;
use Spiral\Interceptors\Handler\CallableHandler;
use App\Interceptor\ExecutionTimeInterceptor;
use App\Interceptor\AuthorizationInterceptor;
// Create interceptors
$interceptors = [
new ExecutionTimeInterceptor($logger),
new AuthorizationInterceptor($auth),
];
// Build the pipeline
$pipeline = (new PipelineBuilder())
->withInterceptors(...$interceptors)
->build(new CallableHandler());
// Create call context
$context = new CallContext(
target: Target::fromPair(UserController::class, 'show'),
arguments: ['id' => 42],
attributes: ['request' => $request]
);
// Execute the pipeline
$result = $pipeline->handle($context);
The pipeline should end with a handler that executes the target. Spiral provides several built-in handlers:
The CallableHandler
simply calls the target without any additional processing:
use Spiral\Interceptors\Handler\CallableHandler;
$handler = new CallableHandler();
The AutowireHandler
resolves missing arguments using the container:
use Spiral\Interceptors\Handler\AutowireHandler;
$handler = new AutowireHandler($container);
This handler is useful when working with controllers where you want to automatically inject service dependencies.
Here's a more comprehensive example of using interceptors:
namespace App\Controller;
use App\Interceptor\AuthorizationInterceptor;
use App\Interceptor\CacheInterceptor;
use App\Interceptor\ExecutionTimeInterceptor;
use App\User\UserService;
use Psr\Container\ContainerInterface;
use Spiral\Core\Attribute\Proxy;
use Spiral\Core\CompatiblePipelineBuilder;
use Spiral\Interceptors\Context\CallContext;
use Spiral\Interceptors\Context\Target;
use Spiral\Interceptors\Handler\AutowireHandler;
class UserController
{
private $pipeline;
public function __construct(
private readonly UserService $userService,
#[Proxy] ContainerInterface $container
) {
// Build the pipeline with interceptors
$this->pipeline = (new CompatiblePipelineBuilder())
->withInterceptors(
new ExecutionTimeInterceptor($container->get(LoggerInterface::class)),
new AuthorizationInterceptor($container->get(AuthInterface::class)),
new CacheInterceptor($container->get(CacheInterface::class))
)
->build(new AutowireHandler($container));
}
public function show(int $id)
{
// Create a context for the target method
$context = new CallContext(
target: Target::fromReflectionMethod(
new \ReflectionMethod($this->userService, 'findUser'),
$this->userService
),
arguments: ['id' => $id]
);
// Execute the pipeline
return $this->pipeline->handle($context);
}
}
In this example:
AutowireHandler
to resolve missing arguments from the containerfindUser
method of the UserService
Note
To learn about Container Scopes and Proxy objects, see the IoC Scopes section in our documentation.
Note
The old implementation of interceptors based onspiral/hmvc
is no longer recommended. You can find the old documentation at https://spiral.dev/docs/framework-interceptors/3.13.
In Spiral 3.14.0, a new implementation of interceptors was introduced in the spiral/interceptors
package.
Here's how the new implementation differs from the legacy one:
namespace App\Interceptor;
use Psr\SimpleCache\CacheInterface;
use Spiral\Core\CoreInterface;
use Spiral\Core\CoreInterceptorInterface;
class CacheInterceptor implements CoreInterceptorInterface
{
public function __construct(
private readonly CacheInterface $cache,
private readonly int $ttl = 3600,
) {}
public function process(string $controller, string $action, array $parameters, CoreInterface $core): mixed
{
// Step 1: Generate a cache key based on controller, action, and parameters
$cacheKey = $this->generateCacheKey($controller, $action, $parameters);
// Step 2: Check if the result is already cached
if ($this->cache->has($cacheKey)) {
// Return cached result if available
return $this->cache->get($cacheKey);
}
// Step 3: Execute the controller action if no cached result
$result = $core->callAction($controller, $action, $parameters);
// Step 4: Cache the result for future requests
if ($this->isCacheable($result)) {
$this->cache->set($cacheKey, $result, $this->ttl);
}
return $result;
}
private function generateCacheKey(string $controller, string $action, array $parameters): string
{
// Create a deterministic cache key from controller, action, and parameters
return \md5($controller . '::' . $action . '::' . \serialize($parameters));
}
private function isCacheable(mixed $result): bool
{
// Only cache serializable results
return !\is_resource($result) && (
\is_scalar($result) ||
\is_array($result) ||
$result instanceof \Serializable ||
$result instanceof \stdClass
);
}
}
Legacy Interface:
interface CoreInterceptorInterface
{
public function process(string $controller, string $action, array $parameters, CoreInterface $core): mixed;
}
New Interface:
interface InterceptorInterface
{
public function intercept(CallContextInterface $context, HandlerInterface $handler): mixed;
}
The main differences:
$controller
and $action
parameters have been replaced with a more flexible Target
concept$parameters
array is now part of the CallContext
CoreInterface
, there's a HandlerInterface
which offers more flexibilityIn Spiral 3.x, both legacy interceptors (CoreInterceptorInterface
) and new interceptors (InterceptorInterface
) are supported.
However, the legacy implementation is deprecated and will be excluded in Spiral 4.x.
If you need to use both implementations together, use the CompatiblePipelineBuilder
:
use Spiral\Core\CompatiblePipelineBuilder;
use Spiral\Core\CoreInterceptorInterface; // Legacy interceptor
use Spiral\Interceptors\InterceptorInterface; // New interceptor
$pipeline = (new CompatiblePipelineBuilder())
->withInterceptors(
new LegacyStyleInterceptor(), // Implements CoreInterceptorInterface
new NewStyleInterceptor() // Implements InterceptorInterface
)
->build(new CallableHandler());
When migrating from the legacy implementation:
CoreInterceptorInterface
with InterceptorInterface
Target
instead of $controller
and $action
parameters$parameters
to the CallContext
$core->callAction()
with $handler->handle()
Event | Description |
---|---|
Spiral\Interceptors\Event\InterceptorCalling | Fired before calling an interceptor. |
Warning
TheSpiral\Core\Event\InterceptorCalling
event is only dispatched by the deprecated\Spiral\Core\InterceptorPipeline
from the legacy implementation. The framework's new implementation does not use this event.
Note
To learn more about dispatching events, see the Events section in our documentation.