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, whose contract is the getViewModel() method:

interface PresenterInterface
{
    public function getViewModel(): array|object;
}

Each presenter stores its ViewModel in its own typed property and declares getViewModel() with the concrete return type:

final class ListBooksHtmlPresenter extends PresenterAbstract implements PresenterInterface
{
    private ListBooksHtmlViewModel $htmlViewModel;

    public function present(ListBooksResponse $response): self
    {
        $this->htmlViewModel = new ListBooksHtmlViewModel(
            books: $response->books,
            count: count($response->books),
            can_create_book: $this->context?->userCan('book.create') ?? false
        );

        return $this;
    }

    #[Override]
    public function getViewModel(): ListBooksHtmlViewModel
    {
        return $this->htmlViewModel;
    }
}

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->htmlViewModel = new ListBooksHtmlViewModel(
    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->htmlViewModel = 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