PHPackages                             rasuvaeff/yii3-correlation-id - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Logging &amp; Monitoring](/categories/logging)
4. /
5. rasuvaeff/yii3-correlation-id

ActiveLibrary[Logging &amp; Monitoring](/categories/logging)

rasuvaeff/yii3-correlation-id
=============================

Request correlation ID for Yii3: PSR-15 middleware, request-scoped holder, and yiisoft/log context provider

v1.0.0(1mo ago)01↓66.7%BSD-3-ClausePHPPHP 8.3 - 8.5CI passing

Since Jul 16Pushed 1w agoCompare

[ Source](https://github.com/rasuvaeff/yii3-correlation-id)[ Packagist](https://packagist.org/packages/rasuvaeff/yii3-correlation-id)[ Docs](https://github.com/rasuvaeff/yii3-correlation-id)[ RSS](/packages/rasuvaeff-yii3-correlation-id/feed)WikiDiscussions master Synced 1w ago

READMEChangelog (3)Dependencies (16)Versions (3)Used By (0)

rasuvaeff/yii3-correlation-id
=============================

[](#rasuvaeffyii3-correlation-id)

[![Latest Stable Version](https://camo.githubusercontent.com/5d9f258fb767c0aefbcf9402fe2d848f623fec21abc78b1f0f3a401335ada4be/68747470733a2f2f706f7365722e707567782e6f72672f7261737576616566662f796969332d636f7272656c6174696f6e2d69642f76)](https://packagist.org/packages/rasuvaeff/yii3-correlation-id)[![Total Downloads](https://camo.githubusercontent.com/e74f9bc869cdb0d6ed0659fec3cf785d1af6567f4f7ef43a732d9362f3d6a247/68747470733a2f2f706f7365722e707567782e6f72672f7261737576616566662f796969332d636f7272656c6174696f6e2d69642f646f776e6c6f616473)](https://packagist.org/packages/rasuvaeff/yii3-correlation-id)[![Build](https://github.com/rasuvaeff/yii3-correlation-id/actions/workflows/build.yml/badge.svg)](https://github.com/rasuvaeff/yii3-correlation-id/actions/workflows/build.yml)[![Static analysis](https://github.com/rasuvaeff/yii3-correlation-id/actions/workflows/static-analysis.yml/badge.svg)](https://github.com/rasuvaeff/yii3-correlation-id/actions/workflows/static-analysis.yml)[![Psalm level](https://camo.githubusercontent.com/68f7f31799f2b93c710b14ba3877072e7fe07ec9d7cee3fdf67e14beab3e1b6f/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f7073616c6d2d6c6576656c5f312d626c75652e737667)](https://github.com/rasuvaeff/yii3-correlation-id/actions/workflows/static-analysis.yml)[![PHP](https://camo.githubusercontent.com/42bfb78904562afee4f8be75982dd11b2479c069d3200ba436f2ea1ddec2bffb/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f646570656e64656e63792d762f7261737576616566662f796969332d636f7272656c6174696f6e2d69642f706870)](https://packagist.org/packages/rasuvaeff/yii3-correlation-id)[![License](https://camo.githubusercontent.com/6cb285b57819f8de0acfb34923298f4f569f962544e8fe35331da2d163f4e485/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4253442d2d332d2d436c617573652d626c75652e737667)](LICENSE.md)[Русская версия](README.ru.md)

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](llms.txt) contains a compact API reference you can share with the model.

Requirements
------------

[](#requirements)

- PHP 8.3+
- `psr/http-message` ^2.0, `psr/http-server-middleware` ^1.0
- `yiisoft/log` ^2.1 (2.1.0 introduced the `ContextProvider` API)

Installation
------------

[](#installation)

```
composer require rasuvaeff/yii3-correlation-id
```

Usage
-----

[](#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:

1. Adopts the ID an outer instance of itself already published in the `correlationId` request attribute, if there is one.
2. Otherwise reads `X-Request-ID` and reuses the value if it is acceptable — only when configured with `acceptIncoming: true`, which is not the default.
3. Generates a UUIDv4 otherwise.
4. Publishes the ID as the `correlationId` request attribute.
5. Publishes the ID in `CorrelationIdHolder`, replacing whatever was there.
6. Clears the holder in a `finally` block — unless it adopted the ID at step 1, in which case the outer instance owns the cleanup.
7. Sets `X-Request-ID` on 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, // the default: the caller does not choose this service's ID
        'validationPattern' => CorrelationIdMiddleware::UUID_V4_PATTERN,
        'maxLength' => 128,
        'contextKey' => 'requestId',
    ],
];
```

### Reading the ID

[](#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.

The middleware **owns** the request scope: it overwrites whatever the holder held and clears it in `finally`. A stray ID — left by worker bootstrap or by a handler that called `exit()` — is dropped on the next request instead of failing every request that worker will ever handle again. `set()` keeps its set-once contract for application and queue code, where a second write really is a mistake.

### Registered twice

[](#registered-twice)

A second instance further down the stack — the middleware added twice, a module that ships its own copy — **adopts** the ID the outer one already published in the request attribute. It does not read the incoming header again, does not mint a rival ID, and does not clear the holder on the way out, since the scope belongs to the outer instance. Without that, one request would carry two IDs: the inner one in the logs and the handler, the outer one in the response header.

It also keeps `acceptIncoming: false` meaningful: the caller's header is still on the request after the outer instance decided to ignore it, and an inner instance configured with `acceptIncoming: true` would otherwise read it right back.

The attribute is adopted only after passing the same control-character, `maxLength` and `validationPattern` checks as an incoming header, so unrelated code writing that attribute (a route parameter of the same name) cannot decide the correlation ID.

### Queue and console scopes

[](#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),
);
```

`set()`, `override()` and `runWith()` each validate their argument and throw `InvalidArgumentException` for an ID that is empty, longer than 4096 bytes, or carrying a control character (`\x00`-`\x1F`, `\x7F`). That matters most here: `$message->correlationId` above usually started life as an untrusted HTTP header on the service that enqueued the job, and from the holder it reaches every log line and every outgoing request header verbatim.

Validation happens before the holder's state is touched, so a rejected call leaves the current scope exactly as it was and the callback never runs. The 4096-byte ceiling is a sanity limit against log bloat, not a format check — it sits far above the middleware's `maxLength` default of 128, so a custom format configured on the middleware is never rejected by the holder afterwards.

The holder's guarantee is deliberately minimal: it is not the HTTP validation contract. If a scope ID must match a specific format, check it against `CorrelationIdMiddleware::UUID_V4_PATTERN` (or your own pattern) before calling `runWith()`.

### Outgoing HTTP requests

[](#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](examples/06-outgoing-request.php).

### Log context

[](#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

[](#configuration)

`params.php`, under the `rasuvaeff/yii3-correlation-id` key:

ParamDefaultMeaning`headerName``X-Request-ID`Read from the request, written to the response`attributeName``correlationId`Request attribute carrying the ID; also how a nested instance recognises the outer one's scope`acceptIncoming``false`Ignore the caller's ID and mint one here. Set it to `true` only on a service that direct client traffic cannot reach, to keep the ID propagating across hops`validationPattern`UUIDv4 regexInvalid 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 keyA 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](examples/04-custom-generator.php).

Control characters (`\x00`-`\x1F`, `\x7F`) are rejected before `validationPattern` runs, so a permissive custom pattern cannot let an ANSI escape, a NUL byte, or a smuggled newline reach the holder, the logs, or an outgoing header.

### Trusting the caller's ID

[](#trusting-the-callers-id)

`acceptIncoming` defaults to `false`. A middleware that has not been told where it sits assumes a public trust boundary and mints its own ID, so a caller cannot choose what this service's logs are keyed by, nor make two unrelated requests share one ID.

Set it to `true` on a service that direct client traffic cannot reach, where the forwarded ID keeps the two services' logs correlated:

```
// config/common/params.php — internal service behind a gateway
return [
    'rasuvaeff/yii3-correlation-id' => [
        'acceptIncoming' => true,
    ],
];
```

Prior to 2.0.0 the default was `true`; see [UPGRADE.md](UPGRADE.md).

### The UUID constant

[](#the-uuid-constant)

`CorrelationIdMiddleware::UUID_V4_PATTERN` is the default `validationPattern`and is safe to reuse on its own — validating a queue message's correlation id before `runWith()`, checking an ID read back from a database:

```
if (preg_match(CorrelationIdMiddleware::UUID_V4_PATTERN, $id) !== 1) {
    $id = $generator->generate();
}
```

It is anchored with `\z`, which matches only at the very end of the subject. Up to 1.0.1 it was anchored with `$`, which PCRE also matches before a single trailing `\n` — so the old value returned `1` for `"\n"` when used this way. Middleware behaviour never differed: the control-character guard rejects a trailing newline before any pattern runs.

### Incoming trust policy

[](#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` — the default — is the hard off switch: it skips the policy and always mints a new ID, so a policy is only ever consulted on a middleware configured with `acceptIncoming: true`. See [examples/07-trusted-proxy-policy.php](examples/07-trusted-proxy-policy.php).

### Public API

[](#public-api)

ClassDescription`CorrelationIdMiddleware`PSR-15 middleware: resolve, publish, echo back`CorrelationIdProvider`Read-only `get`/`tryGet` access for application services`CorrelationIdHolder`Mutable infrastructure holder: set-once `set()`, unconditional `override()`, and `runWith()` scopes. Every write validates the ID`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 requestWhen to use this instead of yii3-telemetry
------------------------------------------

[](#when-to-use-this-instead-of-yii3-telemetry)

`yii3-correlation-id``yii3-telemetry`ScopeOne service: correlate its own logsDistributed tracing across servicesModelOne ID per requestSpans, parent/child, samplingPropagation`X-Request-ID` headerW3C Trace Context, OTLP exportCostMiddleware + holder, no exporterCollector, exporter, sampling configBoth 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

[](#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
--------

[](#security)

RiskWhat the package doesHeader injectionControl characters (`\x00`-`\x1F`, `\x7F`) are rejected unconditionally, before `validationPattern`, so a permissive custom pattern stays safe; the pattern then rejects anything that is not a well-formed ID, including content smuggled after a spaceOversized header`maxLength` (default 128) rejects long values before the pattern runsClient-spoofed ID`acceptIncoming` defaults to `false`, so a caller's ID is ignored unless the service explicitly opts in. Opt in only on internal services that direct client traffic cannot reachLog injectionBoth incoming and generated IDs must pass the control-character guard, the validation pattern, and the length limit before reaching the holder or log context. The guard runs first, so the anchor of `validationPattern` cannot weaken the middleware. `CorrelationIdHolder` applies its own control-character and length guard to `set()`, `override()` and `runWith()`, so an ID entering from a queue or console path cannot carry an ANSI escape or a CR/LF into the logs or an outgoing header eitherInfo leakA 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
--------

[](#examples)

See [examples/](examples/) for runnable scripts. Examples are expected to execute without fatal errors and stay aligned with the documented public API.

ScriptShowsNeeds server?[01-middleware-setup.php](examples/01-middleware-setup.php)Middleware in a PSR-15 stack under `acceptIncoming: true`: generate / reuse / replaceno[02-log-context.php](examples/02-log-context.php)`yiisoft/log` + context provider: `requestId` on every lineno[03-access-in-action.php](examples/03-access-in-action.php)Reading the ID from the attribute and from the holderno[04-custom-generator.php](examples/04-custom-generator.php)ULID-like generator with a matching validation patternno[05-gateway-mode.php](examples/05-gateway-mode.php)Public gateway replaces an untrusted ID, internal service preserves the gateway IDno[06-outgoing-request.php](examples/06-outgoing-request.php)Queue scope and outgoing PSR-7 header propagationno[07-trusted-proxy-policy.php](examples/07-trusted-proxy-policy.php)Accept a valid incoming ID only from a trusted gateway IP (needs `acceptIncoming: true`)noDevelopment
-----------

[](#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.

License
-------

[](#license)

[BSD-3-Clause](LICENSE.md)

###  Health Score

41

—

FairBetter than 87% of packages

Maintenance95

Actively maintained with recent releases

Popularity1

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity52

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

46d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/b0812d5572a7041dfe36e222d295b2e6dc55833a605350fcde58a51a5965ed30?d=identicon)[rasuvaeff](/maintainers/rasuvaeff)

---

Top Contributors

[![rasuvaeff](https://avatars.githubusercontent.com/u/1352718?v=4)](https://github.com/rasuvaeff "rasuvaeff (11 commits)")

---

Tags

correlation-idloggingmiddlewareobservabilityphppsr-15request-idyii3yii3-extensionsmiddlewareloggingpsr-15correlation-idyii3request id

###  Code Quality

Static AnalysisPsalm, Rector

Code StylePHP CS Fixer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/rasuvaeff-yii3-correlation-id/health.svg)

```
[![Health](https://phpackages.com/badges/rasuvaeff-yii3-correlation-id/health.svg)](https://phpackages.com/packages/rasuvaeff-yii3-correlation-id)
```

###  Alternatives

[cakephp/cakephp

The CakePHP framework

8.9k20.4M1.9k](/packages/cakephp-cakephp)[typo3/cms

TYPO3 CMS is a free open source Content Management Framework initially created by Kasper Skaarhoj and licensed under GNU/GPL.

1.2k1.9M122](/packages/typo3-cms)[typo3/cms-core

TYPO3 CMS Core

3714.0M5.9k](/packages/typo3-cms-core)[mcp/sdk

Model Context Protocol SDK for Client and Server applications in PHP

1.6k3.0M159](/packages/mcp-sdk)[cakephp/authentication

Authentication plugin for CakePHP

1184.6M126](/packages/cakephp-authentication)[typo3/cms-adminpanel

TYPO3 CMS Admin Panel - The Admin Panel displays information about your site in the frontend and contains a range of metrics including debug and caching information.

115.9M74](/packages/typo3-cms-adminpanel)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
