diff --git a/config/audit.php b/config/audit.php index 612455db..812a4837 100644 --- a/config/audit.php +++ b/config/audit.php @@ -30,14 +30,18 @@ | User Keys, Model |-------------------------------------------------------------------------- | - | Define the User primary key, foreign key and Eloquent model. + | Define the User Eloquent model, morph prefix and authentication guards + | to use in the User resolver. | */ 'user' => [ - 'primary_key' => 'id', - 'foreign_key' => 'user_id', - 'model' => App\User::class, + 'model' => App\User::class, + 'morph_prefix' => 'user', + 'guards' => [ + 'web', + 'api', + ], ], /* diff --git a/database/migrations/audits.stub b/database/migrations/audits.stub index d9f8bf3f..d380416e 100644 --- a/database/migrations/audits.stub +++ b/database/migrations/audits.stub @@ -27,7 +27,7 @@ class CreateAuditsTable extends Migration { Schema::create('audits', function (Blueprint $table) { $table->increments('id'); - $table->unsignedInteger('user_id')->nullable(); + $table->nullableMorphs('user'); $table->string('event'); $table->morphs('auditable'); $table->text('old_values')->nullable(); diff --git a/src/Audit.php b/src/Audit.php index 9c7ab265..8484a9bf 100644 --- a/src/Audit.php +++ b/src/Audit.php @@ -16,7 +16,6 @@ use DateTimeInterface; use Illuminate\Database\Eloquent\Model; -use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\MorphTo; use Illuminate\Support\Facades\Config; @@ -70,13 +69,9 @@ public function auditable(): MorphTo /** * {@inheritdoc} */ - public function user(): BelongsTo + public function user(): MorphTo { - return $this->belongsTo( - Config::get('audit.user.model'), - Config::get('audit.user.foreign_key', 'user_id'), - Config::get('audit.user.primary_key', 'id') - ); + return $this->morphTo(); } /** @@ -84,6 +79,8 @@ public function user(): BelongsTo */ public function resolveData(): array { + $morphPrefix = Config::get('audit.user.morph_prefix', 'user'); + // Metadata $this->data = [ 'audit_id' => $this->id, @@ -94,7 +91,8 @@ public function resolveData(): array 'audit_tags' => $this->tags, 'audit_created_at' => $this->serializeDate($this->created_at), 'audit_updated_at' => $this->serializeDate($this->updated_at), - 'user_id' => $this->getAttribute(Config::get('audit.user.foreign_key', 'user_id')), + 'user_id' => $this->getAttribute($morphPrefix.'_id'), + 'user_type' => $this->getAttribute($morphPrefix.'_type'), ]; if ($this->user) { diff --git a/src/Auditable.php b/src/Auditable.php index a360f9ca..afff8572 100644 --- a/src/Auditable.php +++ b/src/Auditable.php @@ -278,21 +278,24 @@ public function toAudit(): array } } - $userForeignKey = Config::get('audit.user.foreign_key', 'user_id'); + $morphPrefix = Config::get('audit.user.morph_prefix', 'user'); $tags = implode(',', $this->generateTags()); + $user = $this->resolveUser(); + return $this->transformAudit([ - 'old_values' => $old, - 'new_values' => $new, - 'event' => $this->auditEvent, - 'auditable_id' => $this->getKey(), - 'auditable_type' => $this->getMorphClass(), - $userForeignKey => $this->resolveUser(), - 'url' => $this->resolveUrl(), - 'ip_address' => $this->resolveIpAddress(), - 'user_agent' => $this->resolveUserAgent(), - 'tags' => empty($tags) ? null : $tags, + 'old_values' => $old, + 'new_values' => $new, + 'event' => $this->auditEvent, + 'auditable_id' => $this->getKey(), + 'auditable_type' => $this->getMorphClass(), + $morphPrefix.'_id' => $user ? $user->getAuthIdentifier() : null, + $morphPrefix.'_type' => $user ? $user->getMorphClass() : null, + 'url' => $this->resolveUrl(), + 'ip_address' => $this->resolveIpAddress(), + 'user_agent' => $this->resolveUserAgent(), + 'tags' => empty($tags) ? null : $tags, ]); } diff --git a/src/Contracts/Audit.php b/src/Contracts/Audit.php index 04699d6c..427387cb 100644 --- a/src/Contracts/Audit.php +++ b/src/Contracts/Audit.php @@ -14,7 +14,6 @@ namespace OwenIt\Auditing\Contracts; -use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\MorphTo; interface Audit @@ -43,9 +42,9 @@ public function auditable(): MorphTo; /** * User responsible for the changes. * - * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + * @return \Illuminate\Database\Eloquent\Relations\MorphTo */ - public function user(): BelongsTo; + public function user(): MorphTo; /** * Audit data resolver. diff --git a/src/Resolvers/UserResolver.php b/src/Resolvers/UserResolver.php index b5c9c61d..8e06e969 100644 --- a/src/Resolvers/UserResolver.php +++ b/src/Resolvers/UserResolver.php @@ -15,6 +15,7 @@ namespace OwenIt\Auditing\Resolvers; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Config; class UserResolver implements \OwenIt\Auditing\Contracts\UserResolver { @@ -23,6 +24,15 @@ class UserResolver implements \OwenIt\Auditing\Contracts\UserResolver */ public static function resolve() { - return Auth::check() ? Auth::user()->getAuthIdentifier() : null; + $guards = Config::get('audit.user.guards', [ + 'web', + 'api', + ]); + + foreach ($guards as $guard) { + if (Auth::guard($guard)->check()) { + return Auth::guard($guard)->user(); + } + } } } diff --git a/tests/Unit/AuditTest.php b/tests/Unit/AuditTest.php index 49c06bbe..b60132fe 100644 --- a/tests/Unit/AuditTest.php +++ b/tests/Unit/AuditTest.php @@ -16,41 +16,12 @@ use Carbon\Carbon; use DateTimeInterface; -use Mockery; use OwenIt\Auditing\Models\Audit; use OwenIt\Auditing\Tests\Models\Article; use OwenIt\Auditing\Tests\Models\User; class AuditTest extends AuditingTestCase { - /** - * @group Audit::user - * @test - */ - public function itRelatesToUserWithCustomKeys() - { - $audit = Mockery::mock(Audit::class) - ->makePartial(); - - $this->app['config']->set('audit.user.model', User::class); - $this->app['config']->set('audit.user.primary_key', 'pk_id'); - $this->app['config']->set('audit.user.foreign_key', 'fk_id'); - - $this->assertInstanceOf(User::class, $audit->user()->getRelated()); - - // Up to Laravel 5.3, the ownerKey attribute was called otherKey - if (method_exists($audit->user(), 'getOtherKey')) { - $this->assertSame('pk_id', $audit->user()->getOtherKey()); - } - - // From Laravel 5.4 onward, the otherKey attribute was renamed to ownerKey - if (method_exists($audit->user(), 'getOwnerKey')) { - $this->assertSame('pk_id', $audit->user()->getOwnerKey()); - } - - $this->assertSame('fk_id', $audit->user()->getForeignKey()); - } - /** * @group Audit::resolveData * @test @@ -68,7 +39,7 @@ public function itResolvesAuditData() $audit = $article->audits()->first(); - $this->assertCount(14, $resolvedData = $audit->resolveData()); + $this->assertCount(15, $resolvedData = $audit->resolveData()); $this->assertArraySubset([ 'audit_id' => 1, @@ -80,6 +51,7 @@ public function itResolvesAuditData() 'audit_created_at' => $audit->created_at->toDateTimeString(), 'audit_updated_at' => $audit->updated_at->toDateTimeString(), 'user_id' => null, + 'user_type' => null, 'new_title' => 'How To Audit Eloquent Models', 'new_content' => 'First step: install the laravel-auditing package.', 'new_published_at' => $now->toDateTimeString(), @@ -114,7 +86,7 @@ public function itResolvesAuditDataIncludingUserAttributes() $audit = $article->audits()->first(); - $this->assertCount(20, $resolvedData = $audit->resolveData()); + $this->assertCount(21, $resolvedData = $audit->resolveData()); $this->assertArraySubset([ 'audit_id' => 2, @@ -126,6 +98,7 @@ public function itResolvesAuditDataIncludingUserAttributes() 'audit_created_at' => $audit->created_at->toDateTimeString(), 'audit_updated_at' => $audit->updated_at->toDateTimeString(), 'user_id' => '1', + 'user_type' => User::class, 'user_is_admin' => '1', 'user_first_name' => 'rick', 'user_last_name' => 'Sanchez', @@ -164,7 +137,7 @@ public function itReturnsTheAppropriateAuditableDataValues() ])->audits()->first(); // Resolve data, making it available to the getDataValue() method - $this->assertCount(20, $audit->resolveData()); + $this->assertCount(21, $audit->resolveData()); // Mutate value $this->assertSame('HOW TO AUDIT ELOQUENT MODELS', $audit->getDataValue('new_title')); @@ -194,7 +167,7 @@ public function itReturnsAuditMetadataAsArray() { $audit = factory(Article::class)->create()->audits()->first(); - $this->assertCount(9, $metadata = $audit->getMetadata()); + $this->assertCount(10, $metadata = $audit->getMetadata()); $this->assertArraySubset([ 'audit_id' => 1, @@ -206,6 +179,7 @@ public function itReturnsAuditMetadataAsArray() 'audit_created_at' => $audit->created_at->toDateTimeString(), 'audit_updated_at' => $audit->updated_at->toDateTimeString(), 'user_id' => null, + 'user_type' => null, ], $metadata, true); } @@ -226,7 +200,7 @@ public function itReturnsAuditMetadataIncludingUserAttributesAsArray() $audit = factory(Article::class)->create()->audits()->first(); - $this->assertCount(15, $metadata = $audit->getMetadata()); + $this->assertCount(16, $metadata = $audit->getMetadata()); $this->assertArraySubset([ 'audit_id' => 2, @@ -238,6 +212,7 @@ public function itReturnsAuditMetadataIncludingUserAttributesAsArray() 'audit_created_at' => $audit->created_at->toDateTimeString(), 'audit_updated_at' => $audit->updated_at->toDateTimeString(), 'user_id' => 1, + 'user_type' => User::class, 'user_is_admin' => true, 'user_first_name' => 'Rick', 'user_last_name' => 'Sanchez', @@ -267,7 +242,8 @@ public function itReturnsAuditMetadataAsJsonString() "audit_tags": null, "audit_created_at": "$audit->created_at", "audit_updated_at": "$audit->updated_at", - "user_id": null + "user_id": null, + "user_type": null } EOF; @@ -304,6 +280,7 @@ public function itReturnsAuditMetadataIncludingUserAttributesAsJsonString() "audit_created_at": "$audit->created_at", "audit_updated_at": "$audit->updated_at", "user_id": 1, + "user_type": "OwenIt\\\Auditing\\\Tests\\\Models\\\User", "user_is_admin": true, "user_first_name": "Rick", "user_last_name": "Sanchez", diff --git a/tests/Unit/AuditableObserverTest.php b/tests/Unit/AuditableObserverTest.php index fca6deb8..7f631d55 100644 --- a/tests/Unit/AuditableObserverTest.php +++ b/tests/Unit/AuditableObserverTest.php @@ -41,7 +41,7 @@ public function itExecutesTheAuditorSuccessfully(string $eventMethod, bool $expe $this->assertSame($expectedBefore, $observer::$restoring); - call_user_func([$observer, $eventMethod], $model); + $observer->$eventMethod($model); $this->assertSame($expectedAfter, $observer::$restoring); } @@ -49,7 +49,7 @@ public function itExecutesTheAuditorSuccessfully(string $eventMethod, bool $expe /** * @return array */ - public function auditableObserverTestProvider() + public function auditableObserverTestProvider(): array { return [ [ diff --git a/tests/Unit/AuditableTest.php b/tests/Unit/AuditableTest.php index b9488fff..d81c55f5 100644 --- a/tests/Unit/AuditableTest.php +++ b/tests/Unit/AuditableTest.php @@ -260,7 +260,7 @@ public function itFailsWhenTheCustomAttributeGettersAreMissing( /** * @return array */ - public function auditCustomAttributeGetterFailTestProvider() + public function auditCustomAttributeGetterFailTestProvider(): array { return [ [ @@ -388,7 +388,7 @@ public function itReturnsTheAuditData() $model->setAuditEvent('created'); - $this->assertCount(10, $auditData = $model->toAudit()); + $this->assertCount(11, $auditData = $model->toAudit()); $this->assertArraySubset([ 'old_values' => [], @@ -402,6 +402,7 @@ public function itReturnsTheAuditData() 'auditable_id' => null, 'auditable_type' => Article::class, 'user_id' => null, + 'user_type' => null, 'url' => 'console', 'ip_address' => '127.0.0.1', 'user_agent' => 'Symfony/3.X', @@ -413,12 +414,27 @@ public function itReturnsTheAuditData() * @group Auditable::setAuditEvent * @group Auditable::toAudit * @test + * + * @dataProvider userResolverProvider + * + * @param string $guard + * @param string $driver + * @param int $id + * @param string $type */ - public function itReturnsTheAuditDataIncludingUserAttributes() - { + public function itReturnsTheAuditDataIncludingUserAttributes( + string $guard, + string $driver, + int $id = null, + string $type = null + ) { + $this->app['config']->set('audit.user.guards', [ + $guard, + ]); + $user = factory(User::class)->create(); - $this->actingAs($user); + $this->actingAs($user, $driver); $now = Carbon::now(); @@ -431,7 +447,7 @@ public function itReturnsTheAuditDataIncludingUserAttributes() $model->setAuditEvent('created'); - $this->assertCount(10, $auditData = $model->toAudit()); + $this->assertCount(11, $auditData = $model->toAudit()); $this->assertArraySubset([ 'old_values' => [], @@ -444,7 +460,8 @@ public function itReturnsTheAuditDataIncludingUserAttributes() 'event' => 'created', 'auditable_id' => null, 'auditable_type' => Article::class, - 'user_id' => 1, + 'user_id' => $id, + 'user_type' => $type, 'url' => 'console', 'ip_address' => '127.0.0.1', 'user_agent' => 'Symfony/3.X', @@ -452,6 +469,39 @@ public function itReturnsTheAuditDataIncludingUserAttributes() ], $auditData, true); } + /** + * @return array + */ + public function userResolverProvider(): array + { + return [ + [ + 'api', + 'web', + null, + null, + ], + [ + 'web', + 'api', + null, + null, + ], + [ + 'api', + 'api', + 1, + User::class, + ], + [ + 'web', + 'web', + 1, + User::class, + ], + ]; + } + /** * @group Auditable::setAuditEvent * @group Auditable::toAudit @@ -479,7 +529,7 @@ public function itExcludesAttributesFromTheAuditDataWhenInStrictMode() $model->setAuditEvent('created'); - $this->assertCount(10, $auditData = $model->toAudit()); + $this->assertCount(11, $auditData = $model->toAudit()); $this->assertArraySubset([ 'old_values' => [], @@ -491,6 +541,7 @@ public function itExcludesAttributesFromTheAuditDataWhenInStrictMode() 'auditable_id' => null, 'auditable_type' => Article::class, 'user_id' => null, + 'user_type' => null, 'url' => 'console', 'ip_address' => '127.0.0.1', 'user_agent' => 'Symfony/3.X', @@ -587,7 +638,7 @@ public function transformAudit(array $data): array $model->setAuditEvent('created'); - $this->assertCount(10, $auditData = $model->toAudit()); + $this->assertCount(11, $auditData = $model->toAudit()); $this->assertArraySubset([ 'new_values' => [ @@ -1004,7 +1055,7 @@ public function itTransitionsToAnotherModelState( /** * @return array */ - public function auditableTransitionTestProvider() + public function auditableTransitionTestProvider(): array { return [ // diff --git a/tests/database/factories/AuditFactory.php b/tests/database/factories/AuditFactory.php index 31ec886e..f9f017f3 100644 --- a/tests/database/factories/AuditFactory.php +++ b/tests/database/factories/AuditFactory.php @@ -28,6 +28,7 @@ 'user_id' => function () { return factory(User::class)->create()->id; }, + 'user_type' => User::class, 'event' => 'updated', 'auditable_id' => function () { return factory(Article::class)->create()->id; diff --git a/tests/database/migrations/0000_00_00_000001_create_audits_test_table.php b/tests/database/migrations/0000_00_00_000001_create_audits_test_table.php index 42c16426..20afbc3b 100644 --- a/tests/database/migrations/0000_00_00_000001_create_audits_test_table.php +++ b/tests/database/migrations/0000_00_00_000001_create_audits_test_table.php @@ -26,7 +26,7 @@ public function up() { Schema::create('audits', function (Blueprint $table) { $table->increments('id'); - $table->unsignedInteger('user_id')->nullable(); + $table->nullableMorphs('user'); $table->string('event'); $table->morphs('auditable'); $table->text('old_values')->nullable();