Skip to content

Commit d2d0a64

Browse files
authored
Merge pull request #396 from WebFiori/feat/middleware-transitive-deps
feat(middleware): auto-resolve transitive dependencies
2 parents 9485367 + 35763a9 commit d2d0a64

2 files changed

Lines changed: 184 additions & 1 deletion

File tree

WebFiori/Framework/Router/RouterUri.php

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,8 @@ public function getLanguages() : array {
299299
*/
300300
public function getMiddleware() : array {
301301
if (count($this->assignedMiddlewareList) != count($this->sortedMiddleeareList)) {
302-
$this->sortedMiddleeareList = $this->sortByDependencies($this->assignedMiddlewareList);
302+
$resolved = $this->resolveDependencies($this->assignedMiddlewareList);
303+
$this->sortedMiddleeareList = $this->sortByDependencies($resolved);
303304
}
304305

305306
return $this->sortedMiddleeareList;
@@ -624,6 +625,43 @@ private function compareUriPathHelper(Uri $requestedUri): bool {
624625
* @param array $middlewareList Array of AbstractMiddleware instances.
625626
*
626627
* @return array Sorted array.
628+
/**
629+
* Resolves transitive dependencies by pulling missing middleware from the registry.
630+
*
631+
* @param array $middlewareList The initially assigned middleware.
632+
*
633+
* @return array The expanded list including all transitive dependencies.
634+
*/
635+
private function resolveDependencies(array $middlewareList): array {
636+
$byName = [];
637+
638+
foreach ($middlewareList as $mw) {
639+
$byName[$mw->getName()] = $mw;
640+
}
641+
642+
$queue = $middlewareList;
643+
644+
while (!empty($queue)) {
645+
$current = array_shift($queue);
646+
647+
foreach ($current->getDependencies() as $depName) {
648+
if (isset($byName[$depName])) {
649+
continue;
650+
}
651+
652+
$dep = MiddlewareManager::getMiddleware($depName);
653+
654+
if ($dep !== null) {
655+
$byName[$depName] = $dep;
656+
$queue[] = $dep;
657+
}
658+
}
659+
}
660+
661+
return array_values($byName);
662+
}
663+
/**
664+
* Sorts middleware using topological sort (Kahn's algorithm).
627665
*
628666
* @throws RoutingException If circular dependency detected.
629667
*/
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
<?php
2+
/**
3+
* This file is licensed under MIT License.
4+
*
5+
* Copyright (c) 2026-present WebFiori Framework
6+
*
7+
* For more information on the license, please visit:
8+
* https://github.com/WebFiori/.github/blob/main/LICENSE
9+
*
10+
*/
11+
namespace WebFiori\Framework\Test\Middleware;
12+
13+
use PHPUnit\Framework\TestCase;
14+
use WebFiori\Framework\Middleware\AbstractMiddleware;
15+
use WebFiori\Framework\Middleware\MiddlewareManager;
16+
use WebFiori\Framework\Router\RouterUri;
17+
use WebFiori\Http\Request;
18+
use WebFiori\Http\Response;
19+
20+
class MwA extends AbstractMiddleware {
21+
public function __construct() { parent::__construct('mw-a'); }
22+
public function before(Request $r, Response $res) {}
23+
public function after(Request $r, Response $res) {}
24+
public function afterSend(Request $r, Response $res) {}
25+
}
26+
27+
class MwB extends AbstractMiddleware {
28+
public function __construct() { parent::__construct('mw-b'); }
29+
public function getDependencies(): array { return ['mw-a']; }
30+
public function before(Request $r, Response $res) {}
31+
public function after(Request $r, Response $res) {}
32+
public function afterSend(Request $r, Response $res) {}
33+
}
34+
35+
class MwC extends AbstractMiddleware {
36+
public function __construct() { parent::__construct('mw-c'); }
37+
public function getDependencies(): array { return ['mw-b']; }
38+
public function before(Request $r, Response $res) {}
39+
public function after(Request $r, Response $res) {}
40+
public function afterSend(Request $r, Response $res) {}
41+
}
42+
43+
class MwOrphan extends AbstractMiddleware {
44+
public function __construct() { parent::__construct('mw-orphan'); }
45+
public function getDependencies(): array { return ['mw-nonexistent']; }
46+
public function before(Request $r, Response $res) {}
47+
public function after(Request $r, Response $res) {}
48+
public function afterSend(Request $r, Response $res) {}
49+
}
50+
51+
class MiddlewareTransitiveDepsTest extends TestCase {
52+
protected function setUp(): void {
53+
parent::setUp();
54+
MiddlewareManager::register(new MwA());
55+
MiddlewareManager::register(new MwB());
56+
MiddlewareManager::register(new MwC());
57+
}
58+
59+
protected function tearDown(): void {
60+
MiddlewareManager::reset();
61+
parent::tearDown();
62+
}
63+
64+
/** @test */
65+
public function testSingleDependencyAutoResolved() {
66+
$uri = new RouterUri('https://example.com/test', '');
67+
$uri->addMiddleware(new MwB()); // depends on mw-a
68+
69+
$middleware = $uri->getMiddleware();
70+
$names = array_map(fn($mw) => $mw->getName(), $middleware);
71+
72+
$this->assertContains('mw-a', $names);
73+
$this->assertContains('mw-b', $names);
74+
$this->assertCount(2, $middleware);
75+
}
76+
77+
/** @test */
78+
public function testTransitiveChainResolved() {
79+
$uri = new RouterUri('https://example.com/test', '');
80+
$uri->addMiddleware(new MwC()); // C depends on B, B depends on A
81+
82+
$middleware = $uri->getMiddleware();
83+
$names = array_map(fn($mw) => $mw->getName(), $middleware);
84+
85+
$this->assertContains('mw-a', $names);
86+
$this->assertContains('mw-b', $names);
87+
$this->assertContains('mw-c', $names);
88+
$this->assertCount(3, $middleware);
89+
}
90+
91+
/** @test */
92+
public function testExecutionOrderCorrect() {
93+
$uri = new RouterUri('https://example.com/test', '');
94+
$uri->addMiddleware(new MwC());
95+
96+
$middleware = $uri->getMiddleware();
97+
$names = array_map(fn($mw) => $mw->getName(), $middleware);
98+
99+
// A must come before B, B before C
100+
$this->assertLessThan(
101+
array_search('mw-b', $names),
102+
array_search('mw-a', $names)
103+
);
104+
$this->assertLessThan(
105+
array_search('mw-c', $names),
106+
array_search('mw-b', $names)
107+
);
108+
}
109+
110+
/** @test */
111+
public function testNoDuplicateWhenDependencyAlreadyAssigned() {
112+
$uri = new RouterUri('https://example.com/test', '');
113+
$uri->addMiddleware(new MwA());
114+
$uri->addMiddleware(new MwB());
115+
116+
$middleware = $uri->getMiddleware();
117+
$names = array_map(fn($mw) => $mw->getName(), $middleware);
118+
119+
$this->assertCount(2, $middleware);
120+
$this->assertEquals(1, count(array_keys($names, 'mw-a')));
121+
}
122+
123+
/** @test */
124+
public function testMissingDependencySkippedSilently() {
125+
$uri = new RouterUri('https://example.com/test', '');
126+
$uri->addMiddleware(new MwOrphan()); // depends on mw-nonexistent
127+
128+
$middleware = $uri->getMiddleware();
129+
$names = array_map(fn($mw) => $mw->getName(), $middleware);
130+
131+
$this->assertContains('mw-orphan', $names);
132+
$this->assertNotContains('mw-nonexistent', $names);
133+
$this->assertCount(1, $middleware);
134+
}
135+
136+
/** @test */
137+
public function testNoDependenciesUnchanged() {
138+
$uri = new RouterUri('https://example.com/test', '');
139+
$uri->addMiddleware(new MwA()); // no dependencies
140+
141+
$middleware = $uri->getMiddleware();
142+
$this->assertCount(1, $middleware);
143+
$this->assertEquals('mw-a', $middleware[0]->getName());
144+
}
145+
}

0 commit comments

Comments
 (0)