From 4d15ebc078f7dd7263669cec29efd15e2ef91c17 Mon Sep 17 00:00:00 2001 From: Matt McDonald Date: Wed, 16 Oct 2019 10:45:36 +0100 Subject: [PATCH] Add helper methods `updateWithUserstamps`, `deleteWithUserstamps` --- README.md | 26 +++++++-- composer.json | 2 +- src/Userstamps.php | 2 + src/UserstampsScope.php | 53 ++++++++++++++++++ tests/UserstampsTest.php | 113 +++++++++++++++++++++++---------------- 5 files changed, 147 insertions(+), 49 deletions(-) create mode 100644 src/UserstampsScope.php diff --git a/README.md b/README.md index b1e3dbb..a2caeff 100644 --- a/README.md +++ b/README.md @@ -87,13 +87,13 @@ $model -> stopUserstamping(); // stops userstamps being maintained on the model $model -> startUserstamping(); // resumes userstamps being maintained on the model ``` -## Limitations +## Workarounds This package works by by hooking into Eloquent's model event listeners, and is subject to the same limitations of all such listeners. When you make changes to models that bypass Eloquent, the event listeners won't be fired and userstamps will not be updated. -Commonly this would happen if mass updating or deleting models, or their relations. +Commonly this will happen if bulk updating or deleting models, or their relations. In this example, model relations are updated via Eloquent and userstamps **will** be maintained: @@ -104,7 +104,7 @@ $model->foos->each(function ($item) { }); ``` -However, in this example, model relations are mass updated and bypass Eloquent. Userstamps **will not** be maintained: +However in this example, model relations are bulk updated and bypass Eloquent. Userstamps **will not** be maintained: ```php $model->foos()->update([ @@ -112,6 +112,26 @@ $model->foos()->update([ ]); ``` +As a workaroud to this issue two helper methods are available - `updateWithUserstamps` and `deleteWithUserstamps`. Their behaviour is identical to `update` and `delete`, but they ensure the `updated_by` and `deleted_by` properties are maintained on the model. + + You generally won't have to use these methods, unless making bulk updates that bypass Eloquent events. + + In this example, models are bulk updated and userstamps **will not** be maintained: + +```php +$model->where('name', 'foo')->update([ + 'name' => 'bar', +]); +``` + +However in this example, models are bulk updated using the helper method and userstamps **will** be maintained: + +```php +$model->where('name', 'foo')->updateWithUserstamps([ + 'name' => 'bar', +]); +``` + ## Sponsors diff --git a/composer.json b/composer.json index d802baf..0164470 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "wildside/userstamps", - "description": "Eloquent trait to automatically maintain created_by and updated_by (and deleted_by when using SoftDeletes) on your models", + "description": "Laravel Userstamps provides an Eloquent trait which automatically maintains `created_by` and `updated_by` columns on your model, populated by the currently authenticated user in your application.", "license": "MIT", "keywords": [ "laravel", diff --git a/src/Userstamps.php b/src/Userstamps.php index 03596bb..55a930a 100644 --- a/src/Userstamps.php +++ b/src/Userstamps.php @@ -18,6 +18,8 @@ trait Userstamps */ public static function bootUserstamps() { + static::addGlobalScope(new UserstampsScope); + static::registerListeners(); } diff --git a/src/UserstampsScope.php b/src/UserstampsScope.php new file mode 100644 index 0000000..9ce26fa --- /dev/null +++ b/src/UserstampsScope.php @@ -0,0 +1,53 @@ +macro('updateWithUserstamps', function (Builder $builder, $values) { + if (! $builder->getModel()->isUserstamping() || is_null(auth()->id())) { + return $builder->update($values); + } + + $values[$builder->getModel()->getUpdatedByColumn()] = auth()->id(); + + return $builder->update($values); + }); + + $builder->macro('deleteWithUserstamps', function (Builder $builder) { + if (! $builder->getModel()->isUserstamping() || is_null(auth()->id())) { + return $builder->delete(); + } + + $builder->update([ + $builder->getModel()->getDeletedByColumn() => auth()->id(), + ]); + + return $builder->delete(); + }); + } +} diff --git a/tests/UserstampsTest.php b/tests/UserstampsTest.php index 22663e4..1a26ed8 100644 --- a/tests/UserstampsTest.php +++ b/tests/UserstampsTest.php @@ -44,6 +44,7 @@ protected static function handleSetUp() Schema::create('foos', function (Blueprint $table) { $table->increments('id'); $table->string('bar'); + $table->timestamps(); $table->softDeletes(); $table->unsignedBigInteger('created_by')->nullable(); $table->unsignedBigInteger('updated_by')->nullable(); @@ -53,16 +54,6 @@ protected static function handleSetUp() $table->unsignedBigInteger('alt_deleted_by')->nullable(); }); - Schema::create('bars', function (Blueprint $table) { - $table->increments('id'); - $table->unsignedBigInteger('foo_id'); - $table->string('foo'); - $table->softDeletes(); - $table->unsignedBigInteger('created_by')->nullable(); - $table->unsignedBigInteger('updated_by')->nullable(); - $table->unsignedBigInteger('deleted_by')->nullable(); - }); - TestUser::create([ 'id' => 1, ]); @@ -100,19 +91,6 @@ protected function createFooWithNullColumnNames() ]); } - protected function createFooWithBars() - { - $foo = $this->createFoo(); - - $foo->bars()->saveMany([ - new Bar([ - 'foo' => 1, - ]), - ]); - - return $foo; - } - public function testCreatedByAndUpdatedByIsSetOnNewModelWhenUserIsPresent() { $this->app['auth']->loginUsingId(1); @@ -345,6 +323,73 @@ public function testDestroyerMethodWorks() $this->assertEquals($user, $foo->destroyer); } + + public function testUpdateWithUserstampsMethod() + { + $this->app['auth']->loginUsingId(1); + + $this->createFoo(); + + $this->app['auth']->loginUsingId(2); + + Foo::where('bar', 'foo')->updateWithUserstamps([ + 'bar' => 'bar', + ]); + + $this->assertEquals(2, Foo::first()->updated_by); + } + + public function testDeleteWithUserstampsMethod() + { + $this->app['auth']->loginUsingId(1); + + $this->createFooWithSoftDeletes(); + + $this->app['auth']->loginUsingId(2); + + FooWithSoftDeletes::where('bar', 'foo')->deleteWithUserstamps(); + + $this->assertEquals(2, FooWithSoftDeletes::withTrashed()->first()->deleted_by); + } + + public function testDeleteWithUserstampsMethodDoesntTouchUpdatedBy() + { + $this->app['auth']->loginUsingId(1); + + $foo = $this->createFooWithSoftDeletes(); + $updatedAt = $foo->updated_at; + + $this->app['auth']->loginUsingId(2); + + FooWithSoftDeletes::where('bar', 'foo')->deleteWithUserstamps(); + + $foo = FooWithSoftDeletes::withTrashed()->first(); + + $this->assertEquals(1, $foo->updated_by); + $this->assertEquals(2, $foo->deleted_by); + } + + public function testBuilderMethodWorksWithCustomColumnNames() + { + $this->app['auth']->loginUsingId(1); + + $this->createFooWithCustomColumnNames(); + + $this->app['auth']->loginUsingId(2); + + FooWithCustomColumnNames::where('bar', 'foo')->updateWithUserstamps([ + 'bar' => 'bar', + ]); + + FooWithCustomColumnNames::where('bar', 'bar')->deleteWithUserstamps(); + + $foo = FooWithCustomColumnNames::withTrashed()->where('bar', 'bar')->first(); + + $this->assertNull($foo->updated_by); + $this->assertNull($foo->deleted_by); + $this->assertEquals(2, $foo->alt_updated_by); + $this->assertEquals(2, $foo->alt_deleted_by); + } } class Foo extends Model @@ -354,11 +399,6 @@ class Foo extends Model public $table = 'foos'; public $timestamps = false; protected $guarded = []; - - public function bars() - { - return $this->hasMany(Bar::class)->withTrashed(); - } } class FooWithSoftDeletes extends Model @@ -366,7 +406,6 @@ class FooWithSoftDeletes extends Model use SoftDeletes, Userstamps; public $table = 'foos'; - public $timestamps = false; protected $guarded = []; } @@ -375,7 +414,6 @@ class FooWithCustomColumnNames extends Model use SoftDeletes, Userstamps; public $table = 'foos'; - public $timestamps = false; protected $guarded = []; const CREATED_BY = 'alt_created_by'; @@ -388,7 +426,6 @@ class FooWithNullColumnNames extends Model use SoftDeletes, Userstamps; public $table = 'foos'; - public $timestamps = false; protected $guarded = []; const CREATED_BY = null; @@ -396,20 +433,6 @@ class FooWithNullColumnNames extends Model const DELETED_BY = null; } -class Bar extends Model -{ - use SoftDeletes, Userstamps; - - public $table = 'bars'; - public $timestamps = false; - protected $guarded = []; - - public function foo() - { - return $this->belongsTo(Foo::class); - } -} - class TestUser extends Authenticatable { public $table = 'users';