Installation:
composer require sofa/model-locking:"~5.3"
php artisan vendor:publish --provider="Sofa\ModelLocking\ServiceProvider"
php artisan migrate
Sofa\ModelLocking\ServiceProvider::class to config/app.php.Enable Locking:
Add the Locking trait to your Eloquent model:
use Sofa\ModelLocking\Locking;
class Post extends Model
{
use Locking;
}
First Use Case: Lock a model in a controller to prevent concurrent edits:
public function edit(Post $post)
{
if ($post->isLocked()) {
return response()->json(['error' => 'Post is locked'], 423);
}
$post->lock();
// Proceed with edit logic...
}
config/model_locking.php (default: 30-minute lock duration, locks table name).LockAcquired/LockReleased events (extendable via Locking trait).database/migrations/[timestamp]_create_model_locks_table.php (customize if needed).Locking a Model:
// Optimistic lock (checks before locking)
if (!$post->isLocked()) {
$post->lock(); // Creates a record in `locks` table
}
// Force lock (overrides existing locks)
$post->forceLock();
Handling Locks in Controllers:
public function update(Request $request, Post $post)
{
if ($post->isLocked()) {
return back()->with('error', 'Post is locked by another user');
}
$post->lock();
try {
$post->update($request->all());
} finally {
$post->releaseLock(); // Critical: Always release!
}
}
Broadcasting Lock Events:
Subscribe to events in EventServiceProvider:
protected $listen = [
'Sofa\ModelLocking\Events\LockAcquired' => [
'App\Listeners\NotifyUserOfLock',
],
];
Custom Lock Logic: Override trait methods in your model:
class Post extends Model
{
use Locking;
protected function getLockDuration()
{
return now()->addMinutes(60); // Custom duration
}
}
Queue Lock Releases:
Use Laravel queues to defer releaseLock() for long-running operations:
$post->lock();
dispatch(new ReleaseLockJob($post))->delay(now()->addMinutes(10));
Lock Validation Middleware: Create middleware to auto-check locks:
public function handle($request, Closure $next)
{
if ($request->post->isLocked()) {
abort(423, 'Resource locked');
}
return $next($request);
}
Soft Deletes:
Extend the Locking trait to handle soft-deleted models:
protected function getLockQuery()
{
return $this->newQuery()->whereNull('deleted_at');
}
Forgetting to Release Locks:
finally blocks or queues to ensure release:
try {
$post->lock();
// Critical section
} finally {
$post->releaseLock();
}
Race Conditions:
isLocked() simultaneously and both proceed to lock.forceLock() if you need to override existing locks (but handle conflicts gracefully).Lock Table Bloat:
// app/Console/Commands/CleanLocks.php
public function handle()
{
\Sofa\ModelLocking\Lock::where('expires_at', '<', now())
->delete();
}
Broadcasting Delays:
event(new LockAcquired($this));
SELECT * FROM locks WHERE model_type = 'App\Post' AND model_id = 1;
public function handle(LockAcquired $event)
{
Log::info('Lock acquired', ['model' => $event->model]);
}
Custom Lock Storage:
Override getLockQuery() to use a different table or database:
protected function getLockQuery()
{
return \DB::connection('mysql_replica')->table('locks')->where(...);
}
Lock Conditions: Add dynamic lock logic (e.g., user-specific locks):
public function lockForUser($userId)
{
$this->lock();
\DB::table('user_locks')->insert([
'model_type' => $this->getMorphClass(),
'model_id' => $this->id,
'user_id' => $userId,
'expires_at' => now()->addMinutes(30),
]);
}
Lock Expiration Callbacks:
Extend the Locking trait to trigger actions on expiration:
protected function onLockExpired()
{
// Notify user, log, or trigger cleanup
}
config/model_locking.php). Adjust based on workflow needs (e.g., 5 minutes for checkout flows).locks. Change via locks_table in config if conflicts arise.QUEUE_CONNECTION in .env is configured if using queues for events.How can I help you explore Laravel packages today?