From 8b02f6c95cae2e87368dd3b2ba1b038d07e643ef Mon Sep 17 00:00:00 2001 From: jtgrimes Date: Sun, 28 Sep 2025 14:18:41 -0700 Subject: [PATCH 01/16] Events thrown for create, update, delete links Archiving moved to event listener --- app/Events/LinkCreated.php | 17 +++++ app/Events/LinkDeleted.php | 16 +++++ app/Events/LinkUpdated.php | 18 ++++++ app/Listeners/ArchiveNewLinks.php | 31 +++++++++ app/Models/Link.php | 21 ------ app/Providers/AppServiceProvider.php | 5 ++ app/Repositories/LinkRepository.php | 9 ++- .../Controller/Models/LinkControllerTest.php | 64 +++++++++++-------- tests/Listeners/ArchiveNewLinksTest.php | 48 ++++++++++++++ 9 files changed, 178 insertions(+), 51 deletions(-) create mode 100644 app/Events/LinkCreated.php create mode 100644 app/Events/LinkDeleted.php create mode 100644 app/Events/LinkUpdated.php create mode 100644 app/Listeners/ArchiveNewLinks.php create mode 100644 tests/Listeners/ArchiveNewLinksTest.php diff --git a/app/Events/LinkCreated.php b/app/Events/LinkCreated.php new file mode 100644 index 000000000..8915331fa --- /dev/null +++ b/app/Events/LinkCreated.php @@ -0,0 +1,17 @@ +link->visibility === ModelAttribute::VISIBILITY_PRIVATE + && usersettings('archive_private_backups_enabled') === false + ) { + return; + } + + SaveLinkToWaybackmachine::dispatchAfterResponse($event->link); + } +} diff --git a/app/Models/Link.php b/app/Models/Link.php index 980c443de..695dd2c00 100644 --- a/app/Models/Link.php +++ b/app/Models/Link.php @@ -239,27 +239,6 @@ public function addedAt(): string return $output; } - /** - * Dispatch the SaveLinkToWaybackmachine job, if Internet Archive backups - * are enabled. - * If the link is private, private Internet Archive backups must be enabled - * too. - */ - public function initiateInternetArchiveBackup(): void - { - if (usersettings('archive_backups_enabled') === false) { - return; - } - - if ($this->visibility === ModelAttribute::VISIBILITY_PRIVATE - && usersettings('archive_private_backups_enabled') === false - ) { - return; - } - - SaveLinkToWaybackmachine::dispatchAfterResponse($this); - } - /** * Create a base uri of the link url, consisting of a possible auth, the * hostname, a port if present, and the path. The scheme, fragments and diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index f8a27f5e2..d5722db3f 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -2,7 +2,10 @@ namespace App\Providers; +use App\Events\LinkCreated; +use App\Listeners\ArchiveNewLinks; use Illuminate\Pagination\Paginator; +use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Schema; use Illuminate\Support\ServiceProvider; @@ -18,6 +21,8 @@ public function boot(): void Schema::defaultStringLength(191); Paginator::useBootstrap(); + + Event::listen(LinkCreated::class, ArchiveNewLinks::class); } /** diff --git a/app/Repositories/LinkRepository.php b/app/Repositories/LinkRepository.php index 69d6c47b5..5f8beb935 100644 --- a/app/Repositories/LinkRepository.php +++ b/app/Repositories/LinkRepository.php @@ -3,6 +3,9 @@ namespace App\Repositories; use App\Enums\ModelAttribute; +use App\Events\LinkCreated; +use App\Events\LinkDeleted; +use App\Events\LinkUpdated; use App\Helper\HtmlMeta; use App\Helper\LinkIconMapper; use App\Models\Link; @@ -46,7 +49,7 @@ public static function create(array $data, bool $flashAlerts = false): Link self::processLinkTaxonomies($link, $data); - $link->initiateInternetArchiveBackup(); + LinkCreated::dispatch($link); return $link; } @@ -65,6 +68,7 @@ public static function update(Link $link, array $data): Link $link->update($data); self::processLinkTaxonomies($link, $data); + LinkUpdated::dispatch($link); return $link; } @@ -104,6 +108,7 @@ public static function bulkUpdate(array $models, array $data): Collection public static function delete(Link $link): bool { try { + $id = $link->id; $link->tags()->detach(); $link->lists()->detach(); $link->delete(); @@ -111,7 +116,7 @@ public static function delete(Link $link): bool Log::error($e); return false; } - + LinkDeleted::dispatch($id); return true; } diff --git a/tests/Controller/Models/LinkControllerTest.php b/tests/Controller/Models/LinkControllerTest.php index 61f9d70d2..cec877007 100644 --- a/tests/Controller/Models/LinkControllerTest.php +++ b/tests/Controller/Models/LinkControllerTest.php @@ -2,7 +2,9 @@ namespace Tests\Controller\Models; -use App\Jobs\SaveLinkToWaybackmachine; +use App\Events\LinkCreated; +use App\Events\LinkDeleted; +use App\Events\LinkUpdated; use App\Models\Link; use App\Models\LinkList; use App\Models\Tag; @@ -10,9 +12,9 @@ use App\Settings\UserSettings; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Http\Client\ConnectionException; +use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Queue; -use Spatie\LaravelSettings\Settings; use Tests\Controller\Traits\PreparesTestData; use Tests\TestCase; @@ -233,12 +235,9 @@ public function test_store_request_with_continue(): void $this->assertEquals('https://example.com', $databaseLink->url); } - public function test_store_request_without_archive_backup(): void + public function test_store_request_fires_store_event(): void { - UserSettings::fake([ - 'archive_backups_enabled' => false, - ]); - + Event::fake(); $this->post('links', [ 'url' => 'https://example.com', 'title' => null, @@ -247,27 +246,7 @@ public function test_store_request_without_archive_backup(): void 'tags' => null, 'visibility' => 1, ]); - - Queue::assertNotPushed(SaveLinkToWaybackmachine::class); - } - - public function test_store_request_without_private_archive_backup(): void - { - UserSettings::fake([ - 'archive_backups_enabled' => true, - 'archive_private_backups_enabled' => false, - ]); - - $this->post('links', [ - 'url' => 'https://example.com', - 'title' => null, - 'description' => null, - 'lists' => null, - 'tags' => null, - 'visibility' => 3, - ]); - - Queue::assertNotPushed(SaveLinkToWaybackmachine::class); + Event::assertDispatched(LinkCreated::class); } public function test_validation_error_for_create(): void @@ -436,6 +415,24 @@ public function test_validation_error_for_update(): void ]); } + public function test_update_request_fires_update_event(): void + { + Event::fake(); + + $baseLink = Link::factory()->create(); + + $this->patch('links/1', [ + 'link_id' => $baseLink->id, + 'url' => 'https://new-example.com', + 'title' => 'New Title', + 'description' => 'New Description', + 'lists' => null, + 'tags' => null, + 'visibility' => 1, + ]); + Event::assertDispatched(LinkUpdated::class); + } + public function test_delete_response(): void { $this->createTestLinks(); @@ -449,6 +446,17 @@ public function test_delete_response(): void $this->delete('links/3')->assertForbidden(); } + public function test_delete_request_fires_event(): void + { + Event::fake(); + + $this->createTestLinks(); + + $this->delete('links/1'); + + Event::assertDispatched(LinkDeleted::class); + } + public function test_missing_model_error_for_delete(): void { $this->delete('links/1')->assertNotFound(); diff --git a/tests/Listeners/ArchiveNewLinksTest.php b/tests/Listeners/ArchiveNewLinksTest.php new file mode 100644 index 000000000..014099835 --- /dev/null +++ b/tests/Listeners/ArchiveNewLinksTest.php @@ -0,0 +1,48 @@ + false, + ]); + + $this->post('links', [ + 'url' => 'https://example.com', + 'title' => null, + 'description' => null, + 'lists' => null, + 'tags' => null, + 'visibility' => 1, + ]); + + Queue::assertNotPushed(SaveLinkToWaybackmachine::class); + } + + public function test_store_request_without_private_archive_backup(): void + { + UserSettings::fake([ + 'archive_backups_enabled' => true, + 'archive_private_backups_enabled' => false, + ]); + + $this->post('links', [ + 'url' => 'https://example.com', + 'title' => null, + 'description' => null, + 'lists' => null, + 'tags' => null, + 'visibility' => 3, + ]); + + Queue::assertNotPushed(SaveLinkToWaybackmachine::class); + } +} From c4582cfb746464614655c52b7f91b8a1fa120932 Mon Sep 17 00:00:00 2001 From: jtgrimes Date: Tue, 2 Dec 2025 16:42:09 -0800 Subject: [PATCH 02/16] Move archive handling from event listener to plugin --- .../NewLinkToWaybackMachine.php} | 4 ++-- app/Providers/AppServiceProvider.php | 4 ++-- config/linkace.php | 3 +++ .../NewLinkToWaybackMachineTest.php} | 8 ++++---- 4 files changed, 11 insertions(+), 8 deletions(-) rename app/{Listeners/ArchiveNewLinks.php => Plugins/NewLinkToWaybackMachine.php} (93%) rename tests/{Listeners/ArchiveNewLinksTest.php => Plugins/NewLinkToWaybackMachineTest.php} (81%) diff --git a/app/Listeners/ArchiveNewLinks.php b/app/Plugins/NewLinkToWaybackMachine.php similarity index 93% rename from app/Listeners/ArchiveNewLinks.php rename to app/Plugins/NewLinkToWaybackMachine.php index 622c952dc..f05510d37 100644 --- a/app/Listeners/ArchiveNewLinks.php +++ b/app/Plugins/NewLinkToWaybackMachine.php @@ -1,12 +1,12 @@ [ + \App\Plugins\NewLinkToWaybackMachine::class, + ], ]; diff --git a/tests/Listeners/ArchiveNewLinksTest.php b/tests/Plugins/NewLinkToWaybackMachineTest.php similarity index 81% rename from tests/Listeners/ArchiveNewLinksTest.php rename to tests/Plugins/NewLinkToWaybackMachineTest.php index 014099835..d211a5627 100644 --- a/tests/Listeners/ArchiveNewLinksTest.php +++ b/tests/Plugins/NewLinkToWaybackMachineTest.php @@ -1,15 +1,15 @@ false, @@ -27,7 +27,7 @@ public function test_store_request_without_archive_backup(): void Queue::assertNotPushed(SaveLinkToWaybackmachine::class); } - public function test_store_request_without_private_archive_backup(): void + public function test_store_request_doesnt_archive_private_links(): void { UserSettings::fake([ 'archive_backups_enabled' => true, From 35eb15ec92d710226380639ef9abee38cf6c38f2 Mon Sep 17 00:00:00 2001 From: jtgrimes Date: Tue, 2 Dec 2025 18:15:23 -0800 Subject: [PATCH 03/16] Plugins running through plugin manager --- app/Helper/PluginManager.php | 23 +++++++++++ app/Plugins/NewLinkToWaybackMachine.php | 1 + app/Providers/AppServiceProvider.php | 6 +-- tests/Helper/PluginManagerTest.php | 33 ++++++++++++++++ tests/Plugins/NewLinkToWaybackMachineTest.php | 38 +++++++++++++++++++ tests/Plugins/SamplePlugin.php | 15 ++++++++ 6 files changed, 112 insertions(+), 4 deletions(-) create mode 100644 app/Helper/PluginManager.php create mode 100644 tests/Helper/PluginManagerTest.php create mode 100644 tests/Plugins/SamplePlugin.php diff --git a/app/Helper/PluginManager.php b/app/Helper/PluginManager.php new file mode 100644 index 000000000..53401969f --- /dev/null +++ b/app/Helper/PluginManager.php @@ -0,0 +1,23 @@ +assertTrue(true, 'no exception thrown'); + } + + public function test_plugin_manager_registers_event_listeners() + { + Config::set('linkace.plugins', [SamplePlugin::class]); + PluginManager::registerPlugins(); + Event::assertListening(LinkUpdated::class, SamplePlugin::class); + } +} diff --git a/tests/Plugins/NewLinkToWaybackMachineTest.php b/tests/Plugins/NewLinkToWaybackMachineTest.php index d211a5627..90b4413e2 100644 --- a/tests/Plugins/NewLinkToWaybackMachineTest.php +++ b/tests/Plugins/NewLinkToWaybackMachineTest.php @@ -3,12 +3,50 @@ namespace Tests\Plugins; use App\Jobs\SaveLinkToWaybackmachine; +use App\Models\User; use App\Settings\UserSettings; +use Illuminate\Support\Facades\Config; +use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Queue; use Tests\TestCase; class NewLinkToWaybackMachineTest extends TestCase { + protected function setUp(): void + { + parent::setUp(); + + $user = User::factory()->create(); + $this->actingAs($user); + + Http::preventStrayRequests(); + Http::fake([ + 'example.com' => Http::response('ok'), + ]); + + Queue::fake(); + Config::set('plugins', [\App\Plugins\NewLinkToWaybackMachine::class]); + } + + public function test_store_request_archives_links(): void + { + UserSettings::fake([ + 'archive_backups_enabled' => true, + 'archive_private_backups_enabled' => false, + ]); + + $this->post('links', [ + 'url' => 'https://example.com', + 'title' => null, + 'description' => null, + 'lists' => null, + 'tags' => null, + 'visibility' => 1, + ]); + + Queue::assertPushed(SaveLinkToWaybackmachine::class); + } + public function test_store_request_doesnt_archive_without_being_enabled(): void { UserSettings::fake([ diff --git a/tests/Plugins/SamplePlugin.php b/tests/Plugins/SamplePlugin.php new file mode 100644 index 000000000..0574005f1 --- /dev/null +++ b/tests/Plugins/SamplePlugin.php @@ -0,0 +1,15 @@ + Date: Tue, 2 Dec 2025 18:54:51 -0800 Subject: [PATCH 04/16] Use reflection to register plugin listeners --- app/Helper/PluginManager.php | 19 +++++++++++++++---- app/Plugins/NewLinkToWaybackMachine.php | 1 - tests/Helper/PluginManagerTest.php | 22 +++++++++++++++++++++- tests/Plugins/SamplePlugin.php | 5 ++--- 4 files changed, 38 insertions(+), 9 deletions(-) diff --git a/app/Helper/PluginManager.php b/app/Helper/PluginManager.php index 53401969f..c79e82197 100644 --- a/app/Helper/PluginManager.php +++ b/app/Helper/PluginManager.php @@ -2,11 +2,11 @@ namespace App\Helper; -use App\Events\LinkCreated; -use App\Plugins\NewLinkToWaybackMachine; use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Log; +use Illuminate\Support\Reflector; +use ReflectionClass; class PluginManager { @@ -14,10 +14,21 @@ public function registerPlugins() { $plugins = Config::get('linkace.plugins'); foreach ($plugins as $plugin) { - foreach ($plugin::$events as $event) { + $parameterTypes = $this->getParameterTypesFor($plugin); + foreach ($parameterTypes as $event) { Event::listen($event, $plugin); - Log::debug("$plugin listening for $event"); } } } + + private function getParameterTypesFor(string $pluginClass) + { + $reflectionClass = new ReflectionClass($pluginClass); + $reflectionMethod = $reflectionClass->getMethod('handle'); + $parameters = $reflectionMethod->getParameters(); + if (count($parameters) !== 1) { + throw new \Exception('Plugins should have exactly 1 parameter'); + } + return Reflector::getParameterClassNames($parameters[0]); + } } diff --git a/app/Plugins/NewLinkToWaybackMachine.php b/app/Plugins/NewLinkToWaybackMachine.php index 4715aa934..f05510d37 100644 --- a/app/Plugins/NewLinkToWaybackMachine.php +++ b/app/Plugins/NewLinkToWaybackMachine.php @@ -8,7 +8,6 @@ class NewLinkToWaybackMachine { - public static array $events = [LinkCreated::class]; /** * Dispatch the SaveLinkToWaybackmachine job, if Internet Archive backups * are enabled. diff --git a/tests/Helper/PluginManagerTest.php b/tests/Helper/PluginManagerTest.php index 2f6b4baa4..d1a62ad3b 100644 --- a/tests/Helper/PluginManagerTest.php +++ b/tests/Helper/PluginManagerTest.php @@ -2,7 +2,9 @@ namespace Tests\Helper; +use App\Events\LinkCreated; use App\Events\LinkUpdated; +use App\Plugins\NewLinkToWaybackMachine; use Facades\App\Helper\PluginManager; use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\Event; @@ -24,10 +26,28 @@ public function test_plugin_manager_doesnt_bomb_with_no_plugins() $this->assertTrue(true, 'no exception thrown'); } - public function test_plugin_manager_registers_event_listeners() + public function test_plugin_manager_registers_single_listener() + { + Config::set('linkace.plugins', [NewLinkToWaybackMachine::class]); + PluginManager::registerPlugins(); + Event::assertListening(LinkCreated::class, NewLinkToWaybackMachine::class); + } + + public function test_plugin_manager_registers_all_events_for_union_types() { Config::set('linkace.plugins', [SamplePlugin::class]); PluginManager::registerPlugins(); + Event::assertListening(LinkCreated::class, SamplePlugin::class); + Event::assertListening(LinkUpdated::class, SamplePlugin::class); + } + + public function test_plugin_manager_handles_multiple_plugins() + { + Config::set('linkace.plugins', [SamplePlugin::class, NewLinkToWaybackMachine::class]); + PluginManager::registerPlugins(); + Event::assertListening(LinkCreated::class, SamplePlugin::class); + Event::assertListening(LinkCreated::class, NewLinkToWaybackMachine::class); Event::assertListening(LinkUpdated::class, SamplePlugin::class); } + } diff --git a/tests/Plugins/SamplePlugin.php b/tests/Plugins/SamplePlugin.php index 0574005f1..84ef0e383 100644 --- a/tests/Plugins/SamplePlugin.php +++ b/tests/Plugins/SamplePlugin.php @@ -2,13 +2,12 @@ namespace Tests\Plugins; +use App\Events\LinkCreated; use App\Events\LinkUpdated; class SamplePlugin { - public static array $events = [LinkUpdated::class]; - - public function handle(LinkUpdated $event) : void + public function handle(LinkCreated|LinkUpdated $event) : void { // do nothing } From 6601473e21e2d535428d355a32bbd595fd841d81 Mon Sep 17 00:00:00 2001 From: jtgrimes Date: Tue, 2 Dec 2025 18:56:38 -0800 Subject: [PATCH 05/16] Use reflection to register plugin listeners --- tests/Helper/PluginManagerTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/Helper/PluginManagerTest.php b/tests/Helper/PluginManagerTest.php index d1a62ad3b..9c2accaed 100644 --- a/tests/Helper/PluginManagerTest.php +++ b/tests/Helper/PluginManagerTest.php @@ -49,5 +49,4 @@ public function test_plugin_manager_handles_multiple_plugins() Event::assertListening(LinkCreated::class, NewLinkToWaybackMachine::class); Event::assertListening(LinkUpdated::class, SamplePlugin::class); } - } From 1624bd768d0427abd81a1f950862c01b7faa4de7 Mon Sep 17 00:00:00 2001 From: jtgrimes Date: Tue, 2 Dec 2025 19:11:32 -0800 Subject: [PATCH 06/16] set default plugin list so existing installs don't blow up --- app/Helper/PluginManager.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/Helper/PluginManager.php b/app/Helper/PluginManager.php index c79e82197..563f78b54 100644 --- a/app/Helper/PluginManager.php +++ b/app/Helper/PluginManager.php @@ -2,6 +2,7 @@ namespace App\Helper; +use App\Plugins\NewLinkToWaybackMachine; use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Log; @@ -12,7 +13,7 @@ class PluginManager { public function registerPlugins() { - $plugins = Config::get('linkace.plugins'); + $plugins = Config::get('linkace.plugins', [NewLinkToWaybackMachine::class]); foreach ($plugins as $plugin) { $parameterTypes = $this->getParameterTypesFor($plugin); foreach ($parameterTypes as $event) { From 96a66552b39a8bcee3dcd8cb865e0ab1bd213ce6 Mon Sep 17 00:00:00 2001 From: "J.T. Grimes" Date: Wed, 3 Dec 2025 07:02:41 -0800 Subject: [PATCH 07/16] Remove unnecessary use statement in app/Helper/PluginManager.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- app/Helper/PluginManager.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/Helper/PluginManager.php b/app/Helper/PluginManager.php index 563f78b54..2d73d479f 100644 --- a/app/Helper/PluginManager.php +++ b/app/Helper/PluginManager.php @@ -4,8 +4,7 @@ use App\Plugins\NewLinkToWaybackMachine; use Illuminate\Support\Facades\Config; -use Illuminate\Support\Facades\Event; -use Illuminate\Support\Facades\Log; +use Illuminate\Support\Facades/Event; use Illuminate\Support\Reflector; use ReflectionClass; From e259a95334871f7ce5c0dfafa8d0eae179b2d57e Mon Sep 17 00:00:00 2001 From: "J.T. Grimes" Date: Wed, 3 Dec 2025 07:03:25 -0800 Subject: [PATCH 08/16] Add return type to registerPlugins in app/Helper/PluginManager.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- app/Helper/PluginManager.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Helper/PluginManager.php b/app/Helper/PluginManager.php index 2d73d479f..a8efcbbc7 100644 --- a/app/Helper/PluginManager.php +++ b/app/Helper/PluginManager.php @@ -10,7 +10,7 @@ class PluginManager { - public function registerPlugins() + public function registerPlugins(): void { $plugins = Config::get('linkace.plugins', [NewLinkToWaybackMachine::class]); foreach ($plugins as $plugin) { From df4e9637653deacf5e93a59a4e020ac319e0b6c1 Mon Sep 17 00:00:00 2001 From: "J.T. Grimes" Date: Wed, 3 Dec 2025 07:04:19 -0800 Subject: [PATCH 09/16] Improve exception message in app/Helper/PluginManager.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- app/Helper/PluginManager.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Helper/PluginManager.php b/app/Helper/PluginManager.php index a8efcbbc7..580077173 100644 --- a/app/Helper/PluginManager.php +++ b/app/Helper/PluginManager.php @@ -27,7 +27,7 @@ private function getParameterTypesFor(string $pluginClass) $reflectionMethod = $reflectionClass->getMethod('handle'); $parameters = $reflectionMethod->getParameters(); if (count($parameters) !== 1) { - throw new \Exception('Plugins should have exactly 1 parameter'); + throw new \Exception("Plugin {$pluginClass} should have exactly 1 parameter in its handle method"); } return Reflector::getParameterClassNames($parameters[0]); } From a7bd9f651d8f3d1496f1c99d1bc99ffad880d742 Mon Sep 17 00:00:00 2001 From: "J.T. Grimes" Date: Wed, 3 Dec 2025 07:05:19 -0800 Subject: [PATCH 10/16] add return type to getParameterTypesFor app/Helper/PluginManager.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- app/Helper/PluginManager.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Helper/PluginManager.php b/app/Helper/PluginManager.php index 580077173..819ded8f7 100644 --- a/app/Helper/PluginManager.php +++ b/app/Helper/PluginManager.php @@ -21,7 +21,7 @@ public function registerPlugins(): void } } - private function getParameterTypesFor(string $pluginClass) + private function getParameterTypesFor(string $pluginClass): array { $reflectionClass = new ReflectionClass($pluginClass); $reflectionMethod = $reflectionClass->getMethod('handle'); From 94e46067f6f211d9b59b92cbb33ca4719c4cc92f Mon Sep 17 00:00:00 2001 From: jtgrimes Date: Wed, 3 Dec 2025 08:18:36 -0800 Subject: [PATCH 11/16] remove comment from empty constructors --- app/Events/LinkCreated.php | 1 - app/Events/LinkDeleted.php | 1 - app/Events/LinkUpdated.php | 2 -- 3 files changed, 4 deletions(-) diff --git a/app/Events/LinkCreated.php b/app/Events/LinkCreated.php index 8915331fa..5355b5912 100644 --- a/app/Events/LinkCreated.php +++ b/app/Events/LinkCreated.php @@ -12,6 +12,5 @@ class LinkCreated public function __construct(public Link $link) { - // no op constructor } } diff --git a/app/Events/LinkDeleted.php b/app/Events/LinkDeleted.php index 5baadcd44..ce8446317 100644 --- a/app/Events/LinkDeleted.php +++ b/app/Events/LinkDeleted.php @@ -11,6 +11,5 @@ class LinkDeleted public function __construct(public int $link_id) { - // no op constructor } } diff --git a/app/Events/LinkUpdated.php b/app/Events/LinkUpdated.php index ccc4c4e63..427ab5bf9 100644 --- a/app/Events/LinkUpdated.php +++ b/app/Events/LinkUpdated.php @@ -12,7 +12,5 @@ class LinkUpdated public function __construct(public Link $link) { - // no op constructor } - } From e0d5c1f5ebe03c3a3b94565bf2eb984af3eb658f Mon Sep 17 00:00:00 2001 From: jtgrimes Date: Wed, 3 Dec 2025 08:22:46 -0800 Subject: [PATCH 12/16] Add PluginException, expand test coverage for all the ways a plugin can go wrong --- app/Exceptions/PluginException.php | 37 +++++++++++++++++ app/Helper/PluginManager.php | 30 +++++++++++--- tests/Helper/PluginManagerTest.php | 53 ++++++++++++++++++++----- tests/Mocks/NoHandlePlugin.php | 7 ++++ tests/Mocks/NoParameterPlugin.php | 10 +++++ tests/Mocks/SimplePlugin.php | 12 ++++++ tests/Mocks/TooManyParametersPlugin.php | 12 ++++++ tests/Mocks/UnionPlugin.php | 13 ++++++ 8 files changed, 158 insertions(+), 16 deletions(-) create mode 100644 app/Exceptions/PluginException.php create mode 100644 tests/Mocks/NoHandlePlugin.php create mode 100644 tests/Mocks/NoParameterPlugin.php create mode 100644 tests/Mocks/SimplePlugin.php create mode 100644 tests/Mocks/TooManyParametersPlugin.php create mode 100644 tests/Mocks/UnionPlugin.php diff --git a/app/Exceptions/PluginException.php b/app/Exceptions/PluginException.php new file mode 100644 index 000000000..ae3684390 --- /dev/null +++ b/app/Exceptions/PluginException.php @@ -0,0 +1,37 @@ +handle() should contain exactly 1 parameter"); + } + + public static function noParameters(object|string $class) + { + $className = self::resolveClassName($class); + return new static("{$className}->handle() does not specify any events to listen for"); + } + + public static function noHandleFunction(object|string $class) + { + $className = self::resolveClassName($class); + return new static("required function {$className}->handle() does not exist"); + } + + public static function pluginClassNotFound(object|string $class) + { + $className = self::resolveClassName($class); + return new static("plugin {$className} not found"); + } + + private static function resolveClassName(object|string $class) + { + return (is_object($class)) ? $class->getName() : $class; + } + + +} diff --git a/app/Helper/PluginManager.php b/app/Helper/PluginManager.php index 819ded8f7..b8c659393 100644 --- a/app/Helper/PluginManager.php +++ b/app/Helper/PluginManager.php @@ -2,14 +2,18 @@ namespace App\Helper; +use App\Exceptions\PluginException; use App\Plugins\NewLinkToWaybackMachine; use Illuminate\Support\Facades\Config; -use Illuminate\Support\Facades/Event; +use Illuminate\Support\Facades\Event; use Illuminate\Support\Reflector; use ReflectionClass; class PluginManager { + /** + * @throws PluginException + */ public function registerPlugins(): void { $plugins = Config::get('linkace.plugins', [NewLinkToWaybackMachine::class]); @@ -21,13 +25,27 @@ public function registerPlugins(): void } } - private function getParameterTypesFor(string $pluginClass): array + /** + * @throws PluginException + */ + private function getParameterTypesFor(object|string $pluginClass): array { - $reflectionClass = new ReflectionClass($pluginClass); - $reflectionMethod = $reflectionClass->getMethod('handle'); + try { + $reflectionClass = new ReflectionClass($pluginClass); + } catch (\ReflectionException $exception) { + throw PluginException::pluginClassNotFound($pluginClass); + } + try { + $reflectionMethod = $reflectionClass->getMethod('handle'); + } catch (\ReflectionException $re) { + throw PluginException::noHandleFunction($pluginClass); + } $parameters = $reflectionMethod->getParameters(); - if (count($parameters) !== 1) { - throw new \Exception("Plugin {$pluginClass} should have exactly 1 parameter in its handle method"); + if (count($parameters) > 1) { + throw PluginException::tooManyParameters($pluginClass); + } + if (count($parameters) < 1) { + throw PluginException::noParameters($pluginClass); } return Reflector::getParameterClassNames($parameters[0]); } diff --git a/tests/Helper/PluginManagerTest.php b/tests/Helper/PluginManagerTest.php index 9c2accaed..da7d986c8 100644 --- a/tests/Helper/PluginManagerTest.php +++ b/tests/Helper/PluginManagerTest.php @@ -4,10 +4,15 @@ use App\Events\LinkCreated; use App\Events\LinkUpdated; -use App\Plugins\NewLinkToWaybackMachine; +use App\Exceptions\PluginException; use Facades\App\Helper\PluginManager; use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\Event; +use Tests\Mocks\NoHandlePlugin; +use Tests\Mocks\NoParameterPlugin; +use Tests\Mocks\SimplePlugin; +use Tests\Mocks\TooManyParametersPlugin; +use Tests\Mocks\UnionPlugin; use Tests\Plugins\SamplePlugin; use Tests\TestCase; @@ -28,25 +33,53 @@ public function test_plugin_manager_doesnt_bomb_with_no_plugins() public function test_plugin_manager_registers_single_listener() { - Config::set('linkace.plugins', [NewLinkToWaybackMachine::class]); + Config::set('linkace.plugins', [SimplePlugin::class]); PluginManager::registerPlugins(); - Event::assertListening(LinkCreated::class, NewLinkToWaybackMachine::class); + Event::assertListening(LinkCreated::class, SimplePlugin::class); } public function test_plugin_manager_registers_all_events_for_union_types() { - Config::set('linkace.plugins', [SamplePlugin::class]); + Config::set('linkace.plugins', [UnionPlugin::class]); PluginManager::registerPlugins(); - Event::assertListening(LinkCreated::class, SamplePlugin::class); - Event::assertListening(LinkUpdated::class, SamplePlugin::class); + Event::assertListening(LinkCreated::class, UnionPlugin::class); + Event::assertListening(LinkUpdated::class, UnionPlugin::class); } public function test_plugin_manager_handles_multiple_plugins() { - Config::set('linkace.plugins', [SamplePlugin::class, NewLinkToWaybackMachine::class]); + Config::set('linkace.plugins', [SimplePlugin::class, UnionPlugin::class]); + PluginManager::registerPlugins(); + Event::assertListening(LinkCreated::class, SimplePlugin::class); + Event::assertListening(LinkCreated::class, UnionPlugin::class); + Event::assertListening(LinkUpdated::class, UnionPlugin::class); + } + + public function test_plugin_manager_throws_plugin_exception_with_missing_plugin() + { + Config::set('linkace.plugins', ['noSuchPlugin']); + $this->expectException(PluginException::class); + PluginManager::registerPlugins(); + } + + public function test_plugin_manager_throws_plugin_exception_with_missing_handle_function() + { + Config::set('linkace.plugins', [NoHandlePlugin::class]); + $this->expectException(PluginException::class); + PluginManager::registerPlugins(); + } + + public function test_plugin_manager_throws_plugin_exception_with_too_many_parameters() + { + Config::set('linkace.plugins', [TooManyParametersPlugin::class]); + $this->expectException(PluginException::class); + PluginManager::registerPlugins(); + } + + public function test_plugin_manager_throws_plugin_exception_with_not_enough_parameters() + { + Config::set('linkace.plugins', [NoParameterPlugin::class]); + $this->expectException(PluginException::class); PluginManager::registerPlugins(); - Event::assertListening(LinkCreated::class, SamplePlugin::class); - Event::assertListening(LinkCreated::class, NewLinkToWaybackMachine::class); - Event::assertListening(LinkUpdated::class, SamplePlugin::class); } } diff --git a/tests/Mocks/NoHandlePlugin.php b/tests/Mocks/NoHandlePlugin.php new file mode 100644 index 000000000..9d3600ebf --- /dev/null +++ b/tests/Mocks/NoHandlePlugin.php @@ -0,0 +1,7 @@ + Date: Wed, 3 Dec 2025 08:33:29 -0800 Subject: [PATCH 13/16] document LinkDeleted event and only throw on successful deletion --- app/Events/LinkDeleted.php | 4 ++++ app/Repositories/LinkRepository.php | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/Events/LinkDeleted.php b/app/Events/LinkDeleted.php index ce8446317..99aa4363c 100644 --- a/app/Events/LinkDeleted.php +++ b/app/Events/LinkDeleted.php @@ -5,6 +5,10 @@ use Illuminate\Foundation\Events\Dispatchable; use Illuminate\Queue\SerializesModels; +/* + * Because this event is dispatched after the link is deleted, only the ID is available + * rather than the entire Link model (as found in LinkCreated and LinkUpdated) + */ class LinkDeleted { use Dispatchable, SerializesModels; diff --git a/app/Repositories/LinkRepository.php b/app/Repositories/LinkRepository.php index 5f8beb935..faf7f97d9 100644 --- a/app/Repositories/LinkRepository.php +++ b/app/Repositories/LinkRepository.php @@ -112,11 +112,11 @@ public static function delete(Link $link): bool $link->tags()->detach(); $link->lists()->detach(); $link->delete(); + LinkDeleted::dispatch($id); } catch (Exception $e) { Log::error($e); return false; } - LinkDeleted::dispatch($id); return true; } From 1f8a5ee1ac9409182a155ff7ab41630429dd54df Mon Sep 17 00:00:00 2001 From: jtgrimes Date: Wed, 3 Dec 2025 08:34:12 -0800 Subject: [PATCH 14/16] correct config key --- tests/Plugins/NewLinkToWaybackMachineTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Plugins/NewLinkToWaybackMachineTest.php b/tests/Plugins/NewLinkToWaybackMachineTest.php index 90b4413e2..40b4cd74e 100644 --- a/tests/Plugins/NewLinkToWaybackMachineTest.php +++ b/tests/Plugins/NewLinkToWaybackMachineTest.php @@ -25,7 +25,7 @@ protected function setUp(): void ]); Queue::fake(); - Config::set('plugins', [\App\Plugins\NewLinkToWaybackMachine::class]); + Config::set('linkace.plugins', [\App\Plugins\NewLinkToWaybackMachine::class]); } public function test_store_request_archives_links(): void From 11934e96a98f3f953142c43ffbfa4f722001611a Mon Sep 17 00:00:00 2001 From: jtgrimes Date: Wed, 3 Dec 2025 09:53:22 -0800 Subject: [PATCH 15/16] minor code cleanup --- app/Exceptions/PluginException.php | 14 +++++++------- app/Helper/PluginManager.php | 7 ++++--- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/app/Exceptions/PluginException.php b/app/Exceptions/PluginException.php index ae3684390..ebc415acd 100644 --- a/app/Exceptions/PluginException.php +++ b/app/Exceptions/PluginException.php @@ -2,27 +2,29 @@ namespace App\Exceptions; -class PluginException extends \Exception +use Exception; + +class PluginException extends Exception { - public static function tooManyParameters(object|string $class) + public static function tooManyParameters(object|string $class): self { $className = self::resolveClassName($class); return new static("{$className}->handle() should contain exactly 1 parameter"); } - public static function noParameters(object|string $class) + public static function noParameters(object|string $class): self { $className = self::resolveClassName($class); return new static("{$className}->handle() does not specify any events to listen for"); } - public static function noHandleFunction(object|string $class) + public static function noHandleFunction(object|string $class): self { $className = self::resolveClassName($class); return new static("required function {$className}->handle() does not exist"); } - public static function pluginClassNotFound(object|string $class) + public static function pluginClassNotFound(object|string $class): self { $className = self::resolveClassName($class); return new static("plugin {$className} not found"); @@ -32,6 +34,4 @@ private static function resolveClassName(object|string $class) { return (is_object($class)) ? $class->getName() : $class; } - - } diff --git a/app/Helper/PluginManager.php b/app/Helper/PluginManager.php index b8c659393..265e59637 100644 --- a/app/Helper/PluginManager.php +++ b/app/Helper/PluginManager.php @@ -8,6 +8,7 @@ use Illuminate\Support\Facades\Event; use Illuminate\Support\Reflector; use ReflectionClass; +use ReflectionException; class PluginManager { @@ -16,7 +17,7 @@ class PluginManager */ public function registerPlugins(): void { - $plugins = Config::get('linkace.plugins', [NewLinkToWaybackMachine::class]); + $plugins = Config::get('linkace.plugins', [NewLinkToWaybackMachine::class::class]); foreach ($plugins as $plugin) { $parameterTypes = $this->getParameterTypesFor($plugin); foreach ($parameterTypes as $event) { @@ -32,12 +33,12 @@ private function getParameterTypesFor(object|string $pluginClass): array { try { $reflectionClass = new ReflectionClass($pluginClass); - } catch (\ReflectionException $exception) { + } catch (ReflectionException) { throw PluginException::pluginClassNotFound($pluginClass); } try { $reflectionMethod = $reflectionClass->getMethod('handle'); - } catch (\ReflectionException $re) { + } catch (ReflectionException) { throw PluginException::noHandleFunction($pluginClass); } $parameters = $reflectionMethod->getParameters(); From 07fcd8cfccd4a734b86286f9a9ae7b5059971904 Mon Sep 17 00:00:00 2001 From: jtgrimes Date: Thu, 4 Dec 2025 17:07:48 -0800 Subject: [PATCH 16/16] minor code cleanup --- app/Helper/PluginManager.php | 2 +- tests/Helper/PluginManagerTest.php | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/Helper/PluginManager.php b/app/Helper/PluginManager.php index 265e59637..33d5040a5 100644 --- a/app/Helper/PluginManager.php +++ b/app/Helper/PluginManager.php @@ -17,7 +17,7 @@ class PluginManager */ public function registerPlugins(): void { - $plugins = Config::get('linkace.plugins', [NewLinkToWaybackMachine::class::class]); + $plugins = Config::get('linkace.plugins', [NewLinkToWaybackMachine::class]); foreach ($plugins as $plugin) { $parameterTypes = $this->getParameterTypesFor($plugin); foreach ($parameterTypes as $event) { diff --git a/tests/Helper/PluginManagerTest.php b/tests/Helper/PluginManagerTest.php index da7d986c8..da4074267 100644 --- a/tests/Helper/PluginManagerTest.php +++ b/tests/Helper/PluginManagerTest.php @@ -13,7 +13,6 @@ use Tests\Mocks\SimplePlugin; use Tests\Mocks\TooManyParametersPlugin; use Tests\Mocks\UnionPlugin; -use Tests\Plugins\SamplePlugin; use Tests\TestCase; class PluginManagerTest extends TestCase