Alpha ZealPHP is early-stage and under active development. APIs may change between minor versions until v1.0. Feedback and bug reports welcome on GitHub.

Returning a Response

Most frameworks make you construct a response object. ZealPHP infers it from what you return — like a thoughtful waiter who doesn't need you to spell out medium-rare, no onions.

You will learn

  • The six return-value conventions ZealPHP recognizes
  • When to return $data vs reach for the $response object
  • How streaming (Generators) fits into the same conventions
  • What happens to echo output (and when it matters)

The six conventions

Whatever your handler returns, ZealPHP’s ResponseMiddleware looks at the type and translates it into the right HTTP response. Six types, six behaviors:

Return typeWhat happensExample
intStatus code, empty bodyreturn 404;
stringHTML body, 200 OKreturn '<h1>Hello</h1>';
array / objectJSON-encoded, Content-Type: application/jsonreturn ['ok' => true];
\GeneratorStreaming — each yield sent immediatelyyield "<li>{$row->name}</li>";
void + echoCaptured output buffer becomes the bodyecho App::render(...);
ResponseInterfacePSR-7 passthrough — sent verbatimreturn $response->redirect('/');

The first three cover 90% of handlers. The other three exist for the cases where 90% isn’t enough.

The default path: return data

JSON APIs are the cleanest case. Return an array. Done.

$app->route('/api/users/{id}', function ($id) {
    $user = User::find($id);
    return $user ? $user->toArray() : 404;
});

Note the : 404 branch — an int return-value short-circuits to a status code. No $response->status(404)->end(); no http_response_code(404); exit;. The framework reads "404" and does the right thing.

When to reach for $response

The $response object is for cases that don’t fit a single return value: setting cookies, redirects, custom headers, streaming, sending a file. You mutate the response object, then either return the result or just return:

$app->route('/logout', function ($response) {
    $response->cookie('session', '', time() - 3600, '/');
    return $response->redirect('/');
});

Same handler, the redirect() call returns a PSR-7 response which is then returned from the closure. ResponseMiddleware sees the ResponseInterface and ships it.

Streaming via Generator

Yield from your handler and you’re streaming. The framework flushes each yielded chunk immediately — no buffer, no waiting for the function to return.

$app->route('/feed', function () {
    return (function () {
        yield "<html><body><ul>";
        foreach (Post::recent() as $p) {
            yield "<li>{$p->title}</li>";
        }
        yield "</ul></body></html>";
    })();
});

Lesson 11 (Streaming Done Right) covers the four patterns in detail. For now: any Generator return triggers streaming mode. The browser starts rendering before your handler finishes.

echo + void: the legacy escape hatch

You can still write PHP the old way — echo to stdout, let the framework capture it:

$app->route('/about', function () {
    App::render('about', ['user' => auth()]);
    // no return — rendered output is captured via ob_get_clean()
});

App::render() echoes the rendered template. The handler returns void. ResponseMiddleware grabs the output buffer and sends it. This is how every WordPress page handler works, more or less.

Prefer returning a value when you can. Returning is cheaper than buffering, easier to test, and works inside coroutines without surprises. But if you’re porting Apache code, echo-and-void is the bridge.

Try it live

Every response convention has a demo at /demo/response/{method}:

Your handler is function () { return "<p>hi</p>"; }. What Content-Type does the browser see?

Key Takeaways

  • Return an int for status-only responses (return 404).
  • Return a string for HTML, an array for JSON — the framework picks the Content-Type.
  • Return a Generator to stream chunks as they're yielded.
  • Reach for the $response object when you need cookies, headers, or redirects alongside the body.
  • echo + void works for legacy code — the framework captures stdout, but returning is cleaner.