Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 12 additions & 13 deletions composer.json
Original file line number Diff line number Diff line change
@@ -1,22 +1,21 @@
{
"name": ":vendor_slug/:package_slug",
"description": ":package_description",
"name": "stafe/optimistic-locking",
"description": "Optimistic locking utilities for Laravel Eloquent models.",
"keywords": [
":vendor_name",
"laravel",
":package_slug"
"optimistic-locking"
],
"homepage": "https://github.com/:vendor_slug/:package_slug",
"homepage": "https://example.com/optimistic-locking",
"license": "MIT",
"authors": [
{
"name": ":author_name",
"email": "author@domain.com",
"name": "Package Developer",
"email": "developer@example.com",
"role": "Developer"
}
],
"require": {
"php": "^8.4",
"php": "^8.1",
"spatie/laravel-package-tools": "^1.16",
"illuminate/contracts": "^10.0||^11.0||^12.0"
},
Expand All @@ -35,13 +34,13 @@
},
"autoload": {
"psr-4": {
"VendorName\\Skeleton\\": "src/",
"VendorName\\Skeleton\\Database\\Factories\\": "database/factories/"
"Stafe\\OptimisticLocking\\": "src/",
"Stafe\\OptimisticLocking\\Database\\Factories\\": "database/factories/"
}
},
"autoload-dev": {
"psr-4": {
"VendorName\\Skeleton\\Tests\\": "tests/",
"Stafe\\OptimisticLocking\\Tests\\": "tests/",
"Workbench\\App\\": "workbench/app/"
}
},
Expand All @@ -63,10 +62,10 @@
"extra": {
"laravel": {
"providers": [
"VendorName\\Skeleton\\SkeletonServiceProvider"
"Stafe\\OptimisticLocking\\OptimisticLockingServiceProvider"
],
"aliases": {
"Skeleton": "VendorName\\Skeleton\\Facades\\Skeleton"
"OptimisticLocking": "Stafe\\OptimisticLocking\\Facades\\OptimisticLocking"
}
}
},
Expand Down
7 changes: 7 additions & 0 deletions config/optimistic.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

return [
'column' => 'lock_version',
'start_value' => 1,
'diff_max_len' => 250,
];
6 changes: 0 additions & 6 deletions config/skeleton.php

This file was deleted.

2 changes: 1 addition & 1 deletion phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
backupStaticProperties="false"
>
<testsuites>
<testsuite name="VendorName Test Suite">
<testsuite name="OptimisticLocking Test Suite">
<directory>tests</directory>
</testsuite>
</testsuites>
Expand Down
35 changes: 35 additions & 0 deletions src/Commands/LockCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

namespace Stafe\OptimisticLocking\Commands;

use Illuminate\Console\Command;

class LockCommand extends Command
{
public $signature = 'schema:lock {--apply}';

public $description = 'Manage optimistic lock columns';

public function handle(): int
{
$column = config('optimistic.column', 'lock_version');
if ($this->option('apply')) {
foreach (\File::allFiles(app_path('Models')) as $file) {
$class = 'App\\Models\\'.pathinfo($file, PATHINFO_FILENAME);
if (is_subclass_of($class, \Illuminate\Database\Eloquent\Model::class)) {
if (! \Schema::hasColumn((new $class)->getTable(), $column)) {
\Schema::table((new $class)->getTable(), function ($table) use ($column) {
$table->unsignedInteger($column)->nullable();
});
$this->info("Added {$column} column to ".(new $class)->getTable());
}
}
}

return self::SUCCESS;
}
$this->comment('No action');

return self::SUCCESS;
}
}
19 changes: 0 additions & 19 deletions src/Commands/SkeletonCommand.php

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
<?php

namespace VendorName\Skeleton\Facades;
namespace Stafe\OptimisticLocking\Facades;

use Illuminate\Support\Facades\Facade;

/**
* @see \VendorName\Skeleton\Skeleton
*/
class Skeleton extends Facade
class OptimisticLocking extends Facade
{
protected static function getFacadeAccessor(): string
{
return \VendorName\Skeleton\Skeleton::class;
return \Stafe\OptimisticLocking\OptimisticLocking::class;
}
}
5 changes: 5 additions & 0 deletions src/OptimisticLocking.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?php

namespace Stafe\OptimisticLocking;

class OptimisticLocking {}
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
<?php

namespace VendorName\Skeleton;
namespace Stafe\OptimisticLocking;

use Spatie\LaravelPackageTools\Package;
use Spatie\LaravelPackageTools\PackageServiceProvider;
use VendorName\Skeleton\Commands\SkeletonCommand;
use Stafe\OptimisticLocking\Commands\LockCommand;

