spatie/laravel-model-flags
Add lightweight flags to Eloquent models without extra columns. Set and check flags, then query with handy flagged/notFlagged scopes. Ideal for idempotent, restartable jobs and commands (e.g., send a mail only once per user).
Installation:
composer require spatie/laravel-model-flags
Publish the config (optional, default settings work out-of-the-box):
php artisan vendor:publish --provider="Spatie\LaravelModelFlags\LaravelModelFlagsServiceProvider"
Usage on a Model:
Use the HasFlags trait in your Eloquent model:
use Spatie\LaravelModelFlags\HasFlags;
class User extends Model
{
use HasFlags;
}
First Use Case: Check or set a flag on a model instance:
$user = User::find(1);
$user->hasFlag('received_email'); // false
$user->flag('received_email');
$user->hasFlag('received_email'); // true
Querying Flags: Fetch models with/without a flag:
User::flagged('received_email')->get(); // Users with the flag
User::notFlagged('received_email')->get(); // Users without the flag
Idempotent Operations: Use flags to track progress in long-running processes (e.g., batch emails, migrations, or API calls):
foreach ($users as $user) {
if (!$user->hasFlag('processed_payment')) {
$user->processPayment();
$user->flag('processed_payment');
}
}
Feature Rollouts: Gradually enable features for users:
if ($user->hasFlag('new_ui_enabled')) {
return view('new-ui');
}
Audit Trails: Track state changes without migrations:
$order->flag('shipped');
$order->flag('refund_processed');
Custom Flag Storage:
Override getFlagsAttribute() or setFlagsAttribute() to use a custom column or storage mechanism:
protected function getFlagsAttribute()
{
return $this->flags ?? json_decode($this->custom_flags_column, true);
}
Scopes for Complex Queries: Combine with other scopes:
User::active()->flagged('premium')->get();
Flag Groups:
Use prefixes or namespacing for related flags (e.g., email_*, payment_*):
$user->flag('email_welcome_sent');
$user->flag('email_invoice_sent');
Artisan Commands: Leverage flags to resume interrupted commands:
$users = User::notFlagged('backup_processed')->get();
foreach ($users as $user) {
$user->backup();
$user->flag('backup_processed');
}
Events/Observers: Trigger actions when flags are set/removed:
class UserObserver {
public function saving(User $user) {
if ($user->isDirty('flags') && $user->hasFlag('admin')) {
event(new AdminFlagUpdated($user));
}
}
}
API Responses: Include flags in API responses for client-side state management:
return UserResource::make($user)->additional([
'flags' => $user->flags,
]);
Flag Collisions:
Avoid using generic flag names (e.g., active, deleted) that conflict with existing model attributes or Laravel conventions. Prefix flags (e.g., email_verified).
Serialization Issues:
Flags are stored as JSON in a flags column. Ensure your database supports JSON (MySQL 5.7+, PostgreSQL). For older databases, override getFlagsAttribute() to use a text column with manual serialization.
Race Conditions: Concurrent processes may overwrite flags. Use transactions or optimistic locking:
DB::transaction(function () use ($user) {
if (!$user->hasFlag('processed')) {
$user->process();
$user->flag('processed');
}
});
Memory Usage:
Loading models with many flags can bloat memory. Use with() to eager-load flags only when needed:
User::with('flags')->find($id); // Avoid if flags aren't required.
Testing Quirks: Reset flags in tests to avoid test pollution:
public function tearDown(): void {
$this->user->flags = [];
$this->user->save();
}
Flag Not Saving:
Verify the flags column exists and is JSON-compatible. Check for typos in flag names (case-sensitive).
Query Performance:
Scopes (flagged, notFlagged) use JSON_CONTAINS (MySQL) or jsonb operators (PostgreSQL). Add indexes to the flags column:
-- MySQL 5.7+
ALTER TABLE users ADD FULLTEXT(flags);
-- PostgreSQL
CREATE INDEX users_flags_idx ON users USING gin(flags);
Flag Data Corruption: If flags become malformed (e.g., invalid JSON), manually repair the column:
$user->flags = json_decode($user->flags, true) ?: [];
$user->save();
Custom Flag Validation:
Override flag() to validate flags before setting:
public function flag(string $flag): void
{
if (!preg_match('/^[a-z_]+$/', $flag)) {
throw new \InvalidArgumentException("Flag must be lowercase alphanumeric.");
}
parent::flag($flag);
}
Flag Expiry:
Add TTL to flags using a flagged_at column:
public function flag(string $flag, ?Carbon $expiresAt = null): void
{
$this->flags[$flag] = $expiresAt?->timestamp;
$this->save();
}
public function hasFlag(string $flag): bool
{
return $this->flags[$flag] ?? false &&
(!isset($this->flags[$flag]) || now()->timestamp() < $this->flags[$flag]);
}
Flag Events: Dispatch events when flags change:
protected function setFlagsAttribute($value)
{
$oldFlags = $this->flags;
$this->flags = $value;
$this->save();
$this->dispatchFlagEvents($oldFlags);
}
private function dispatchFlagEvents(array $oldFlags)
{
foreach (array_diff($this->flags, $oldFlags) as $flag) {
event(new FlagAdded($this, $flag));
}
foreach (array_diff($oldFlags, $this->flags) as $flag) {
event(new FlagRemoved($this, $flag));
}
}
Bulk Flag Operations: Add static methods for batch updates:
class User extends Model {
public static function flagMany(array $ids, string $flag): void
{
self::whereIn('id', $ids)->get()->each(fn ($user) => $user->flag($flag));
}
}
Flag Migration Helper:
Create a migration helper to add the flags column:
use Spatie\LaravelModelFlags\Migrations\FlagsMigration;
Schema::table('users', function (Blueprint $table) {
FlagsMigration::jsonColumn($table, 'flags');
});
How can I help you explore Laravel packages today?