How to ensure only a single model has a certain value in Laravel?
In every other project I encounter this problem, and although it's not particularly challenging, I have to look up every single step every time I have to solve it.
Here's a quick tutorial, by me, to future me.
The problem
There are loads of scenarios in which your business logic allows only a single model in the database to contain a certain property value.
Some examples include:
- A table of news articles in which only a single one can be highlighted.
- A collection of users, of which only a single one can be appointed the contact person for certain emails.
- Answers to a question, of which only a single one can be the “pinned” answer.
In these cases your CMS will most likely contain a checkbox that will flip this property from yes to no per model.
It’s very important that in case of yes, no other models contain yes!
Let’s stick with the highlighted news article example. Consider a table of articles with a boolean column is_highlighted
. At any given time, only a single article can be the highlighted article!
Here’s what you do.
The model
Using the $dispatchesEvents
property of a model, you can specify lifecycle events. These are events that are dispatched when saving, deleting or retrieving a model.
We can use the saving
event to listen for changes in the model. It’ll be dispatched before inserting and updating a model.
namespace App\Models;
use App\Events\SavingArticle;
use Illuminate\Database\Eloquent\Model;
class Article extends Model
{
protected $casts = [
'is_highlighted' => 'boolean',
];
protected $dispatchesEvents = [
'saving' => SavingArticle::class,
];
}
Events are an elegant way of decoupling business logic from your model and dividing functionality in bite-sized, condensed pieces that are easy to understand at a glance, as you’ll see in the next sections.
📖 Read the Laravel documentation about model events
The event
You can easily create the event class using Artisan:
$ php artisan make:event SavingArticle
This will create an object scaffolding for you.
We don’t have to add a lot to it, but the final class should look like this:
namespace App\Events;
use App\Models\Article;
use Illuminate\Queue\SerializesModels;
class SavingArticle
{
use SerializesModels;
public Article $article;
public function __construct(Article $article)
{
$this->article = $article;
}
}
As you can see, an event object is a fairly shallow object that’s used to transport values from the model to any listeners. It’ll receive the model being updated in the constructor. In our case that’s all we need.
📖 Read more about defining Laravel events
The listener
Now that we have an event object, we need something to listen for the event. Again, Laravel helps out with an Artisan command:
$ php artisan make:listener EnsureSingleHighlight
This will scaffold a listener class. We will want to tweak it so it looks like this:
namespace App\Listeners;
use App\Events\SavingArticle;
use App\Models\Article;
class EnsureSingleHighlight
{
public function handle(SavingArticle $event): void
{
if (!$event->article->is_highlighted) {
return;
}
Article::where('id', '!=', $event->article->id)->update([
'is_highlighted' => false,
]);
}
}
A listener needs a single handle
method, which will receive the event object as defined above. Because we made the model a public property of the event class, the listener can easily read its values.
From there we can run an UPDATE
query, which removes the highlight from any models that are not the one currently being saved.
Almost there!
The Service Provider
Last but not least, we can glue listener to event in the EventServiceProvider
. In Laravel, this is the place where all event listeners are registered.
Map the event to the listener inside its $listen
array:
namespace App\Providers;
use App\Events\SavingArticle;
use App\Listeners\EnsureSingleHighlight;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
class EventServiceProvider extends ServiceProvider
{
protected $listen = [
SavingArticle::class => [EnsureSingleHighlight::class],
];
}
💡 Note that the values in this array are arrays as well. That way, you can specify multiple listeners for a single event, and create a truly decoupled application.
All done!
And that’s it!
This will ensure only a single model in your table will ever contain a true
value for the is_highlighted
property.
You can stretch this concept and use events and listeners in all kinds of ways to ensure database integrity.