androsamp / filament-resource-lock
Resource locking for Filament edit pages via Livewire heartbeat or broadcast.
Package info
github.com/AndroSamp-it/filament-resource-lock
pkg:composer/androsamp/filament-resource-lock
Requires
- php: ^8.2
- filament/filament: ^5.0
- laravel/framework: ^12.0 || ^13.0
Requires (Dev)
- larastan/larastan: ^3.0
- orchestra/testbench: ^10.0 || ^11.0
- phpunit/phpunit: ^11.0 || ^12.0
README
Record-level locking for Filament v5 edit pages with optional audit history.
When one user edits a record, others immediately see who owns the lock, get blocked from accidental overwrite, and can request handoff. The package supports both classic polling and Laravel Echo push updates.
Preview
Lock state, collaboration actions, and audit history in one flow.
- PHP:
^8.3 - Laravel:
^12.0 || ^13.0 - Filament:
^5.0 - Packagist:
androsamp/filament-resource-lock
Contents
- Why this package
- Install
- Quick start (3 steps)
- Configuration
- Broadcast mode (Laravel Echo)
- Audit history (snapshots + rollback)
- Custom fields in audit diff
- Soft release route
- Localization
- Development notes
- License
Why this package
Main goals
- Safe collaborative editing on
EditRecordpages. - Predictable behavior in SPA (
wire:navigate). - Simple integration without extra panel plugin registration.
- Configurable transport and storage (
heartbeat/broadcast,database/redis). - Optional built-in audit with visual per-field diff and selective rollback.
Key behavior
- User A opens record -> lock is acquired (or refreshed).
- User B opens same record -> form/save are disabled, lock owner is shown.
- User B can request unlock (
ask_to_unblock), if enabled. - User A can save and hand over lock (
save_and_unlock), if enabled. - In
broadcastmode, updates are pushed via Echo with lower latency than polling.
Install
composer require androsamp/filament-resource-lock php artisan filament-resource-lock:install php artisan migrate
filament-resource-lock:install does the following:
- publishes
config/filament-resource-lock.php; - publishes package migrations (locks + audit tables);
- publishes
resources/js/filament-resource-lock/echo.js; - injects
import './filament-resource-lock/echo';intoresources/js/bootstrap.js(if missing).
Publish only specific resources
php artisan vendor:publish --tag=filament-resource-lock-config php artisan vendor:publish --tag=filament-resource-lock-migrations php artisan vendor:publish --tag=filament-resource-lock-assets
Quick start (3 steps)
1) Add lock relation to model
use Androsamp\FilamentResourceLock\Concerns\HasResourceLocks; use Illuminate\Database\Eloquent\Model; class Customer extends Model { use HasResourceLocks; }
2) Add lock behavior to EditRecord page
use Androsamp\FilamentResourceLock\Concerns\InteractsWithResourceLock; use Filament\Resources\Pages\EditRecord; class EditCustomer extends EditRecord { use InteractsWithResourceLock; protected static string $resource = CustomerResource::class; }
3) Show lock indicator in list table
use Androsamp\FilamentResourceLock\Resources\Columns\ResourceLockColumn; use Filament\Tables\Table; public static function table(Table $table): Table { return $table->columns([ ResourceLockColumn::make(), // ... ]); }
Configuration
All options live in config/filament-resource-lock.php.
Most important keys
update_driver:heartbeatorbroadcast.storage.driver:databaseorredis.ttl_seconds: lock expiration window without heartbeat.release_grace_seconds: grace period for soft release in broadcast flow.stale_soft_release_ignore_seconds: protection from stale unload pings.user_model: lock owner model class.user_display_column: attribute shown in UI and notifications.permission.save_and_unlock.*: enable/guard transfer action.permission.ask_to_unblock.*: enable/guard unlock request action.audit.*: audit feature toggles and retention.
Permissions
By default, actions use auth()->user()?->can(...):
filament-resource-lock.save_and_unlockfilament-resource-lock.ask_to_unblock
Set permission to null to skip policy check for that action.
Example config skeleton
return [ 'update_driver' => 'heartbeat', // heartbeat | broadcast 'storage' => [ 'driver' => 'database', // database | redis ], 'ttl_seconds' => 20, 'release_grace_seconds' => 3, 'permission' => [ 'save_and_unlock' => [ 'enabled' => true, 'permission' => 'filament-resource-lock.save_and_unlock', ], 'ask_to_unblock' => [ 'enabled' => true, 'permission' => 'filament-resource-lock.ask_to_unblock', ], ], 'audit' => [ 'enabled' => true, 'table' => 'resource_lock_audits', 'max_entries_per_resource' => 500, ], ];
Broadcast mode (Laravel Echo)
heartbeat checks state on interval (for example, every 10 seconds).
broadcast pushes updates through private channels, so lock changes and notifications arrive almost instantly.
Setup checklist
- Configure Laravel broadcasting (Reverb/Pusher/Ably/etc.) per official docs.
- Make sure frontend exposes
window.Echowithprivate(). - Keep published
resources/js/filament-resource-lock/echo.jsaligned with your broker/env setup. - Set:
'update_driver' => 'broadcast'
- Optionally tune:
transports.broadcast.channel_prefixtransports.broadcast.eventtransports.broadcast.renew_interval_seconds
Official guide: Laravel Broadcasting
Audit history (snapshots + rollback)
The package can store versioned snapshots of form state and render visual per-field diffs.
What happens on save
- Snapshot of previous state is captured.
- New snapshot is captured after save.
- Changed fields are computed (
oldvsnew). - A new audit version is stored in
resource_lock_audits. - If limit is exceeded, oldest rows are pruned (
audit.max_entries_per_resource).
Add audit to EditRecord
use Androsamp\FilamentResourceLock\Concerns\HasResourceAudit; use Androsamp\FilamentResourceLock\Concerns\InteractsWithResourceLock; use Filament\Resources\Pages\EditRecord; class EditProduct extends EditRecord { use InteractsWithResourceLock; use HasResourceAudit; protected function getHeaderActions(): array { return [ // ... other actions $this->getAuditHistoryAction(), ]; } }
HasResourceAudit works standalone, but together with lock trait it groups entries by lock_cycle_id.
Overriding save()
The trait defines save() that calls syncResourceAuditBeforeSave(), then parent::save(), then syncResourceAuditAfterSave(). In PHP, a save() method on your page class replaces the trait’s method entirely, so that wrapper is skipped unless you repeat it.
If you override save(), keep audit working by invoking the same two bridges around your persistence (typically parent::save()):
public function save(bool $shouldRedirect = true, bool $shouldSendSavedNotification = true): void { $this->syncResourceAuditBeforeSave(); parent::save($shouldRedirect, $shouldSendSavedNotification); $this->syncResourceAuditAfterSave(); }
If your implementation does not call parent::save(), call syncResourceAuditBeforeSave() before the record is written and syncResourceAuditAfterSave() after it is successfully persisted (and only then).
There is no Filament or PHP mechanism in this package that can inject “always run before/after save” when the page replaces save() with a completely custom flow: alternatives such as beforeSave() / afterSave() suffer from the same issue if those methods are overridden on the page. Until a better integration exists, the explicit calls above are the supported approach.
Rollback selected fields
From the audit history slide-over:
- open a version;
- choose fields via checkbox list;
- apply rollback.
The service restores selected field values and creates a new audit version representing rollback changes.
Supported diff renderers
TextInput-> plain before/after.TextInput (numeric)-> before/after + proportional bar.Select-> label-aware badge diff.Toggle-> visual on/off diff.RichEditor,MarkdownEditor-> rendered rich content diff blocks.KeyValue / JSON-> unified+/-style lines.- Other fields (
Textarea,DatePicker, etc.) -> plain before/after.
Custom fields in audit diff
For custom Filament fields, add HasAuditDiffPreview to provide custom HTML previews in history modal.
use Androsamp\FilamentResourceLock\Forms\Concerns\HasAuditDiffPreview; use Filament\Forms\Components\Field; class MapPicker extends Field { use HasAuditDiffPreview; protected function setUp(): void { parent::setUp(); $this->auditDiffPreviewUsing(function (mixed $state): string { $lat = is_array($state) ? ($state['lat'] ?? '-') : '-'; $lng = is_array($state) ? ($state['lng'] ?? '-') : '-'; return '<p class="text-sm">' . e($lat) . ', ' . e($lng) . '</p>'; }); } }
Security note: callback output is rendered as trusted HTML. Always escape user-controlled fragments.
Soft release route
The package registers signed route filament-resource-lock.release (web, signed middleware).
In broadcast flow it is used on tab close / SPA leave:
- lock is marked as releasing for a short grace period;
- same session can quickly reclaim after refresh;
- other sessions respect grace window.
Make sure APP_URL is correct, otherwise signed URL validation may fail.
Localization
Translations are loaded from:
filament-resource-lock::resource-lock.*
Included locales:
enru
Development notes
If package is connected via local path repository in monorepo, after code changes it is usually enough to run:
composer dump-autoload
License
MIT.
