Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Filament Resource Lock Laravel Package

androsamp/filament-resource-lock

Record-level locking for Filament v5 edit pages: others instantly see who owns the lock, are blocked from accidental overwrite, and can request handoff. Includes optional audit history with per-field diff and selective rollback, plus polling or Laravel Echo push updates.

View on GitHub
Deep Wiki
Context7

Getting Started

Install the package, run the installer, and migrate — the installer wires up everything including a JS hook in resources/js/bootstrap.js:

composer require androsamp/filament-resource-lock
php artisan filament-resource-lock:install
php artisan migrate

Two files matter on day one:

  • config/filament-resource-lock.php — the whole behavioural contract (update_driver, storage.driver, ttl_seconds, permissions, audit).
  • The two published migrations (locks table + resource_lock_audits). Verify they actually land in database/migrations before migrating, especially in a monorepo with a path repository.

Then the three-trait quick start:

// 1. Model
class Customer extends Model
{
    use \Androsamp\FilamentResourceLock\Concerns\HasResourceLocks;
}

// 2. Edit page
class EditCustomer extends EditRecord
{
    use \Androsamp\FilamentResourceLock\Concerns\InteractsWithResourceLock;

    protected static string $resource = CustomerResource::class;
}

// 3. Table columns
public static function table(Table $table): Table
{
    return $table->columns([
        ResourceLockColumn::make(),
        ResourceAuditCreatorColumn::make(), // optional, needs audit on CreateRecord
    ]);
}

First use case to prove out: a single EditRecord page with update_driver => 'heartbeat' and storage.driver => 'database'. Open the same record in two browser profiles, confirm the second session gets a disabled form plus owner name, then exercise ask_to_unblock and save_and_unlock. That one loop validates config, migrations, permissions, and the polling route in about ten minutes. Do this before touching broadcast or audit — they are independent layers and each adds its own failure modes.

Note there is no panel plugin registration required. That is a deliberate design choice and the biggest ergonomic win: the traits hook into the page lifecycle directly, so you can retrofit locking onto existing resources without touching your panel provider.

Implementation Patterns

Put the lock trait on a base page, not per-resource. If more than a couple of resources need locking, a LockableEditRecord extends EditRecord (and LockableCreateRecord) that uses InteractsWithResourceLock keeps the codebase honest and makes a later config change a one-liner:

abstract class LockableEditRecord extends EditRecord
{
    use InteractsWithResourceLock;
    use HasResourceAudit;   // if audit is on for the whole panel
}

Treat lock and audit as separate adoption phases. HasResourceAudit works standalone (HasResourceLocks not required), but the two together group history entries by lock_cycle_id, which is what makes the history slide-over readable — one editing session == one group. Add audit later, once locking is stable in production.

Wire audit into both EditRecord and CreateRecord for a complete history. Without the create-page trait there is no event = created entry, so ResourceAuditCreatorColumn renders — for every row. If you want "who created this" in the list table, you need:

class CreateProduct extends CreateRecord
{
    use HasResourceAudit;
}

Expose the history action via getHeaderActions() rather than burying it — it returns a Filament action you place yourself:

protected function getHeaderActions(): array
{
    return [
        $this->getAuditHistoryAction(),
        Actions\DeleteAction::make(),
    ];
}

Use policies, not the config null, for real permission control. The defaults check auth()->user()?->can('filament-resource-lock.save_and_unlock'). Grant them to team leads via a Gate::before or a policy rather than setting permission => null, which silently disables the check for everyone — including users who should only be able to request a handoff, not force one.

Prefer broadcast only when you already run Echo. Migration path is heartbeat → broadcast with one config flip, but it requires a working broadcasting stack, window.Echo with private(), and a correctly edited resources/js/filament-resource-lock/echo.js. Do not adopt Reverb just for this package; 10-second polling is fine for most admin panels and dramatically simplifies ops.

Tune ttl_seconds against your heartbeat interval, not your patience. The TTL is the abandon window — a tab closed without a clean unload. If ttl_seconds is too short you get spurious lock theft during a slow save; too long and a crashed browser blocks a record for minutes. Keep release_grace_seconds (default ~3s) small and stale_soft_release_ignore_seconds large enough to absorb delayed unload pings.

