A Laravel package to add flags to Eloquent models
I'm proud to announce that our team has released a new package called spatie/laravel-model-flags. This one makes it easy to add flags to any model in your app.
In this post, I'd like to share why we've built it and showcase its usage.
The problem we are going to solve
In multiple projects, such as Oh Dear, Flare, and Mailcoach Cloud, we sometimes have to inform our users with important information about the service. Maybe a new feature was introduced that we like to highlight, or maybe we had some downtime and want to give a post-mortem...
Usually, we create a new Mailable and an Artisan command to inform all of our users. It might look something like this.
namespace App\Console\Commands;
use App\Models\User;
use App\Mails\ExtraMail;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Mail;
class SendExtraMailCommand extends Command
{
protected $signature = 'send-extra-mail';
protected $description = 'Send extra mail to all users';
public function handle()
{
$this->info('Sending extra mail to all users...');
User::each(function(User $user) {
$this->comment("Sending mail to {$user->email}");
Mail::to($user->email)->queue(new ExtraMail());
});
$this->info('All done!');
}
}
If everything goes well, that command works just fine! But imagine something goes wrong while running the command. Maybe your database connection or whole server goes down during the command? How would you know, which users have been mailed already, and which have not? You could start digging your email logs (if your email provider offers that), but that is not handy and likely time-consuming.
Also, imagine that you accidentally execute this command twice. Congratulations, you've now mailed every user the identical mail twice, and this is obviously not good.
Both of these problems can be solved by making your command idempotent. This is a fancy way of saying that your command is restartable. Using our new spatie/laravel-model-flags package, you can make any piece of code idempotent very quickly.
I shall say... err, execute this only once
After having installed spatie/laravel-model-flags, you can make a model flaggable by letting it use Spatie\ModelFlags\Models\Concerns\HasFlags
.
use Illuminate\Database\Eloquent\Model;
use Spatie\ModelFlags\Models\Concerns\HasFlags;
class User extends Model
{
use HasFlags;
}
Now, you can add flags to your model.
// add a flag
$user->flag('myFlag');
// returns true if the model has a flag with the given name
$user->hasFlag('myFlag');
// remove a flag
$user->unflag('myFlag');
Behind the scenes, the flags and the relation to a model will be stored in the flags
table.
Additionally, you'll also get some very handy scopes.
// query all models that have a flag with the given name
User::flagged('myFlag');
// query all models which not have a flag with the given name
User::notFlagged('myFlag');
Let's revisit our mail-sending Artisan command mentioned above. Using flags, we can ensure that a user will only be mailed once.
namespace App\Console\Commands;
use App\Models\User;
use App\Mails\ExtraMail;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Mail;
class SendExtraMailCommand extends Command
{
protected $signature = 'send-extra-mail';
protected $description = 'Send extra mail to all users';
public function handle()
{
$this->info('Sending extra mail to all users...');
User::notFlagged('hasReceivedExtraMail')
->each(function (User $user) {
$this->comment("Sending mail to {$user->email}");
Mail::to($user->email)->queue(new ExtraMail());
$user->flag('hasReceivedExtraMail');
});
$this->info('All done!');
}
}
The code above will only select users that have not been flagged. When a user is mailed, it will be flagged immediately. Executing the command multiple times would not be a problem at all now. No users would be selected in the second (or third, or fourth, ...) invocation, as they all have been flagged in the first invocation.
Also, should your command crash (or be canceled) while it's running, you can just execute it again. Only the users that didn't receive the mail yet will get one.
It's not 100% water-tight, as the command could fail or crash after the mail has been sent and before the flag has been added. In most cases however, using flags is good enough to protect you from the problems described above.
In closing
I've coded flag-like functionality in multiple projects before, and I'm glad I finally took the time to extract it to package. I hope this one will be useful for you too. I want to thank my colleagues Alex and Seb for their advice on naming things.
This isn't the first time our team has extracted common functionality from a project to a reusable package. Please look at this extensive list of packages we created before.
If you want to support our open-source efforts, consider picking up one of our paid products or subscribe to our premium services Flare or Mailcoach.
that's very useful, I can already imagine this simplifies database design for many projects,
Permission package is always included in all my projects, I guess I'll include this one to that list as well