0

Building a Response class — status, headers, body stream

Advanced5 min read·fw-04-004

Concept

Building a Response class encapsulates an HTTP response: status code, headers, and body. The framework constructs a Response in the controller/handler layer and the emitter sends it to the browser.

HTTP response structure:

  1. Status line: HTTP/1.1 200 OK
  2. Headers: Content-Type: application/json\r\n...
  3. Blank line: \r\n
  4. Body: response body bytes

Status code + reason phrase: Status codes have standard reason phrases (200 → OK, 404 → Not Found, 422 → Unprocessable Content). Frameworks typically provide a map of code → phrase.

Header storage: Headers are multi-value — a response can have multiple Set-Cookie headers. Store as array<string, string[]> (name → array of values). Single-value headers are stored as [name => ['value']].

Body: Can be a simple string for small responses. For streaming (large file downloads), it should be a stream/resource. For now, a string is fine.

Immutable design: Like the Request, a Response can be designed as immutable. with*() methods return new instances. This is PSR-7's design.

Convenience constructors:

  • Response::json(mixed $data, int $status = 200): Creates a JSON response.
  • Response::html(string $html, int $status = 200): Creates an HTML response.
  • Response::redirect(string $url, int $status = 302): Creates a redirect response.

Code Example

php
<?php
namespace Framework\Http;

class Response
{
    private static array $phrases = [
        200 => 'OK', 201 => 'Created', 204 => 'No Content',
        301 => 'Moved Permanently', 302 => 'Found', 304 => 'Not Modified',
        400 => 'Bad Request', 401 => 'Unauthorized', 403 => 'Forbidden',
        404 => 'Not Found', 405 => 'Method Not Allowed', 422 => 'Unprocessable Content',
        429 => 'Too Many Requests', 500 => 'Internal Server Error',
    ];

    private array $headers = [];

    public function __construct(
        private string $body   = '',
        private int    $status = 200,
        array          $headers = [],
    ) {
        foreach ($headers as $name => $value) {
            $this->headers[strtolower($name)][] = $value;
        }
    }

    // Static constructors
    public static function json(mixed $data, int $status = 200): static
    {
        return new static(
            json_encode($data, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR),
            $status,
            ['Content-Type' => 'application/json; charset=UTF-8'],
        );
    }

    public static function html(string $html, int $status = 200): static
    {
        return new static($html, $status, ['Content-Type' => 'text/html; charset=UTF-8']);
    }

    public static function redirect(string $url, int $status = 302): static
    {
        return new static('', $status, ['Location' => $url]);
    }

    public static function noContent(): static
    {
        return new static('', 204);
    }

    // Immutable modifiers
    public function withStatus(int $status): static
    {
        $clone = clone $this;
        $clone->status = $status;
        return $clone;
    }

    public function withHeader(string $name, string $value): static
    {
        $clone = clone $this;
        $clone->headers[strtolower($name)] = [$value];
        return $clone;
    }

    public function withAddedHeader(string $name, string $value): static
    {
        $clone = clone $this;
        $clone->headers[strtolower($name)][] = $value;
        return $clone;
    }

    // Readers
    public function getStatus(): int           { return $this->status; }
    public function getBody(): string          { return $this->body; }
    public function getHeaders(): array        { return $this->headers; }
    public function getHeader(string $name): array { return $this->headers[strtolower($name)] ?? []; }
    public function getReasonPhrase(): string  { return static::$phrases[$this->status] ?? 'Unknown'; }
}