Immutable request design — withMethod(), withUri()
Concept
Immutable request design means once a Request is constructed, its properties cannot be changed. Modifications return a new Request instance with the change applied. This is PSR-7's design and it prevents subtle bugs.
Why immutability matters for middleware:
- Middleware chains process a request through multiple handlers.
- If middleware A mutates the request, middleware B and the original caller see different data unexpectedly.
- With immutable requests, each middleware can only create a modified COPY. The original is preserved.
- Example: auth middleware adds a
userattribute to the request. It returns a NEW request with that attribute. The next middleware uses the new request.
with*() pattern: Methods that would mutate state instead clone the object and modify the clone:
public function withMethod(string $method): static {
$clone = clone $this;
$clone->method = $method;
return $clone;
}PHP clone: Shallow copy — scalar properties are copied by value, objects are copied by reference. For deep immutability, recursively clone nested objects.
withAttribute() / getAttribute(): The primary mechanism for middleware to pass data to downstream handlers. $request = $request->withAttribute('user', $authenticatedUser).
Trade-off: Immutability means more object creation. For high-traffic applications, this can add GC pressure. PHP 8's JIT mitigates this somewhat. Measured impact is typically negligible for web applications.
Code Example
<?php
namespace Framework\Http;
class Request
{
private function __construct(
private readonly string $method,
private readonly string $uri,
private readonly array $headers,
private readonly array $queryParams,
private readonly mixed $parsedBody, // array or null
private readonly array $attributes, // middleware-added data
private readonly string $rawBody,
) {}
// Static factory
public static function create(
string $method,
string $uri,
array $headers = [],
mixed $body = null,
): static {
return new static($method, $uri, $headers, [], $body, [], '');
}
// Immutable modifiers — return new instance
public function withMethod(string $method): static
{
$clone = clone $this;
$clone->method = $method; // Note: PHP clone doesn't support readonly overrides
return $clone;
// For PHP 8.1+ readonly properties, use named constructor pattern instead:
// return new static($method, $this->uri, $this->headers, ...);
}
public function withUri(string $uri): static
{
return new static($this->method, $uri, $this->headers, $this->queryParams, $this->parsedBody, $this->attributes, $this->rawBody);
}
public function withHeader(string $name, string $value): static
{
$headers = $this->headers;
$headers[strtolower($name)] = $value;
return new static($this->method, $this->uri, $headers, $this->queryParams, $this->parsedBody, $this->attributes, $this->rawBody);
}
public function withAttribute(string $name, mixed $value): static
{
$attrs = $this->attributes;
$attrs[$name] = $value;
return new static($this->method, $this->uri, $this->headers, $this->queryParams, $this->parsedBody, $attrs, $this->rawBody);
}
// Readers
public function getMethod(): string { return $this->method; }
public function getUri(): string { return $this->uri; }
public function getAttribute(string $name, mixed $default = null): mixed { return $this->attributes[$name] ?? $default; }
public function getHeader(string $name): ?string { return $this->headers[strtolower($name)] ?? null; }
public function getParsedBody(): mixed { return $this->parsedBody; }
public function getQueryParams(): array { return $this->queryParams; }
// Example: auth middleware using withAttribute
// Auth middleware:
// $user = $this->auth->authenticate($request);
// $request = $request->withAttribute('user', $user);
// return $handler->handle($request); // downstream gets $request with 'user' set
}