From 8c3e152e84643f7eae21e01ebcbda7f3d0ba3e54 Mon Sep 17 00:00:00 2001 From: Greg Bowler Date: Mon, 18 May 2026 10:49:15 +0100 Subject: [PATCH] feature: AmbigousPathException closes #699 --- router.default.php | 39 ++++++++++++++++++++++++++++++ src/AmbiguousPathException.php | 4 +++ test/phpunit/DefaultRouterTest.php | 28 +++++++++++++++++++++ 3 files changed, 71 insertions(+) create mode 100644 src/AmbiguousPathException.php diff --git a/router.default.php b/router.default.php index 7c99667d..0d76d33d 100644 --- a/router.default.php +++ b/router.default.php @@ -2,6 +2,7 @@ namespace GT\WebEngine; use Gt\Http\Request; +use GT\WebEngine\AmbiguousPathException; use GT\Routing\BaseRouter; use GT\Routing\Method\Any; use GT\Routing\Path\FileMatch\BasicFileMatch; @@ -92,6 +93,7 @@ public function page(Request $request):void { "page", "php" ); + $this->assertNoAmbiguousPath($matchingLogics, $uriPath, "page", "php"); usort($matchingLogics, $sortNestLevelCallback); foreach($matchingLogics as $path) { $this->addToLogicAssembly($path); @@ -102,6 +104,7 @@ public function page(Request $request):void { "page", "html" ); + $this->assertNoAmbiguousPath($matchingViews, $uriPath, "page", "html"); usort($matchingViews, $headerFooterSort); foreach($matchingViews as $path) { $this->addToViewAssembly($path); @@ -127,6 +130,12 @@ public function api( "api", "php" ); + $this->assertNoAmbiguousPath( + $matchingLogics, + $request->getUri()->getPath(), + "api", + "php", + ); usort($matchingLogics, $sortNestLevelCallback); foreach($matchingLogics as $path) { $this->addToLogicAssembly($path); @@ -137,11 +146,41 @@ public function api( "api", "xml" ); + $this->assertNoAmbiguousPath( + $matchingViews, + $request->getUri()->getPath(), + "api", + "xml", + ); foreach($matchingViews as $path) { $this->addToViewAssembly($path); } } + /** @param array $matchingPaths */ + private function assertNoAmbiguousPath( + array $matchingPaths, + string $uriPath, + string $baseDir, + string $extension, + ):void { + $trimmedUriPath = trim($uriPath, "/"); + if($trimmedUriPath === "") { + return; + } + + $directPath = "$baseDir/$trimmedUriPath.$extension"; + $indexPath = "$baseDir/$trimmedUriPath/index.$extension"; + + if(in_array($directPath, $matchingPaths, true) + && in_array($indexPath, $matchingPaths, true)) { + throw new AmbiguousPathException( + "Ambiguous route for '$uriPath': both '$directPath' and " + . "'$indexPath' match." + ); + } + } + public function pathMatcherFilter(PathMatcher $pathMatcher):void { $pathMatcher->addFilter(function(string $filePath, string $uriPath, string $baseDir):bool { foreach(glob($baseDir . $uriPath . ".*") as $globMatch) { diff --git a/src/AmbiguousPathException.php b/src/AmbiguousPathException.php new file mode 100644 index 00000000..56c5b695 --- /dev/null +++ b/src/AmbiguousPathException.php @@ -0,0 +1,4 @@ +processPartialContent($viewModel, $sut->getViewAssembly()); } + public function testRoute_pageRequest_withDirectAndIndexView_throwsAmbiguousPathException():void { + mkdir($this->tmpDir . "/page/contact", recursive: true); + file_put_contents($this->tmpDir . "/page/contact.html", "
contact
"); + file_put_contents($this->tmpDir . "/page/contact/index.html", "
contact index
"); + + chdir($this->tmpDir); + + $request = self::createMock(Request::class); + $request->method("getMethod")->willReturn("GET"); + $request->method("getHeaderLine") + ->with("accept") + ->willReturn("text/html"); + $request->method("getUri")->willReturn(new Uri("https://example.test/contact")); + + $sut = new DefaultRouter(new RouterConfig(307, "text/html")); + $container = new Container(); + $container->set($request); + $sut->setContainer($container); + + $this->expectException(AmbiguousPathException::class); + $this->expectExceptionMessage( + "Ambiguous route for '/contact': both 'page/contact.html' and " + . "'page/contact/index.html' match." + ); + $sut->route($request); + } + private function removeDirectory(string $dir):void { if(!is_dir($dir)) { return;