Responders

The Responder is the final step in the ADR cycle. It takes the result the Domain returned and formats it as HTML or JSON. It is the only layer that decides what a view calls things.

Returning a View

From a Responder, render a view and pass data to it:

namespace Responders;

use Domains\Results\Blog as BlogResult;
use TetherPHP\framework\Http\Response;

class Blog extends Responder
{
    public function __invoke(BlogResult $result): Response
    {
        return $this->view('pages.blog.index', [
            'posts' => $result->posts,
            'heading' => $result->title,
        ]);
    }
}

This renders app/Views/pages/blog/index.php. The array is extracted — each key becomes a variable in the view — and building it is the Responder's job. A Domain that returned that array directly would be naming template variables from inside your business logic.

The parameter is the result type the matching Domain returns, so a mismatched pair is a TypeError at the boundary rather than an undefined variable inside a template.

Returning JSON

For APIs or AJAX endpoints, return a JSON response:

namespace Responders;

use Domains\Results\ApiStatus as ApiStatusResult;
use TetherPHP\framework\Http\Response;

class ApiStatus extends Responder
{
    public function __invoke(ApiStatusResult $result): Response
    {
        return $this->json([
            'status' => $result->status,
            'checked_at' => $result->checkedAt,
        ]);
    }
}

This sets the Content-Type header to application/json and returns the encoded data.

You can also set a custom status code:

return $this->json(['error' => 'Not found'], 404);

One result type per outcome

When a feature can end more than one way, the Domain returns a different type for each ending, and the Responder picks the view and the status from the type:

public function __invoke(BlogPost|BlogPostNotFound $result): Response
{
    if ($result instanceof BlogPostNotFound) {
        return $this->view('pages.blog.notFound', ['slug' => $result->slug], 404);
    }

    return $this->view('pages.blog.post', [
        'title' => $result->title,
        'body' => $result->body,
    ]);
}

The alternative — one array with a found key in it — puts the same decision in a place the type system cannot see, and leaves every branch reading keys that may not be there.

View Routes (without a Responder)

For simple static pages that don't need data, skip the Responder entirely and use a view route:

$router->view('/about', 'pages.about');

This renders the view directly from the router — no Action, Domain, or Responder needed.

Redirecting

A write answers with a redirect rather than a page, so refreshing after saving does not submit the form again:

return Response::redirect('/posts/' . $result->id, 303);

Use 303 rather than the default 302 after a write: only 303 is defined to make the next request a GET whatever this one was, which is not guaranteed after a PUT or a DELETE. See CRUD.