1 | <?php namespace Limoncello\Core\Application; |
||
41 | abstract class Application implements ApplicationInterface |
||
42 | { |
||
43 | use CheckCallableTrait; |
||
44 | |||
45 | /** Method name for default request factory. */ |
||
46 | const FACTORY_METHOD = 'defaultRequestFactory'; |
||
47 | |||
48 | /** |
||
49 | * @var SapiInterface|null |
||
50 | */ |
||
51 | private $sapi; |
||
52 | |||
53 | /** |
||
54 | * @var RouterInterface|null |
||
55 | */ |
||
56 | private $router = null; |
||
57 | |||
58 | /** |
||
59 | * @return SettingsProviderInterface |
||
60 | */ |
||
61 | abstract protected function createSettingsProvider(): SettingsProviderInterface; |
||
62 | |||
63 | /** |
||
64 | * @return LimoncelloContainerInterface |
||
65 | */ |
||
66 | abstract protected function createContainerInstance(): LimoncelloContainerInterface; |
||
67 | |||
68 | /** |
||
69 | * Exception handler should not use container before actual error occurs. |
||
70 | * |
||
71 | * @param SapiInterface $sapi |
||
72 | * @param PsrContainerInterface $container |
||
73 | * |
||
74 | * @return void |
||
75 | */ |
||
76 | abstract protected function setUpExceptionHandler(SapiInterface $sapi, PsrContainerInterface $container): void; |
||
77 | |||
78 | /** |
||
79 | * @inheritdoc |
||
80 | */ |
||
81 | 6 | public function setSapi(SapiInterface $sapi): ApplicationInterface |
|
87 | |||
88 | /** |
||
89 | * @inheritdoc |
||
90 | * |
||
91 | * @SuppressWarnings(PHPMD.StaticAccess) |
||
92 | * @SuppressWarnings(PHPMD.CyclomaticComplexity) |
||
93 | */ |
||
94 | 7 | public function run(): void |
|
95 | { |
||
96 | 7 | if ($this->sapi === null) { |
|
97 | 1 | throw new LogicException('SAPI not set.'); |
|
98 | } |
||
99 | |||
100 | 6 | $container = $this->createContainerInstance(); |
|
101 | |||
102 | 6 | $this->setUpExceptionHandler($this->sapi, $container); |
|
103 | |||
104 | 6 | $settingsProvider = $this->createSettingsProvider(); |
|
105 | 6 | $container->offsetSet(SettingsProviderInterface::class, $settingsProvider); |
|
106 | |||
107 | 6 | $coreSettings = $settingsProvider->get(CoreSettingsInterface::class); |
|
108 | |||
109 | // match route from `Request` to handler, route container configurators/middleware, etc |
||
110 | list($matchCode, $allowedMethods, $handlerParams, $handler, |
||
111 | 6 | $routeMiddleware, $routeConfigurators, $requestFactory) = $this->initRouter($coreSettings) |
|
112 | 6 | ->match($this->sapi->getMethod(), $this->sapi->getUri()->getPath()); |
|
113 | |||
114 | // configure container |
||
115 | 6 | $globalConfigurators = BaseCoreSettings::getGlobalConfiguratorsFromData($coreSettings); |
|
116 | 6 | $this->configureContainer($container, $globalConfigurators, $routeConfigurators); |
|
117 | |||
118 | // build pipeline for handling `Request`: global middleware -> route middleware -> handler (e.g. controller) |
||
119 | |||
120 | // select terminal handler |
||
121 | switch ($matchCode) { |
||
122 | 6 | case RouterInterface::MATCH_FOUND: |
|
123 | 4 | $handler = $this->createTerminalHandler($handler, $handlerParams, $container); |
|
124 | 4 | break; |
|
125 | 2 | case RouterInterface::MATCH_METHOD_NOT_ALLOWED: |
|
126 | 1 | $handler = $this->createMethodNotAllowedTerminalHandler($allowedMethods); |
|
127 | 1 | break; |
|
128 | default: |
||
129 | 1 | assert($matchCode === RouterInterface::MATCH_NOT_FOUND); |
|
130 | 1 | $handler = $this->createNotFoundTerminalHandler(); |
|
131 | 1 | break; |
|
132 | } |
||
133 | |||
134 | 6 | $globalMiddleware = BaseCoreSettings::getGlobalMiddlewareFromData($coreSettings); |
|
135 | 6 | $hasMiddleware = empty($globalMiddleware) === false || empty($routeMiddleware) === false; |
|
136 | |||
137 | 6 | $handler = $hasMiddleware === true ? |
|
138 | 6 | $this->addMiddlewareChain($handler, $container, $globalMiddleware, $routeMiddleware) : $handler; |
|
139 | |||
140 | 6 | $request = $requestFactory === null && $hasMiddleware === false && $matchCode === RouterInterface::MATCH_FOUND ? |
|
141 | 1 | null : |
|
142 | 6 | $this->createRequest($this->sapi, $container, $requestFactory ?? static::getDefaultRequestFactory()); |
|
143 | |||
144 | // Execute the pipeline by sending `Request` down all middleware (global then route's then |
||
145 | // terminal handler in `Controller` and back) and then send `Response` to SAPI |
||
146 | 6 | $this->sapi->handleResponse($this->handleRequest($handler, $request)); |
|
147 | } |
||
148 | |||
149 | /** |
||
150 | * @return callable |
||
|
|||
151 | */ |
||
152 | 6 | public static function getDefaultRequestFactory(): callable |
|
153 | { |
||
154 | 6 | return [static::class, static::FACTORY_METHOD]; |
|
155 | } |
||
156 | |||
157 | /** |
||
158 | * @param SapiInterface $sapi |
||
159 | * |
||
160 | * @return ServerRequestInterface |
||
161 | */ |
||
162 | 4 | public static function defaultRequestFactory(SapiInterface $sapi): ServerRequestInterface |
|
163 | { |
||
164 | 4 | return new ServerRequest( |
|
165 | 4 | $sapi->getServer(), |
|
166 | 4 | $sapi->getFiles(), |
|
167 | 4 | $sapi->getUri(), |
|
168 | 4 | $sapi->getMethod(), |
|
169 | 4 | $sapi->getRequestBody(), |
|
170 | 4 | $sapi->getHeaders(), |
|
171 | 4 | $sapi->getCookies(), |
|
172 | 4 | $sapi->getQueryParams(), |
|
173 | 4 | $sapi->getParsedBody(), |
|
174 | 4 | $sapi->getProtocolVersion() |
|
175 | ); |
||
176 | } |
||
177 | |||
178 | /** |
||
179 | * @param Closure $handler |
||
180 | * @param RequestInterface|null $request |
||
181 | * |
||
182 | * @return ResponseInterface |
||
183 | */ |
||
184 | 6 | protected function handleRequest(Closure $handler, RequestInterface $request = null): ResponseInterface |
|
185 | { |
||
186 | 6 | $response = call_user_func($handler, $request); |
|
187 | |||
188 | 6 | assert($response instanceof ResponseInterface); |
|
189 | |||
190 | 6 | return $response; |
|
191 | } |
||
192 | |||
193 | /** |
||
194 | * @param int $status |
||
195 | * @param array $headers |
||
196 | * |
||
197 | * @return ResponseInterface |
||
198 | */ |
||
199 | 2 | protected function createEmptyResponse($status = 204, array $headers = []): ResponseInterface |
|
200 | { |
||
201 | 2 | $response = new EmptyResponse($status, $headers); |
|
202 | |||
203 | 2 | return $response; |
|
204 | } |
||
205 | |||
206 | /** |
||
207 | * @return RouterInterface|null |
||
208 | */ |
||
209 | 1 | protected function getRouter(): ?RouterInterface |
|
210 | { |
||
211 | 1 | return $this->router; |
|
212 | } |
||
213 | |||
214 | /** |
||
215 | * @param LimoncelloContainerInterface $container |
||
216 | * @param callable[]|null $globalConfigurators |
||
217 | * @param callable[]|null $routeConfigurators |
||
218 | * |
||
219 | * @return void |
||
220 | */ |
||
221 | 6 | protected function configureContainer( |
|
222 | LimoncelloContainerInterface $container, |
||
223 | array $globalConfigurators = null, |
||
224 | array $routeConfigurators = null |
||
225 | ): void { |
||
226 | 6 | if (empty($globalConfigurators) === false) { |
|
227 | 6 | foreach ($globalConfigurators as $configurator) { |
|
228 | 6 | assert($this->checkPublicStaticCallable($configurator, [LimoncelloContainerInterface::class])); |
|
229 | 6 | $configurator($container); |
|
230 | } |
||
231 | } |
||
232 | 6 | if (empty($routeConfigurators) === false) { |
|
233 | 2 | foreach ($routeConfigurators as $configurator) { |
|
234 | 2 | assert($this->checkPublicStaticCallable($configurator, [LimoncelloContainerInterface::class])); |
|
235 | 2 | $configurator($container); |
|
236 | } |
||
237 | } |
||
238 | } |
||
239 | |||
240 | /** |
||
241 | * @param Closure $handler |
||
242 | * @param PsrContainerInterface $container |
||
243 | * @param array|null $globalMiddleware |
||
244 | * @param array|null $routeMiddleware |
||
245 | * |
||
246 | * @return Closure |
||
247 | */ |
||
248 | 5 | protected function addMiddlewareChain( |
|
249 | Closure $handler, |
||
250 | PsrContainerInterface $container, |
||
251 | array $globalMiddleware, |
||
252 | array $routeMiddleware = null |
||
253 | ): Closure { |
||
254 | 5 | $handler = $this->createMiddlewareChainImpl($handler, $container, $routeMiddleware); |
|
255 | 5 | $handler = $this->createMiddlewareChainImpl($handler, $container, $globalMiddleware); |
|
256 | |||
257 | 5 | return $handler; |
|
258 | } |
||
259 | |||
260 | /** |
||
261 | * @param callable $handler |
||
262 | * @param array $handlerParams |
||
263 | * @param PsrContainerInterface $container |
||
264 | * @param ServerRequestInterface|null $request |
||
265 | * |
||
266 | * @return ResponseInterface |
||
267 | */ |
||
268 | 4 | protected function callHandler( |
|
269 | callable $handler, |
||
270 | array $handlerParams, |
||
271 | PsrContainerInterface $container, |
||
272 | ServerRequestInterface $request = null |
||
273 | ): ResponseInterface { |
||
274 | // check the handler method signature |
||
275 | 4 | assert( |
|
276 | 4 | $this->checkPublicStaticCallable( |
|
277 | 4 | $handler, |
|
278 | 4 | ['array', PsrContainerInterface::class, ServerRequestInterface::class], |
|
279 | 4 | ResponseInterface::class |
|
280 | ), |
||
281 | 'Handler method should have signature ' . |
||
282 | 4 | '`public static methodName(array, PsrContainerInterface, ServerRequestInterface): ResponseInterface`' |
|
283 | ); |
||
284 | |||
285 | 4 | $response = call_user_func($handler, $handlerParams, $container, $request); |
|
286 | |||
287 | 4 | return $response; |
|
288 | } |
||
289 | |||
290 | /** |
||
291 | * @param array $coreSettings |
||
292 | * |
||
293 | * @return RouterInterface |
||
294 | * |
||
295 | * @SuppressWarnings(PHPMD.StaticAccess) |
||
296 | */ |
||
297 | 6 | protected function initRouter(array $coreSettings): RouterInterface |
|
298 | { |
||
299 | 6 | $routerParams = BaseCoreSettings::getRouterParametersFromData($coreSettings); |
|
300 | 6 | $routesData = BaseCoreSettings::getRoutesDataFromData($coreSettings); |
|
301 | 6 | $generatorClass = BaseCoreSettings::getGeneratorFromParametersData($routerParams); |
|
302 | 6 | $dispatcherClass = BaseCoreSettings::getDispatcherFromParametersData($routerParams); |
|
303 | |||
304 | 6 | $this->router = new Router($generatorClass, $dispatcherClass); |
|
305 | 6 | $this->router->loadCachedRoutes($routesData); |
|
306 | |||
307 | 6 | return $this->router; |
|
308 | } |
||
309 | |||
310 | /** |
||
311 | * @param SapiInterface $sapi |
||
312 | * @param PsrContainerInterface $container |
||
313 | * @param callable $requestFactory |
||
314 | * |
||
315 | * @return ServerRequestInterface |
||
316 | */ |
||
317 | 5 | private function createRequest( |
|
318 | SapiInterface $sapi, |
||
319 | PsrContainerInterface $container, |
||
320 | callable $requestFactory |
||
321 | ): ServerRequestInterface { |
||
322 | // check the factory method signature |
||
323 | 5 | assert( |
|
324 | 5 | $this->checkPublicStaticCallable( |
|
325 | 5 | $requestFactory, |
|
326 | 5 | [SapiInterface::class, PsrContainerInterface::class], |
|
327 | 5 | ServerRequestInterface::class |
|
328 | ), |
||
329 | 'Factory method should have signature ' . |
||
330 | 5 | '`public static methodName(SapiInterface, PsrContainerInterface): ServerRequestInterface`' |
|
331 | ); |
||
332 | |||
333 | 5 | $request = call_user_func($requestFactory, $sapi, $container); |
|
334 | |||
335 | 5 | return $request; |
|
336 | } |
||
337 | |||
338 | /** |
||
339 | * @param callable $handler |
||
340 | * @param array $handlerParams |
||
341 | * @param PsrContainerInterface $container |
||
342 | * |
||
343 | * @return Closure |
||
344 | */ |
||
345 | private function createTerminalHandler( |
||
354 | |||
355 | /** |
||
356 | * @param array $allowedMethods |
||
357 | * |
||
358 | * @return Closure |
||
359 | */ |
||
360 | private function createMethodNotAllowedTerminalHandler(array $allowedMethods): Closure |
||
361 | { |
||
362 | // 405 Method Not Allowed |
||
363 | 1 | return function () use ($allowedMethods) { |
|
364 | 1 | return $this->createEmptyResponse(405, ['Accept' => implode(',', $allowedMethods)]); |
|
365 | 1 | }; |
|
367 | |||
368 | /** |
||
369 | * @return Closure |
||
370 | */ |
||
371 | private function createNotFoundTerminalHandler(): Closure |
||
378 | |||
379 | /** |
||
380 | * @param Closure $handler |
||
381 | * @param PsrContainerInterface $container |
||
382 | * @param array|null $middleware |
||
383 | * |
||
384 | * @return Closure |
||
385 | */ |
||
386 | 5 | private function createMiddlewareChainImpl( |
|
398 | |||
399 | /** |
||
400 | * @param Closure $next |
||
401 | * @param callable $middleware |
||
402 | * @param PsrContainerInterface $container |
||
403 | * |
||
404 | * @return Closure |
||
405 | */ |
||
406 | 5 | private function createMiddlewareChainLink( |
|
426 | } |
||
427 |
This check looks for the generic type
array
as a return type and suggests a more specific type. This type is inferred from the actual code.