From 35763a9d8641fa233489bee0e162a8b527e79ab5 Mon Sep 17 00:00:00 2001 From: Ibrahim BinAlshikh Date: Sun, 14 Jun 2026 11:08:45 +0300 Subject: [PATCH] feat(middleware): auto-resolve transitive dependencies from registry When a middleware declares dependencies via getDependencies(), assigning only that middleware to a route now auto-includes all transitive dependencies from MiddlewareRegistry via BFS traversal. - resolveDependencies() walks the dependency graph before sorting - Missing dependencies are silently skipped - No duplicates when dependency is already assigned - Execution order preserved via existing topological sort Closes #380 --- WebFiori/Framework/Router/RouterUri.php | 40 ++++- .../MiddlewareTransitiveDepsTest.php | 145 ++++++++++++++++++ 2 files changed, 184 insertions(+), 1 deletion(-) create mode 100644 tests/WebFiori/Framework/Tests/Middleware/MiddlewareTransitiveDepsTest.php diff --git a/WebFiori/Framework/Router/RouterUri.php b/WebFiori/Framework/Router/RouterUri.php index 36efa5162..11944227a 100644 --- a/WebFiori/Framework/Router/RouterUri.php +++ b/WebFiori/Framework/Router/RouterUri.php @@ -299,7 +299,8 @@ public function getLanguages() : array { */ public function getMiddleware() : array { if (count($this->assignedMiddlewareList) != count($this->sortedMiddleeareList)) { - $this->sortedMiddleeareList = $this->sortByDependencies($this->assignedMiddlewareList); + $resolved = $this->resolveDependencies($this->assignedMiddlewareList); + $this->sortedMiddleeareList = $this->sortByDependencies($resolved); } return $this->sortedMiddleeareList; @@ -624,6 +625,43 @@ private function compareUriPathHelper(Uri $requestedUri): bool { * @param array $middlewareList Array of AbstractMiddleware instances. * * @return array Sorted array. + /** + * Resolves transitive dependencies by pulling missing middleware from the registry. + * + * @param array $middlewareList The initially assigned middleware. + * + * @return array The expanded list including all transitive dependencies. + */ + private function resolveDependencies(array $middlewareList): array { + $byName = []; + + foreach ($middlewareList as $mw) { + $byName[$mw->getName()] = $mw; + } + + $queue = $middlewareList; + + while (!empty($queue)) { + $current = array_shift($queue); + + foreach ($current->getDependencies() as $depName) { + if (isset($byName[$depName])) { + continue; + } + + $dep = MiddlewareManager::getMiddleware($depName); + + if ($dep !== null) { + $byName[$depName] = $dep; + $queue[] = $dep; + } + } + } + + return array_values($byName); + } + /** + * Sorts middleware using topological sort (Kahn's algorithm). * * @throws RoutingException If circular dependency detected. */ diff --git a/tests/WebFiori/Framework/Tests/Middleware/MiddlewareTransitiveDepsTest.php b/tests/WebFiori/Framework/Tests/Middleware/MiddlewareTransitiveDepsTest.php new file mode 100644 index 000000000..6485fca66 --- /dev/null +++ b/tests/WebFiori/Framework/Tests/Middleware/MiddlewareTransitiveDepsTest.php @@ -0,0 +1,145 @@ +addMiddleware(new MwB()); // depends on mw-a + + $middleware = $uri->getMiddleware(); + $names = array_map(fn($mw) => $mw->getName(), $middleware); + + $this->assertContains('mw-a', $names); + $this->assertContains('mw-b', $names); + $this->assertCount(2, $middleware); + } + + /** @test */ + public function testTransitiveChainResolved() { + $uri = new RouterUri('https://example.com/test', ''); + $uri->addMiddleware(new MwC()); // C depends on B, B depends on A + + $middleware = $uri->getMiddleware(); + $names = array_map(fn($mw) => $mw->getName(), $middleware); + + $this->assertContains('mw-a', $names); + $this->assertContains('mw-b', $names); + $this->assertContains('mw-c', $names); + $this->assertCount(3, $middleware); + } + + /** @test */ + public function testExecutionOrderCorrect() { + $uri = new RouterUri('https://example.com/test', ''); + $uri->addMiddleware(new MwC()); + + $middleware = $uri->getMiddleware(); + $names = array_map(fn($mw) => $mw->getName(), $middleware); + + // A must come before B, B before C + $this->assertLessThan( + array_search('mw-b', $names), + array_search('mw-a', $names) + ); + $this->assertLessThan( + array_search('mw-c', $names), + array_search('mw-b', $names) + ); + } + + /** @test */ + public function testNoDuplicateWhenDependencyAlreadyAssigned() { + $uri = new RouterUri('https://example.com/test', ''); + $uri->addMiddleware(new MwA()); + $uri->addMiddleware(new MwB()); + + $middleware = $uri->getMiddleware(); + $names = array_map(fn($mw) => $mw->getName(), $middleware); + + $this->assertCount(2, $middleware); + $this->assertEquals(1, count(array_keys($names, 'mw-a'))); + } + + /** @test */ + public function testMissingDependencySkippedSilently() { + $uri = new RouterUri('https://example.com/test', ''); + $uri->addMiddleware(new MwOrphan()); // depends on mw-nonexistent + + $middleware = $uri->getMiddleware(); + $names = array_map(fn($mw) => $mw->getName(), $middleware); + + $this->assertContains('mw-orphan', $names); + $this->assertNotContains('mw-nonexistent', $names); + $this->assertCount(1, $middleware); + } + + /** @test */ + public function testNoDependenciesUnchanged() { + $uri = new RouterUri('https://example.com/test', ''); + $uri->addMiddleware(new MwA()); // no dependencies + + $middleware = $uri->getMiddleware(); + $this->assertCount(1, $middleware); + $this->assertEquals('mw-a', $middleware[0]->getName()); + } +}