Laravel model presenters with the Reflection API

Photo by Pepe Reyes on Unsplash

I’ve spent a number of years writing on Medium, and I’ve really enjoyed the experience. However, after waiting forever for Medium to provide some better tools for writing these sorts of technical articles, I’ve realised that it’s probably not going to happen. Therefore, I’ve decided to move some of my content to my own blog platform where I have more control. Since I also run a small dev company, it made sense to me to create a company blog instead.

I still love Medium and I’ll continue to publish here for the foreseeable future, but it won’t be everything. I’m trying to force myself to write more these days, so there will likely be more content there and only some stuff on Medium.

If you’re interested in supporting me, then please take a look at https://blog.thepublicgood.dev/. There’s no charge, I promise. It’s still early days and I’m still working things out, but I like where I’m going with this.

If you want to read this article on the blog instead, you can find it here: https://blog.thepublicgood.dev/using-reflection-api-to-create-laravel-model-presenters/.

Thanks for reading my stuff.

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');
}

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');
}
}

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:

<?phpnamespace 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:

<?phpnamespace 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:

<?phpnamespace 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 getPresentableMethodsreturn 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:

<?phpnamespace 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:

<?phpnamespace 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.

--

--

--

Programmer, musician, cyclist (well... I own a bike), husband and father.

Love podcasts or audiobooks? Learn on the go with our new app.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Warrick Bayman

Warrick Bayman

Programmer, musician, cyclist (well... I own a bike), husband and father.

More from Medium

Laravel Eloquent relationships have a bunch of “with” methods you might not know about.

Laravel - Send an asynchronous request with Guzzle

Little Disastrous Mistakes : Laravel

Laravel — validate “true”​ or “false”​ as boolean