Revision: Mon, 25 Sep 2023 09:33:25 GMT
v3.3 – outdated
This version of the documentation is outdated. Consider upgrading your project to Spiral Framework 3.8
Edit this page

WebSockets - Interceptors

Spiral Framework provides interceptors for WebSockets services that allow you to intercept and modify requests and responses at various points in the request lifecycle.

They are typically used to add cross-cutting functionality such as logging, authentication, or monitoring to the server.

Example

Authentication interceptor

The following example shows how to create an interceptor that checks the user's authentication token.

php
<?php

declare(strict_types=1);

namespace App\Centrifuge\Interceptor;

use RoadRunner\Centrifugo\Request\RequestInterface;
use Spiral\Auth\TokenStorageInterface;
use Spiral\Core\CoreInterceptorInterface;
use Spiral\Core\CoreInterface;

final class AuthInterceptor implements CoreInterceptorInterface
{
    public function __construct(
        private readonly TokenStorageInterface $tokenStorage,
    ) {
    }

    public function process(string $controller, string $action, array $parameters, CoreInterface $core): mixed
    {
        \assert($parameters['request'] instanceof RequestInterface);

        $userId = null;
        
        $authToken = $parameters['request']->getData()['authToken'] ?? null;
        if ($authToken && $token = $this->tokenStorage->load($authToken)) {
            $userId = $token->getPayload()['userID'] ?? null
        }
        
        if(!$userId) {
            $parameters['request']->disconnect('403', 'Connection is not allowed.');
            return;
        }

        // Adds the user id to the request as an attribute.
        $parameters['request'] = $parameters['request']->withAttribute(
            'user_id',
            $token->getPayload()['userID'] ?? null
        );
            
        return $core->callAction($controller, $action, $parameters);
    }
}

And example of how to use it in a service:

php
/**
 * @param Connect $request
 */
public function handle(RequestInterface $request): void
{
    try {
        $request->respond(
            new ConnectResponse(
                user: (string) $request->getAttribute('user_id'),
            )
        );
    } catch (\Throwable $e) {
        $request->error($e->getCode(), $e->getMessage());
    }
}

Error handling interceptor

The following example shows how to create an interceptor that handles errors.

php
<?php

declare(strict_types=1);

namespace App\GRPC\Interceptor;

use Spiral\Core\CoreInterceptorInterface;
use Spiral\Core\CoreInterface;
use Spiral\Exceptions\ExceptionReporterInterface;
use Spiral\RoadRunner\GRPC\Exception\GRPCException;
use Spiral\RoadRunner\GRPC\Exception\GRPCExceptionInterface;

final class ExceptionHandlerInterceptor implements CoreInterceptorInterface
{
    public function __construct(
        private readonly ExceptionReporterInterface $reporter
    ) {
    }

    public function process(string $controller, string $action, array $parameters, CoreInterface $core): mixed
    {
        try {
            \assert($parameters['request'] instanceof RequestInterface);

            return $core->callAction($controller, $action, $parameters);
        } catch (\Throwable $e) {
            $this->reporter->report($e);

            $request->error($e->getCode(), $e->getMessage());
        }
    }
}

After that, you don't need to code into try/catch blocks in your services:

php
/**
 * @param Connect $request
 */
public function handle(RequestInterface $request): void
{
    $request->respond(
        new ConnectResponse(
            user: (string) $request->getAttribute('user_id'),
        )
    );
}

Registering interceptors

To use this interceptor, you will need to register them in the configuration file app/config/centrifugo.php.

php
<?php

declare(strict_types=1);

use RoadRunner\Centrifugo\Request\RequestType;
use App\Centrifuge;

return [
    'services' => [
        //...
    ],
    'interceptors' => [
        RequestType::Connect->value => [
            Centrifuge\Interceptor\AuthInterceptor::class,
        ],
        //...
        '*' => [
            Centrifuge\Interceptor\ExceptionHandlerInterceptor::class,
            Centrifuge\Interceptor\TelemetryInterceptor::class,
        ],
    ],
];

You can register interceptors for specific requests or for all requests using the * wildcard.