spatie/laravel-model-flags
Add lightweight “flags” to Eloquent models via a trait—store process state without extra columns. Check, set, and clear flags, and query with flagged/notFlagged scopes. Ideal for idempotent, restartable jobs like one-time emails or migrations.
composer require spatie/laravel-model-flags
php artisan vendor:publish --tag="model-flags-migrations"
php artisan migrate
use Spatie\ModelFlags\Models\Concerns\HasFlags;
class User extends Model
{
use HasFlags;
}
Start by flagging a model to mark it as processed:
$user->flag('processed_payment');
Check if a model has a flag:
if (!$user->hasFlag('processed_payment')) {
// Process payment logic
$user->flag('processed_payment');
}
$user->flag('verified_email');
$user->hasFlag('verified_email'); // true
$user->flag(['verified_email', 'active_subscription']);
$user->hasFlag('verified_email'); // true
$user->hasFlag('active_subscription'); // true
User::flagged('verified_email')->get(); // All users with 'verified_email'
User::notFlagged('verified_email')->get(); // All users without 'verified_email'
User::flagged('verified_email')
->notFlagged('active_subscription')
->get();
User::where('role', 'admin')->get()->each->flag('admin_notified');
User::flagged('admin_notified')->each->unflag('admin_notified');
$user->lastFlaggedAt('verified_email'); // Carbon instance
$user->lastFlaggedAt(); // Last flagged time across all flags
Use flags to make commands idempotent:
// In an Artisan command
User::notFlagged('sent_welcome_email')
->each(function (User $user) {
Mail::to($user->email)->send(new WelcomeEmail());
$user->flag('sent_welcome_email');
});
Trigger actions when flags are set:
use Spatie\ModelFlags\Events\Flagged;
Flagged::listen(function (Flagged $event) {
if ($event->flagName === 'verified_email') {
event(new UserVerified($event->model));
}
});
Include flag status in API responses:
return UserResource::make($user)->additional([
'flags' => $user->flagNames(),
]);
Mock flags in tests:
$user = User::factory()->create();
$user->shouldReceive('flag')->once()->with('test_flag');
'active' for different purposes) can lead to unintended behavior.'auth.verified_email', 'billing.active_subscription').flagged(), notFlagged()) can be slow on large tables.flags table:
Schema::table('flags', function (Blueprint $table) {
$table->index(['model_type', 'model_id', 'name']);
});
1.1.0+ update).User::observe(FlagObserver::class);
class FlagObserver {
public function deleted(User $user) {
$user->flags()->delete();
}
}
DB::transaction(function () use ($user) {
if (!$user->hasFlag('processed')) {
$user->flag('processed');
// Process logic
}
});
\DB::table('flags')->where('name', 'verified_email')->exists();
flags relation:
$user->flags()->with('flaggable')->get();
$user->flag('admin_action');
\Log::info("Flagged user {$user->id} with 'admin_action'");
Flag model in config/model-flags.php:
'flag_model' => App\Models\CustomFlag::class,
class CustomFlag extends \Spatie\ModelFlags\Models\Flag {
protected $casts = [
'metadata' => 'array',
];
}
use Spatie\ModelFlags\Enums\FlagName;
$user->flag(FlagName::VerifiedEmail);
use Illuminate\Database\Eloquent\SoftDeletes;
class Flag extends \Spatie\ModelFlags\Models\Flag {
use SoftDeletes;
}
Flagged event:
class CustomFlagged implements ShouldBroadcast {
public function broadcastOn() {
return new PrivateChannel('flags');
}
}
'auth.*') and use a prefix:
$user->flag('auth.verified');
$user->hasFlag('auth.*'); // Wildcard check (requires custom scope)
expires_at column to flags and check expiry:
$user->flags()->where('expires_at', '>', now())->exists();
\DB::table('flags')
->where('name', 'verified_email')
->count(); // Total flagged models
flags table migration:
Schema::create('flags', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('model_type');
$table->unsignedBigInteger('model_id');
$table->timestamps();
$table->json('metadata')->nullable(); // Add custom fields
});
use Illuminate\Support\Facades\Validator;
$validator = Validator::make(['flag' => 'invalid flag'], [
'flag' => 'required|regex:/^[a-z_]+(\.[a-z_]+)*$/i',
]);
How can I help you explore Laravel packages today?