Laravel model presenters with the Reflection API

Laravel model presenters with the Reflection API
Photo by Pepe Reyes / Unsplash

There are many instances where I've needed to format data stored in a model so it can be presented in a view somehow. In the passed I've approached this particular problem in a few different ways, and yes, I could easily  format that data directly in the view, but that quickly becomes a chore and then it tends to become a bit repetitive. It’s also a place for errors or inconsistencies to creep in. Things like this can become common, and they're nasty:

{{ $article->created_at->format('d F Y H:i a') }}

I wanted a solution that I could use over and over again that would provide an easy way to present model data in an elegant way. I started with just creating basic accessor methods on the model class:

public function getPresentableCreatedAtAttribute(): string
{
    return $this->created_at->format('d F Y H:i a');
}
You can use accessor methods as model presenters

This can then be used in a view whenever I need it:

{{ $article->presentable_created_at }}

This actually worked really well. Maybe I have the wrong term here, but I like to call them model presenters. Laravel already provides everything we need to make this work. You can even make it reusable by putting that method in a trait and using it in each model where needed. If this works for you, then read no further.

// app\Http\Presenters\ArticlePresenter.php

namespace App\Http\Presenters;

trait ArticlePresenter
{
    public function getPresentableCreatedAtAttribute(): string
    {
        return $this->created_at->format('d F Y H:i a');
    }
}
Presenter Trait

But if you’re like me and you like to figure out alternate solutions to things like this, then you’ll understand why I wrote a small package that does this for me. It’s called IsPresentable. The first version really just built a little on the idea of the accessor method. Simply adding the IsPresentable trait to a model gave you thr abilty to create create presentable methods like this:

public function presentableCreatedAt(): string
{
    return $this->created_at->format('d F Y H:i a');
}

Then, in a Blade view, it could be used like this:

{{ $article->presentable()->created_at }}

Again, putting these presentable methods in a trait made them portable. I could simply add the trait to any model I needed the presentable methods in.

Since that first version, the package has grown up a little and now adds a bunch of new features, like presentable classes.

IsPresentable also overrides the toArray method on the model so when it get cast to an array or a JSON string, presenters are included automatically. This means it can be used in a JavaScript front-end like this:

article.presentable.created_at;

There's a few other features that make IsPresentable special, but at it's heart is the PHP reflection API.

What is the Reflection API?

PHP's Reflection API allows us to inspect our code at runtime. For example, the IsPresentable package needs to get a list of methods from the current class whose names begin with the word presentable. The Reflection API gives us that ability without needing to ask the user to explicitly state the method names.

A simple example

Using a Laravel model as as example, let's get all the methods that start with the word presentable and return a Laravel collection. To help make this reusable we'll create a new trait called IsPresentable. Create a new trait at app/Http/Presenters/IsPresentable.php and add a protected method named getPresentableMethods:

namespace App\Http\Presenters;

use Illuminate\Support\Collection;

trait IsPresentable
{
    protected function getPresentableMethods(): Collection
    {
        //...
    }
}

This method needs to get all the methods that start with presentable and return their a collection of names. The simplest start to the Reflection API is passing the current class to a new instance of ReflectionClass. This will give us a bunch of information about the current class including all its methods:

<?php

namespace App\Http\Presenters;

use Illuminate\Support\Collection;
use ReflectionClass;

trait IsPresentable
{
    protected function getPresentableMethods(): Collection
    {
        $reflection = new ReflectionClass($this);
        $methods = $reflection->getMethods();
    }
}

Obviously, we don't want all the methods, so we can create a new collection and use the filter method to get only the methods we want:

<?php

namespace App\Http\Presenters;

use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use ReflectionClass;
use ReflectionMethod;

trait IsPresentable
{
    protected function getPresentableMethods(): Collection
    {
        $reflection = new ReflectionClass($this);
        
        $methods = collect($reflection->getMethods())
            ->filter(fn (ReflectionMethod $method) => Str::startsWith($method->name, 'presentable'));
    }
}

Now we can use map to get the names of the methods and return the resulting collection:

<?php

namespace App\Http\Presenters;

use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use ReflectionClass;
use ReflectionMethod;

trait IsPresentable
{
    protected function getPresentableMethods(): Collection
    {
        $reflection = new ReflectionClass($this);
        
        $methods = collect($reflection->getMethods())
            ->filter(fn (ReflectionMethod $method) => Str::startsWith($method->name, 'presentable'));

        return $methods->map(fn (ReflectionMethod $method) => $method->name);
    }
}

Cool. Now we have a collection of method names that all start with the word presentable.  Let's add another method called getPresentables which will return a snake-cased version of the method names and their return values as an array:

protected function getPresentables(): array
{
    return $this->getPresentableMethods()->mapWithKeys(function (string $methodName) {
        return [Str::snake(Str::after($methodName, 'presentable')) => $this->$methodName()];
    })->toArray();
}

The mapWithKeys method on the collection let's us specify the resulting key and the value. The Str::after($methodName, 'presentable') bit will remove the presentable word from the method name and Str::snake(...) will give us the snake case version of the remaining method name.

So if I have method named presentableCreatedAt which returns a date string, I'd end up with the following in my array:

[
    'created_at' => '2022-02-11 16:45:31',
]

Great! How do I use it? I think it would be cool to have something like this now:

$user->present()->created_at;
The IsPresentable package uses presentable() instead of present(). You can do the same here if you wish, but remember that our getPresentableMethods return ALL methods that start with presentable which would return this method as well.

To get this right, we'll need another class which we'll call Presenter. We'll pass the array we get from getPresentables(). The Presenter class doesn't need to be very complex. Create a new file at app/Http/Presenters/Presenter.php:

<?php

namespace App\Http\Presenters;

class Presenter
{
    public function __construct(protected array $presentables = [])
    {}

    public function __get($name): mixed
    {
        return Arr::get($this->presentables, $name);
    }
}

This class will simply accept the array of presentable keys and values. The magic __get() method will return the value of the of the given key.

Lastly, let's update the IsPresentable trait with a present() method:

<?php

namespace App\Traits;

use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use App\Presenter;
use ReflectionClass;
use ReflectionMethod;

trait IsPresentable
{
    public function present(): Presenter
    {
        return new Presenter($this->getPresentables());
    }

    protected function getPresentables(): array
    {
        return $this->getPresentableMethods()->mapWithKeys(function (string $methodName) {
            return [Str::snake(Str::after($methodName, 'presentable')) => $this->$methodName()];
        })->toArray();
    }

    protected function getPresentableMethods(): Collection
    {
        $reflection = new ReflectionClass($this);
        
        $methods = collect($reflection->getMethods())
            ->filter(fn (ReflectionMethod $method) => Str::startsWith($method->name, 'presentable'));

        return $methods->map(fn (ReflectionMethod $method) => $method->name);
    }
}

So now we can add the IsPresentable trait to any model and create new presentable methods by prefixing them with the word presentable. For example, say you wanted to format the created_at property on the User model, we can now do this:

namespace App\Models;

use App\Http\Presenters\IsPresentable;
use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    use IsPresentable;
    
    protected function presentableCreatedAt(): string
    {
        return $this->created_at->format('d F Y H:i a');
    }
}

And you can output it like this:

$user->present()->created_at

There's a lot we can do with the Reflection API and this is a really simple example. Our IsPresentable package is able to do a lot more and is quite flexible.

Subscribe to THEPUBLICGOOD Blog

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe