Code Coverage |
||||||||||||||||
Lines |
Branches |
Paths |
Functions and Methods |
Classes and Traits |
||||||||||||
| Total | |
100.00% |
39 / 39 |
|
100.00% |
29 / 29 |
|
76.19% |
16 / 21 |
|
100.00% |
12 / 12 |
CRAP | |
100.00% |
1 / 1 |
| Group | |
100.00% |
39 / 39 |
|
100.00% |
29 / 29 |
|
76.19% |
16 / 21 |
|
100.00% |
12 / 12 |
22.37 | |
100.00% |
1 / 1 |
| __construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| make | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| controller | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| middleware | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| before | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| after | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| addRoute | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| group | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| register | |
100.00% |
11 / 11 |
|
100.00% |
12 / 12 |
|
33.33% |
2 / 6 |
|
100.00% |
1 / 1 |
8.74 | |||
| assertCollecting | |
100.00% |
2 / 2 |
|
100.00% |
3 / 3 |
|
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
| receive | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| forwardRoute | |
100.00% |
9 / 9 |
|
100.00% |
5 / 5 |
|
75.00% |
3 / 4 |
|
100.00% |
1 / 1 |
3.14 | |||
| 1 | <?php |
| 2 | |
| 3 | declare(strict_types=1); |
| 4 | |
| 5 | namespace Celemas\Router; |
| 6 | |
| 7 | use Celemas\Router\Exception\RuntimeException; |
| 8 | use Closure; |
| 9 | use Override; |
| 10 | use Psr\Http\Server\MiddlewareInterface as Middleware; |
| 11 | |
| 12 | /** @psalm-api */ |
| 13 | final class Group implements RouteAdder |
| 14 | { |
| 15 | use AddsBeforeAfter { |
| 16 | before as private addBeforeHandler; |
| 17 | after as private addAfterHandler; |
| 18 | } |
| 19 | use AddsMiddleware { |
| 20 | middleware as private addMiddlewareHandlers; |
| 21 | } |
| 22 | use AddsRoutes; |
| 23 | |
| 24 | /** @var list<Route|Group> */ |
| 25 | private array $entries = []; |
| 26 | |
| 27 | private ?RouteAdder $routeAdder = null; |
| 28 | private ?string $controller = null; |
| 29 | private bool $registered = false; |
| 30 | private bool $collecting = false; |
| 31 | |
| 32 | /** |
| 33 | * Groups are callback-scoped. Use Router::group() or nested Group::group() |
| 34 | * so the router controls registration, ordering, and group finalization. |
| 35 | */ |
| 36 | private function __construct( |
| 37 | private string $patternPrefix, |
| 38 | private Closure $createClosure, |
| 39 | private string $namePrefix = '', |
| 40 | ) {} |
| 41 | |
| 42 | /** |
| 43 | * Advanced escape hatch for route providers that must build a detached group. |
| 44 | * Prefer Router::group() for normal route definitions. |
| 45 | * |
| 46 | * Order matters: define routes inside the make() callback, then register the |
| 47 | * group with a router or parent group. Calling route helpers after register() |
| 48 | * throws because group finalization already happened. |
| 49 | * |
| 50 | * Example: |
| 51 | * |
| 52 | * $group = Group::make('/api', static function (Group $api): void { |
| 53 | * $api->get('/health', static fn() => 'ok', 'health'); |
| 54 | * }, 'api.'); |
| 55 | * $group->register($router); |
| 56 | * |
| 57 | * @internal |
| 58 | */ |
| 59 | public static function make( |
| 60 | string $patternPrefix, |
| 61 | Closure $createClosure, |
| 62 | string $namePrefix = '', |
| 63 | ): self { |
| 64 | return new self($patternPrefix, $createClosure, $namePrefix); |
| 65 | } |
| 66 | |
| 67 | public function controller(string $controller): static |
| 68 | { |
| 69 | $this->assertCollecting(); |
| 70 | $this->controller = $controller; |
| 71 | |
| 72 | return $this; |
| 73 | } |
| 74 | |
| 75 | public function middleware(Middleware ...$middleware): static |
| 76 | { |
| 77 | $this->assertCollecting(); |
| 78 | |
| 79 | return $this->addMiddlewareHandlers(...$middleware); |
| 80 | } |
| 81 | |
| 82 | public function before(Before $beforeHandler): static |
| 83 | { |
| 84 | $this->assertCollecting(); |
| 85 | |
| 86 | return $this->addBeforeHandler($beforeHandler); |
| 87 | } |
| 88 | |
| 89 | public function after(After $afterHandler): static |
| 90 | { |
| 91 | $this->assertCollecting(); |
| 92 | |
| 93 | return $this->addAfterHandler($afterHandler); |
| 94 | } |
| 95 | |
| 96 | #[Override] |
| 97 | public function addRoute(Route $route): Route |
| 98 | { |
| 99 | $this->assertCollecting(); |
| 100 | $this->entries[] = $route; |
| 101 | |
| 102 | return $route; |
| 103 | } |
| 104 | |
| 105 | #[Override] |
| 106 | public function group( |
| 107 | string $patternPrefix, |
| 108 | Closure $createClosure, |
| 109 | string $namePrefix = '', |
| 110 | ): void { |
| 111 | $this->assertCollecting(); |
| 112 | $this->entries[] = self::make($patternPrefix, $createClosure, $namePrefix); |
| 113 | } |
| 114 | |
| 115 | /** @internal */ |
| 116 | public function register(RouteAdder $adder): void |
| 117 | { |
| 118 | if ($this->registered) { |
| 119 | return; |
| 120 | } |
| 121 | |
| 122 | $this->registered = true; |
| 123 | $this->routeAdder = $adder; |
| 124 | $this->collecting = true; |
| 125 | |
| 126 | try { |
| 127 | ($this->createClosure)($this); |
| 128 | } finally { |
| 129 | $this->collecting = false; |
| 130 | } |
| 131 | |
| 132 | foreach ($this->entries as $entry) { |
| 133 | if ($entry instanceof Route) { |
| 134 | $this->forwardRoute($entry); |
| 135 | } else { |
| 136 | $entry->register($this); |
| 137 | } |
| 138 | } |
| 139 | } |
| 140 | |
| 141 | private function assertCollecting(): void |
| 142 | { |
| 143 | if (!$this->collecting) { |
| 144 | throw new RuntimeException('Cannot modify group outside the group callback.'); |
| 145 | } |
| 146 | } |
| 147 | |
| 148 | private function receive(Route $route): Route |
| 149 | { |
| 150 | return $this->forwardRoute($route); |
| 151 | } |
| 152 | |
| 153 | private function forwardRoute(Route $route): Route |
| 154 | { |
| 155 | $route->prefix($this->patternPrefix, $this->namePrefix); |
| 156 | |
| 157 | if ($this->controller !== null) { |
| 158 | $route->controller($this->controller); |
| 159 | } |
| 160 | |
| 161 | $route->replaceMiddleware(array_merge($this->middleware, $route->getMiddleware())); |
| 162 | $route->setBeforeHandlers($this->mergeBeforeHandlers($route->beforeHandlers())); |
| 163 | $route->setAfterHandlers($this->mergeAfterHandlers($route->afterHandlers())); |
| 164 | |
| 165 | assert($this->routeAdder !== null, 'RouteAdder must be set before forwarding routes.'); |
| 166 | |
| 167 | if ($this->routeAdder instanceof self) { |
| 168 | return $this->routeAdder->receive($route); |
| 169 | } |
| 170 | |
| 171 | return $this->routeAdder->addRoute($route); |
| 172 | } |
| 173 | } |