Design Patterns And Data Architecture
Presenter And View Model Pattern
A presenter or view model prepares data for display. In PHP applications, this usually means turning domain objects, read models, and application results into values that a template, JSON renderer, or UI component can use directly. The pattern is useful when templates contain repeated formatting, business-looking conditionals, messy date and money formatting, or authorization-derived display flags.
A view model is not the same as a domain model. A domain object represents business state and behavior. A view model represents what one screen or response needs to show. It may contain strings formatted for display, booleans such as canCancel, labels, links, badges, and grouped rows. Those values belong close to presentation because they answer presentation questions.
A presenter is the object or function that builds the view model. Some teams use presenter and view model interchangeably. A useful distinction is that the presenter does the transformation and the view model carries the prepared data. The important design point is that templates should not become the place where business and formatting decisions are reimplemented.
Why Templates Need Help
Templates need some logic. They loop over rows, choose markup for empty states, escape values, and show or hide sections. That is normal presentation logic. Problems appear when templates decide business rules, duplicate formatting rules, or know too much about domain objects.
For example, a subscription page might check renewal dates, payment status, user role, cancellation window, and plan type directly in Blade or Twig to decide whether to show a cancel button. If an API endpoint or another page needs the same decision, the rule will be duplicated. A presenter can receive the result of the business decision and expose a simple canCancel flag and explanatory message.
The view still controls markup. The presenter prepares meaning for the view.
A Small View Model Example
This example prepares product display data from internal values.
<?php
declare(strict_types=1);
final readonly class Product
{
public function __construct(
public string $name,
public int $pricePence,
public bool $inStock,
) {
}
}
final readonly class ProductViewModel
{
public function __construct(
public string $title,
public string $priceLabel,
public string $stockLabel,
) {
}
}
final class ProductPresenter
{
public function present(Product $product): ProductViewModel
{
return new ProductViewModel(
title: $product->name,
priceLabel: '£' . number_format($product->pricePence / 100, 2),
stockLabel: $product->inStock ? 'In stock' : 'Out of stock',
);
}
}
$view = (new ProductPresenter())->present(new Product('Notebook', 1299, true));
echo $view->title . ' - ' . $view->priceLabel . ' - ' . $view->stockLabel . PHP_EOL;
// Prints:
// Notebook - £12.99 - In stock
The template can display title, priceLabel, and stockLabel without knowing about pence conversion or stock label wording. Escaping still belongs in the template or rendering layer because output context matters.
Presenter Versus Response Transformer
Presenters and response transformers overlap. A JSON response transformer prepares data for an API client. A presenter often prepares data for a human-facing view. Both are presentation boundaries.
The difference is audience and contract. An HTML view model may include labels, CSS state names, action URLs, and help text. An API response model should avoid human-only labels unless the API contract requires them. A public API usually needs stronger versioning discipline than a private server-rendered view.
Do not force one model to serve both HTML and public JSON when their needs differ. The page may need priceLabel; the API may need amount_minor and currency.
What Belongs In A View Model
A view model can contain prepared values such as:
- formatted dates and money labels
- display status labels
- links and route targets
- booleans for visible actions
- empty-state messages
- grouped rows for tables
- pagination labels
- safe IDs needed by the template
- public error messages
It should not contain database connections, provider SDKs, raw HTTP requests, or hidden business workflows. It should not decide whether an operation is legally allowed by querying repositories from inside a template helper. The application or policy layer should decide the rule; the presenter can format the result.
View Models And Authorization
A presenter can include authorization-derived display flags, but the underlying authorization must still be enforced at the action or controller boundary. Hiding a button is not security. If a user cannot cancel a subscription, the cancellation action must reject the request even if the button is absent.
This distinction matters because users can craft requests, old pages can be cached, and APIs can be called directly. A view model improves user experience; it does not replace server-side authorization.
Use names that make meaning clear: canCancel, cancelDisabledReason, showAdminTools, or downloadUrl. Avoid making templates inspect roles and internal states repeatedly.
Formatting And Localization
Presenters are a good place to centralize formatting decisions for a view. Money, dates, relative times, pluralization, and status labels should not be hand-built differently in every template.
Localization can make this more important. A presenter may receive a translator, locale-aware formatter, or route generator and produce labels for the current request. Keep the dependencies explicit and scoped. Do not store the current locale in a long-lived singleton if the runtime handles several requests in one process.
For APIs, be cautious with localized strings. Clients often prefer stable codes and raw values. Human-facing HTML pages usually benefit from formatted labels.
Avoiding Template Business Logic
A template condition such as if ($orders === []) is fine. A template condition that checks five fields to decide refund eligibility is not. The former is presentation. The latter is business policy.
Move policy into domain objects, policies, actions, or services. Then pass the result to the presenter. The presenter can choose the label and message for that result. This keeps business tests away from rendered HTML and keeps templates readable.
Repeated template snippets are another signal. If three templates format subscription status with slightly different wording, create a presenter or shared view model method and test it.
Tables, Pagination, And Empty States
Presenters are especially helpful for tables and paginated screens. A repository or query object may return rows, total count, current page, and page size. The template still needs column labels, formatted cells, previous and next URLs, disabled states, empty-state text, and sometimes row-level action flags. Building all of that directly in the template makes pages hard to review.
A table view model can group rows, pagination metadata, filter labels, sort links, and empty-state copy in one object. The presenter can preserve raw identifiers for URLs while preparing human labels for display. It can also make pagination behavior explicit: whether page numbers are one-based, whether the next link exists, and which filters must be retained in generated URLs.
Keep query decisions separate. The presenter should not decide which database rows belong on the page. It should prepare the rows it receives for display. If a table is slow, inspect the query and pagination strategy rather than hiding the cost inside presentation code.
This separation keeps performance diagnosis honest: data selection belongs to queries, while display preparation belongs to presenters.
Name that boundary explicitly during review.
Testing Presenters
Presenter tests should be small and concrete. Give the presenter a representative domain object or read model, then assert the view model fields. Test money formatting, date formatting, status labels, action flags, empty states, and omitted values.
Do not test the entire template just to prove a label. A separate rendering test can verify that the template escapes and displays fields correctly. Presenter tests prove preparation; template tests prove rendering.
When localization is involved, test at least one non-default locale if the formatting rules differ. When route generation is involved, test generated paths without requiring a browser.
Common Failure Modes
One failure mode is a view model that exposes the entire domain object. If the template still calls $order->customer()->billingAccount()->status(), the view model has not simplified the view.
Another failure mode is putting policy queries inside presenters. A presenter that loads permissions from the database for each row can create hidden N+1 queries and mix presentation with authorization.
A third failure mode is formatting too early. If the view model formats money as a string and later code needs to calculate totals, the model is being used outside presentation. Keep raw values available at the application layer and formatted values at the view boundary.
A fourth failure mode is ignoring escaping. A view model can prepare display strings, but the template still needs context-appropriate escaping.
Review Criteria
When reviewing a presenter, ask which view or response it serves. Check that it receives data that already represents the application decision. Check that it prepares values the template can use directly without repeating business rules.
Check dependencies. Translators, URL generators, and formatters can be appropriate. Repositories, entity managers, and provider SDKs are suspicious unless the presenter is actually a query service under the wrong name.
Check tests for representative states: available action, disabled action, empty list, important status labels, dates, money, and sensitive-field omission.
What To Check
Before moving on, make sure you can:
- explain presenters and view models as presentation preparation patterns
- distinguish view models from domain models and API response models
- move repeated formatting out of templates
- keep authorization enforcement outside hidden view logic
- prepare display flags and labels without replacing server-side checks
- test presenter output separately from full template rendering
- spot view models that leak raw domain objects or trigger hidden queries
After this lesson, you should be able to review a PHP page or response and decide whether its template is doing appropriate presentation work or whether a presenter and view model would make the display contract clearer and easier to test.
Practice
Task: Build A View Model
A product page needs product title, formatted price, stock label, and whether to show a low-stock warning.
Design a presenter and view model.
Requirements
Name the classes, input data, output fields, and which decisions belong outside the template.
Check your work
The template should not calculate pence-to-pounds formatting itself.
Show solution
The presenter converts pence to a currency label, chooses stock wording, and sets the low-stock flag from a prepared stock count or product method. The template displays those fields and escapes them. It should not repeat money formatting or inspect several stock fields to decide labels.
If low-stock policy is a business rule, put the threshold in the domain or application layer and let the presenter display the result.
Task: Review Template Logic
A subscription template checks user role, renewal date, payment status, plan type, and cancellation window before showing a cancel button.
Review the design and propose a presenter/view-model approach.
Requirements
Explain what policy belongs elsewhere, what the view model should contain, and why hiding a button is not authorization.
Check your work
The final cancellation action must still enforce permission.
Show solution
The template is duplicating business policy. Move cancellation eligibility into a policy, domain service, or action-level check. The presenter should receive the eligibility result and prepare fields such as canCancel, cancelButtonLabel, cancelDisabledReason, and cancelUrl.
The template then only decides markup: show the button, show disabled text, or show no action area. It no longer knows renewal-date and payment-status rules.
The cancellation action must still enforce authorization and eligibility. Hiding the button improves the interface, but users can craft requests or use stale pages.
Task: Test Presenter Output
A dashboard presenter prepares rows with status label, formatted amount, detail URL, and action visibility.
Plan presenter tests.
Requirements
Include active, blocked, empty, sensitive-field, date, money, and route-generation cases.
Check your work
The tests should assert prepared view-model fields, not browser rendering.
Show solution
Create presenter tests with representative dashboard data. Active rows should include the expected status label, formatted amount, detail URL, and visible action. Blocked rows should include a disabled action or reason according to the view contract.
An empty input should produce an empty-state message or empty row list. Sensitive internal fields should be absent from the view model. Date and money tests should assert exact formatting for the selected locale or formatter.
Route-generation can use a fake URL generator or framework route helper in a focused test. Full browser rendering is separate; these tests prove the presenter prepares correct fields.