Skip to content

Controllers

Controllers are thin orchestrators handling HTTP concerns. They delegate business logic to Commands, Queries, or Use Cases.

Responsibilities

Controllers should:

  • Parse HTTP request data
  • Create Command/Query objects
  • Dispatch to appropriate bus
  • Handle exceptions and return HTTP responses

Controllers should NOT:

  • Contain business logic
  • Access database directly
  • Format data (use Presenters)

HTTP Controller

public function index(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
{
    $queryResponse = $this->queryBus->dispatch(new ListBooksQuery());
    $viewModel = $this->presenter->present($queryResponse)->getViewModel();
    $html = $this->twig->render('ListBooks.html.twig', (array) $viewModel);

    return $this->responseBuilder
        ->withResponse($response)
        ->withHtml($html)
        ->build();
}

API Controller

public function index(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
{
    $queryResponse = $this->queryBus->dispatch(new ListBooksQuery());
    $viewModel = $this->presenter->present($queryResponse)->getViewModel();

    return $this->jsonSuccess($response, $viewModel);
}

Request Data Access

$id = $args['id'];                              // Route parameters
$page = $request->getQueryParams()['page'];     // Query string
$data = (array) $request->getParsedBody();      // POST body

Request Objects

Request objects encapsulate HTTP request parsing and validation for form submissions. They provide a clean interface between HTTP data and command creation.

Structure

final readonly class CreateBookRequest
{
    private function __construct(
        public string $title,
        public string $author,
        public string $isbn
    ) {}

    public static function fromHttpRequest(ServerRequestInterface $request): self
    {
        $data = (array) $request->getParsedBody();

        return new self(
            $data['title'] ?? '',
            $data['author'] ?? '',
            $data['isbn'] ?? ''
        );
    }

    public function validate(): array
    {
        // Returns array of field => error message
    }

    public function toArray(): array
    {
        return [
            'title' => $this->title,
            'author' => $this->author,
            'isbn' => $this->isbn,
        ];
    }
}

Usage in Controllers

public function create(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
{
    $bookRequest = CreateBookRequest::fromHttpRequest($request);
    $errors = $bookRequest->validate();

    if ($errors !== []) {
        return $this->renderFormWithErrors($response, $bookRequest->toArray(), $errors);
    }

    $command = new CreateBookCommand(
        $this->idGenerator->generate(),
        $bookRequest->title,
        $bookRequest->author,
        $bookRequest->isbn
    );

    $this->commandBus->dispatch($command);
    // ...
}

Separation of Concerns

Request objects handle HTTP-level validation (required fields, format checks), while domain validation occurs in Value Objects and Commands. This separation allows:

  • Consistent error handling across similar forms
  • Reusable validation logic via shared validators
  • Clean controller code without inline parsing

Source Files

  • app/demo/Library/Presentation/Request/CreateBookRequest.php
  • app/demo/Library/Presentation/Request/UpdateBookRequest.php
  • app/demo/Loan/Presentation/Request/BorrowBookRequest.php

Response Patterns

// HTML
return $this->responseBuilder->withResponse($response)->withHtml($html)->build();

// JSON
return $this->jsonSuccess($response, $data);

// Redirect
return $this->responseBuilder
    ->withResponse($response)
    ->withStatus(302)
    ->withHeader('Location', '/books')
    ->build();

See Also