Skip to content

Commit

Permalink
Add source files
Browse files Browse the repository at this point in the history
  • Loading branch information
frankdekker committed Nov 27, 2023
1 parent 5deb40a commit 5772752
Show file tree
Hide file tree
Showing 13 changed files with 399 additions and 0 deletions.
4 changes: 4 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
},
"require": {
"php": ">=8.1",
"symfony/config": " ^6.3",
"symfony/dependency-injection": " ^6.3",
"symfony/http-foundation": " ^6.3",
"symfony/http-kernel": " ^6.3",
"symfony/framework-bundle": "^6.3"
},
"require-dev": {
Expand Down
Empty file removed src/.gitkeep
Empty file.
56 changes: 56 additions & 0 deletions src/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

declare(strict_types=1);

namespace DR\SymfonyRequestId\DependencyInjection;

use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;

/**
* @internal
*/
class Configuration implements ConfigurationInterface
{
public function getConfigTreeBuilder(): TreeBuilder
{
$tree = new TreeBuilder('symfony_request_id');

$tree->getRootNode()
->children()
->scalarNode('request_header')
->cannotBeEmpty()
->defaultValue('X-Request-Id')
->info('The header in which the bundle will look for and set request IDs')
->end()
->booleanNode('trust_request_header')
->defaultValue(true)
->info("Whether or not to trust the incoming request's `Request-Id` header as a real ID")
->end()
->scalarNode('response_header')
->cannotBeEmpty()
->defaultValue('X-Request-Id')
->info('The header the bundle will set the request ID at in the response')
->end()
->scalarNode('storage_service')
->info('The service name for request ID storage. Defaults to `SimpleIdStorage`')
->end()
->scalarNode('generator_service')
->info('The service name for the request ID generator. Defaults to `symfony/uid` or `ramsey/uuid`')
->end()
->booleanNode('enable_monolog')
->info('Whether or not to turn on the request ID processor for monolog')
->defaultTrue()
->end()
->booleanNode('enable_console')
->info('Whether or not to turn on the request ID processor for monolog')
->defaultTrue()
->end()
->booleanNode('enable_twig')
->info('Whether or not to enable the twig `request_id()` function. Only works if TwigBundle is present.')
->defaultTrue()
->end();

return $tree;
}
}
78 changes: 78 additions & 0 deletions src/DependencyInjection/SymfonyRequestIdExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?php

declare(strict_types=1);

namespace DR\SymfonyRequestId\DependencyInjection;

use Ramsey\Uuid\UuidFactory;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\HttpKernel\DependencyInjection\ConfigurableExtension;
use DR\SymfonyRequestId\SimpleIdStorage;
use DR\SymfonyRequestId\RequestIdStorage;
use DR\SymfonyRequestId\RequestIdGenerator;
use DR\SymfonyRequestId\Generator\RamseyUuid4Generator;
use DR\SymfonyRequestId\EventListener\RequestIdListener;
use DR\SymfonyRequestId\Monolog\RequestIdProcessor;
use DR\SymfonyRequestId\Twig\RequestIdExtension;

/**
* Registers some container configuration with the application.
* @internal
*/
final class SymfonyRequestIdExtension extends ConfigurableExtension
{
/**
* @param array{
* request_header: string,
* trust_request_header: bool,
* response_header: string,
* storage_service: ?string,
* generator_service: ?string,
* enable_monolog: bool,
* enable_console: bool,
* enable_twig: bool,
* } $config
*/
protected function loadInternal(array $config, ContainerBuilder $container) : void
{
$container->register(SimpleIdStorage::class)
->setPublic(false);
$container->register(RamseyUuid4Generator::class)
->setPublic(false);

$storeId = empty($config['storage_service']) ? SimpleIdStorage::class : $config['storage_service'];
$genId = empty($config['generator_service']) ? RamseyUuid4Generator::class : $config['generator_service'];

$container->setAlias(RequestIdStorage::class, $storeId)
->setPublic(true);
$container->setAlias(RequestIdGenerator::class, $genId)
->setPublic(true);

$container->register(RequestIdListener::class)
->setArguments([
$config['request_header'],
$config['response_header'],
$config['trust_request_header'],
new Reference($storeId),
new Reference($genId),
])
->setPublic(false)
->addTag('kernel.event_subscriber');

if (!empty($config['enable_monolog'])) {
$container->register(RequestIdProcessor::class)
->addArgument(new Reference($storeId))
->setPublic(false)
->addTag('monolog.processor');
}

if (class_exists('Twig\Extension\AbstractExtension') && !empty($config['enable_twig'])) {
$container->register(RequestIdExtension::class)
->addArgument(new Reference($storeId))
->setPublic(false)
->addTag('twig.extension');
}
}
}
86 changes: 86 additions & 0 deletions src/EventListener/RequestIdListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<?php

declare(strict_types=1);

namespace DR\SymfonyRequestId\EventListener;

use DR\SymfonyRequestId\RequestIdGenerator;
use DR\SymfonyRequestId\RequestIdStorage;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;

/**
* Listens for requests and responses and sets up the request ID on each.
* @internal
*/
final class RequestIdListener implements EventSubscriberInterface
{
/**
* @param string $requestHeader The header to inspect for the incoming request ID.
* @param string $responseHeader The header that will contain the request ID in the response.
* @param bool $trustRequest Trust the value from the request? Or generate?
* @param RequestIdStorage $idStorage The request ID storage, used to store the ID from the request or a newly generated ID.
* @param RequestIdGenerator $idGenerator Used to generate a request ID if one isn't present.
*/
public function __construct(
private readonly string $requestHeader,
private readonly string $responseHeader,
private readonly bool $trustRequest,
private readonly RequestIdStorage $idStorage,
private readonly RequestIdGenerator $idGenerator
) {
}

/**
* @inheritDoc
*/
public static function getSubscribedEvents(): array
{
return [
KernelEvents::REQUEST => ['onRequest', 100],
KernelEvents::RESPONSE => ['onResponse', -99],
];
}

public function onRequest(RequestEvent $event): void
{
if ($event->isMainRequest() === false) {
return;
}

$req = $event->getRequest();

// always give the incoming request priority. If it has the ID in
// its headers already put that into our ID storage.
if ($this->trustRequest && $req->headers->get($this->requestHeader) !== null) {
$this->idStorage->setRequestId($req->headers->get($this->requestHeader));

return;
}

// similarly, if the request ID storage already has an ID set we
// don't need to do anything other than put it into the request headers
if ($this->idStorage->getRequestId() !== null) {
$req->headers->set($this->requestHeader, $this->idStorage->getRequestId());

return;
}

$id = $this->idGenerator->generate();
$req->headers->set($this->requestHeader, $id);
$this->idStorage->setRequestId($id);
}

public function onResponse(ResponseEvent $event): void
{
if ($event->isMainRequest() === false) {
return;
}

if ($this->idStorage->getRequestId() !== null) {
$event->getResponse()->headers->set($this->responseHeader, $this->idStorage->getRequestId());
}
}
}
24 changes: 24 additions & 0 deletions src/Generator/RamseyUuid4Generator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

namespace DR\SymfonyRequestId\Generator;

use DR\SymfonyRequestId\RequestIdGenerator;
use Ramsey\Uuid\UuidFactory;
use Ramsey\Uuid\UuidFactoryInterface;

/**
* Uses `ramsey/uuid` to generator v4 UUIDs for request ids.
*/
final class RamseyUuid4Generator implements RequestIdGenerator
{
public function __construct(private readonly UuidFactoryInterface $factory = new UuidFactory())
{
}

public function generate(): string
{
return (string)$this->factory->uuid4();
}
}
24 changes: 24 additions & 0 deletions src/Generator/SymfonyUuid4Generator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

namespace DR\SymfonyRequestId\Generator;

use DR\SymfonyRequestId\RequestIdGenerator;
use Symfony\Component\Uid\Factory\UuidFactory;
use Symfony\Component\Uid\UuidV4;

/**
* Uses symfony/uid to generate a UUIDv4 request ID.
*/
final class SymfonyUuid4Generator implements RequestIdGenerator
{
public function __construct(private readonly UuidFactory $factory = new UuidFactory(UuidV4::class, UuidV4::class, UuidV4::class, UuidV4::class))
{
}

public function generate(): string
{
return (string)$this->factory->create();
}
}
30 changes: 30 additions & 0 deletions src/Monolog/RequestIdProcessor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

namespace DR\SymfonyRequestId\Monolog;

use DR\SymfonyRequestId\RequestIdStorage;
use Monolog\LogRecord;
use Monolog\Processor\ProcessorInterface;

/**
* Adds the request ID to the Monolog record's `extra` key, so it can be used in formatters, etc.
* @internal
*/
final class RequestIdProcessor implements ProcessorInterface
{
public function __construct(private readonly RequestIdStorage $storage)
{
}

public function __invoke(LogRecord $record): LogRecord
{
$id = $this->storage->getRequestId();
if ($id !== null) {
$record->extra['request_id'] = $id;
}

return $record;
}
}
12 changes: 12 additions & 0 deletions src/RequestIdBundle.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

declare(strict_types=1);

namespace DR\SymfonyRequestId;

use Symfony\Component\HttpKernel\Bundle\Bundle;

final class RequestIdBundle extends Bundle
{
// noop
}
17 changes: 17 additions & 0 deletions src/RequestIdGenerator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

declare(strict_types=1);

namespace DR\SymfonyRequestId;

/**
* Generates new (hopefully) unique request ID's for incoming requests if they
* lack an ID.
*/
interface RequestIdGenerator
{
/**
* Create a new request ID.
*/
public function generate(): string;
}
18 changes: 18 additions & 0 deletions src/RequestIdStorage.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

namespace DR\SymfonyRequestId;

/**
* Stores the identifiers for the request.
*/
interface RequestIdStorage
{
/**
* @return string|null Null if the request does not have an identifier
*/
public function getRequestId() : ?string;

public function setRequestId(?string $id) : void;
}
22 changes: 22 additions & 0 deletions src/SimpleIdStorage.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);

namespace DR\SymfonyRequestId;

/**
* And ID storage backed by a property, simple.
*/
final class SimpleIdStorage implements RequestIdStorage
{
private ?string $requestId = null;

public function getRequestId(): ?string
{
return $this->requestId;
}

public function setRequestId(?string $id): void
{
$this->requestId = $id;
}
}
Loading

0 comments on commit 5772752

Please sign in to comment.