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
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.phpapp/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.phpapp/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
- Queries & Handlers - Transform QueryResponse
- RBAC Permissions - Permission-aware ViewModels