Skip to content

Presenters

Presenters transform QueryResponse objects into ViewModels. They isolate presentation logic from application logic, enabling multiple output formats from the same data.

Structure

Presenters implement PresenterInterface:

final class ListBooksHtmlPresenter extends PresenterAbstract
{
    public function present(ResponseInterface $response): self
    {
        $this->viewModel = new ListBooksHtmlViewModel(
            books: $response->books,
            count: count($response->books),
            can_create_book: $this->context?->userCan('book.create') ?? false
        );

        return $this;
    }
}

Usage in Controllers

$queryResponse = $this->queryBus->dispatch(new ListBooksQuery());
$viewModel = $this->presenter
    ->withContext($context)
    ->present($queryResponse)
    ->getViewModel();

$html = $this->twig->render('ListBooks.html.twig', (array) $viewModel);

Presentation Context

The PresentationContextInterface provides user permissions to presenters for building permission-aware ViewModels.

Interface

interface PresentationContextInterface
{
    public function userCan(string $permission): bool;
}

Implementation

The demo application implements this interface by delegating to UserContext:

final readonly class PresentationContext implements PresentationContextInterface
{
    public function __construct(private UserContext $userContext) {}

    public function userCan(string $permission): bool
    {
        return $this->userContext->can($permission);
    }
}

Injecting Context

Controllers pass context to presenters via withContext():

$viewModel = $this->presenter
    ->withContext($this->presentationContext)
    ->present($queryResponse)
    ->getViewModel();

Usage in Presenters

Context enables permission-based ViewModel properties:

$this->viewModel = new ListBooksViewModel(
    books: $response->books,
    can_create_book: $this->context?->userCan('book.create') ?? false,
    can_delete_books: $this->context?->userCan('book.delete') ?? false,
);

The null-safe operator (?->) handles cases where context is not set.

Source Files

  • src/Presentation/PresentationContextInterface.php
  • app/demo/Shared/Presentation/PresentationContext.php

Responsibilities

Do: - Transform domain data to display format - Apply formatting (dates, currency) - Prepare CSS classes based on state - Include permission flags

Don't: - Fetch data from repositories - Modify domain state - Contain business logic

Badge Services

Badge services provide CSS class mapping for status-based display elements. They encapsulate the visual representation of domain states.

BookStatusBadgeService

final readonly class BookStatusBadgeService
{
    public function __invoke(string $status): string
    {
        return match ($status) {
            BookStatus::Available->value => 'bg-success',
            BookStatus::Borrowed->value => 'bg-danger',
            default => 'bg-dark',
        };
    }
}

LoanStatusBadgeService

Handles additional context like overdue status:

final readonly class LoanStatusBadgeService
{
    public function __invoke(string $status, bool $isOverdue = false): string
    {
        if ($status === 'active' && $isOverdue) {
            return 'bg-danger';
        }

        return match ($status) {
            'active' => 'bg-warning text-dark',
            'returned' => 'bg-success',
            default => 'bg-secondary',
        };
    }
}

Usage in Presenters

Badge services are injected into presenters and used when building ViewModels:

$this->viewModel = new BookViewModel(
    statusBadgeClass: ($this->badgeService)($book->status),
    // ...
);

Source Files

  • app/demo/Library/Presentation/Service/BookStatusBadgeService.php
  • app/demo/Loan/Presentation/Service/LoanStatusBadgeService.php

Naming Conventions

  • {Name}HtmlPresenter - Web HTML output
  • {Name}JsonPresenter - API JSON output
  • {Name}BadgeService - Status to CSS class mapping
  • One presenter per view/endpoint

See Also