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 | } |
Below are the source code lines that represent each code branch as identified by Xdebug. Please note a branch is not
necessarily coterminous with a line, a line may contain multiple branches and therefore show up more than once.
Please also be aware that some branches may be implicit rather than explicit, e.g. an if statement
always has an else as part of its logical flow even if you didn't write one.
| 37 | private string $patternPrefix, |
| 38 | private Closure $createClosure, |
| 39 | private string $namePrefix = '', |
| 40 | ) {} |
| 97 | public function addRoute(Route $route): Route |
| 98 | { |
| 99 | $this->assertCollecting(); |
| 100 | $this->entries[] = $route; |
| 101 | |
| 102 | return $route; |
| 103 | } |
| 89 | public function after(After $afterHandler): static |
| 90 | { |
| 91 | $this->assertCollecting(); |
| 92 | |
| 93 | return $this->addAfterHandler($afterHandler); |
| 94 | } |
| 143 | if (!$this->collecting) { |
| 144 | throw new RuntimeException('Cannot modify group outside the group callback.'); |
| 146 | } |
| 82 | public function before(Before $beforeHandler): static |
| 83 | { |
| 84 | $this->assertCollecting(); |
| 85 | |
| 86 | return $this->addBeforeHandler($beforeHandler); |
| 87 | } |
| 67 | public function controller(string $controller): static |
| 68 | { |
| 69 | $this->assertCollecting(); |
| 70 | $this->controller = $controller; |
| 71 | |
| 72 | return $this; |
| 73 | } |
| 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())); |
| 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); |
| 171 | return $this->routeAdder->addRoute($route); |
| 172 | } |
| 107 | string $patternPrefix, |
| 108 | Closure $createClosure, |
| 109 | string $namePrefix = '', |
| 110 | ): void { |
| 111 | $this->assertCollecting(); |
| 112 | $this->entries[] = self::make($patternPrefix, $createClosure, $namePrefix); |
| 113 | } |
| 60 | string $patternPrefix, |
| 61 | Closure $createClosure, |
| 62 | string $namePrefix = '', |
| 63 | ): self { |
| 64 | return new self($patternPrefix, $createClosure, $namePrefix); |
| 65 | } |
| 75 | public function middleware(Middleware ...$middleware): static |
| 76 | { |
| 77 | $this->assertCollecting(); |
| 78 | |
| 79 | return $this->addMiddlewareHandlers(...$middleware); |
| 80 | } |
| 148 | private function receive(Route $route): Route |
| 149 | { |
| 150 | return $this->forwardRoute($route); |
| 151 | } |
| 116 | public function register(RouteAdder $adder): void |
| 117 | { |
| 118 | if ($this->registered) { |
| 119 | return; |
| 122 | $this->registered = true; |
| 123 | $this->routeAdder = $adder; |
| 124 | $this->collecting = true; |
| 125 | |
| 126 | try { |
| 127 | ($this->createClosure)($this); |
| 128 | } finally { |
| 128 | } finally { |
| 129 | $this->collecting = false; |
| 132 | foreach ($this->entries as $entry) { |
| 132 | foreach ($this->entries as $entry) { |
| 133 | if ($entry instanceof Route) { |
| 133 | if ($entry instanceof Route) { |
| 134 | $this->forwardRoute($entry); |
| 132 | foreach ($this->entries as $entry) { |
| 133 | if ($entry instanceof Route) { |
| 134 | $this->forwardRoute($entry); |
| 135 | } else { |
| 136 | $entry->register($this); |
| 132 | foreach ($this->entries as $entry) { |
| 132 | foreach ($this->entries as $entry) { |
| 133 | if ($entry instanceof Route) { |
| 134 | $this->forwardRoute($entry); |
| 135 | } else { |
| 136 | $entry->register($this); |
| 137 | } |
| 138 | } |
| 139 | } |