0

Immutable request design — withMethod(), withUri()

Advanced5 min read·fw-04-003
psr

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 user attribute 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:

php
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
<?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
}