laravel laravel-models model-observers

Laravel model observers

Written on 11th Jul 2022
It will take between 3 and 4 minutes to read this article

Laravel provides a lot of functionality out of the box and one thing I'd like to showcase today is Laravel Model Observers. Model Observers effectively are listeners for events happening on the model, for example when the model is created or updated.

The observer pattern is a software design pattern in which an object, called the subject, maintains a list of its dependents, called observers, and notifies them automatically of any state changes, usually by calling one of their methods. 1

Whilst sometimes at first glance it's not overly obvious what use case you might have for model observers, they do have a number of useful uses. Today, we're going to look at a real life need I have on this site. My blog is run using the themsaid/wink package. Whilst it is great to be able to use another package so I don't have to write a load of code to generate a blog system, there are sometimes limitations. For me, this limitation was that when I had created a post, the slug of the blog remained the same even if you updated the title. You can manually change it in the settings, but life's too short hey!

This is where Model Observers come into their own. Specified lifecycle elements of the lifecycle of a model can be 'hooked' into. The lifecycle hooks for observers are Retrieved, Creating, Created, Updating, Updated, Saving, Saved, Deleting, Deleted, Restoring and Restored.

So ideally what I want to do is when ever a blog post is created or updated, I want to give it my own slug, not the one that is automatically generated (or the one which doesn't update). We can do this by hooking into the creating and updating hooks.

To create an observer, you can use the artisan command

php artisan make:observer BlogObserver --model=WinkPost

This will create your new observer class in App\Observers but you can move it to where you'd like it to be stored. You should not it won't have all hooks listed in the generated file, but can publish the stubs and add them all if you want.

The two hooks we will need to use are creating and updating so these will need to be added as methods in our class and since we'll be doing the same code for both of these methods, I will place a call inside both to call our shared logic.

public function creating(WinkPost $post): void
{
// this gets fired before the model is created
$this->updateSlug($post);
}
 
public function updating(WinkPost $post): void
{
// this gets fired before the model is updated
$this->updateSlug($post);
}

Now we have our two 'hook' methods, it's time to create that shared functionality. So what we aim to do it update the slug before it's created or saved.

/**
* Update the slug
*
* @param WinkPost $post
*/
protected function updateSlug(WinkPost $post): void
{
$title = $post->getAttribute('title');
$revisedSlug = ($title === 'draft') ? Str::slug('draft-' . uuid()) : Str::slug($title);
 
$post->setAttribute('slug', $revisedSlug);
}

So in the above function we're getting the title of the post, we then check to see if the title is just 'draft', if it is we'll append a uuid to the end to make it a bit more unique, otherwise we'll use the title to make our slug. Either way we'll use laravel's \Illuminate\Support\Str::slug function to make sure we're 'slug safe'. Once we've got our slug we'll just update our slug attribute so it can be saved into the database.

However, those who are a bit more experience will say what happens if you change your slug, all the google indexed pages won't be able to find your post anymore. This is a potential problem and one which I solve by having a slug history table.

In my observer, I do a check to see if the slug is different from before, and if it is and the post has already been published, then it records the old slug for future use.

/**
* Update the slug
*
* @param WinkPost $post
*/
protected function updateSlug(WinkPost $post): void
{
$title = $post->getAttribute('title');
$originalSlug = $post->getAttribute('slug');
$published = $post->getAttribute('published');
$revisedSlug = ($title === 'draft') ? Str::slug('draft-' . uuid()) : Str::slug($title);
 
$post->setAttribute('slug', $revisedSlug);
 
if ($originalSlug !== $revisedSlug && $published == 1) {
$this->action->slugChange($post, $originalSlug);
}
}

$this->action->slugChange is another class with functionality to save the old slug into the history table. You could write in your model create method inside the updateSlug but I like to try to adhere to single responsibility.

Footnotes:

1: Observer Pattern - Wikipedia.

Code highlighting provided by torchlight.dev.