CONTENTS05 +
Have you ever wanted to build the URL of your post in the model, while storing only the slug? Or to do some calculation based on your stored data?
Well, here’s the solution!
The model
This is your average Laravel model:
1<?php23namespace App;45use Illuminate\Database\Eloquent\Model;67class Post extends Model8{9 //10}
The posts table has a slug column, and there’s a named route that turns it into a URL:
1Route::get('/blog/{slug}', 'PostController@show')->name('post');
The accessor
An accessor is a method named get{Attribute}Attribute. Eloquent calls it whenever you read $post->url, even though there’s no url column:
1<?php23namespace App;45use Illuminate\Database\Eloquent\Model;67class Post extends Model8{9 /**10 * The post's public URL, built from its slug.11 *12 * @return string13 */14 public function getUrlAttribute()15 {16 return route('post', [$this->slug]);17 }18}
The method name is the attribute in StudlyCase: getUrlAttribute gives you url, and getReadingTimeAttribute would give you reading_time.
That’s already enough for Blade:
Appending it to JSON
Accessors aren’t part of the model’s array form by default, so toArray() and toJson(), and with them every API response, leave url out. To include it, add the attribute to $appends:
1<?php23namespace App;45use Illuminate\Database\Eloquent\Model;67class Post extends Model8{9 /**10 * The accessors to append to the model's array form.11 *12 * @var array13 */14 protected $appends = ['url'];1516 /**17 * The post's public URL, built from its slug.18 *19 * @return string20 */21 public function getUrlAttribute()22 {23 return route('post', [$this->slug]);24 }25}
Now return Post::all(); from a controller gives every post its url, and the front end never has to know how URLs are put together.
If you only need it in some responses, skip $appends and add it on the spot instead:
1return $post->append('url');
Calculations
The same trick works for anything that can be derived from the stored data. For example, an estimated reading time from the body:
1/**2 * Estimated reading time in minutes, at 200 words per minute.3 *4 * @return int5 */6public function getReadingTimeAttribute()7{8 return (int) max(1, ceil(str_word_count(strip_tags($this->body)) / 200));9}
$post->reading_time is always in step with the body, and there’s no extra column to keep up to date.
A few things to keep in mind
- Appended attributes are computed every time the model is serialised. That’s fine for building a URL, but an accessor that runs a query will run it once per model. Return a list of 50 posts and you have 50 extra queries.
- Read relations you’ve eager loaded. If an accessor needs a relation, load it with
with()in the query, or the accessor will trigger the N+1 problem on its own. - Accessors are read-only. Writing
$post->url = '…'doesn’t change the slug. If you need that, add a matchingsetUrlAttributemutator.
And that’s it! You can add almost any kind of logic to the models this way, but be aware that the models should only provide data for your application, not run it!