guilhermegonzaga/presenter
A Laravel presenter package that wraps models and other objects in dedicated presenter classes, keeping views and controllers clean. Define reusable presentation methods for formatting, computed values, and view-ready output while preserving access to the underlying object.
Install via Composer; the service provider is auto-discovered on Laravel 5.5+.
composer require guilhermegonzaga/presenter
Where to look first: open vendor/guilhermegonzaga/presenter/src and read the PresentableTrait and the abstract Presenter base class. The trait is the contract you attach to models (it exposes the present() entry point), and the base presenter defines how property access is proxied back to the underlying model. Those two files tell you the exact method names and magic-access behaviour for the version you installed — confirm them before copying snippets from blog posts, since the API surface is small and naming has shifted between tags.
Minimal wiring:
use GuilhermeGonzaga\Presenter\PresentableTrait;
class User extends Model
{
use PresentableTrait;
protected $presenter = \App\Presenters\UserPresenter::class;
}
use GuilhermeGonzaga\Presenter\Presenter;
class UserPresenter extends Presenter
{
public function fullName(): string
{
return trim($this->first_name . ' ' . $this->last_name);
}
public function price(): string
{
return 'R$ ' . number_format($this->model->price / 100, 2, ',', '.');
}
}
First use case: stop formatting in Blade. Replace {{ $user->first_name . ' ' . $user->last_name }} with {{ $user->present()->fullName() }}, and move every number_format, Carbon::parse(...)->format(...), and status-to-label match out of your views and into the presenter. That single refactor is the fastest way to see whether the pattern fits your codebase.
Blade-first formatting. The presenter is a view-model: it is the only place that knows how a value should be displayed. Keep raw values on the Eloquent model and presentation strings on the presenter, so the same model can back an API (raw) and a web page (presented) without conditional logic leaking into either.
One presenter per concern, not per model. Rather than a 600-line UserPresenter, compose small presenters (UserProfilePresenter, UserBillingPresenter) and pick them at the call site or via a factory. The package resolves the presenter class from the model, so if you need runtime selection, instantiate presenters directly with the model instead of relying on the $presenter property.
Accessing relationships. Inside a presenter, $this->relation proxies to the model's loaded relation, which lets you write $this->address->city in a getter. This is convenient but dangerous — see the N+1 note below. Prefer explicit $this->model->relation when you want the intent to be obvious to reviewers.
Derived/conditional labels. Status badges are the canonical case:
public function statusLabel(): string
{
return match ($this->model->status) {
'a' => 'Active',
's' => 'Suspended',
default => 'Unknown',
};
}
public function statusColor(): string
{
return $this->model->status === 'a' ? 'green' : 'red';
}
Caching expensive derivations. Presenters are typically instantiated fresh per call, so a getter that hits the database or does heavy formatting runs every time it is touched. If a value is reused across a view, compute it once and stash it on a property:
private ?string $cachedTotal = null;
public function total(): string
{
return $this->cachedTotal ??= number_format($this->model->total_cents / 100, 2);
}
API + Blade coexistence. Do not add presenters to $appends; that couples serialization to display formatting. Instead, in API Resources, call the presenter explicitly for fields that genuinely belong in the payload, and keep the rest raw.
Testing. Presenters are plain classes — unit-test them without a database by constructing them with a model instance populated in memory. Assert on output strings, not on the magic __get proxy, so a future package upgrade that changes proxying doesn't silently break your suite.
Every present() call builds a new instance. Don't assume memoization. If your Blade partial touches the presenter five times, either assign it once in the controller ($user = $user->present()) or cache derived values on the presenter. This is the most common source of "why is this slow" reports.
__get hides missing attributes. Because property access is proxied through the model, a typo like $this->ful_name may return null instead of throwing, and the bug surfaces as a blank cell in production. Prefer $this->model->attribute where you control the name, or add explicit return types (: string) so PHP fails loudly on null.
N+1 in presenter getters. A presenter that touches $this->address->city inside a @foreach over 500 users is an invisible N+1 — the query is one level removed from the loop. Eager-load in the controller (User::with('address')) and treat relationship access in presenters as a code smell to review.
Never query from a presenter. Presenters should be pure formatting. The moment a getter calls Model::where(...), the class stops being unit-testable and starts being a hidden repository. Inject or preload what you need.
Config/property quirks. The presenter class is usually read from a $presenter property on the model; some versions accept a presentable() method override instead. If present() throws "presenter not defined," check the property name and visibility against the installed tag — a protected vs public mismatch is a frequent cause.
Serialization is not automatic. Presenters do not change toArray()/toJson() output. Anything you want in a JSON response must be added explicitly. This is a feature (raw data for APIs, formatted for views) but surprises people migrating from accessor-heavy models.
Extension points. The base presenter is deliberately thin: override the constructor if you need dependency injection, add __call passthrough only as a last resort (it obscures stack traces), and wrap shared formatting helpers (money(), mask(), date()) in a trait that multiple presenters use. Keep the base class untouched so package upgrades stay painless.
Debugging tip. When output looks wrong, dd($user->present()) shows the presenter wrapper, not the data — dump $user->present()->model (or your version's accessor) to see the underlying attributes. That distinction resolves most "the presenter is empty" confusion in seconds.
Version pinning. With a small, low-traffic package (46 stars), treat the tag as an API contract: pin a minor version in composer.json and read the diff before upgrading, since there is no large community to surface breaking changes for you.
How can I help you explore Laravel packages today?