Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
88.61% covered (warning)
88.61%
70 / 79
87.50% covered (warning)
87.50%
7 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
DashboardAndAnalyticsRouteDispatcher
88.46% covered (warning)
88.46%
69 / 78
87.50% covered (warning)
87.50%
7 / 8
33.57
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 canDispatch
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
6
 dispatch
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
8
 dispatchDashboardWebRoute
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
2
 dispatchMapRoute
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 dispatchHtmxMapRoutes
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
4
 dispatchHtmxCommentsRoutes
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 dispatchHtmxDashboardRoutes
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
8
1<?php
2
3declare(strict_types=1);
4
5/** @license For full copyright and license information, please see the LICENSE.md file. */
6
7namespace App\Core\Bootstrap\Dispatcher;
8
9defined('AMMONLY_APP') || exit('Direct script access is forbidden.');
10
11use App\Core\Bootstrap\Dispatcher\Handler\AuthProtectedRequestHandler;
12use App\Core\Bootstrap\Dispatcher\Handler\HtmxCommentsRouteHandler;
13use App\Core\Engine\Application\Security\PermissionContextFactory;
14use App\Core\Security\Middleware\CsrfMiddleware;
15use App\Modules\Comments\Presentation\Htmx\CommentsHtmxController;
16use App\Modules\Comments\Presentation\Htmx\MentionsHtmxController;
17use App\Modules\Dashboard\Presentation\Htmx\DashboardHtmxController;
18use App\Modules\Dashboard\Presentation\Web\DashboardWebController;
19use App\Modules\Map\Presentation\Htmx\MapHtmxController;
20use App\Modules\Map\Presentation\Web\MapWebController;
21use App\Modules\User\Presentation\Middleware\AuthenticatedOnlyMiddleware;
22use Nyholm\Psr7\Factory\Psr17Factory;
23use Psr\Http\Message\ResponseInterface;
24use Psr\Http\Message\ServerRequestInterface;
25use Psr\Http\Server\RequestHandlerInterface;
26
27/**
28 * Modular route dispatcher for Dashboard, Maps, and Comments subsystems.
29 */
30final readonly class DashboardAndAnalyticsRouteDispatcher implements ModuleRouteDispatcherInterface
31{
32    public function __construct(
33        private ?DashboardWebController $dashboardWebController,
34        private ?DashboardHtmxController $dashboardHtmxController,
35        private ?MapWebController $mapWebController,
36        private ?MapHtmxController $mapHtmxController,
37        private ?CommentsHtmxController $commentsHtmxController,
38        private ?MentionsHtmxController $mentionsHtmxController,
39        private PermissionContextFactory $contextFactory,
40        private AuthenticatedOnlyMiddleware $authMiddleware,
41        private CsrfMiddleware $csrfMiddleware,
42        private Psr17Factory $psr17Factory
43    ) {
44    }
45
46    public function canDispatch(string $path): bool
47    {
48        return $path === '/'
49            || $path === '/dashboard'
50            || $path === '/maps'
51            || str_starts_with($path, '/htmx/dashboard/')
52            || str_starts_with($path, '/htmx/maps/')
53            || str_starts_with($path, '/htmx/comments/');
54    }
55
56    public function dispatch(string $path, ServerRequestInterface $request): ?ResponseInterface
57    {
58        return match (true) {
59            $path === '/' || $path === '/dashboard' => $this->dispatchDashboardWebRoute($request),
60            $path === '/maps' => $this->dispatchMapRoute($request),
61            str_starts_with($path, '/htmx/dashboard/') => $this->dispatchHtmxDashboardRoutes($path, $request),
62            str_starts_with($path, '/htmx/maps/') => $this->dispatchHtmxMapRoutes($path, $request),
63            str_starts_with($path, '/htmx/comments/') => $this->dispatchHtmxCommentsRoutes($path, $request),
64            default => null,
65        };
66    }
67
68    private function dispatchDashboardWebRoute(ServerRequestInterface $request): ResponseInterface
69    {
70        if ($this->dashboardWebController === null) {
71            return $this->psr17Factory->createResponse(404);
72        }
73
74        return $this->authMiddleware->process(
75            $request,
76            new class($this->dashboardWebController, $this->contextFactory) implements RequestHandlerInterface {
77                public function __construct(
78                    private DashboardWebController $controller,
79                    private PermissionContextFactory $contextFactory
80                ) {
81                }
82
83                public function handle(ServerRequestInterface $req): ResponseInterface
84                {
85                    $ctx = $this->contextFactory->createFromRequest($req);
86                    return $this->controller->actionIndex($req, $ctx);
87                }
88            }
89        );
90    }
91
92    private function dispatchMapRoute(ServerRequestInterface $request): ResponseInterface
93    {
94        if ($this->mapWebController === null) {
95            return $this->psr17Factory->createResponse(404);
96        }
97
98        return $this->authMiddleware->process(
99            $request,
100            new class($this->mapWebController) implements RequestHandlerInterface {
101                public function __construct(private MapWebController $controller)
102                {
103                }
104
105                public function handle(ServerRequestInterface $req): ResponseInterface
106                {
107                    return $this->controller->actionIndex($req);
108                }
109            }
110        );
111    }
112
113    private function dispatchHtmxMapRoutes(string $path, ServerRequestInterface $request): ResponseInterface
114    {
115        if ($this->mapHtmxController === null) {
116            return $this->psr17Factory->createResponse(404);
117        }
118
119        $ctrl = $this->mapHtmxController;
120        $psr17 = $this->psr17Factory;
121
122        $innerHandler = new class($ctrl, $path, $psr17) implements RequestHandlerInterface {
123            public function __construct(
124                private MapHtmxController $ctrl,
125                private string $path,
126                private Psr17Factory $psr17
127            ) {
128            }
129
130            public function handle(ServerRequestInterface $req): ResponseInterface
131            {
132                if (preg_match('#^/htmx/maps/record-modal/([a-zA-Z0-9_-]+)/(\d+)$#', $this->path, $m)) {
133                    return $this->ctrl->recordModal($m[1], (int) $m[2]);
134                }
135                if ($this->path === '/htmx/maps/batch-modal') {
136                    return $this->ctrl->batchModal($req);
137                }
138                return $this->psr17->createResponse(404);
139            }
140        };
141
142        $authProtectedHandler = new AuthProtectedRequestHandler($this->authMiddleware, $innerHandler);
143
144        return $this->csrfMiddleware->process($request, $authProtectedHandler);
145    }
146
147    private function dispatchHtmxCommentsRoutes(string $path, ServerRequestInterface $request): ResponseInterface
148    {
149        $innerHandler = new HtmxCommentsRouteHandler(
150            $this->commentsHtmxController,
151            $this->mentionsHtmxController,
152            $path,
153            $this->psr17Factory,
154            $this->contextFactory
155        );
156        $authProtectedHandler = new AuthProtectedRequestHandler($this->authMiddleware, $innerHandler);
157
158        return $this->csrfMiddleware->process($request, $authProtectedHandler);
159    }
160
161    private function dispatchHtmxDashboardRoutes(
162        string $path,
163        ServerRequestInterface $request
164    ): ResponseInterface {
165        if ($this->dashboardHtmxController === null) {
166            return $this->psr17Factory->createResponse(404);
167        }
168
169        $ctrl = $this->dashboardHtmxController;
170        $ctxFactory = $this->contextFactory;
171        $psr17 = $this->psr17Factory;
172
173        $innerHandler = new class($ctrl, $path, $ctxFactory, $psr17) implements RequestHandlerInterface {
174            public function __construct(
175                private DashboardHtmxController $ctrl,
176                private string $path,
177                private PermissionContextFactory $ctxFactory,
178                private Psr17Factory $psr17
179            ) {
180            }
181
182            public function handle(ServerRequestInterface $req): ResponseInterface
183            {
184                $ctx = $this->ctxFactory->createFromRequest($req);
185
186                if (preg_match('#^/htmx/dashboard/event-modal/(\d+)$#', $this->path, $m)) {
187                    return $this->ctrl->eventModal((int) $m[1], $ctx);
188                }
189
190                return match ($this->path) {
191                    '/htmx/dashboard/widget/current-events' => $this->ctrl->widgetCurrentEvents($req, $ctx),
192                    '/htmx/dashboard/widget/overdue-events' => $this->ctrl->widgetOverdueEvents($req, $ctx),
193                    '/htmx/dashboard/widget-picker' => $this->ctrl->widgetPickerButton($ctx),
194                    '/htmx/dashboard/widget/toggle' => $this->ctrl->toggleWidget($req, $ctx),
195                    default => $this->psr17->createResponse(404),
196                };
197            }
198        };
199
200        $authProtectedHandler = new class($this->authMiddleware, $innerHandler) implements RequestHandlerInterface {
201            public function __construct(
202                private AuthenticatedOnlyMiddleware $authMiddleware,
203                private RequestHandlerInterface $next
204            ) {
205            }
206
207            public function handle(ServerRequestInterface $req): ResponseInterface
208            {
209                return $this->authMiddleware->process($req, $this->next);
210            }
211        };
212
213        return $this->csrfMiddleware->process($request, $authProtectedHandler);
214    }
215}