BLACKFYRE
MG—01
LANG
←
ALL POSTS
05 · Writing / 2018
12 NOV 2018 · 3 MIN READ

Dynamic Attributes for Eloquent models

Store the slug, not the URL: accessors and $appends give an Eloquent model attributes that are computed from its data, in PHP and in JSON.

CONTENTS
05 +

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:

app/Post.php
PHP
1<?php
2
3namespace App;
4
5use Illuminate\Database\Eloquent\Model;
6
7class Post extends Model
8{
9 //
10}

The posts table has a slug column, and there’s a named route that turns it into a URL:

routes/web.php
PHP
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:

app/Post.php
PHP
1<?php
2
3namespace App;
4
5use Illuminate\Database\Eloquent\Model;
6
7class Post extends Model
8{
9 /**
10 * The post's public URL, built from its slug.
11 *
12 * @return string
13 */
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:

resources/views/posts/index.blade.php
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:

app/Post.php
PHP
1<?php
2
3namespace App;
4
5use Illuminate\Database\Eloquent\Model;
6
7class Post extends Model
8{
9 /**
10 * The accessors to append to the model's array form.
11 *
12 * @var array
13 */
14 protected $appends = ['url'];
15
16 /**
17 * The post's public URL, built from its slug.
18 *
19 * @return string
20 */
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:

PHP
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:

app/Post.php
PHP
1/**
2 * Estimated reading time in minutes, at 200 words per minute.
3 *
4 * @return int
5 */
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 matching setUrlAttribute mutator.

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!

TAGS
Laravel
PHP
Eloquent
06 · CONTACT

Have a system that needs building?

GET IN TOUCH → PROJECTS →
PRODUCT DATA SHEET
MG—01
BLACKFYRE
S/N MG-1985-1027
Miklós Galicz — Golang Advocate · Solution Architect
MODEL
MG—01 "Miklós Galicz"
SERIES
1985
ORIGIN
Nagykovácsi, Hungary
FUNCTION
Senior Full Stack Engineer · Solution Architect
CORE LANGUAGES
Go · PHP · JavaScript
SPOKEN
Hungarian · English · German
SERVICE LIFE
~20 years in software, ongoing
POWER SUPPLY
Coffee, 2–4 cups / day
DIMENSIONS
1 × human, standard size
OPERATING TEMP.
Calm under production incidents
CONNECTIVITY
[email protected] · github.com/blackfyre · linkedin.com/in/galiczmiklos
Less, but better. Specifications subject to continuous improvement.
● ● ●
MG—01 · SERIES 1985
№ MG-1985-1027
CERTIFICATE OF OPERATION
Certified Operator
Has located every documented feature of the MG—01 without reading the manual. Probably.
TIME
—
FEATURES
—
DATE
—
SIGNED
Miklós Galicz