Cookbook

Provide a unique ID in your request

The problem

You want a request-specific identifier via an HTTP request header for purposes of logging, tracking, etc. If you add it via middleware, however, it is not present in your access logs.

Request identifiers are usually generated at the web-server level. When you use dedicated web servers such as Apache or nginx, a load balancer, or a reverse proxy, these can be configured to create and inject a request ID before it reaches your application. However, when using mezzio-swoole, the request handler runner we create is your web server. It has listeners that take care of logging, which means that the request generated must already have the identifier if you want to be able to log it.

This poses a problem: normally you will use middleware to propagate changes to the request. How can you do it at the Swoole web server level?

The solution

The answer is to provide a delegator factory on the service that converts the Swoole HTTP request instance into the equivalent PSR-7 HTTP request instance that is then passed to your application.

mezzio-swoole maps the Psr\Http\Message\ServerRequestInterface service to its Mezzio\Swoole\ServerRequestSwooleFactory. That factory returns a callable that accepts a Swoole\HTTP\Request instance and returns a Psr\Http\Message\ServerRequestInterface instance. As such, your delegator factory will need to return a callable with the same signature.

In the following example, we use ramsey/uuid to generate a unique request ID, and add it to an X-Request-ID header when returning the request.

// In your App module's top-level source directory

declare(strict_types=1);

namespace App;

use Psr\Container\ContainerInterface;
use Psr\Http\Message\ServerRequestInterface;
use Ramsey\Uuid\Uuid;
use Swoole\Http\Request as SwooleHttpRequest;

class ServerRequestIdDecorator
{
    public function __invoke(ContainerInterface $container, string $serviceName, callable $factory): callable
    {
        return static fn (SwooleHttpRequest $swooleRequest): ServerRequestInterface => $factory($swooleRequest)
            ->withHeader('X-Request-ID', Uuid::uuid1());
    }
}

Then, in our App module's ConfigProvider, we would modify the dependency configuration to add the following:

    public function getDependencies(): array
    {
        return [
            'delegators' => [
                \Psr\Http\Message\ServerRequestInterface::class => [
                    ServerRequestIdDecorator::class,
                ],
            ],
        ];
    }

This approach:

  • Keeps the logic close to the web server.
  • Utilizes facilities already built-in to Mezzio and mezzio-swoole.
  • Allows other code to perform similar work in order to manipulate and modify the request.

Alternate solution

Info

Available since 4.3.0

As of mezzio-swoole 4.3.0, you can now define and consume a Laminas\Diactoros\ServerRequestFilter\FilterServerRequestInterface service to modify the ServerRequestInterface generated when a request arrives. This can be done in one of two ways, depending on whether or not you explicitly define the FilterServerRequestInterface service.

Without a defined FilterServerRequestInterface

If you have not defined the FilterServerRequestInterface you will do so now. First, create a factory class; we will do so here in src/App/FilterAddingRequestId.php:

<?php

declare(strict_types=1);

namespace App;

use Laminas\Diactoros\ServerRequestFilter\FilterServerRequestInterface;
use Ramsey\Uuid\Uuid;
use Psr\Http\Message\ServerRequestInterface;

class FilterAddingRequestId implements FilterServerRequestInterface
{
    public function __invoke(ServerRequestInterface $request): ServerRequestInterface
    {
        return $request->withHeader('X-Request_ID', Uuid::uuid1());
    }
}

Next, configure the FilterServerRequestInterface service to use this filter:

return [
    'dependencies' => [
        'invokables' => [
            \Laminas\Diactoros\ServerRequestFilter\FilterServerRequestInterface::class =>
                \App\FilterAddingRequestId::class,
        ],
    ],
];

At this point, any generated requests will now have a unique request ID on creation.

With a defined FilterServerRequestInterface

If you have already defined the FilterServerRequestInterface service, you will now need to decorate it to ensure you can add your request identifier. We will do that via a delegator factory and an anonymous class. Create the class file src/App/FilterAddingRequestIdDecorator.php with the following contents:

<?php

declare(strict_types=1);

namespace App;

use Laminas\Diactoros\ServerRequestFilter\FilterServerRequestInterface;
use Ramsey\Uuid\Uuid;
use Psr\Container\ContainerInterface
use Psr\Http\Message\ServerRequestInterface;

class FilterAddingRequestIdDecorator
{
    public function __invoke(
        ContainerInterface $container,
        string $serviceName,
        callable $factory
    ): FilterServerRequestInterface {
        $primaryFilter = $factory();

        return new class($primaryFilter) implements FilterServerRequestInterface {
            private FilterServerRequestInterface $primaryFilter;

            public function __construct(FilterServerRequestInterface $primaryFilter)
            {
                $this->primaryFilter = $primaryFilter;
            }

            public function __invoke(ServerRequestInterface $request): ServerRequestInterface
            {
                return ($this->primaryFilter)($request)
                    ->withHeader('X-Request_ID', Uuid::uuid1());
            }
        };
    }
}

Next, register the delegator factory against the FilterServerRequestInterface:

return [
    'dependencies' => [
        'delegators' => [
            \Laminas\Diactoros\ServerRequestFilter\FilterServerRequestInterface::class => [
                \App\FilterAddingRequestIdDecorator::class,
            ],
        ],
    ],
];