stevebauman/viewer
Viewer is a lightweight Laravel package by Steve Bauman that provides a clean, customizable way to view and inspect content within your application, with simple configuration and a framework-friendly API that drops neatly into any Laravel project.
composer require stevebauman/viewer
The package is deliberately small — one base class and one trait — so the fastest path to understanding it is to read two files after installing: src/Viewer.php (the base presenter) and src/Traits/Viewable.php (the model-side accessor). The README is short; the source is the documentation.
Step 1 — attach the trait to a model:
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Stevebauman\Viewer\Traits\Viewable;
use App\Viewers\UserViewer;
class User extends Model
{
use Viewable;
protected $viewer = UserViewer::class;
}
Step 2 — create the viewer:
namespace App\Viewers;
use Stevebauman\Viewer\Viewer;
class UserViewer extends Viewer
{
public function badge()
{
return $this->view('viewers.user.badge', [
'user' => $this->model,
]);
}
}
Step 3 — use it in Blade:
{!! $user->viewer->badge() !!}
First real use case: pick a partial you already repeat — an avatar block, a status pill, an author byline — and move it out of resources/views/partials into a viewer method. That immediately removes the @include('partials.user.badge', ['user' => $user]) boilerplate from every loop and gives you one obvious place to change markup.
Mirror your model namespace. App\Models\User → App\Viewers\UserViewer. When the number of viewers grows this convention saves you from hunting, and it makes a viewer resolver on your base model trivial if you want to infer the class instead of declaring $viewer manually.
One method = one fragment. Keep viewer methods single-purpose and named for what they render (badge(), byline(), meta()), not for the entity. Composite views then read like a table of contents:
<div class="user-card">
{!! $user->viewer->avatar() !!}
{!! $user->viewer->byline() !!}
{!! $user->viewer->badge() !!}
</div>
Viewers compose, they don't compute. A viewer method may call another viewer method, but it should never hit the database, dispatch events, or contain conditional business rules. If a method needs a query, the relationship should be eager-loaded at the call site (User::with('roles')->paginate()) and passed in — otherwise a listing page quietly turns into N+1.
Return type discipline matters. Depending on version, a viewer method may hand back an Illuminate\View\View or an already-rendered string. Standardise on one in your codebase (calling ->render() inside the viewer keeps Blade clean but kills @include-style data injection later) and be consistent. If you return View objects, {!! !!} is required — {{ }} will print the object's string cast in some cases and escape the markup in others.
Where it pays off beyond Blade: transactional email templates, PDF invoices (dompdf/snappy), and Livewire components all need HTML assembled from a model. A viewer gives you a testable unit instead of a chain of view()->make() calls sprinkled through job classes.
Test at the seam. Viewers are cheap to test because they produce a named view with known data:
$view = $user->viewer->badge();
$this->assertSame('viewers.user.badge', $view->name());
$this->assertTrue($view->getData()['user']->is($user));
That assertion set catches the most common regression in this pattern — a renamed partial that nobody notices until a 500 fires in production.
Multiple viewers per model is legitimate for genuinely different surfaces (public profile vs. admin row). Rather than bloating one class, add a second viewer and expose it via a distinct accessor or a small factory, e.g. $user->adminViewer.
composer.json, read src/ end to end (it's small enough), and be prepared to copy the two classes into app/Support/ if the upstream goes quiet. The pattern is more valuable than the package.viewer attribute, a viewer() relationship, or a Viewable-adjacent trait, PHP will fatal on the method clash. Alias on import (use Viewable as ViewableTrait;) or rename via insteadof. Check existing accessors before adopting.$appends, it will appear in toArray() / JSON responses — dumping HTML into your API payloads and possibly blowing up on models with circular relations. Keep viewers out of API resources entirely.view()->make() raises an InvalidArgumentException only at render time, so a broken viewer path shows up as a 500 on a specific page rather than at boot. A cheap guard in CI: a test that renders every viewer method for a factory-made model.$this->model is populated in Viewer::__construct() before writing helpers around it; some versions expect the record under a different property name. Get this right once and copy the pattern.dd($user->viewer->badge()->name()) before render() isolates "wrong view" from "wrong data" in one step. If the name is right and the output is empty, it's a data key mismatch, not a view-path problem.Viewer class is the intended hook — subclass it with shared helpers (formatDate(), link(), a viewPath prefix) so every viewer inherits them, rather than duplicating utility methods across ten classes.@include is clearer. Adopt it for fragments that recur across three or more templates.How can I help you explore Laravel packages today?