Emitting a PSR-7 response — sending headers and body to the client
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:
- Check
headers_sent()— if already sent, can't emit headers. - Send status code.
- Send headers.
- 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
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);