In PSR-7, what are ServerRequestInterface attributes for, and how does a middleware hand a derived value to the next handler?
answer
- values derived from the request
- withAttribute() returns a new request
- getAttribute($name, $default = null)
- no hasAttribute(): use the default
- pass the new request to handle()
basics
~10 sServerRequestInterface attributes carry values derived from the request, such as route matches or an authenticated client id. A middleware calls $request->withAttribute('name', $value) and passes that new request on; later code reads getAttribute('name', $default).
solid answer
~40 sAttributes are the one bag on a PSR-7 server request that is not data from the wire: they hold values the application derived from it, such as route parameters, decrypted cookie data or a resolved client identity. `withAttribute($name, $value)` returns a new request carrying the attribute, so a middleware must pass that returned instance to `$handler->handle()`; passing the original loses it. Downstream code reads `getAttribute($name, $default = null)`, and the default argument replaces a `hasAttribute()` method, which the interface deliberately lacks. `getAttributes()` returns them all and `withoutAttribute()` removes one. In a rate limiter, an authentication middleware can set a `client_id` attribute that the limiter then uses as its bucket key, falling back to the remote address from `getServerParams()`.
code
php · 22 lines<?php
declare(strict_types=1);
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
final class ApiKeyMiddleware implements MiddlewareInterface
{
public function __construct(private ApiKeyDirectory $keys) {}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$clientId = $this->keys->clientFor($request->getHeaderLine('X-Api-Key'));
if ($clientId !== null) {
$request = $request->withAttribute('client_id', $clientId);
}
return $handler->handle($request); // pass the NEW request on
}
}go deeper
Remember that attributes hold values the application worked out from the request, and that getAttribute() takes a default for when the attribute is missing.
Explain the set-and-forward pattern: withAttribute() returns a new request, and only the instance passed to handle() carries it. Contrast attributes with query params and the parsed body.
Design attribute keys that cannot collide across packages, store typed value objects, and document which attributes a reusable middleware reads and writes.
Decide how much per-request context travels as request attributes versus explicit services, trading loose coupling between middleware against hidden, stringly typed dependencies.
## What attributes are `ServerRequestInterface` exposes several kinds of request data. Most of it comes from the incoming message or from PHP's SAPI: | Method | Holds | Typical source | |---|---|---| | `getServerParams()` | environment and CGI variables | `$_SERVER` | | `getQueryParams()` | decoded query string | `$_GET` | | `getParsedBody()` | decoded body: array, object or `null` | `$_POST` or a JSON decoder | | `getCookieParams()` | cookies | `$_COOKIE` | | `getUploadedFiles()` | a tree of `UploadedFileInterface` | `$_FILES` | | `getAttributes()` | values **derived** by the application | middleware, router | **Attributes** are the last row. The specification describes them as parameters derived from the request, for example the results of path matching, of decrypting cookies or of deserialising a non-form body. They are how one request consumer passes information to the next without a global variable or a shared service holding per-request state. ## The methods - `getAttributes()` returns all attributes as an array. - `getAttribute($name, $default = null)` returns one attribute, or `$default` when it has not been set. - `withAttribute($name, $value)` returns a **new** request with the attribute set. - `withoutAttribute($name)` returns a new request with the attribute removed. There is no `hasAttribute()`. The specification says the default argument makes it unnecessary: pass a sentinel default when `null` is a meaningful value. ## Handing a value to the next handler Because the request is immutable, the attribute exists only on the instance `withAttribute()` returns. In a **PSR-15** middleware the pattern is: 1. derive the value, for example resolve an API key to a client id; 2. call `$request = $request->withAttribute('client_id', $clientId);` 3. call `$handler->handle($request)` with that new instance. If step 3 passes the original `$request`, every later component sees no attribute. This is the with* pitfall in its most common server-side form. ## The rate-limiting example A framework-agnostic rate limiter needs a key per caller. An authentication middleware earlier in the pipeline has already resolved the caller, so the limiter reads: ```php $key = $request->getAttribute('client_id') ?? ($request->getServerParams()['REMOTE_ADDR'] ?? 'unknown'); ``` The attribute is preferred because it identifies an account; the remote address is the fallback for anonymous traffic. Whether `REMOTE_ADDR` is the real client depends on proxies in front of the application, which is a deployment concern outside PSR-7. ## Naming and typing attributes PSR-7 fixes no names, so a package has to document the ones it reads and writes. Common practice: - use a **class-name constant** or a namespaced string as the key, for example `ClientId::class`, to avoid collisions between packages; - store a small, typed value object rather than a loose array, so readers can check it with `instanceof`; - keep the attribute name in one constant shared by the writer and the reader. ## What attributes are not - They are not the query string: `getQueryParams()` holds that, and `withQueryParams()` does not change the URI. - They are not sent anywhere: attributes never become headers or cookies in a response. - They are not immutable all the way down: the spec says attributes are application specific and CAN be mutable, meaning a stored object can still change internally even though the request holding it cannot be changed except through `withAttribute()`. ## Router parameters Routers that work with PSR-7 usually store matched path parameters, such as an `id` from `/tasks/{id}`, as attributes, and the handler reads them with `getAttribute('id')`. Which names a router uses is that router's convention, not part of PSR-7. ## Testing code that reads attributes Attributes make handlers easy to test in isolation. A test builds a server request through a PSR-17 `ServerRequestFactoryInterface::createServerRequest('GET', '/quota')`, adds `withAttribute('client_id', 'acme')`, and passes it straight to the handler, without running the authentication middleware at all. Two consequences follow: - the handler's dependency on the attribute becomes visible in the test setup, which documents the contract; - a test that forgets the attribute exercises the default branch of `getAttribute()`, which is exactly the path an anonymous request takes in production.
- Why does ServerRequestInterface have no hasAttribute() method?The specification says `getAttribute($name, $default = null)` makes it unnecessary: you supply the value to return when the attribute is absent. When `null` is a legitimate stored value, pass a unique sentinel object as the default and compare against it.
- Where should a middleware put a JSON body it decoded: an attribute or the parsed body?The parsed body. `withParsedBody()` exists for the results of deserialising the body, and PSR-7 allows an array or object there. Attributes are for other derived values, such as route matches or identity. Putting the body in an attribute makes readers look in two places.
saying these in an interview costs you the question
- Calls withAttribute() and then passes the original request to the handler.
- Thinks attributes are the parsed query string.
- Expects a hasAttribute() method on ServerRequestInterface.
- Believes attributes are sent back to the client as headers.
- Assumes PSR-7 defines standard attribute names for route parameters.