kevjo / laravel-collab
Real-time collaborative editing for Laravel with intelligent locking and conflict resolution.
Requires
- php: ^8.5
- illuminate/console: ^12.0
- illuminate/database: ^12.0
- illuminate/events: ^12.0
- illuminate/support: ^12.0
Requires (Dev)
- laravel/pint: ^1.0
- mockery/mockery: ^1.6
- orchestra/testbench: ^10.0
- phpunit/phpunit: ^11.0
This package is auto-updated.
Last update: 2026-04-16 10:27:26 UTC
README
Pessimistic locking for Laravel Eloquent models. Prevent multiple users from editing the same record simultaneously.
Requirements
- PHP 8.5+
- Laravel 12+
- MySQL 8+ or PostgreSQL (required for row-level locking via
lockForUpdate())
Note: SQLite works for development/testing but does not support row-level locking. Race condition protection requires MySQL or PostgreSQL in production.
Installation
composer require kevjo/laravel-collab
Run the install command:
php artisan collab:install
This publishes the config file, migrations, and runs the migrations.
Quick Start
1. Add the Trait to Your Model
use Kevjo\LaravelCollab\Traits\HasConcurrentEditing; class Post extends Model { use HasConcurrentEditing; }
2. Acquire and Release Locks
// Acquire a lock $result = $post->acquireLock(auth()->user()); if ($result->isFailed()) { return back()->with('error', "This post is being edited by {$result->getLockedBy()->name}" ); } // Release a lock $post->releaseLock(auth()->user()); // Lock is also auto-released after model update (configurable) $post->update($request->validated());
3. Check Lock Status
$post->isLocked(); // Is it locked by anyone? $post->isLockedByUser(auth()->user()); // Is it locked by me? $post->isLockedByAnother(auth()->user()); // Is it locked by someone else? $post->lockOwner(); // Get the User who holds the lock $post->lockExpiresAt(); // Carbon instance of expiration $post->lockRemainingTime(); // Seconds until expiration
Middleware
The package provides a collab.lock middleware that returns HTTP 423 (Locked) when a route-bound model is locked by another user.
// Specify which route parameter to check Route::put('/posts/{post}', [PostController::class, 'update']) ->middleware('collab.lock:post'); // Auto-detect all lockable models on the route Route::put('/posts/{post}', [PostController::class, 'update']) ->middleware('collab.lock');
The middleware:
- Returns 423 Locked with lock info if the model is locked by another user
- Passes through if the model is unlocked or locked by the current user
- Skips the check entirely if no user is authenticated
Configuration
Publish the config:
php artisan vendor:publish --tag=collab-config
// config/collab.php return [ 'default_strategy' => 'pessimistic', 'lock_duration' => [ 'default' => 3600, // 1 hour 'min' => 60, // 1 minute minimum 'max' => 86400, // 24 hours maximum ], 'auto_release_after_update' => true, // Release lock when model is updated 'prevent_update_if_locked' => true, // Throw exception if locked by another 'tables' => [ 'locks' => 'model_locks', 'history' => 'model_lock_history', ], 'history' => [ 'enabled' => true, 'retention_days' => 30, ], ];
Lock Options
// Custom duration $post->acquireLock($user, ['duration' => 600]); // 10 minutes // Field-level locking $post->acquireLock($user, ['fields' => ['title', 'content']]); // Check field-level locks $post->isFieldLocked('title'); // true $post->getLockedFields(); // ['title', 'content'] // Custom metadata $post->acquireLock($user, ['metadata' => ['reason' => 'bulk update']]);
Lock Management
// Extend a lock $post->extendLock(1800, $user); // 30 more minutes // Force release (admin use) $post->forceReleaseLock(); // Request lock from owner (fires LockRequested event) $post->requestLock($requester); // Get structured lock info for API responses $post->getLockInfo(); // Returns: ['is_locked' => true, 'locked_by' => [...], 'expires_at' => '...', ...] $post->getLockStatus($user); // Returns: ['is_locked' => true, 'can_edit' => false, 'is_owner' => false, ...]
Facade
The Collab facade provides system-wide lock management:
use Kevjo\LaravelCollab\Facades\Collab; // Query locks Collab::activeLocks(); Collab::expiredLocks(); Collab::getLocksFor($post); Collab::getActiveLockFor($post); Collab::isModelLocked(Post::class, 1); Collab::getLocksForModelType(Post::class); // Bulk operations Collab::releaseAllLocksForUser($userId); Collab::releaseAllLocks(); // Cleanup Collab::cleanupExpiredLocks(); Collab::cleanupOldHistory(); Collab::runCleanup(); // History Collab::getHistoryFor($post); Collab::getUserHistory($userId); // Stats Collab::getStatistics();
Events
All events are in the Kevjo\LaravelCollab\Events namespace:
| Event | Fired When | Properties |
|---|---|---|
LockAcquired |
Lock is successfully acquired | $model, $lock, $user |
LockReleased |
Lock is released by owner | $model, $user |
LockForceReleased |
Lock is force-released (admin) | $model, $lockOwner, $releasedBy |
LockRequested |
User requests lock from owner | $model, $requester, $lockOwner |
LockExpired |
Expired lock is cleaned up | $model, $lock |
Listen to events in your EventServiceProvider or with closures:
use Kevjo\LaravelCollab\Events\LockRequested; Event::listen(LockRequested::class, function (LockRequested $event) { $event->lockOwner->notify(new LockRequestNotification( $event->requester, $event->model )); });
Artisan Commands
# Clean up expired locks php artisan collab:cleanup # Clean up expired locks + old history php artisan collab:cleanup --all # Preview what would be deleted php artisan collab:cleanup --dry-run # Install the package php artisan collab:install
Add to your scheduler for automatic cleanup:
// app/Console/Kernel.php $schedule->command('collab:cleanup')->hourly();
Automatic Behaviors
The trait hooks into Eloquent model events:
- Before update: If
prevent_update_if_lockedistrueand the model is locked by another user, aModelLockedException(HTTP 423) is thrown. - After update: If
auto_release_after_updateistrue, the lock is automatically released. - On delete: All locks on the model are released with history entries created.
Testing
composer test
Credits
License
The MIT License (MIT). Please see License File for more information.