Formatting and Computed Fields
The Presenter has three primary mechanisms for shaping output values: compute, formatDate, and formatCurrency.
Computed fields
->compute(string $name, Closure $factory): self
Closure signature: fn(DataObject $dto, PresentationContext $ctx): mixed.
Presenter::for($product)
->compute('priceFormatted', fn ($p, $ctx) => wc_price($p->price))
->compute('isOnSale', fn ($p) => $p->salePrice > 0 && $p->salePrice < $p->price);
- The DTO type can be narrowed in the closure:
fn (ProductDto $p) => ... - The closure runs lazily — only invoked if the field name survives
only()andhide()filters - Computed fields can override DTO properties; if you
compute('price', ...), the originalpriceproperty value is replaced - Use
computeto callSecret::reveal()when you actually need the plaintext in output (auditable at the call site)
->compute('rawApiKey', fn ($p) => $p->apiKey?->reveal())
Date formatting
->formatDate(string $field, string $format, ?string $as = null): self
- Reads the named field, expects a
DateTimeInterface - Formats via
DateTimeFormatterhonoring the context's locale and timezone - If
$asisnull, replaces the original field. Otherwise creates a new field under$asand leaves the original alone.
->formatDate('publishedAt', 'F j, Y') // replace
->formatDate('publishedAt', 'F j, Y', as: 'publishedDate') // add new field
PHP's DateTime::format() syntax applies. The locale affects month and day names (F, l, M, D).
Currency formatting
->formatCurrency(string $field, ?string $as = null, ?string $currency = null, bool $html = false): self
- Reads the named field, expects a numeric value (
intorfloat) - Formats via
CurrencyFormatter— uses WooCommerce'swc_price()when WC is active and the$htmlflag is true; falls back to a plain locale-aware number-format otherwise $currencyis a 3-letter ISO code ('USD','HUF','EUR'); when null, uses WooCommerce's default or the context locale's currency$html: truereturns a string with WC's HTML wrapping (<span class="woocommerce-Price-amount">...</span>);falsereturns plain text
->formatCurrency('price') // replace, plain
->formatCurrency('price', as: 'priceDisplay', currency: 'HUF') // add new field
->formatCurrency('price', as: 'priceHtml', html: true) // WC HTML
Combining date and currency with computed fields
Presenter::for($order)
->context(PresentationContext::email(userId: $order->customerId))
->formatDate('createdAt', 'F j, Y', as: 'orderDate')
->formatCurrency('total', as: 'totalDisplay')
->compute('lineCount', fn ($o) => count($o->items))
->compute('subtotalDisplay', fn ($o) => wc_price($o->subtotal))
->only(['orderNumber', 'orderDate', 'totalDisplay', 'subtotalDisplay', 'lineCount'])
->toJson();
Presets for nested fields
->preset(string $field, Closure $renderer): self
Closure signature: fn(mixed $value, PresentationContext $ctx): mixed.
preset runs before the recursive render of nested values. Useful when you want to override how a nested DTO appears without subclassing its presenter.
->preset('billingAddress', fn ($address, $ctx) => [
'line' => "{$address->city}, {$address->country}",
])
Working with collections
CollectionPresenter exposes the same fluent methods. Computed fields are evaluated per-DTO:
$rows = Presenter::forCollection($products)
->context(PresentationContext::admin())
->compute('priceFormatted', fn ($p) => wc_price($p->price))
->only(['id', 'title', 'priceFormatted'])
->toArray();
Common mistakes
- Expecting
compute('foo', ...)to run whenfoowas hidden byhide('foo', ...)or excluded byonly([...])— closures are lazy; they don't fire when the output won't include the field - Forgetting to add the computed name to
only()—only(['id', 'price'])excludes acompute('priceFormatted', ...)becausepriceFormattedisn't in the whitelist - Using
formatCurrencyon a non-numeric field — fails with type error inCurrencyFormatter. Usecomputefor non-trivial cases - Locale not changing the output — make sure the
PresentationContextcarries the locale (PresentationContext::email(locale: 'hu_HU'))