From cde4d870e0b7836ed7839cc665a962ee61fac31b Mon Sep 17 00:00:00 2001 From: Jordan Welch Date: Wed, 8 Apr 2026 12:40:51 -0500 Subject: [PATCH 01/10] Add fallback controller for root-level slug finding --- routes/web.php | 3 +++ src/Controller/FallbackController.php | 30 +++++++++++++++++++++ tests/Controller/FallbackControllerTest.php | 29 ++++++++++++++++++++ 3 files changed, 62 insertions(+) create mode 100644 src/Controller/FallbackController.php create mode 100644 tests/Controller/FallbackControllerTest.php diff --git a/routes/web.php b/routes/web.php index 32b5e417d..fc235bb3f 100644 --- a/routes/web.php +++ b/routes/web.php @@ -15,6 +15,7 @@ use App\Controller\Demo\DemoDetailController; use App\Controller\Demo\ProcessDemoFormController; use App\Controller\DocumentationController; +use App\Controller\FallbackController; use App\Controller\FindRuleController; use App\Controller\HireTeamController; use App\Controller\HomepageController; @@ -76,3 +77,5 @@ Route::get('ast/{hash}', AstDetailController::class); Route::get('ast', AstController::class); Route::post('process-ast', ProcessAstFormController::class); + +Route::fallback(FallbackController::class); diff --git a/src/Controller/FallbackController.php b/src/Controller/FallbackController.php new file mode 100644 index 000000000..c6b03b5b3 --- /dev/null +++ b/src/Controller/FallbackController.php @@ -0,0 +1,30 @@ +path(); + $ruleMetadata = $this->rectorFinder->findBySlug($slug); + + if (! $ruleMetadata instanceof RuleMetadata) { + abort(404); + } + + return redirect()->action(RuleDetailController::class, [ + 'slug' => $ruleMetadata->getSlug(), + ]); + } +} diff --git a/tests/Controller/FallbackControllerTest.php b/tests/Controller/FallbackControllerTest.php new file mode 100644 index 000000000..ed549a7eb --- /dev/null +++ b/tests/Controller/FallbackControllerTest.php @@ -0,0 +1,29 @@ +withoutMiddleware(PreventRequestForgery::class); + } + + public function testRedirectsRuleSlug(): void + { + $response = $this->get('/simplify-array-search-rector'); + + $response->assertRedirect('/rule-detail/simplify-array-search-rector'); + } + + public function testReturns404ForUnknownSlug(): void + { + $response = $this->get('/unknown-rule'); + + $response->assertNotFound(); + } +} From 90e9c5af6f56ee65663e1e2e3c3caca182cb398e Mon Sep 17 00:00:00 2001 From: Jordan Welch Date: Wed, 8 Apr 2026 12:45:02 -0500 Subject: [PATCH 02/10] add ability to use Pascal case name as slug --- src/Controller/FallbackController.php | 2 +- tests/Controller/FallbackControllerTest.php | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Controller/FallbackController.php b/src/Controller/FallbackController.php index c6b03b5b3..5ff657530 100644 --- a/src/Controller/FallbackController.php +++ b/src/Controller/FallbackController.php @@ -16,7 +16,7 @@ public function __construct( public function __invoke(Request $request) { - $slug = $request->path(); + $slug = str($request->path())->kebab(); $ruleMetadata = $this->rectorFinder->findBySlug($slug); if (! $ruleMetadata instanceof RuleMetadata) { diff --git a/tests/Controller/FallbackControllerTest.php b/tests/Controller/FallbackControllerTest.php index ed549a7eb..2fe910f22 100644 --- a/tests/Controller/FallbackControllerTest.php +++ b/tests/Controller/FallbackControllerTest.php @@ -26,4 +26,11 @@ public function testReturns404ForUnknownSlug(): void $response->assertNotFound(); } + + public function testRedirectsWithPascalCaseSlug(): void + { + $response = $this->get('/SimplifyArraySearchRector'); + + $response->assertRedirect('/rule-detail/simplify-array-search-rector'); + } } From 30ebe7c43b2116304c85b64c4251cad59e9d8304 Mon Sep 17 00:00:00 2001 From: Jordan Welch Date: Wed, 8 Apr 2026 12:46:21 -0500 Subject: [PATCH 03/10] Fix namespace --- tests/Controller/FallbackControllerTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Controller/FallbackControllerTest.php b/tests/Controller/FallbackControllerTest.php index 2fe910f22..037524399 100644 --- a/tests/Controller/FallbackControllerTest.php +++ b/tests/Controller/FallbackControllerTest.php @@ -1,6 +1,6 @@ Date: Wed, 8 Apr 2026 12:47:44 -0500 Subject: [PATCH 04/10] Remove unneeded setup --- tests/Controller/FallbackControllerTest.php | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/Controller/FallbackControllerTest.php b/tests/Controller/FallbackControllerTest.php index 037524399..d4a73d97d 100644 --- a/tests/Controller/FallbackControllerTest.php +++ b/tests/Controller/FallbackControllerTest.php @@ -7,12 +7,6 @@ class FallbackControllerTest extends AbstractTestCase { - protected function setUp(): void - { - parent::setUp(); - $this->withoutMiddleware(PreventRequestForgery::class); - } - public function testRedirectsRuleSlug(): void { $response = $this->get('/simplify-array-search-rector'); From 0d15c9487376813c8e400b0ca2f77ce7c8acf503 Mon Sep 17 00:00:00 2001 From: Jordan Welch Date: Wed, 8 Apr 2026 12:52:44 -0500 Subject: [PATCH 05/10] Add `withoutVite` for testing frontend --- tests/AbstractTestCase.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/AbstractTestCase.php b/tests/AbstractTestCase.php index dc93fcb42..0ec3b37e4 100644 --- a/tests/AbstractTestCase.php +++ b/tests/AbstractTestCase.php @@ -11,6 +11,12 @@ abstract class AbstractTestCase extends TestCase { + public function setup(): void + { + parent::setup(); + $this->withoutVite(); + } + #[Override] public function createApplication(): Application { From abc7b24496ea514f202d2adfddc927c0b57c6cea Mon Sep 17 00:00:00 2001 From: Jordan Welch Date: Wed, 8 Apr 2026 12:54:31 -0500 Subject: [PATCH 06/10] backfill `RuleDetailControllerTest` test --- tests/Controller/RuleDetailControllerTest.php | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 tests/Controller/RuleDetailControllerTest.php diff --git a/tests/Controller/RuleDetailControllerTest.php b/tests/Controller/RuleDetailControllerTest.php new file mode 100644 index 000000000..6336deba8 --- /dev/null +++ b/tests/Controller/RuleDetailControllerTest.php @@ -0,0 +1,31 @@ +get('/rule-detail/simplify-array-search-rector'); + + $response + ->assertOk() + ->assertViewIs('homepage.rule_detail') + ->assertViewHas('ruleMetadata') + ->assertViewHas('codeMirror', true) + ->assertViewHas('page_title', 'SimplifyArraySearchRector') + ->assertSeeText('SimplifyArraySearchRector'); + } + + public function testItRedirectsToFindRuleIfRuleNotFound(): void + { + $response = $this->get('/rule-detail/unknown-rule'); + + $response->assertRedirect('/find-rule'); + } +} From 29734a306ec13fe1511a40348a72fd903d56a8b0 Mon Sep 17 00:00:00 2001 From: Jordan Welch Date: Wed, 8 Apr 2026 12:57:25 -0500 Subject: [PATCH 07/10] Redirect with Pascal case names --- src/Controller/FallbackController.php | 2 +- src/Controller/RuleDetailController.php | 9 ++++++++- tests/Controller/RuleDetailControllerTest.php | 7 +++++++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/Controller/FallbackController.php b/src/Controller/FallbackController.php index 5ff657530..db3cb4e08 100644 --- a/src/Controller/FallbackController.php +++ b/src/Controller/FallbackController.php @@ -16,7 +16,7 @@ public function __construct( public function __invoke(Request $request) { - $slug = str($request->path())->kebab(); + $slug = str($request->path())->kebab()->toString(); $ruleMetadata = $this->rectorFinder->findBySlug($slug); if (! $ruleMetadata instanceof RuleMetadata) { diff --git a/src/Controller/RuleDetailController.php b/src/Controller/RuleDetailController.php index b17722931..6f2a6534e 100644 --- a/src/Controller/RuleDetailController.php +++ b/src/Controller/RuleDetailController.php @@ -19,13 +19,20 @@ public function __construct( public function __invoke(string $slug): View|RedirectResponse { - $ruleMetadata = $this->rectorFinder->findBySlug($slug); + $kebab = str($slug)->kebab()->toString(); + $ruleMetadata = $this->rectorFinder->findBySlug($kebab); if (! $ruleMetadata instanceof RuleMetadata) { // nothing found, get back return redirect()->action(FindRuleController::class); } + if ($kebab !== $slug) { + return redirect()->action(RuleDetailController::class, [ + 'slug' => $kebab, + ]); + } + return \view('homepage/rule_detail', [ 'page_title' => $ruleMetadata->getRuleShortClass(), 'ruleMetadata' => $ruleMetadata, diff --git a/tests/Controller/RuleDetailControllerTest.php b/tests/Controller/RuleDetailControllerTest.php index 6336deba8..9aff32e33 100644 --- a/tests/Controller/RuleDetailControllerTest.php +++ b/tests/Controller/RuleDetailControllerTest.php @@ -28,4 +28,11 @@ public function testItRedirectsToFindRuleIfRuleNotFound(): void $response->assertRedirect('/find-rule'); } + + public function testItRedirectsToKebabCaseSlugIfPascalCaseSlug(): void + { + $response = $this->get('/rule-detail/SimplifyArraySearchRector'); + + $response->assertRedirect('/rule-detail/simplify-array-search-rector'); + } } From 196b55e4b6484b4a6e7faa8da751f9ac1f47ba4d Mon Sep 17 00:00:00 2001 From: Jordan Welch Date: Wed, 8 Apr 2026 13:03:07 -0500 Subject: [PATCH 08/10] Use 301s --- src/Controller/FallbackController.php | 2 +- src/Controller/RuleDetailController.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Controller/FallbackController.php b/src/Controller/FallbackController.php index db3cb4e08..77181a6c4 100644 --- a/src/Controller/FallbackController.php +++ b/src/Controller/FallbackController.php @@ -23,7 +23,7 @@ public function __invoke(Request $request) abort(404); } - return redirect()->action(RuleDetailController::class, [ + return redirect(status: 301)->action(RuleDetailController::class, [ 'slug' => $ruleMetadata->getSlug(), ]); } diff --git a/src/Controller/RuleDetailController.php b/src/Controller/RuleDetailController.php index 6f2a6534e..1e60ab139 100644 --- a/src/Controller/RuleDetailController.php +++ b/src/Controller/RuleDetailController.php @@ -28,7 +28,7 @@ public function __invoke(string $slug): View|RedirectResponse } if ($kebab !== $slug) { - return redirect()->action(RuleDetailController::class, [ + return redirect(status: 301)->action(RuleDetailController::class, [ 'slug' => $kebab, ]); } From 352cda5e18413793a5416d4aa7681ded4908348c Mon Sep 17 00:00:00 2001 From: Jordan Welch Date: Wed, 8 Apr 2026 13:56:25 -0500 Subject: [PATCH 09/10] `setup` is `protected` --- tests/AbstractTestCase.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/AbstractTestCase.php b/tests/AbstractTestCase.php index 0ec3b37e4..4230d81ec 100644 --- a/tests/AbstractTestCase.php +++ b/tests/AbstractTestCase.php @@ -11,7 +11,7 @@ abstract class AbstractTestCase extends TestCase { - public function setup(): void + protected function setup(): void { parent::setup(); $this->withoutVite(); From 2638ba4abac0a17579369879b0046fdbad2e3669 Mon Sep 17 00:00:00 2001 From: Jordan Welch Date: Wed, 8 Apr 2026 13:58:46 -0500 Subject: [PATCH 10/10] Add return type --- src/Controller/FallbackController.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Controller/FallbackController.php b/src/Controller/FallbackController.php index 77181a6c4..8ca1e5a04 100644 --- a/src/Controller/FallbackController.php +++ b/src/Controller/FallbackController.php @@ -4,6 +4,7 @@ use App\FileSystem\RectorFinder; use App\RuleFilter\ValueObject\RuleMetadata; +use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Routing\Controller; @@ -14,7 +15,7 @@ public function __construct( ) { } - public function __invoke(Request $request) + public function __invoke(Request $request): RedirectResponse { $slug = str($request->path())->kebab()->toString(); $ruleMetadata = $this->rectorFinder->findBySlug($slug);