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}

Branches

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.

Group->__construct
37        private string $patternPrefix,
38        private Closure $createClosure,
39        private string $namePrefix = '',
40    ) {}
Group->addRoute
97    public function addRoute(Route $route): Route
98    {
99        $this->assertCollecting();
100        $this->entries[] = $route;
101
102        return $route;
103    }
Group->after
89    public function after(After $afterHandler): static
90    {
91        $this->assertCollecting();
92
93        return $this->addAfterHandler($afterHandler);
94    }
Group->assertCollecting
143        if (!$this->collecting) {
144            throw new RuntimeException('Cannot modify group outside the group callback.');
146    }
Group->before
82    public function before(Before $beforeHandler): static
83    {
84        $this->assertCollecting();
85
86        return $this->addBeforeHandler($beforeHandler);
87    }
Group->controller
67    public function controller(string $controller): static
68    {
69        $this->assertCollecting();
70        $this->controller = $controller;
71
72        return $this;
73    }
Group->forwardRoute
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    }
Group->group
107        string $patternPrefix,
108        Closure $createClosure,
109        string $namePrefix = '',
110    ): void {
111        $this->assertCollecting();
112        $this->entries[] = self::make($patternPrefix, $createClosure, $namePrefix);
113    }
Group->make
60        string $patternPrefix,
61        Closure $createClosure,
62        string $namePrefix = '',
63    ): self {
64        return new self($patternPrefix, $createClosure, $namePrefix);
65    }
Group->middleware
75    public function middleware(Middleware ...$middleware): static
76    {
77        $this->assertCollecting();
78
79        return $this->addMiddlewareHandlers(...$middleware);
80    }
Group->receive
148    private function receive(Route $route): Route
149    {
150        return $this->forwardRoute($route);
151    }
Group->register
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    }