0

Emitting a PSR-7 response — sending headers and body to the client

Intermediate5 min read·fw-04-005

Concept

Emitting a response is the final step: taking the Response object and actually sending data to the browser. This happens in public/index.php after the entire application pipeline runs.

PHP output functions:

  • http_response_code(int $code): Set the HTTP status code.
  • header(string $header, bool $replace = true, int $code = 0): Send a raw header.
  • echo / print: Output the body to stdout (which PHP sends as the HTTP body).

Status line: PHP automatically sends the HTTP/1.1 200 OK status line when you call http_response_code().

Header sending rules:

  • Headers must be sent BEFORE any body output.
  • PHP buffers output by default (output buffering is usually on). The buffer is flushed when the script ends or ob_flush() is called.
  • headers_sent(): Returns true if headers have already been sent. Call before sending to check.

Multiple values for one header: Some headers like Set-Cookie can appear multiple times. Use header('Set-Cookie: ...', false) — the second arg false means don't replace previous headers with the same name.

ResponseEmitter class: Encapsulate the emission logic:

  1. Check headers_sent() — if already sent, can't emit headers.
  2. Send status code.
  3. Send headers.
  4. Echo body.

Sending body in chunks: For large responses (file downloads), read and echo in chunks to avoid memory issues.

Connection: close optimization: Add this header and flush immediately after sending. PHP process can finish cleanup while the client reads the response.

Code Example

php
<?php
namespace Framework\Http;

class ResponseEmitter
{
    public function emit(Response $response): void
    {
        $this->sendStatus($response);
        $this->sendHeaders($response);
        $this->sendBody($response);
    }

    private function sendStatus(Response $response): void
    {
        if (headers_sent()) {
            return; // can't modify headers — already sent (shouldn't happen in prod)
        }

        http_response_code($response->getStatus());
    }

    private function sendHeaders(Response $response): void
    {
        if (headers_sent()) return;

        foreach ($response->getHeaders() as $name => $values) {
            $normalizedName = $this->normalizeHeaderName($name);
            $replace = true;
            foreach ($values as $value) {
                header("{$normalizedName}: {$value}", $replace);
                $replace = false; // subsequent values with same name: don't replace
            }
        }
    }

    private function sendBody(Response $response): void
    {
        // For HEAD requests, send no body (headers only)
        if (isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'HEAD') {
            return;
        }
        echo $response->getBody();
    }

    private function normalizeHeaderName(string $name): string
    {
        // content-type → Content-Type
        return implode('-', array_map('ucfirst', explode('-', $name)));
    }
}

// public/index.php — the application entry point
require_once __DIR__ . '/../vendor/autoload.php';

$app     = require_once __DIR__ . '/../bootstrap/app.php';
$request = \Framework\Http\Request::fromGlobals();

try {
    $response = $app->handle($request);
} catch (\Throwable $e) {
    $response = $app->handleException($e, $request);
}

(new \Framework\Http\ResponseEmitter())->emit($response);