class SkeletonServiceProvider extends PackageServiceProvider
class OptimisticLockingServiceProvider extends PackageServiceProvider
{
public function configurePackage(Package $package): void
{
Expand All @@ -16,10 +16,8 @@ public function configurePackage(Package $package): void
* More info: https://github.com/spatie/laravel-package-tools
*/
$package
->name('skeleton')
->hasConfigFile()
->hasViews()
->hasMigration('create_migration_table_name_table')
->hasCommand(SkeletonCommand::class);
->name('optimistic-locking')
->hasConfigFile('optimistic')
->hasCommand(LockCommand::class);
}
}
5 changes: 0 additions & 5 deletions src/Skeleton.php

This file was deleted.

42 changes: 42 additions & 0 deletions src/StaleModelException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

namespace Stafe\OptimisticLocking;

use Illuminate\Database\Eloquent\Model;
use RuntimeException;

class StaleModelException extends RuntimeException
{
protected array $diff;

public function __construct(Model $model, $current)

Check failure on line 12 in src/StaleModelException.php

View workflow job for this annotation

GitHub Actions / phpstan

Constructor of class Stafe\OptimisticLocking\StaleModelException has an unused parameter $current.
{
parent::__construct('Stale model detected.');

$column = config('optimistic.column', 'lock_version');
$maxLen = (int) config('optimistic.diff_max_len', 250);

$changes = $model->getDirty();
unset($changes[$column]);

$original = $model->newQuery()->whereKey($model->getKey())->first();
$diff = [];
foreach ($changes as $key => $value) {
$old = data_get($original, $key);
$new = $value;
if (is_string($old)) {
$old = mb_strimwidth($old, 0, $maxLen, '...');
}
if (is_string($new)) {
$new = mb_strimwidth($new, 0, $maxLen, '...');
}
$diff[$key] = ['old' => $old, 'new' => $new];
}
$this->diff = $diff;
}

public function diff(): array
{
return $this->diff;
}
}
62 changes: 62 additions & 0 deletions src/Traits/OptimisticLocking.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php

namespace Stafe\OptimisticLocking\Traits;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Stafe\OptimisticLocking\StaleModelException;

trait OptimisticLocking

Check failure on line 9 in src/Traits/OptimisticLocking.php

View workflow job for this annotation

GitHub Actions / phpstan

Trait Stafe\OptimisticLocking\Traits\OptimisticLocking is used zero times and is not analysed.
{
public static function bootOptimisticLocking(): void
{
static::creating(function (Model $model) {
$column = $model->getOptimisticLockColumn();
if (! $model->isDirty($column)) {
$model->{$column} = config('optimistic.start_value', 1);
}
});

static::updating(function (Model $model) {
$column = $model->getOptimisticLockColumn();
$model->{$column} = ($model->getOriginal($column) ?? 0) + 1;
});
}

public function getOptimisticLockColumn(): string
{
return config('optimistic.column', 'lock_version');
}

protected function performUpdate(Builder $query)
{
if ($this->fireModelEvent('updating') === false) {
return false;
}

if ($this->usesTimestamps()) {
$this->updateTimestamps();
}

$column = $this->getOptimisticLockColumn();
$current = $this->getOriginal($column);
$query->where($column, $current);

$dirty = $this->getDirtyForUpdate();
$dirty[$column] = $this->{$column};

if (count($dirty) > 0) {
$updated = $this->setKeysForSaveQuery($query)->update($dirty);

if ($updated === 0) {
throw new StaleModelException($this, $current);
}

$this->syncChanges();

$this->fireModelEvent('updated', false);
}

return true;
}
}
15 changes: 15 additions & 0 deletions tests/Models/Post.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

namespace Stafe\OptimisticLocking\Tests\Models;

use Illuminate\Database\Eloquent\Model;
use Stafe\OptimisticLocking\Traits\OptimisticLocking;

class Post extends Model
{
use OptimisticLocking;

protected $fillable = ['title'];

public $timestamps = false;
}
26 changes: 26 additions & 0 deletions tests/OptimisticLockingTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

use Stafe\OptimisticLocking\StaleModelException;
use Stafe\OptimisticLocking\Tests\Models\Post;

it('increments version on save', function () {
$post = Post::create(['title' => 'A']);
expect($post->lock_version)->toBe(1);
$post->title = 'B';
$post->save();
expect($post->lock_version)->toBe(2);
});

it('throws exception on stale update', function () {
$post = Post::create(['title' => 'A']);
$first = Post::find($post->id);
$second = Post::find($post->id);

$first->title = 'B';
$first->save();

$second->title = 'C';
expect(function () use ($second) {
$second->save();
})->toThrow(StaleModelException::class);
});
2 changes: 1 addition & 1 deletion tests/Pest.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?php

use VendorName\Skeleton\Tests\TestCase;
use Stafe\OptimisticLocking\Tests\TestCase;

uses(TestCase::class)->in(__DIR__);
Loading
Loading