androsamp/filament-resource-lock
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.
^8.3^12.0 || ^13.0^5.0androsamp/filament-resource-lockEditRecord pages.wire:navigate).heartbeat / broadcast, database / redis).ask_to_unblock), if enabled.save_and_unlock), if enabled.broadcast mode, updates are pushed via Echo with lower latency than polling.composer require androsamp/filament-resource-lock
php artisan filament-resource-lock:install
php artisan migrate
filament-resource-lock:install does the following:
config/filament-resource-lock.php;resources/js/filament-resource-lock/echo.js;import './filament-resource-lock/echo'; into resources/js/bootstrap.js (if missing).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
use Androsamp\FilamentResourceLock\Concerns\HasResourceLocks;
use Illuminate\Database\Eloquent\Model;
class Customer extends Model
{
use HasResourceLocks;
}
EditRecord pageuse Androsamp\FilamentResourceLock\Concerns\InteractsWithResourceLock;
use Filament\Resources\Pages\EditRecord;
class EditCustomer extends EditRecord
{
use InteractsWithResourceLock;
protected static string $resource = CustomerResource::class;
}
use Androsamp\FilamentResourceLock\Resources\Columns\ResourceLockColumn;
use Filament\Tables\Table;
public static function table(Table $table): Table
{
return $table->columns([
ResourceLockColumn::make(),
// ...
]);
}
All options live in config/filament-resource-lock.php.
update_driver: heartbeat or broadcast.storage.driver: database or redis.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.By default, actions use auth()->user()?->can(...):
filament-resource-lock.save_and_unlockfilament-resource-lock.ask_to_unblockSet permission to null to skip policy check for that action.
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,
],
];
heartbeat checks state on interval (for example, every 10 seconds).
broadcast pushes updates through private channels, so lock changes and notifications arrive almost instantly.
window.Echo with private().resources/js/filament-resource-lock/echo.js aligned with your broker/env setup.'update_driver' => 'broadcast'
transports.broadcast.channel_prefixtransports.broadcast.eventtransports.broadcast.renew_interval_secondsOfficial guide: Laravel Broadcasting
The package can store versioned snapshots of form state and render visual per-field diffs.
old vs new).resource_lock_audits.audit.max_entries_per_resource).EditRecorduse 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.
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.
From the audit history slide-over:
The service restores selected field values and creates a new audit version representing rollback changes.
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.Textarea, DatePicker, etc.) -> plain before/after.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.
The package registers signed route filament-resource-lock.release (web, signed middleware).
In broadcast flow it is used on tab close / SPA leave:
Make sure APP_URL is correct, otherwise signed URL validation may fail.
Translations are loaded from:
filament-resource-lock::resource-lock.*Included locales:
enruIf package is connected via local path repository in monorepo, after code changes it is usually enough to run:
composer dump-autoload
MIT.
How can I help you explore Laravel packages today?