diff --git a/src/Libs/Router.php b/src/Libs/Router.php index edcbff5..af85b66 100644 --- a/src/Libs/Router.php +++ b/src/Libs/Router.php @@ -50,7 +50,7 @@ class Router * * @var callable $notFoundCallback */ - public static $notFoundCallback = 'Libs\Router::defaultNotFound'; + public static $notFoundCallback = Router::defaultNotFound(...); /** * Default callback function for when @@ -64,6 +64,54 @@ class Router echo '

Error 404 - Page Not Found

'; } + /** + * The callback function to be executed when the router's boundary + * catches an exception thrown anywhere in the matched route chain. + * It receives the exception as a named argument: the handler must + * declare its parameter as $exception (typeable as \Throwable). + * + * @var callable $exceptionCallback + */ + public static $exceptionCallback = Router::defaultException(...); + + /** + * Default callback function for exception responses. + * + * The HTTP status comes from the exception's code only when it is an + * integer in the 400-599 range; anything else (0, arbitrary codes, the + * SQLSTATE string carried by PDOException) answers 500. The body is + * negotiated from the request's Accept header: JSON when the client asks + * for application/json, plain text otherwise. The trace is serialized + * from getTraceAsString() because json_encode() of a Throwable yields an + * empty object: its properties are protected. + * + * @param \Throwable $exception + * + * @return void + */ + public static function defaultException(\Throwable $exception): void + { + $code = $exception->getCode(); + http_response_code(is_int($code) && $code >= 400 && $code <= 599 ? $code : 500); + + $accept = $_SERVER['HTTP_ACCEPT'] ?? ''; + + if (str_contains($accept, 'application/json')) { + header('Content-Type: application/json'); + print(json_encode([ + 'error' => $exception->getMessage(), + 'exception' => get_class($exception), + 'file' => $exception->getFile(), + 'line' => $exception->getLine(), + 'trace' => explode(PHP_EOL, $exception->getTraceAsString()), + ])); + + return; + } + + print($exception->getMessage() . PHP_EOL . $exception->getTraceAsString()); + } + /** * __construct */ @@ -363,50 +411,60 @@ class Router /** * Applies the route configuration. * + * This method is the framework's error boundary: any \Throwable thrown + * while running the matched route's callback chain, while printing the + * returned data, or while resolving the not-found callback is caught + * here and rendered through $exceptionCallback. A failing handler is + * deliberately left uncaught (double failure falls back to PHP itself). + * * @param string|null $path (optional) Path to use. If not defined, it detects the current path. * * @return void */ public static function apply(?string $path = null): void { - $path = $path ?? static::currentPath(); - $routers = match ($_SERVER['REQUEST_METHOD']) { // Selects an array of routers based on the method - 'POST' => static::$post, - 'PUT' => static::$put, - 'PATCH' => static::$patch, - 'DELETE' => static::$delete, - default => static::$get - }; + try { + $path = $path ?? static::currentPath(); + $routers = match ($_SERVER['REQUEST_METHOD']) { // Selects an array of routers based on the method + 'POST' => static::$post, + 'PUT' => static::$put, + 'PATCH' => static::$patch, + 'DELETE' => static::$delete, + default => static::$get + }; - foreach ($routers as $router) { // Checks all routers to see if they match the current path - if (preg_match_all('/^' . $router['path'] . '\/?$/si', $path, $matches, PREG_PATTERN_ORDER)) { - unset($matches[0]); + foreach ($routers as $router) { // Checks all routers to see if they match the current path + if (preg_match_all('/^' . $router['path'] . '\/?$/si', $path, $matches, PREG_PATTERN_ORDER)) { + unset($matches[0]); - // Checking and storing the variable parameters of the route - if (isset($matches[1])) { - static::$params = new Neuron(); - foreach ($matches as $index => $match) { - $paramName = $router['paramNames'][$index - 1]; - static::$params->{$paramName} = urldecode($match[0]); + // Checking and storing the variable parameters of the route + if (isset($matches[1])) { + static::$params = new Neuron(); + foreach ($matches as $index => $match) { + $paramName = $router['paramNames'][$index - 1]; + static::$params->{$paramName} = urldecode($match[0]); + } } - } - // Processes the callback queue - foreach (array_reverse($router['callback']) as $callback) { - $data = Synapsis::resolve($callback); - } + // Processes the callback queue + foreach (array_reverse($router['callback']) as $callback) { + $data = Synapsis::resolve($callback); + } - // By default, prints as JSON if something is returned - if (isset($data)) { - header('Content-Type: application/json'); - print(json_encode($data)); - } + // By default, prints as JSON if something is returned + if (isset($data)) { + header('Content-Type: application/json'); + print(json_encode($data)); + } - return; + return; + } } - } - // If no router matches, call $notFoundCallBack - Synapsis::resolve(static::$notFoundCallback); + // If no router matches, call $notFoundCallBack + Synapsis::resolve(static::$notFoundCallback); + } catch (\Throwable $exception) { + Synapsis::resolve(static::$exceptionCallback, ['exception' => $exception]); + } } }