rasuvaeff / yii3-correlation-id
Request correlation ID for Yii3: PSR-15 middleware, request-scoped holder, and yiisoft/log context provider
Requires
- php: 8.3 - 8.5
- psr/http-message: ^2.0
- psr/http-server-handler: ^1.0
- psr/http-server-middleware: ^1.0
- yiisoft/log: ^2.1
Requires (Dev)
- ergebnis/composer-normalize: ^2.51
- friendsofphp/php-cs-fixer: ^3.95
- infection/infection: ^0.33
- maglnet/composer-require-checker: ^4.17
- nyholm/psr7: ^1.8
- rasuvaeff/property-testing: ^2.1
- rector/rector: ^2.4
- roave/backward-compatibility-check: ^8.0
- testo/bridge-infection: ^0.1.6
- testo/testo: ^0.10.25
- vimeo/psalm: ^6.16
- yiisoft/di: ^1.4
Suggests
- rasuvaeff/yii3-api-problem: Fills the ProblemDetails instance field with the correlation ID
This package is auto-updated.
Last update: 2026-07-16 19:49:46 UTC
README
Request correlation ID for Yii3: PSR-15 middleware, request-scoped holder, and yiisoft/log context provider. Every request gets an ID, every log line carries it, and the client gets it back in the response header.
Using an AI coding assistant? llms.txt contains a compact API reference you can share with the model.
Requirements
- PHP 8.3+
psr/http-message^2.0,psr/http-server-middleware^1.0yiisoft/log^2.1 (2.1.0 introduced theContextProviderAPI)
Installation
composer require rasuvaeff/yii3-correlation-id
Usage
Put CorrelationIdMiddleware first in the stack — everything downstream that
logs should already see the ID.
use Rasuvaeff\Yii3CorrelationId\CorrelationIdHolder; use Rasuvaeff\Yii3CorrelationId\CorrelationIdMiddleware; use Rasuvaeff\Yii3CorrelationId\Uuidv4Generator; $middleware = new CorrelationIdMiddleware( generator: new Uuidv4Generator(), holder: new CorrelationIdHolder(), );
For each request the middleware:
- Reads
X-Request-IDand reuses the value if it is acceptable. - Generates a UUIDv4 otherwise.
- Publishes the ID as the
correlationIdrequest attribute. - Publishes the ID in
CorrelationIdHolder. - Clears the holder in a
finallyblock. - Sets
X-Request-IDon the response.
Under yiisoft/config the bundled config/di.php wires all of this from
params.php, so the middleware only needs adding to your middleware stack.
For a Yii3 application, put the container ID first in the web runner's middleware list (the exact filename depends on the application template):
// config/web.php or config/common/middleware.php use Rasuvaeff\Yii3CorrelationId\CorrelationIdMiddleware; use Yiisoft\ErrorHandler\Middleware\ErrorCatcher; use Yiisoft\Router\Middleware\Router; return [ CorrelationIdMiddleware::class, ErrorCatcher::class, Router::class, ];
Override package defaults in the application params layer, without copying the vendor DI definitions:
// config/common/params.php use Rasuvaeff\Yii3CorrelationId\CorrelationIdMiddleware; return [ 'rasuvaeff/yii3-correlation-id' => [ 'headerName' => 'X-Request-ID', 'attributeName' => 'correlationId', 'acceptIncoming' => false, // public ingress mints its own ID 'validationPattern' => CorrelationIdMiddleware::UUID_V4_PATTERN, 'maxLength' => 128, 'contextKey' => 'requestId', ], ];
Reading the ID
Anything holding the request reads the attribute; application services inject
the read-only CorrelationIdProvider:
use Rasuvaeff\Yii3CorrelationId\CorrelationIdProvider; $id = $request->getAttribute('correlationId'); final readonly class OrderService { public function __construct( private CorrelationIdProvider $correlationId, ) {} } $id = $correlationId->get(); // throws outside a correlation scope $id = $correlationId->tryGet(); // null outside a correlation scope
CorrelationIdHolder must stay a single shared instance — the middleware
writes it and everything else reads it. The yiisoft/di container does this by
autowiring. The package aliases CorrelationIdProvider to that same instance;
application services should not depend on the holder's mutation methods.
Queue and console scopes
Queue consumers and console commands can establish an explicit scope without
manual cleanup. runWith() restores the previous ID in finally, including
for nested scopes and exceptions:
$result = $holder->runWith( id: $message->correlationId, callback: fn () => $consumer->handle($message), );
Scope IDs come from trusted application infrastructure and bypass the HTTP
validation settings. Do not pass arbitrary user input to runWith().
Outgoing HTTP requests
CorrelationIdHeaderInjector propagates the current ID into an outgoing PSR-7
request. It replaces a stale header rather than appending another value and is
a no-op outside a correlation scope:
$request = $injector->inject($request); $response = $httpClient->sendRequest($request);
The bundled DI config uses the same headerName as the server middleware. See
examples/06-outgoing-request.php.
Log context
CorrelationIdContextProvider puts requestId into the context of every
message logged through Yiisoft\Log\Logger. The logger takes exactly one
context provider, so compose yours with the logger's own SystemContextProvider
— dropping it would lose time, category and trace from every message:
// config/common/di/logger.php use Psr\Log\LoggerInterface; use Rasuvaeff\Yii3CorrelationId\CorrelationIdContextProvider; use Yiisoft\Log\ContextProvider\CompositeContextProvider; use Yiisoft\Log\ContextProvider\SystemContextProvider; use Yiisoft\Log\Logger; use Yiisoft\Log\StreamTarget; return [ LoggerInterface::class => static fn ( CorrelationIdContextProvider $correlationId, ): LoggerInterface => new Logger( [new StreamTarget()], new CompositeContextProvider(new SystemContextProvider(), $correlationId), ), ];
ContextProviderInterface belongs to yiisoft/log, so this package never binds
it — composing providers is the application's call.
Outside a request (console command, worker bootstrap) no ID is set, the context provider returns an empty array, and logging keeps working.
Configuration
params.php, under the rasuvaeff/yii3-correlation-id key:
| Param | Default | Meaning |
|---|---|---|
headerName |
X-Request-ID |
Read from the request, written to the response |
attributeName |
correlationId |
Request attribute carrying the ID |
acceptIncoming |
true |
Reuse acceptable caller IDs; use false at a public trust boundary that mints IDs |
validationPattern |
UUIDv4 regex | Invalid incoming IDs are replaced; invalid generated IDs are rejected |
maxLength |
128 |
Longer incoming IDs are replaced; longer generated IDs are rejected |
contextKey |
requestId |
Log context key |
A custom ID format needs a generator, matching pattern, and sufficient maximum
length. A generated value outside that contract throws UnexpectedValueException
before the request handler runs. See
examples/04-custom-generator.php.
Incoming trust policy
After format and length validation, an IncomingCorrelationIdPolicy may reject
an otherwise valid ID based on request context. Rejection generates a fresh ID.
Bind the policy in application DI:
use Psr\Http\Message\ServerRequestInterface; use Rasuvaeff\Yii3CorrelationId\IncomingCorrelationIdPolicy; final readonly class TrustedProxyPolicy implements IncomingCorrelationIdPolicy { #[\Override] public function accepts(ServerRequestInterface $request, string $id): bool { return in_array( $request->getServerParams()['REMOTE_ADDR'] ?? null, ['10.0.0.10', '10.0.0.11'], true, ); } } return [ IncomingCorrelationIdPolicy::class => TrustedProxyPolicy::class, ];
For manual construction, pass it as the named incomingPolicy argument.
acceptIncoming: false remains the hard off switch: it skips the policy and
always mints a new ID. See
examples/07-trusted-proxy-policy.php.
Public API
| Class | Description |
|---|---|
CorrelationIdMiddleware |
PSR-15 middleware: resolve, publish, echo back |
CorrelationIdProvider |
Read-only get/tryGet access for application services |
CorrelationIdHolder |
Mutable infrastructure holder with set-once operations and runWith() scopes |
CorrelationIdGenerator |
Interface for ID generation |
Uuidv4Generator |
Pure-PHP RFC 4122 v4 UUIDs from random_bytes() |
CorrelationIdContextProvider |
yiisoft/log context provider adding requestId |
CorrelationIdHeaderInjector |
Adds the current ID to outgoing PSR-7 requests |
IncomingCorrelationIdPolicy |
Request-aware trust decision for valid incoming IDs |
AcceptAllIncomingCorrelationIdPolicy |
Default policy used when none is supplied |
Exception\CorrelationIdNotSetException |
Thrown by CorrelationIdHolder::get() outside a request |
When to use this instead of yii3-telemetry
yii3-correlation-id |
yii3-telemetry |
|
|---|---|---|
| Scope | One service: correlate its own logs | Distributed tracing across services |
| Model | One ID per request | Spans, parent/child, sampling |
| Propagation | X-Request-ID header |
W3C Trace Context, OTLP export |
| Cost | Middleware + holder, no exporter | Collector, exporter, sampling config |
Both can run together: place this middleware first and read tryGet() into a
span attribute. Do not expect this package to grow traceparent support — that
is what telemetry is for.
Integration recipes
Keep optional package glue in the application. For yii3-audit-log, take the
ID from the provider rather than rereading an untrusted request header:
use Rasuvaeff\Yii3AuditLog\AuditMetadata; $metadata = new AuditMetadata( requestId: $correlationId->tryGet(), ip: $request->getServerParams()['REMOTE_ADDR'] ?? null, userAgent: $request->getHeaderLine('User-Agent'), );
For yii3-telemetry, add it to the currently active span from code running
downstream of both middleware:
$id = $correlationId->tryGet(); if ($id !== null) { $tracer->currentSpan()->setAttribute('request.id', $id); }
Security
| Risk | What the package does |
|---|---|
| Header injection | A conforming PSR-7 implementation already rejects CRLF in a header value; the validation pattern additionally rejects anything that is not a well-formed ID, including content smuggled after a space or a tab |
| Oversized header | maxLength (default 128) rejects long values before the pattern runs |
| Client-spoofed ID | Set acceptIncoming: false at the public gateway; internal services accept that trusted ID and must not be directly reachable by clients |
| Log injection | Both incoming and generated IDs must pass the validation pattern and length limit before reaching the holder or log context |
| Info leak | A request ID carries no user data. UUIDv4 is unguessable but is not a secret — never use it for authorization |
Browser access. CORS does not expose custom response headers to JavaScript by default. When a browser client must include the ID in a support report, configure the application's CORS middleware to send:
Access-Control-Expose-Headers: X-Request-ID
Use the configured custom header name instead when headerName is changed.
Concurrency. The holder is one shared instance cleared in a finally block,
which is correct for sequential request handling: PHP-FPM, and workers that take
one request at a time (RoadRunner). Under coroutine concurrency (Swoole), where
several requests share a worker's memory at once, a shared holder would leak IDs
between them — this package does not support that model.
Examples
See examples/ for runnable scripts. Examples are expected to execute without fatal errors and stay aligned with the documented public API.
| Script | Shows | Needs server? |
|---|---|---|
| 01-middleware-setup.php | Middleware in a PSR-15 stack: generate / reuse / replace | no |
| 02-log-context.php | yiisoft/log + context provider: requestId on every line |
no |
| 03-access-in-action.php | Reading the ID from the attribute and from the holder | no |
| 04-custom-generator.php | ULID-like generator with a matching validation pattern | no |
| 05-gateway-mode.php | Public gateway replaces an untrusted ID, internal service preserves the gateway ID | no |
| 06-outgoing-request.php | Queue scope and outgoing PSR-7 header propagation | no |
| 07-trusted-proxy-policy.php | Accept a valid incoming ID only from a trusted gateway IP | no |
Development
No PHP/Composer on the host — run in Docker via the composer:2 image:
docker run --rm -v "$PWD":/app -w /app composer:2 composer install docker run --rm -v "$PWD":/app -w /app composer:2 composer build docker run --rm -v "$PWD":/app -w /app composer:2 composer cs:fix docker run --rm -v "$PWD":/app -w /app composer:2 composer test docker run --rm -v "$PWD":/app -w /app composer:2 composer release-check
Or with Make:
make install
make build
make cs-fix
make test
make test-coverage
make mutation
make release-check
make test-coverage and make mutation bootstrap pcov inside the
composer:2 container because the base image has no coverage driver.