diff --git a/config/audit.php b/config/audit.php index 7d05f850..5dbcf4e2 100644 --- a/config/audit.php +++ b/config/audit.php @@ -156,6 +156,21 @@ ], ], + /* + |-------------------------------------------------------------------------- + | Audit Queue Configurations + |-------------------------------------------------------------------------- + | + | Available audit queue configurations. + | + */ + + 'queue' => [ + 'connection' => 'sync', + 'queue' => 'default', + 'delay' => 0, + ], + /* |-------------------------------------------------------------------------- | Audit Console diff --git a/src/Auditable.php b/src/Auditable.php index 46b7b328..c909fbb3 100644 --- a/src/Auditable.php +++ b/src/Auditable.php @@ -56,6 +56,11 @@ trait Auditable */ public $isCustomEvent = false; + /** + * @var array Preloaded data to be used by resolvers + */ + public $preloadedResolverData = []; + /** * Auditable boot logic. * @@ -372,7 +377,7 @@ protected function resolveUser() } if (is_subclass_of($userResolver, \OwenIt\Auditing\Contracts\UserResolver::class)) { - return call_user_func([$userResolver, 'resolve']); + return call_user_func([$userResolver, 'resolve'], $this); } throw new AuditingException('Invalid UserResolver implementation'); @@ -403,6 +408,17 @@ protected function runResolvers(): array return $resolved; } + public function preloadResolverData() + { + $this->preloadedResolverData = $this->runResolvers(); + + if (!empty ($this->resolveUser())) { + $this->preloadedResolverData['user'] = $this->resolveUser(); + } + + return $this; + } + /** * Determine if an attribute is eligible for auditing. * diff --git a/src/AuditableObserver.php b/src/AuditableObserver.php index 4f042a83..08237c88 100644 --- a/src/AuditableObserver.php +++ b/src/AuditableObserver.php @@ -3,7 +3,8 @@ namespace OwenIt\Auditing; use OwenIt\Auditing\Contracts\Auditable; -use OwenIt\Auditing\Facades\Auditor; +use OwenIt\Auditing\Events\DispatchAudit; +use OwenIt\Auditing\Events\DispatchingAudit; class AuditableObserver { @@ -23,7 +24,7 @@ class AuditableObserver */ public function retrieved(Auditable $model) { - Auditor::execute($model->setAuditEvent('retrieved')); + $this->dispatchAudit($model->setAuditEvent('retrieved')); } /** @@ -35,7 +36,7 @@ public function retrieved(Auditable $model) */ public function created(Auditable $model) { - Auditor::execute($model->setAuditEvent('created')); + $this->dispatchAudit($model->setAuditEvent('created')); } /** @@ -49,7 +50,7 @@ public function updated(Auditable $model) { // Ignore the updated event when restoring if (!static::$restoring) { - Auditor::execute($model->setAuditEvent('updated')); + $this->dispatchAudit($model->setAuditEvent('updated')); } } @@ -62,7 +63,7 @@ public function updated(Auditable $model) */ public function deleted(Auditable $model) { - Auditor::execute($model->setAuditEvent('deleted')); + $this->dispatchAudit($model->setAuditEvent('deleted')); } /** @@ -89,10 +90,33 @@ public function restoring(Auditable $model) */ public function restored(Auditable $model) { - Auditor::execute($model->setAuditEvent('restored')); + $this->dispatchAudit($model->setAuditEvent('restored')); // Once the model is restored, we need to put everything back // as before, in case a legitimate update event is fired static::$restoring = false; } + + protected function dispatchAudit(Auditable $model) + { + if (!$model->readyForAuditing() || !$this->fireDispatchingAuditEvent($model)) { + return; + } + + // Unload the relations to prevent large amounts of unnecessary data from being serialized. + DispatchAudit::dispatch($model->preloadResolverData()->withoutRelations()); + } + + /** + * Fire the Auditing event. + * + * @param \OwenIt\Auditing\Contracts\Auditable $model + * + * @return bool + */ + protected function fireDispatchingAuditEvent(Auditable $model): bool + { + return app()->make('events') + ->until(new DispatchingAudit($model)) !== false; + } } diff --git a/src/AuditingEventServiceProvider.php b/src/AuditingEventServiceProvider.php index 8778a41a..19c04624 100644 --- a/src/AuditingEventServiceProvider.php +++ b/src/AuditingEventServiceProvider.php @@ -7,15 +7,23 @@ class_alias(\Illuminate\Foundation\Support\Providers\EventServiceProvider::class } else { class_alias(\Laravel\Lumen\Providers\EventServiceProvider::class, '\OwenIt\Auditing\ServiceProvider'); } + +use Illuminate\Support\Facades\Event; +use Illuminate\Support\Facades\Config; use OwenIt\Auditing\Events\AuditCustom; +use OwenIt\Auditing\Events\DispatchAudit; use OwenIt\Auditing\Listeners\RecordCustomAudit; +use OwenIt\Auditing\Listeners\ProcessDispatchAudit; class AuditingEventServiceProvider extends ServiceProvider { protected $listen = [ AuditCustom::class => [ RecordCustomAudit::class, - ] + ], + DispatchAudit::class => [ + ProcessDispatchAudit::class, + ], ]; /** diff --git a/src/AuditingServiceProvider.php b/src/AuditingServiceProvider.php index 5225b5a9..4d2d3860 100644 --- a/src/AuditingServiceProvider.php +++ b/src/AuditingServiceProvider.php @@ -9,7 +9,7 @@ use OwenIt\Auditing\Console\InstallCommand; use OwenIt\Auditing\Contracts\Auditor; -class AuditingServiceProvider extends ServiceProvider implements DeferrableProvider +class AuditingServiceProvider extends ServiceProvider { /** * Bootstrap the service provider. @@ -64,14 +64,4 @@ private function registerPublishing() } } } - - /** - * {@inheritdoc} - */ - public function provides() - { - return [ - Auditor::class, - ]; - } } diff --git a/src/Contracts/UserResolver.php b/src/Contracts/UserResolver.php index 366a0834..2db87fc0 100644 --- a/src/Contracts/UserResolver.php +++ b/src/Contracts/UserResolver.php @@ -9,5 +9,5 @@ interface UserResolver * * @return \Illuminate\Contracts\Auth\Authenticatable|null */ - public static function resolve(); + public static function resolve(Auditable $auditable); } diff --git a/src/Events/DispatchAudit.php b/src/Events/DispatchAudit.php new file mode 100644 index 00000000..4ef0a383 --- /dev/null +++ b/src/Events/DispatchAudit.php @@ -0,0 +1,28 @@ +model = $model; + } +} diff --git a/src/Events/DispatchingAudit.php b/src/Events/DispatchingAudit.php new file mode 100644 index 00000000..837b14e0 --- /dev/null +++ b/src/Events/DispatchingAudit.php @@ -0,0 +1,25 @@ +model = $model; + } +} diff --git a/src/Listeners/ProcessDispatchAudit.php b/src/Listeners/ProcessDispatchAudit.php new file mode 100644 index 00000000..ed370a62 --- /dev/null +++ b/src/Listeners/ProcessDispatchAudit.php @@ -0,0 +1,31 @@ +model); + } +} diff --git a/src/Resolvers/IpAddressResolver.php b/src/Resolvers/IpAddressResolver.php index e4ab848e..61b617fe 100644 --- a/src/Resolvers/IpAddressResolver.php +++ b/src/Resolvers/IpAddressResolver.php @@ -10,6 +10,6 @@ class IpAddressResolver implements Resolver { public static function resolve(Auditable $auditable): string { - return Request::ip(); + return $auditable->preloadedResolverData['ip_address'] ?? Request::ip(); } } diff --git a/src/Resolvers/UrlResolver.php b/src/Resolvers/UrlResolver.php index ad998644..8bc63373 100644 --- a/src/Resolvers/UrlResolver.php +++ b/src/Resolvers/UrlResolver.php @@ -13,6 +13,10 @@ class UrlResolver implements \OwenIt\Auditing\Contracts\Resolver */ public static function resolve(Auditable $auditable): string { + if (! empty($auditable->preloadedResolverData['url'])) { + return $auditable->preloadedResolverData['url']; + } + if (App::runningInConsole()) { return self::resolveCommandLine(); } diff --git a/src/Resolvers/UserAgentResolver.php b/src/Resolvers/UserAgentResolver.php index 95922b56..2847b214 100644 --- a/src/Resolvers/UserAgentResolver.php +++ b/src/Resolvers/UserAgentResolver.php @@ -10,6 +10,6 @@ class UserAgentResolver implements Resolver { public static function resolve(Auditable $auditable) { - return Request::header('User-Agent'); + return $auditable->preloadedResolverData['user_agent'] ?? Request::header('User-Agent'); } } diff --git a/src/Resolvers/UserResolver.php b/src/Resolvers/UserResolver.php index 25c8c9e4..80c35f1f 100644 --- a/src/Resolvers/UserResolver.php +++ b/src/Resolvers/UserResolver.php @@ -4,14 +4,19 @@ use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Config; +use OwenIt\Auditing\Contracts\Auditable; class UserResolver implements \OwenIt\Auditing\Contracts\UserResolver { /** * @return \Illuminate\Contracts\Auth\Authenticatable|null */ - public static function resolve() + public static function resolve(Auditable $auditable) { + if (! empty($auditable->preloadedResolverData['user'])) { + return $auditable->preloadedResolverData['user']; + } + $guards = Config::get('audit.user.guards', [ \config('auth.defaults.guard') ]); diff --git a/tests/Unit/AuditTest.php b/tests/Unit/AuditTest.php index c444e530..637d0432 100644 --- a/tests/Unit/AuditTest.php +++ b/tests/Unit/AuditTest.php @@ -415,14 +415,7 @@ public function itReturnsAuditableModifiedAttributesAsJsonString() */ public function itReturnsDecodedAuditableAttributes() { - $article = new class () extends Article { - protected $table = 'articles'; - - protected $attributeModifiers = [ - 'title' => Base64Encoder::class, - 'content' => LeftRedactor::class, - ]; - }; + $article = new itReturnsDecodedAuditableAttributesArticle(); // Audit with redacted/encoded attributes $audit = factory(Audit::class)->create([ @@ -489,3 +482,13 @@ public function itReturnsEmptyTags() $this->assertEmpty($audit->getTags()); } } + +class itReturnsDecodedAuditableAttributesArticle extends Article +{ + protected $table = 'articles'; + + protected $attributeModifiers = [ + 'title' => Base64Encoder::class, + 'content' => LeftRedactor::class, + ]; +} \ No newline at end of file diff --git a/tests/Unit/AuditableObserverTest.php b/tests/Unit/AuditableObserverTest.php index ddfe0f1f..e7ef7614 100644 --- a/tests/Unit/AuditableObserverTest.php +++ b/tests/Unit/AuditableObserverTest.php @@ -2,11 +2,65 @@ namespace OwenIt\Auditing\Tests; +use OwenIt\Auditing\Models\Audit; +use Illuminate\Support\Facades\Event; use OwenIt\Auditing\AuditableObserver; use OwenIt\Auditing\Tests\Models\Article; +use OwenIt\Auditing\Events\DispatchAudit; +use OwenIt\Auditing\Events\DispatchingAudit; class AuditableObserverTest extends AuditingTestCase { + /** + * @test + * + * @dataProvider auditableObserverDispatchTestProvider + */ + public function itWillCancelTheAuditDispatchingFromAnEventListener($eventMethod) + { + Event::fake( + [ + DispatchAudit::class, + ] + ); + + Event::listen(DispatchingAudit::class, function () { + return false; + }); + + $observer = new AuditableObserver(); + $model = factory(Article::class)->create(); + + $observer->$eventMethod($model); + + $this->assertNull(Audit::first()); + + Event::assertNotDispatched(DispatchAudit::class); + } + + /** + * @test + * + * @dataProvider auditableObserverDispatchTestProvider + */ + public function itDispatchesTheCorrectEvents(string $eventMethod) + { + Event::fake(); + + $observer = new AuditableObserver(); + $model = factory(Article::class)->create(); + + $observer->$eventMethod($model); + + Event::assertDispatched(DispatchingAudit::class, function ($event) use ($model) { + return $event->model->is($model); + }); + + Event::assertDispatched(DispatchAudit::class, function ($event) use ($model) { + return $event->model->is($model); + }); + } + /** * @group AuditableObserver::retrieved * @group AuditableObserver::created @@ -72,4 +126,25 @@ public function auditableObserverTestProvider(): array ], ]; } + + /** + * @return array + */ + public function auditableObserverDispatchTestProvider(): array + { + return [ + [ + 'created', + ], + [ + 'updated', + ], + [ + 'deleted', + ], + [ + 'restored', + ], + ]; + } } diff --git a/tests/Unit/ProcessDispatchAuditTest.php b/tests/Unit/ProcessDispatchAuditTest.php new file mode 100644 index 00000000..0ba4d7fa --- /dev/null +++ b/tests/Unit/ProcessDispatchAuditTest.php @@ -0,0 +1,75 @@ +app->version(), '8.0.0', '<')) { + $this->markTestSkipped('This test is only for Laravel 8.0.0+'); + } + + Event::fake(); + + Event::assertListening( + DispatchAudit::class, + ProcessDispatchAudit::class + ); + } + + /** + * @test + */ + public function itGetsProperlyQueued() + { + Queue::fake(); + + $model = factory(Article::class)->create(); + + DispatchAudit::dispatch($model); + + Queue::assertPushed(CallQueuedListener::class, function ($job) use ($model) { + return $job->class == ProcessDispatchAudit::class + && $job->data[0] instanceof DispatchAudit + && $job->data[0]->model->is($model); + }); + } + + /** + * @test + */ + public function itCanHaveConnectionAndQueueSet() + { + $this->app['config']->set('audit.queue.connection', 'redis'); + $this->app['config']->set('audit.queue.queue', 'audits'); + $this->app['config']->set('audit.queue.delay', 60); + + Queue::fake(); + + $model = factory(Article::class)->create(); + + DispatchAudit::dispatch($model); + + Queue::assertPushedOn('audits', CallQueuedListener::class, function ($job) use ($model) { + $instantiatedJob = new $job->class; + + return $job->class == ProcessDispatchAudit::class + && $job->data[0] instanceof DispatchAudit + && $job->data[0]->model->is($model) + && $instantiatedJob->viaConnection() == 'redis' + && $instantiatedJob->withDelay(new DispatchAudit($model)) == 60; + }); + } +} \ No newline at end of file