From db2b5c444606049bd19b2cb1576f4aa759661663 Mon Sep 17 00:00:00 2001 From: Stephen Young Date: Sat, 11 Apr 2026 17:16:40 -0400 Subject: [PATCH] Extract built-in error page to compiled-error.php template MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Moved the fallback HTML/CSS/fonts from heredocs in HtmlExceptionResponseRenderer into a pre-compiled PHP template at src/Hyper/Templates/compiled-error.php. Executed via extract() + require in an isolated closure scope — deliberately bypasses the Shodo TemplateEngine so the error page renders even when the template engine, cache, or configuration is broken. 424 → 295 lines in the renderer. Visual design is now in its own file where it belongs. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/Hyper/HtmlExceptionResponseRenderer.php | 179 +++----------------- src/Hyper/Templates/compiled-error.php | 143 ++++++++++++++++ 2 files changed, 168 insertions(+), 154 deletions(-) create mode 100644 src/Hyper/Templates/compiled-error.php diff --git a/src/Hyper/HtmlExceptionResponseRenderer.php b/src/Hyper/HtmlExceptionResponseRenderer.php index 0b755a2..3b06944 100644 --- a/src/Hyper/HtmlExceptionResponseRenderer.php +++ b/src/Hyper/HtmlExceptionResponseRenderer.php @@ -201,172 +201,43 @@ private function buildHtmxFragment(StatusCode $status, \Throwable $e): string return '

' . $this->escape($message) . '

'; } + /** + * Render the built-in fallback error page. + * + * Uses a pre-compiled PHP template via extract() + require, deliberately + * bypassing the Shodo TemplateEngine. The error page must render even + * when the template engine, cache, or configuration is broken. + */ private function buildHtml( StatusCode $status, string $title, \Throwable $e, ?string $suggestion, ): string { - $code = $status->value; $rawMessage = $e->getMessage(); - - // When the exception message is just the reason phrase (no custom - // message was provided), use a friendlier default description. if ($rawMessage === $status->reason()->value) { $rawMessage = $this->defaultDescription($status); } - $message = $this->escape($rawMessage); - $escapedTitle = $this->escape($title); - - $suggestionBlock = ''; - if ($this->verboseErrors && $suggestion !== null) { - $escaped = $this->escape($suggestion); - $suggestionBlock = "

{$escaped}

"; - } - - $debugBlock = ''; - if ($this->debug) { - $class = $this->escape(get_class($e)); - $file = $this->escape($e->getFile()); - $line = $e->getLine(); - $trace = $this->escape($e->getTraceAsString()); - - $debugBlock = << -

{$class}

-

{$file}:{$line}

-
- Stack trace -
{$trace}
-
- - HTML; - } - - return << - - - - - {$code} {$escapedTitle} - {$this->fonts()} - {$this->css()} - - -
-

{$code}

-

{$escapedTitle}

-

{$message}

- {$suggestionBlock} - {$debugBlock} - -
- - - HTML; - } - - private function fonts(): string - { - // phpcs:disable Generic.Files.LineLength.TooLong - $families = 'family=Inter:wght@400;500&family=JetBrains+Mono:wght@400&family=Lora:wght@600'; - // phpcs:enable Generic.Files.LineLength.TooLong - - return << - - - HTML; - } - - private function css(): string - { - // phpcs:disable Generic.Files.LineLength.TooLong - $heading = "Lora,Georgia,'Times New Roman',serif"; - $body = "Inter,system-ui,-apple-system,'Segoe UI',sans-serif"; - $mono = "'JetBrains Mono','Fira Code','Source Code Pro',Consolas,monospace"; + $variables = [ + 'code' => $status->value, + 'title' => $this->escape($title), + 'message' => $this->escape($rawMessage), + 'suggestion' => ($this->verboseErrors && $suggestion !== null) + ? $this->escape($suggestion) + : null, + 'exception' => $this->debug ? $this->escape(get_class($e)) : null, + 'file' => $this->debug ? $this->escape($e->getFile()) : null, + 'line' => $this->debug ? $e->getLine() : null, + 'trace' => $this->debug ? $this->escape($e->getTraceAsString()) : null, + ]; - return << - body { - margin:0; padding:0; min-height:100vh; - display:flex; align-items:center; justify-content:center; - background:#faf8f1; - font-family:{$body}; color:#2c2a25; - } - .container { max-width:480px; width:100%; padding:24px; } - .status-code { - margin:0 0 8px; font-family:{$heading}; - font-size:48px; font-weight:600; - line-height:1.10; letter-spacing:-0.5px; color:#b5623f; - } - h1 { - margin:0 0 8px; font-family:{$heading}; - font-size:28px; font-weight:600; line-height:1.20; color:#2c2a25; - } - .message { margin:0; font-size:16px; line-height:1.65; color:#6b675e; } - .suggestion { - margin:16px 0 0; padding:14px 18px; - border-left:3px solid #4a6fa5; background:rgba(74,111,165,0.08); - border-radius:6px; color:#4a6fa5; font-size:14px; line-height:1.55; - } - .debug { - margin-top:32px; padding-top:24px; border-top:1px solid #ddd9ce; - } - .debug-class { - margin:0 0 8px; font-family:{$mono}; - font-size:14px; color:#3d3a34; - } - .debug-file { - margin:0 0 16px; font-family:{$mono}; - font-size:13px; color:#6b675e; - } - details summary { - cursor:pointer; font-family:{$body}; - font-size:14px; font-weight:500; color:#6b675e; margin-bottom:8px; - } - details pre { - margin:0; padding:16px 20px; background:#eae6da; - border:1px solid #ddd9ce; border-radius:6px; overflow-x:auto; - font-family:{$mono}; font-size:13px; line-height:1.55; color:#3d3a34; - } - .actions { margin-top:32px; display:flex; gap:12px; } - .actions a { - display:inline-block; padding:10px 20px; border-radius:6px; - border:1px solid #c4bfb3; background:transparent; color:#b5623f; - font-family:{$body}; font-size:15px; font-weight:500; - text-decoration:none; - } - .actions a:hover { background:#ece9e0; } - @media (prefers-color-scheme: dark) { - body { background:#1a1915; color:#e8e4db; } - .status-code { color:#c8795a; } - h1 { color:#e8e4db; } - .message { color:#9c9789; } - .suggestion { - border-color:#6a8fc0; color:#6a8fc0; - background:rgba(106,143,192,0.1); - } - .debug { border-color:#3d3a34; } - .debug-class { color:#c4bfb3; } - .debug-file { color:#9c9789; } - details summary { color:#9c9789; } - details pre { - background:#2d2b24; border-color:#3d3a34; color:#c4bfb3; - } - .actions a { - border-color:#3d3a34; color:#c8795a; - } - .actions a:hover { background:#2d2b24; } - } - - CSS; - // phpcs:enable Generic.Files.LineLength.TooLong + return (static function (string $__template, array $__variables): string { + extract($__variables); + ob_start(); + require $__template; + return (string) ob_get_clean(); + })(__DIR__ . '/Templates/compiled-error.php', $variables); } /** diff --git a/src/Hyper/Templates/compiled-error.php b/src/Hyper/Templates/compiled-error.php new file mode 100644 index 0000000..0bbcf3d --- /dev/null +++ b/src/Hyper/Templates/compiled-error.php @@ -0,0 +1,143 @@ + + + + + + + <?= $code ?> <?= $title ?> + + + + + + + +
+

+

+

+ +

+ + +
+

+

:

+
+ Stack trace +
+
+
+ + +
+ +