Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
39 / 39
100.00% covered (success)
100.00%
29 / 29
76.19% covered (warning)
76.19%
16 / 21
100.00% covered (success)
100.00%
12 / 12
CRAP
100.00% covered (success)
100.00%
1 / 1
Group
100.00% covered (success)
100.00%
39 / 39
100.00% covered (success)
100.00%
29 / 29
76.19% covered (warning)
76.19%
16 / 21
100.00% covered (success)
100.00%
12 / 12
22.37
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 make
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 controller
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 middleware
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 before
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 after
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addRoute
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 group
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 register
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
12 / 12
33.33% covered (danger)
33.33%
2 / 6
100.00% covered (success)
100.00%
1 / 1
8.74
 assertCollecting
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 receive
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 forwardRoute
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
5 / 5
75.00% covered (warning)
75.00%
3 / 4
100.00% covered (success)
100.00%
1 / 1
3.14
1<?php
2
3declare(strict_types=1);
4
5namespace Celemas\Router;
6
7use Celemas\Router\Exception\RuntimeException;
8use Closure;
9use Override;
10use Psr\Http\Server\MiddlewareInterface as Middleware;
11
12/** @psalm-api */
13final 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}