From bf96489362b37c2785d2448a96e20a838c3b34c9 Mon Sep 17 00:00:00 2001 From: myrmidex Date: Sun, 8 Mar 2026 17:53:43 +0100 Subject: [PATCH] 50 - Add ActionPerformed event and LogActionListener for centralized DB logging --- app/Events/ActionPerformed.php | 18 ++++ app/Jobs/PublishNextArticleJob.php | 8 +- app/Listeners/LogActionListener.php | 21 +++++ .../PublishApprovedArticleListener.php | 8 +- app/Listeners/ValidateArticleListener.php | 4 +- app/Providers/AppServiceProvider.php | 7 ++ app/Services/Log/LogSaver.php | 2 +- tests/Feature/JobsAndEventsTest.php | 4 + tests/Unit/Events/ActionPerformedTest.php | 48 +++++++++++ .../Unit/Listeners/LogActionListenerTest.php | 85 +++++++++++++++++++ 10 files changed, 197 insertions(+), 8 deletions(-) create mode 100644 app/Events/ActionPerformed.php create mode 100644 app/Listeners/LogActionListener.php create mode 100644 tests/Unit/Events/ActionPerformedTest.php create mode 100644 tests/Unit/Listeners/LogActionListenerTest.php diff --git a/app/Events/ActionPerformed.php b/app/Events/ActionPerformed.php new file mode 100644 index 0000000..5d3b757 --- /dev/null +++ b/app/Events/ActionPerformed.php @@ -0,0 +1,18 @@ + */ + public array $context = [], + ) {} +} diff --git a/app/Jobs/PublishNextArticleJob.php b/app/Jobs/PublishNextArticleJob.php index 83bb74f..96c6cda 100644 --- a/app/Jobs/PublishNextArticleJob.php +++ b/app/Jobs/PublishNextArticleJob.php @@ -2,6 +2,8 @@ namespace App\Jobs; +use App\Enums\LogLevelEnum; +use App\Events\ActionPerformed; use App\Exceptions\PublishException; use App\Models\Article; use App\Models\ArticlePublication; @@ -53,7 +55,7 @@ public function handle(ArticleFetcher $articleFetcher, ArticlePublishingService return; } - logger()->info('Publishing next article from scheduled job', [ + ActionPerformed::dispatch('Publishing next article from scheduled job', LogLevelEnum::INFO, [ 'article_id' => $article->id, 'title' => $article->title, 'url' => $article->url, @@ -66,12 +68,12 @@ public function handle(ArticleFetcher $articleFetcher, ArticlePublishingService try { $publishingService->publishToRoutedChannels($article, $extractedData); - logger()->info('Successfully published article', [ + ActionPerformed::dispatch('Successfully published article', LogLevelEnum::INFO, [ 'article_id' => $article->id, 'title' => $article->title, ]); } catch (PublishException $e) { - logger()->error('Failed to publish article', [ + ActionPerformed::dispatch('Failed to publish article', LogLevelEnum::ERROR, [ 'article_id' => $article->id, 'error' => $e->getMessage(), ]); diff --git a/app/Listeners/LogActionListener.php b/app/Listeners/LogActionListener.php new file mode 100644 index 0000000..be5eab8 --- /dev/null +++ b/app/Listeners/LogActionListener.php @@ -0,0 +1,21 @@ +logSaver->log($event->level, $event->message, context: $event->context); + } catch (Exception $e) { + error_log('Failed to log action to database: '.$e->getMessage()); + } + } +} diff --git a/app/Listeners/PublishApprovedArticleListener.php b/app/Listeners/PublishApprovedArticleListener.php index e3207e6..f957809 100644 --- a/app/Listeners/PublishApprovedArticleListener.php +++ b/app/Listeners/PublishApprovedArticleListener.php @@ -2,6 +2,8 @@ namespace App\Listeners; +use App\Enums\LogLevelEnum; +use App\Events\ActionPerformed; use App\Events\ArticleApproved; use App\Services\Article\ArticleFetcher; use App\Services\Publishing\ArticlePublishingService; @@ -40,14 +42,14 @@ public function handle(ArticleApproved $event): void if ($publications->isNotEmpty()) { $article->update(['publish_status' => 'published']); - logger()->info('Published approved article', [ + ActionPerformed::dispatch('Published approved article', LogLevelEnum::INFO, [ 'article_id' => $article->id, 'title' => $article->title, ]); } else { $article->update(['publish_status' => 'error']); - logger()->warning('No publications created for approved article', [ + ActionPerformed::dispatch('No publications created for approved article', LogLevelEnum::WARNING, [ 'article_id' => $article->id, 'title' => $article->title, ]); @@ -55,7 +57,7 @@ public function handle(ArticleApproved $event): void } catch (Exception $e) { $article->update(['publish_status' => 'error']); - logger()->error('Failed to publish approved article', [ + ActionPerformed::dispatch('Failed to publish approved article', LogLevelEnum::ERROR, [ 'article_id' => $article->id, 'error' => $e->getMessage(), ]); diff --git a/app/Listeners/ValidateArticleListener.php b/app/Listeners/ValidateArticleListener.php index d5fff5d..0ef3d9b 100644 --- a/app/Listeners/ValidateArticleListener.php +++ b/app/Listeners/ValidateArticleListener.php @@ -2,6 +2,8 @@ namespace App\Listeners; +use App\Enums\LogLevelEnum; +use App\Events\ActionPerformed; use App\Events\NewArticleFetched; use App\Models\Setting; use App\Services\Article\ValidationService; @@ -37,7 +39,7 @@ public function handle(NewArticleFetched $event): void try { $article = $this->validationService->validate($article); } catch (Exception $e) { - logger()->error('Article validation failed', [ + ActionPerformed::dispatch('Article validation failed', LogLevelEnum::ERROR, [ 'article_id' => $article->id, 'error' => $e->getMessage(), ]); diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 13f3fdf..2a25ea8 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -3,7 +3,9 @@ namespace App\Providers; use App\Enums\LogLevelEnum; +use App\Events\ActionPerformed; use App\Events\ExceptionOccurred; +use App\Listeners\LogActionListener; use App\Listeners\LogExceptionToDatabase; use Error; use Illuminate\Contracts\Debug\ExceptionHandler; @@ -18,6 +20,11 @@ public function register(): void {} public function boot(): void { + Event::listen( + ActionPerformed::class, + LogActionListener::class, + ); + Event::listen( ExceptionOccurred::class, LogExceptionToDatabase::class, diff --git a/app/Services/Log/LogSaver.php b/app/Services/Log/LogSaver.php index 8ee852d..4b3c40a 100644 --- a/app/Services/Log/LogSaver.php +++ b/app/Services/Log/LogSaver.php @@ -43,7 +43,7 @@ public function debug(string $message, ?PlatformChannel $channel = null, array $ /** * @param array $context */ - private function log(LogLevelEnum $level, string $message, ?PlatformChannel $channel = null, array $context = []): void + public function log(LogLevelEnum $level, string $message, ?PlatformChannel $channel = null, array $context = []): void { $logContext = $context; diff --git a/tests/Feature/JobsAndEventsTest.php b/tests/Feature/JobsAndEventsTest.php index 9ae882c..ed85e4e 100644 --- a/tests/Feature/JobsAndEventsTest.php +++ b/tests/Feature/JobsAndEventsTest.php @@ -2,6 +2,7 @@ namespace Tests\Feature; +use App\Events\ActionPerformed; use App\Events\ArticleApproved; // use App\Events\ArticleReadyToPublish; // Class no longer exists use App\Events\ExceptionLogged; @@ -268,6 +269,9 @@ public function test_log_exception_to_database_listener_creates_log(): void public function test_event_listener_registration_works(): void { // Test that events are properly bound to listeners + $listeners = Event::getListeners(ActionPerformed::class); + $this->assertNotEmpty($listeners); + $listeners = Event::getListeners(NewArticleFetched::class); $this->assertNotEmpty($listeners); diff --git a/tests/Unit/Events/ActionPerformedTest.php b/tests/Unit/Events/ActionPerformedTest.php new file mode 100644 index 0000000..5091bbf --- /dev/null +++ b/tests/Unit/Events/ActionPerformedTest.php @@ -0,0 +1,48 @@ +assertEquals('Test message', $event->message); + $this->assertEquals(LogLevelEnum::INFO, $event->level); + $this->assertEquals([], $event->context); + } + + public function test_event_can_be_constructed_with_custom_level_and_context(): void + { + $context = ['article_id' => 1, 'error' => 'Something failed']; + + $event = new ActionPerformed( + 'Article validation failed', + LogLevelEnum::ERROR, + $context, + ); + + $this->assertEquals('Article validation failed', $event->message); + $this->assertEquals(LogLevelEnum::ERROR, $event->level); + $this->assertEquals($context, $event->context); + } + + public function test_event_uses_dispatchable_trait(): void + { + $this->assertContains(Dispatchable::class, class_uses(ActionPerformed::class)); + } + + public function test_event_does_not_use_serializes_models_trait(): void + { + $this->assertNotContains( + \Illuminate\Queue\SerializesModels::class, + class_uses(ActionPerformed::class), + ); + } +} diff --git a/tests/Unit/Listeners/LogActionListenerTest.php b/tests/Unit/Listeners/LogActionListenerTest.php new file mode 100644 index 0000000..fd9e291 --- /dev/null +++ b/tests/Unit/Listeners/LogActionListenerTest.php @@ -0,0 +1,85 @@ + 42], + ); + + $listener = app(LogActionListener::class); + $listener->handle($event); + + $this->assertDatabaseHas('logs', [ + 'level' => 'info', + 'message' => 'Article published successfully', + ]); + + $log = Log::where('message', 'Article published successfully')->first(); + $this->assertEquals(['article_id' => 42], $log->context); + } + + public function test_listener_creates_log_with_warning_level(): void + { + $event = new ActionPerformed( + 'No publications created', + LogLevelEnum::WARNING, + ['article_id' => 7], + ); + + $listener = app(LogActionListener::class); + $listener->handle($event); + + $this->assertDatabaseHas('logs', [ + 'level' => 'warning', + 'message' => 'No publications created', + ]); + } + + public function test_listener_creates_log_with_error_level(): void + { + $event = new ActionPerformed( + 'Publishing failed', + LogLevelEnum::ERROR, + ['error' => 'Connection timeout'], + ); + + $listener = app(LogActionListener::class); + $listener->handle($event); + + $this->assertDatabaseHas('logs', [ + 'level' => 'error', + 'message' => 'Publishing failed', + ]); + } + + public function test_listener_creates_log_with_empty_context(): void + { + $event = new ActionPerformed('Simple action'); + + $listener = app(LogActionListener::class); + $listener->handle($event); + + $this->assertDatabaseHas('logs', [ + 'level' => 'info', + 'message' => 'Simple action', + ]); + + $log = Log::where('message', 'Simple action')->first(); + $this->assertEquals([], $log->context); + } +}