Add HasAuditDiffPreview for every custom field that appears on an audited form. The built-ins cover TextInput, numeric, Select, Toggle, RichEditor/MarkdownEditor, KeyValue/JSON — anything else (map pickers, repeater-derived JSON, custom money widgets) falls back to a plain before/after dump that will look broken next to the styled renderers.

Gotchas and Tips

Overriding save() silently kills audit. This is the single most expensive trap in the package. The trait's save() wraps syncResourceAuditBeforeSave() → parent::save() → syncResourceAuditAfterSave(); a save() on your page class replaces it wholesale. If you need a custom save, reproduce the wrapper exactly:

public function save(bool $shouldRedirect = true, bool $shouldSendSavedNotification = true): void
{
    $this->syncResourceAuditBeforeSave();
    parent::save($shouldRedirect, $shouldSendSavedNotification);
    $this->syncResourceAuditAfterSave();
}

Same class of problem for afterCreate() — call syncResourceAuditAfterCreate() after persistence. v2.1.0 added a warning for the save() case, so if you are on an older tag you will get silent data loss in the audit trail rather than a notice. Grep your resources for function save( and function afterCreate( before rolling audit out.

Pin the version at ^2.1.2 or later. Two fixes matter operationally: v2.1.2 resolves a UniqueConstraintViolationException when the same user opens one record in two tabs (same browser), and v2.1.1 fixes the form not refreshing after a lock is acquired via the unlock-request flow. Both are the kind of bug you would otherwise chase through your own code for an afternoon. With 7 stars and zero dependents, you are effectively an early adopter — use composer.lock and read each release note.

APP_URL must be correct or the signed filament-resource-lock.release route fails validation and soft release breaks. This bites hardest in local Docker setups and behind proxies — verify APP_URL matches the hostname in the browser, and that TrustProxies is configured if you are behind a load balancer, otherwise wire:navigate departures will leave dangling locks until TTL expiry.

Audit growth is pruned per-resource, not globally. audit.max_entries_per_resource (default 500) trims oldest rows on write. On a wide table with heavy editing that is 500 full form snapshots per record; size the resource_lock_audits payload column accordingly (JSON/longText) and confirm pruning runs by inspecting row counts after a few hundred saves.

Rollback creates a new version, it does not rewind. The service writes a fresh audit entry representing the rollback diff. That is the right design — history is append-only — but it means "undo the undo" is just another rollback, and your audit count inflates faster than you expect on records users fight over.

Audit preview callbacks render trusted HTML. auditDiffPreviewUsing() output goes straight into the DOM. Escape every user-controlled fragment with e(); a field holding a customer-supplied name is an XSS vector otherwise.

Redis storage changes your debugging story. With storage.driver => 'redis', lock state is not queryable via SQL — ResourceLockColumn still works, but ad-hoc "who holds this record" debugging now needs redis-cli and your key prefix. Keep database during initial rollout so you can inspect the locks table directly, then switch if write volume justifies it.

Extension points worth knowing: the channel prefix, event name, and renew_interval_seconds under transports.broadcast, plus user_model / user_display_column for panels whose user model is not App\Models\User or whose display name is not name. Both are common early-stumble points in multi-guard apps and are pure config, no subclassing needed.

Localization covers en and ru only via filament-resource-lock::resource-lock.*. Any other locale falls back to English with no warning — plan a publish-and-override step if your panel ships in another language.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
oxygen/data
oxygen/core
jengo/base
xiloryx/log-collector
a21ns1g4ts/filament-stripe
la-php/laravel-admin
artisan-build/community-prompts
vleroy/smart
ayles-software/laravel-australian-public-holidays
cboxdk/laravel-id-spatie
cboxdk/laravel-id-billing
cboxdk/laravel-id-billing-bridge
simple-as-fuck/php-performance-log
hsluv/hsluv
seampact/codegen
seampact/hooks
seampact/ir
mehedimi/laravel-amp-mail
happones/laravel-vue-i18n-generator
jwerd/inertia-table