Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
97.06% |
66 / 68 |
|
90.91% |
10 / 11 |
CRAP | |
0.00% |
0 / 1 |
| WebApp | |
97.06% |
66 / 68 |
|
90.91% |
10 / 11 |
22 | |
0.00% |
0 / 1 |
| __construct | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
| init | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
| process | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
2 | |||
| redirect | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
| sendContent | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
4 | |||
| sendError | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
| useRouteAttributes | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
| useJwtAuth | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
1 | |||
| loadErrorPageContent | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
| isWeb | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| handleException | |
86.67% |
13 / 15 |
|
0.00% |
0 / 1 |
3.02 | |||
| 1 | <?php |
| 2 | |
| 3 | namespace Dynart\Micro; |
| 4 | |
| 5 | use Dynart\Micro\AttributeHandler\AllowAnonymousAttributeHandler; |
| 6 | use Dynart\Micro\AttributeHandler\AuthorizeAttributeHandler; |
| 7 | use Dynart\Micro\AttributeHandler\RouteAttributeHandler; |
| 8 | use Dynart\Micro\Middleware\AttributeProcessor; |
| 9 | use Dynart\Micro\Middleware\JwtValidator; |
| 10 | |
| 11 | /** |
| 12 | * Handles HTTP request/response |
| 13 | * @package Dynart\Micro |
| 14 | */ |
| 15 | class WebApp extends AbstractApp { |
| 16 | |
| 17 | const CONFIG_ERROR_PAGES_FOLDER = 'app.error_pages_folder'; |
| 18 | const CONFIG_USE_ROUTE_ATTRIBUTES = 'app.use_route_attributes'; |
| 19 | const HEADER_CONTENT_TYPE = 'Content-Type'; |
| 20 | const HEADER_LOCATION = 'Location'; |
| 21 | const CONTENT_TYPE_HTML = 'text/html; charset=UTF-8'; |
| 22 | const CONTENT_TYPE_JSON = 'application/json'; |
| 23 | const ERROR_CONTENT_PLACEHOLDER = '<!-- content -->'; |
| 24 | const EVENT_ROUTE_MATCHED = 'webapp:route_matched'; |
| 25 | |
| 26 | protected RouterInterface $router; |
| 27 | protected ResponseInterface $response; |
| 28 | |
| 29 | public function __construct(array $configPaths) { |
| 30 | parent::__construct($configPaths); |
| 31 | Micro::add(RequestInterface::class, Request::class); |
| 32 | Micro::add(ResponseInterface::class, Response::class); |
| 33 | Micro::add(RouterInterface::class, Router::class); |
| 34 | Micro::add(SessionInterface::class, Session::class); |
| 35 | Micro::add(ViewInterface::class, View::class); |
| 36 | } |
| 37 | |
| 38 | /** |
| 39 | * Inits the WebApp |
| 40 | * |
| 41 | * Gets the `$router` and the `$response` instances and initializes the RouteAttributeHandler |
| 42 | * if the `app.use_route_attributes` is true (default). |
| 43 | */ |
| 44 | public function init(): void { |
| 45 | $this->router = Micro::get(RouterInterface::class); |
| 46 | $this->response = Micro::get(ResponseInterface::class); |
| 47 | if ($this->config?->get(self::CONFIG_USE_ROUTE_ATTRIBUTES, true) ?? true) { |
| 48 | $this->useRouteAttributes(); |
| 49 | } |
| 50 | } |
| 51 | |
| 52 | /** |
| 53 | * Runs the current route |
| 54 | * |
| 55 | * Matches the current route, emits the `webapp:route_matched` signal |
| 56 | * then calls the right callable and sends back the returned content. |
| 57 | * |
| 58 | * If no route found sends a 404. |
| 59 | */ |
| 60 | public function process(): void { |
| 61 | list($callable, $params) = $this->router->matchCurrentRoute(); |
| 62 | if ($callable) { |
| 63 | $callable = Micro::getCallable($callable); |
| 64 | $this->eventService->emit(self::EVENT_ROUTE_MATCHED, [$callable, $params]); |
| 65 | $content = call_user_func_array($callable, $params); |
| 66 | $this->sendContent($content); |
| 67 | } else { |
| 68 | $this->sendError(404); |
| 69 | } |
| 70 | } |
| 71 | |
| 72 | /** |
| 73 | * Redirects to the give location and parameters |
| 74 | * |
| 75 | * Clears the current headers, then sends back a `Location` header and empty body, then finishes the request. |
| 76 | * If `$location` NOT starts with `http` will be converted to a full URL with `Router::url` |
| 77 | */ |
| 78 | public function redirect(string $location, array $params = []): void { |
| 79 | $url = str_starts_with($location, 'http') ? $location : $this->router->url($location, $params); |
| 80 | $this->response->clearHeaders(); |
| 81 | $this->response->setHeader(self::HEADER_LOCATION, $url); |
| 82 | $this->response->send(); |
| 83 | $this->finish(); |
| 84 | } |
| 85 | |
| 86 | /** |
| 87 | * Sends the content |
| 88 | * |
| 89 | * If the `$content` is an array will be encoded to a JSON string. |
| 90 | * If no `Content-Type` header was added will be set to |
| 91 | * `html/text; charset=UTF-8` (string) or `application/json` (array). |
| 92 | */ |
| 93 | public function sendContent(mixed $content): void { |
| 94 | $rawContent = is_array($content) |
| 95 | ? json_encode($content) |
| 96 | : $content; |
| 97 | if (!$this->response->header(self::HEADER_CONTENT_TYPE)) { |
| 98 | $this->response->setHeader(self::HEADER_CONTENT_TYPE, is_array($content) |
| 99 | ? self::CONTENT_TYPE_JSON |
| 100 | : self::CONTENT_TYPE_HTML); |
| 101 | } |
| 102 | $this->response->send($rawContent); |
| 103 | } |
| 104 | |
| 105 | /** |
| 106 | * Sends an error response |
| 107 | * @param int $code The error code |
| 108 | * @param string $content The error content |
| 109 | */ |
| 110 | public function sendError(int $code, string $content = ''): void { |
| 111 | if ($this->isWeb()) { // Because of testing in cli (fastest solution) |
| 112 | http_response_code($code); |
| 113 | } |
| 114 | $pageContent = str_replace(self::ERROR_CONTENT_PLACEHOLDER, $content, $this->loadErrorPageContent($code)); |
| 115 | $this->finish($pageContent); |
| 116 | } |
| 117 | |
| 118 | /** |
| 119 | * Initializes the RouteAttributeHandler for the Route attributes |
| 120 | */ |
| 121 | public function useRouteAttributes(): void { |
| 122 | $this->addMiddleware(AttributeProcessor::class); |
| 123 | Micro::add(RouteAttributeHandler::class); |
| 124 | $processor = Micro::get(AttributeProcessor::class); |
| 125 | $processor->add(RouteAttributeHandler::class); |
| 126 | } |
| 127 | |
| 128 | /** |
| 129 | * Initializes the JwtValidator middleware and the Authorize and AllowAnonymous attributes, |
| 130 | * adds the JwtAuthInterface. |
| 131 | */ |
| 132 | public function useJwtAuth(): void { |
| 133 | $this->addMiddleware(AttributeProcessor::class); |
| 134 | $this->addMiddleware(JwtValidator::class); |
| 135 | Micro::add(JwtAuthInterface::class, JwtAuth::class); |
| 136 | Micro::add(AuthorizeAttributeHandler::class); |
| 137 | Micro::add(AllowAnonymousAttributeHandler::class); |
| 138 | $processor = Micro::get(AttributeProcessor::class); |
| 139 | $processor->add(AuthorizeAttributeHandler::class); |
| 140 | $processor->add(AllowAnonymousAttributeHandler::class); |
| 141 | } |
| 142 | |
| 143 | /** |
| 144 | * If it exists, loads the content of an error HTML page otherwise |
| 145 | * returns the HTML comment for the error placeholder |
| 146 | * |
| 147 | * @param int $code The HTTP status code for the error |
| 148 | * @return string The content of the HTML file or the HTML comment for the error placeholder |
| 149 | */ |
| 150 | protected function loadErrorPageContent(int $code): string { |
| 151 | $dir = $this->config->get(self::CONFIG_ERROR_PAGES_FOLDER); |
| 152 | if ($dir) { |
| 153 | $path = $this->config->getFullPath($dir.'/'.$code.'.html'); |
| 154 | if (file_exists($path)) { |
| 155 | return file_get_contents($path); |
| 156 | } |
| 157 | } |
| 158 | return self::ERROR_CONTENT_PLACEHOLDER; |
| 159 | } |
| 160 | |
| 161 | /** |
| 162 | * Returns true if the call is from the web |
| 163 | * @return bool |
| 164 | */ |
| 165 | protected function isWeb(): bool { |
| 166 | return http_response_code() !== false; |
| 167 | } |
| 168 | |
| 169 | /** |
| 170 | * Handles the exception |
| 171 | * |
| 172 | * Calls the parent exception handler, then calls the sendError with HTTP error 500. |
| 173 | * Sets the content for the error placeholder if the environment is not production. |
| 174 | * |
| 175 | * @param \Exception $e The exception for handling |
| 176 | */ |
| 177 | protected function handleException(\Exception $e): void { |
| 178 | if ($e instanceof AuthorizationException) { |
| 179 | $this->sendError($e->getCode()); |
| 180 | return; |
| 181 | } |
| 182 | parent::handleException($e); |
| 183 | $env = $this->config->get(AbstractApp::CONFIG_ENVIRONMENT, AbstractApp::PRODUCTION_ENVIRONMENT); |
| 184 | if ($env != AbstractApp::PRODUCTION_ENVIRONMENT) { |
| 185 | $type = get_class($e); |
| 186 | $file = $e->getFile(); |
| 187 | $line = $e->getLine(); |
| 188 | $message = $e->getMessage(); |
| 189 | $trace = $e->getTraceAsString(); |
| 190 | $content = "<h2>$type</h2>\n<p>In <b>$file</b> on <b>line $line</b> with message: $message</p>\n"; |
| 191 | $content .= "<h3>Stacktrace:</h3>\n<p>".str_replace("\n", "<br>\n", $trace)."</p>"; |
| 192 | } else { |
| 193 | $content = ''; |
| 194 | } |
| 195 | $this->sendError(500, $content); |
| 196 | } |
| 197 | |
| 198 | } |