Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
54.35% covered (warning)
54.35%
50 / 92
27.27% covered (danger)
27.27%
3 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
Routing
54.35% covered (warning)
54.35%
50 / 92
27.27% covered (danger)
27.27%
3 / 11
129.43
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
1
 getOrCreateMiddleware
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getOrCreateShutdownMiddleware
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 handleBeforeMiddlewareCore
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 handleBeforeMiddleware
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 handleAfterMiddleware
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
20
 executeAction
85.19% covered (warning)
85.19%
23 / 27
0.00% covered (danger)
0.00%
0 / 1
5.08
 executeCore
81.25% covered (warning)
81.25%
13 / 16
0.00% covered (danger)
0.00%
0 / 1
8.42
 handleShutdownMiddleware
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 shutdown
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 execute
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5namespace PeServer\Core\Mvc;
6
7use PeServer\Core\Collection\Arr;
8use PeServer\Core\DI\IDiRegisterContainer;
9use PeServer\Core\Environment;
10use PeServer\Core\Http\HttpHeader;
11use PeServer\Core\Http\HttpMethod;
12use PeServer\Core\Http\HttpRequest;
13use PeServer\Core\Http\HttpResponse;
14use PeServer\Core\Http\HttpStatus;
15use PeServer\Core\Http\IResponsePrinterFactory;
16use PeServer\Core\Http\RequestPath;
17use PeServer\Core\Http\ResponsePrinter;
18use PeServer\Core\Log\ILogger;
19use PeServer\Core\Log\ILoggerFactory;
20use PeServer\Core\Log\Logging;
21use PeServer\Core\Mvc\ActionSetting;
22use PeServer\Core\Mvc\ControllerArgument;
23use PeServer\Core\Mvc\ControllerBase;
24use PeServer\Core\Mvc\Middleware\IMiddleware;
25use PeServer\Core\Mvc\Middleware\IShutdownMiddleware;
26use PeServer\Core\Mvc\Middleware\MiddlewareArgument;
27use PeServer\Core\Mvc\Middleware\MiddlewareResult;
28use PeServer\Core\Mvc\Result\IActionResult;
29use PeServer\Core\Mvc\RouteAction;
30use PeServer\Core\Mvc\RouteRequest;
31use PeServer\Core\Mvc\RouteSetting;
32use PeServer\Core\OutputBuffer;
33use PeServer\Core\ReflectionUtility;
34use PeServer\Core\Store\Stores;
35use PeServer\Core\Text;
36
37/**
38 * ルーティング。
39 *
40 * 名前の割に完全に心臓部だけどWebアプリならこれが心臓でいいのか・・・? ふとももなのか。
41 */
42class Routing
43{
44    #region variable
45
46    /**
47     * ルーティング設定。
48     */
49    protected readonly RouteSetting $setting;
50
51    protected readonly Stores $stores;
52    protected readonly Environment $environment;
53
54    protected readonly ILoggerFactory $loggerFactory;
55
56    /**
57     * このルーティング内で使いまわされるDI。
58     */
59    protected readonly IDiRegisterContainer $serviceLocator;
60
61    /**
62     * 前処理済みミドルウェア一覧。
63     *
64     * @var IMiddleware[]
65     */
66    private array $processedMiddleware = [];
67
68    /**
69     * 終了時ミドルウェア。
70     *
71     * 登録の逆順に実行される。
72     *
73     * @var array<IShutdownMiddleware|class-string<IShutdownMiddleware>>
74     */
75    private array $shutdownMiddleware = [];
76
77    /**
78     * 終了処理時に使用する要求データ。
79     *
80     * 要求受付前はダミー値が入っており、要求受付後はその要求値が格納される。
81     *
82     * @var HttpRequest
83     */
84    private HttpRequest $shutdownRequest;
85
86    protected readonly HttpMethod $requestMethod;
87    protected readonly RequestPath $requestPath;
88    protected readonly HttpHeader $requestHeader;
89
90    protected readonly IResponsePrinterFactory $responsePrinterFactory;
91
92    #endregion
93
94    /**
95     * 生成。
96     *
97     * @param RouteRequest $routeRequest
98     * @param RouteSetting $routeSetting
99     * @param Stores $stores
100     */
101    public function __construct(RouteRequest $routeRequest, RouteSetting $routeSetting, Stores $stores, Environment $environment, IResponsePrinterFactory $responsePrinterFactory, ILoggerFactory $loggerFactory, IDiRegisterContainer $serviceLocator)
102    {
103        $this->requestMethod = $routeRequest->method;
104        $this->requestPath = $routeRequest->path;
105        $this->setting = $routeSetting;
106        $this->stores = $stores;
107        $this->environment = $environment;
108        $this->responsePrinterFactory = $responsePrinterFactory;
109        $this->loggerFactory = $loggerFactory;
110        $this->serviceLocator = $serviceLocator;
111
112        $this->requestHeader = $this->stores->special->getRequestHeader();
113        $this->shutdownRequest = new HttpRequest($this->stores->special, $this->requestMethod, $this->requestHeader, []);
114        $this->serviceLocator->registerValue($this->shutdownRequest);
115    }
116
117    #region function
118
119    /**
120     * ミドルウェア取得。
121     *
122     * @param IMiddleware|class-string<IMiddleware> $middleware
123     * @return IMiddleware
124     */
125    protected function getOrCreateMiddleware(IMiddleware|string $middleware): IMiddleware
126    {
127        if (is_string($middleware)) {
128            /** @var IMiddleware */
129            $middleware = $this->serviceLocator->new($middleware);
130        }
131
132        return $middleware;
133    }
134
135    /**
136     * 応答完了ミドルウェア取得。
137     *
138     * @param IShutdownMiddleware|class-string<IShutdownMiddleware> $middleware
139     * @return IShutdownMiddleware
140     */
141    protected function getOrCreateShutdownMiddleware(IShutdownMiddleware|string $middleware): IShutdownMiddleware
142    {
143        if (is_string($middleware)) {
144            /** @var IShutdownMiddleware */
145            $middleware = $this->serviceLocator->new($middleware);
146        }
147
148        return $middleware;
149    }
150
151    /**
152     * ミドルウェア単独処理。
153     *
154     * @param RequestPath $requestPath
155     * @param HttpRequest $request
156     * @param IMiddleware|class-string<IMiddleware> $middleware
157     * @return bool 次のミドルウェアを実行してよいか
158     */
159    private function handleBeforeMiddlewareCore(RequestPath $requestPath, HttpRequest $request, IMiddleware|string $middleware): bool
160    {
161        $middlewareArgument = new MiddlewareArgument($requestPath, $this->stores, $this->environment, $request);
162        $middleware = self::getOrCreateMiddleware($middleware);
163
164        $middlewareResult = $middleware->handleBefore($middlewareArgument);
165
166        if ($middlewareResult->canNext()) {
167            $this->processedMiddleware[] = $middleware;
168            return true;
169        }
170
171        $middlewareResult->apply();
172        return false;
173    }
174
175    /**
176     * ミドルウェア(事前)をグワーッと処理。
177     *
178     * @param array<IMiddleware|class-string<IMiddleware>> $middleware
179     * @param HttpRequest $request
180     * @return bool 後続処理は可能か
181     */
182    protected function handleBeforeMiddleware(array $middleware, HttpRequest $request): bool
183    {
184        foreach ($middleware as $middlewareItem) {
185            $canNext = $this->handleBeforeMiddlewareCore($this->requestPath, $request, $middlewareItem);
186            if (!$canNext) {
187                return false;
188            }
189        }
190
191        return true;
192    }
193
194    /**
195     * ミドルウェア(事後)をグワーッと処理。
196     *
197     * @param HttpRequest $request
198     * @param HttpResponse $response
199     * @return bool
200     */
201    protected function handleAfterMiddleware(HttpRequest $request, HttpResponse $response): bool
202    {
203        if (!Arr::getCount($this->processedMiddleware)) {
204            return true;
205        }
206
207        $middlewareArgument = new MiddlewareArgument($this->requestPath, $this->stores, $this->environment, $request);
208
209        $middleware = Arr::reverse($this->processedMiddleware);
210        foreach ($middleware as $middlewareItem) {
211            $middlewareResult = $middlewareItem->handleAfter($middlewareArgument, $response);
212
213            if (!$middlewareResult->canNext()) {
214                $middlewareResult->apply();
215                return false;
216            }
217        }
218
219        return true;
220    }
221
222    /**
223     * アクション実行。
224     *
225     * @param string $rawControllerName
226     * @param ActionSetting $actionSetting
227     * @param array<non-empty-string,string> $urlParameters
228     * @return void
229     */
230    private function executeAction(string $rawControllerName, ActionSetting $actionSetting, array $urlParameters): void
231    {
232        $splitNames = Text::split($rawControllerName, '/');
233        /** @phpstan-var class-string<ControllerBase> */
234        $controllerName = $splitNames[Arr::getCount($splitNames) - 1];
235
236        // HTTPリクエストデータをDI再登録
237        $request = new HttpRequest($this->stores->special, $this->requestMethod, $this->requestHeader, $urlParameters);
238        $this->serviceLocator->registerValue($request);
239        $this->shutdownRequest = $request;
240
241        // アクション共通ミドルウェア処理
242        $this->shutdownMiddleware += $this->setting->actionShutdownMiddleware;
243        if (!$this->handleBeforeMiddleware($this->setting->actionMiddleware, $request)) {
244            return;
245        }
246
247        // アクションに紐づくミドルウェア処理
248        $this->shutdownMiddleware += $actionSetting->shutdownMiddleware;
249        if (!$this->handleBeforeMiddleware($actionSetting->actionMiddleware, $request)) {
250            return;
251        }
252
253        $logger = $this->loggerFactory->createLogger($controllerName);
254        $controllerArgument = $this->serviceLocator->new(ControllerArgument::class, [Stores::class => $this->stores, ILogger::class => $logger]);
255
256        /** @var IActionResult|null */
257        $actionResult = null;
258        $output = OutputBuffer::get(function () use ($controllerArgument, $controllerName, $actionSetting, &$actionResult) {
259            /** @var ControllerBase */
260            $controller = $this->serviceLocator->new($controllerName, [ControllerArgument::class => $controllerArgument]);
261            $methodName = $actionSetting->controllerMethod;
262            /** @var IActionResult */
263            $actionResult = $this->serviceLocator->call([$controller, $methodName]); //@phpstan-ignore-line callable
264        });
265        // 標準出力は闇に葬る
266        if ($output->count()) {
267            $logger->warn('{0}', $output->raw);
268        }
269
270        $this->stores->apply();
271
272        // 最終出力
273        /** @var IActionResult $actionResult */
274        $response = $actionResult->createResponse();
275        if (!$this->handleAfterMiddleware($request, $response)) {
276            return;
277        }
278
279        $printer = $this->serviceLocator->new(ResponsePrinter::class, [$request, $response]);
280        $printer->execute();
281    }
282
283    /**
284     * メソッド・パスから登録されている処理を実行。
285     */
286    private function executeCore(): void
287    {
288        $this->shutdownMiddleware += $this->setting->globalShutdownMiddleware;
289
290        // グローバルミドルウェアの適用
291        if (Arr::getCount($this->setting->globalMiddleware)) {
292            if (!$this->handleBeforeMiddleware($this->setting->globalMiddleware, $this->shutdownRequest)) {
293                return;
294            }
295        }
296
297        /** @var RouteAction|null */
298        $errorAction = null;
299        foreach ($this->setting->routes as $route) {
300            $action = $route->getAction($this->requestMethod, $this->requestPath);
301            if ($action !== null) {
302                if ($action->status === HttpStatus::None) {
303                    $this->executeAction($action->className, $action->actionSetting, $action->params);
304                    return;
305                } elseif ($errorAction === null) {
306                    $errorAction = $action;
307                }
308            }
309        }
310
311        if ($errorAction === null) {
312            MiddlewareResult::error(HttpStatus::InternalServerError)->apply();
313        } else {
314            MiddlewareResult::error($errorAction->status)->apply();
315        }
316    }
317
318    protected function handleShutdownMiddleware(): void
319    {
320        if (Arr::getCount($this->shutdownMiddleware)) {
321            $middlewareArgument = new MiddlewareArgument($this->requestPath, $this->stores, $this->environment, $this->shutdownRequest);
322
323            $shutdownMiddleware = array_reverse($this->shutdownMiddleware);
324            foreach ($shutdownMiddleware as $middleware) {
325                $middleware = self::getOrCreateShutdownMiddleware($middleware);
326                $middleware->handleShutdown($middlewareArgument);
327            }
328        }
329    }
330
331    /**
332     * 終了処理。
333     */
334    private function shutdown(): void
335    {
336        $this->handleShutdownMiddleware();
337    }
338
339    /**
340     * メソッド・パスから登録されている処理を実行。
341     */
342    public function execute(): void
343    {
344        try {
345            $this->executeCore();
346        } finally {
347            $this->shutdown();
348        }
349    }
350
351    #endregion
352}