xint0 / laravel-optimistic-locking
Adds optimistic locking feature to eloquent models.
Requires
- php: ^7.2|^8.0
- illuminate/database: ~5.5.0|~5.6.0|~5.7.0|~5.8.0|^6.0|^7.0|^8.0|^9.0|^10.0|^11.0
Requires (Dev)
- ext-pdo_sqlite: *
- mockery/mockery: ^1.0.0
- orchestra/testbench: ~3.5.0|~3.6.0|~3.8.0|^4.0|^5.0|^6.0|^7.0|^8.0|^9.0
- phpunit/phpunit: ^7.0|^8.0|^9.0|^10.0
Replaces
README
Adds optimistic locking to Eloquent models.
This package replaces reshadman/laravel-optimistic-locking
.
Installation
composer require xint0/laravel-optimistic-locking
Compatibility
This package supports Laravel 5.5, 5.6, 5.7, 5.8, 6, 7, 8, 9, 10, and 11.
Usage
Basic usage
Use the \Reshadman\OptimisticLocking\OptimisticLocking
trait in your model:
<?php
use Reshadman\OptimisticLocking\OptimisticLocking;
class BlogPost extends Model {
use OptimisticLocking;
}
Add the nullable integer lock_version
field to the table of the model:
<?php # migration to add lock version column
$schema->integer('lock_version')->unsigned()->nullable();
Then you are ready to go, if the same resource is edited by two different processes CONCURRENTLY then the following exception will be raised:
<?php
use Reshadman\OptimisticLocking\StaleModelLockingException;
You should catch the above exception and act properly based on your business logic.
Maintaining lock_version during business transactions
You can keep track of a lock version during a business transaction by informing your API or HTML client about the current version:
<input type="hidden" name="lock_version" value="{{$blogPost->lock_version}}"
and in controller:
<?php
use Reshadman\OptimisticLocking\StaleModelLockingException;
// Explicitly setting the lock version
class PostController {
public function update($id)
{
$post = Post::findOrFail($id);
$post->lock_version = request('lock_version');
try {
$post->save();
// You can also define more implicit reusable methods in your model like Model::saveWithVersion(...$args);
// or just override the default Model::save(...$args); method which accepts $options
// Then automatically read the lock version from Request and set into the model.
} catch (StaleModelLockingException $exception) {
// Occurs when request lock version does not match value stored in database. Which means another user
// has already updated the model.
}
}
}
So if two authors are editing the same content concurrently, you can keep track of your Read State, and ask the second author to rewrite his changes.
Disabling and enabling optimistic locking
You can disable and enable optimistic locking for a specific instance:
<?php
$blogPost->disableLocking();
//
//save model does not verify lock version
//
$blogPost->enableLocking();
//
// save model verifies lock version
//
By default, optimistic locking is enabled when you use OptimisticLocking
trait in your model, to alter the default
behaviour you can set the lock strictly to false
:
<?php
use Illuminate\Database\Eloquent\Model;
use Reshadman\OptimisticLocking\OptimisticLocking;
class BlogPost extends Model
{
use OptimisticLocking;
protected $lock = false;
}
and then you may enable it: $blogPost->enableLocking();
Use a different column for tracking version
By default, the lock_version
column is used for tracking version, you can alter that by overriding the following method
of the trait:
<?php
use Illuminate\Database\Eloquent\Model;
use Reshadman\OptimisticLocking\OptimisticLocking;
class BlogPost extends Model
{
use OptimisticLocking;
/**
* Name of the lock version column.
*
* @return string
*/
protected static function lockVersionColumn()
{
return 'track_version';
}
}
What is optimistic locking?
For detailed explanation read the concurrency section of Patterns of Enterprise Application Architecture by Martin Fowler.
There are two ways to approach generic concurrency race conditions:
- Do not allow other processes (or users) to read and update the same resource (Pessimistic Locking).
- Allow other processes to read the same resource concurrently, but do not allow further update, if one of the processes updated the resource before the others (Optimistic locking).
Laravel allows Pessimistic locking as described in the documentation, this package allows you to have Optimistic locking in a rails like way.
What happens during an optimistic lock?
Every time you perform an update action to your resource(model), the lock_version
counter field in the table is
incremented by 1
. If you read a resource and another process updates the resource after you read it, the true version
counter is incremented by one. If the current process attempts to update the model, simply a
StaleModelLockingException
will be thrown, and you should handle the race condition (merge, retry, ignore) based on
your business logic. That is simply via adding the following criteria to the update query of an
optimistically lockable model:
<?php
$query->where('id', $this->id)
->where('lock_version', $this->lock_version)
->update($changes);
If the resource has been updated before your update attempt, then the above will simply update no records, and it means that the model has been updated before current attempt, or that it has been deleted.
Why don't we use updated_at
for tracking changes?
Because they may remain the same during two concurrent updates.
Contributing
This package is open source software, and you are encouraged to contribute by:
- Reporting an issue through any of the following channels:
- Send email to:
laravel-optimistic-locking@xint0.org
. - Open issue via web in gitlab.com: https://gitlab.com/xint0-open-source/laravel-optimistic-locking/-/issues
- Send email to:
- Creating a fork of the project and submitting a merge request in gitlab.com: https://gitlab.com/xint0-open-source/laravel-optimistic-locing/-/forks/new
Working with the code
The project uses Docker Compose to define containers used for development. Although it is not necessary to use Docker, it makes working with multiple versions of PHP easier.
After forking and cloning the git repository to your local development environment you can execute the following docker compose commands:
Install dependencies
docker compose run --rm php composer install
Execute tests
docker compose run --rm php ./vendor/bin/phpunit
Execute tests with coverage reporting
docker compose run --rm --env=XDEBUG_MODE=coverage php php ./vendor/bin/phpunit --coverage-text
Execute PHPStan analysis
docker compose run --rm phpstan
License
The MIT License (MIT). Please see License File for more information.