Skip to content

Commit 4c55aa2

Browse files
committed
feat: improved error page
1 parent c937ef2 commit 4c55aa2

File tree

8 files changed

+349
-13
lines changed

8 files changed

+349
-13
lines changed

phpmyfaq/api/index.php

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?php
22

33
/**
4-
* phpMyFAQ REST API: api/v3.2/version
4+
* phpMyFAQ REST API: api/v3.2
55
*
66
* This Source Code Form is subject to the terms of the Mozilla Public License,
77
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
@@ -15,12 +15,40 @@
1515
* @since 2023-07-28
1616
*/
1717

18+
declare(strict_types=1);
19+
1820
use phpMyFAQ\Application;
21+
use phpMyFAQ\Core\Exception\DatabaseConnectionException;
22+
use phpMyFAQ\Environment;
1923
use Symfony\Component\Config\FileLocator;
2024
use Symfony\Component\DependencyInjection\ContainerBuilder;
2125
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
26+
use Symfony\Component\HttpFoundation\JsonResponse;
27+
use Symfony\Component\HttpFoundation\Response;
28+
29+
try {
30+
require '../src/Bootstrap.php';
31+
} catch (DatabaseConnectionException $exception) {
32+
$errorMessage = Environment::isDebugMode()
33+
? $exception->getMessage()
34+
: 'The database server is currently unavailable. Please try again later.';
2235

23-
require '../src/Bootstrap.php';
36+
$problemDetails = [
37+
'type' => '/problems/database-unavailable',
38+
'title' => 'Database Connection Error',
39+
'status' => Response::HTTP_INTERNAL_SERVER_ERROR,
40+
'detail' => $errorMessage,
41+
'instance' => $_SERVER['REQUEST_URI'] ?? '/api',
42+
];
43+
44+
$response = new JsonResponse(
45+
data: $problemDetails,
46+
status: Response::HTTP_INTERNAL_SERVER_ERROR,
47+
headers: ['Content-Type' => 'application/problem+json']
48+
);
49+
$response->send();
50+
exit(1);
51+
}
2452

2553
//
2654
// Service Containers
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{% extends 'index.twig' %}
2+
3+
{% block content %}
4+
<div class="container my-5">
5+
<div class="row align-items-center justify-content-center">
6+
<div class="col-md-8 col-lg-6">
7+
<div class="text-center mb-4">
8+
<div class="display-1 fw-bold text-danger mb-3">500</div>
9+
<h1 class="h2 mb-3">{{ 'msgError500' | translate }}</h1>
10+
<p class="lead text-muted mb-4">{{ 'msgError500Description' | translate }}</p>
11+
</div>
12+
13+
{% if errorMessage %}
14+
<div class="alert alert-danger mb-4" role="alert">
15+
<i class="bi bi-exclamation-triangle-fill me-2"></i>
16+
<strong>{{ 'msgErrorDetails' | translate }}:</strong>
17+
<div class="mt-2 font-monospace small">{{ errorMessage }}</div>
18+
</div>
19+
{% endif %}
20+
21+
<div class="d-grid gap-2 d-sm-flex justify-content-sm-center">
22+
<a href="./" class="btn btn-primary btn-lg px-4 gap-3">
23+
<i class="bi bi-house me-2"></i>
24+
{{ 'msgBack2Home' | translate }}
25+
</a>
26+
<button class="btn btn-outline-secondary btn-lg px-4" onclick="window.location.reload()">
27+
<i class="bi bi-arrow-clockwise me-2"></i>
28+
{{ 'msgTryAgain' | translate }}
29+
</button>
30+
</div>
31+
</div>
32+
</div>
33+
</div>
34+
{% endblock %}
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
<!DOCTYPE html>
2+
<html lang="en" data-bs-theme="light">
3+
<head>
4+
<meta charset="utf-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1">
6+
<title>Service Unavailable - phpMyFAQ</title>
7+
<style>
8+
:root {
9+
--bs-primary: #083c83;
10+
--bs-danger: #f01704;
11+
--bs-body-bg: #ffffff;
12+
--bs-body-color: #212529;
13+
--bs-gray-600: #6c757d;
14+
}
15+
body {
16+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
17+
margin: 0;
18+
padding: 0;
19+
background-color: var(--bs-body-bg);
20+
color: var(--bs-body-color);
21+
line-height: 1.6;
22+
}
23+
.container {
24+
max-width: 800px;
25+
margin: 80px auto;
26+
padding: 40px 20px;
27+
}
28+
.text-center { text-align: center; }
29+
.display-1 {
30+
font-size: 6rem;
31+
font-weight: 700;
32+
color: var(--bs-danger);
33+
margin-bottom: 1.5rem;
34+
}
35+
h1 {
36+
font-size: 2rem;
37+
font-weight: 600;
38+
margin-bottom: 1.5rem;
39+
}
40+
.lead {
41+
font-size: 1.25rem;
42+
color: var(--bs-gray-600);
43+
margin-bottom: 2rem;
44+
}
45+
.btn {
46+
display: inline-block;
47+
padding: 12px 24px;
48+
font-size: 1.125rem;
49+
font-weight: 400;
50+
text-align: center;
51+
text-decoration: none;
52+
border: 1px solid transparent;
53+
border-radius: 6px;
54+
cursor: pointer;
55+
transition: all 0.15s ease-in-out;
56+
}
57+
.btn-primary {
58+
color: #fff;
59+
background-color: var(--bs-primary);
60+
border-color: var(--bs-primary);
61+
}
62+
.btn-primary:hover {
63+
background-color: #062e63;
64+
border-color: #062e63;
65+
}
66+
.btn-outline-secondary {
67+
color: var(--bs-gray-600);
68+
background-color: transparent;
69+
border-color: var(--bs-gray-600);
70+
}
71+
.btn-outline-secondary:hover {
72+
color: #fff;
73+
background-color: var(--bs-gray-600);
74+
border-color: var(--bs-gray-600);
75+
}
76+
.btn-group {
77+
display: flex;
78+
gap: 1rem;
79+
justify-content: center;
80+
margin-top: 2rem;
81+
flex-wrap: wrap;
82+
}
83+
.alert {
84+
position: relative;
85+
padding: 1rem;
86+
margin-bottom: 1rem;
87+
border: 1px solid transparent;
88+
border-radius: 6px;
89+
text-align: left;
90+
}
91+
.alert-danger {
92+
color: #842029;
93+
background-color: #f8d7da;
94+
border-color: #f5c2c7;
95+
}
96+
.alert strong {
97+
font-weight: 600;
98+
}
99+
.font-monospace {
100+
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
101+
}
102+
.small {
103+
font-size: 0.875rem;
104+
}
105+
.mt-2 { margin-top: 0.5rem; }
106+
.mb-4 { margin-bottom: 1.5rem; }
107+
@media (max-width: 768px) {
108+
.display-1 { font-size: 4rem; }
109+
h1 { font-size: 1.5rem; }
110+
.lead { font-size: 1.125rem; }
111+
}
112+
</style>
113+
</head>
114+
<body>
115+
<div class="container">
116+
<div class="text-center">
117+
<div class="display-1">500</div>
118+
<h1>Service Unavailable</h1>
119+
<p class="lead">The server encountered an internal error and was unable to complete your request. Please try again later.</p>
120+
</div>
121+
122+
{% if errorMessage %}
123+
<div class="alert alert-danger mb-4" role="alert">
124+
<strong>Error Details:</strong>
125+
<div class="mt-2 font-monospace small">{{ errorMessage }}</div>
126+
</div>
127+
{% endif %}
128+
129+
<div class="btn-group">
130+
<a href="./" class="btn btn-primary">Return to Home</a>
131+
<button class="btn btn-outline-secondary" onclick="window.location.reload()">Try Again</button>
132+
</div>
133+
</div>
134+
</body>
135+
</html>

phpmyfaq/index.php

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,23 +20,29 @@
2020
* @since 2001-02-12
2121
*/
2222

23+
declare(strict_types=1);
24+
2325

2426
use phpMyFAQ\Application;
27+
use phpMyFAQ\Controller\Frontend\ErrorController;
2528
use phpMyFAQ\Core\Exception;
29+
use phpMyFAQ\Core\Exception\DatabaseConnectionException;
30+
use phpMyFAQ\Environment;
2631
use Symfony\Component\Config\FileLocator;
2732
use Symfony\Component\DependencyInjection\ContainerBuilder;
2833
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
2934

30-
//
31-
// Define the named constant used as a check by any included PHP file
32-
//
33-
const IS_VALID_PHPMYFAQ = null;
34-
3535
//
3636
// Bootstrapping
3737
//
38-
require __DIR__ . '/src/Bootstrap.php';
39-
38+
try {
39+
require __DIR__ . '/src/Bootstrap.php';
40+
} catch (DatabaseConnectionException $exception) {
41+
$errorMessage = Environment::isDebugMode() ? $exception->getMessage() : null;
42+
$response = ErrorController::renderBootstrapError($errorMessage);
43+
$response->send();
44+
exit(1);
45+
}
4046

4147
//
4248
// Service Containers
@@ -47,8 +53,6 @@
4753
$loader->load('./src/services.php');
4854
} catch (Exception $exception) {
4955
echo sprintf('Error: %s at line %d at %s', $exception->getMessage(), $exception->getLine(), $exception->getFile());
50-
} catch(\Exception $e) {
51-
5256
}
5357

5458
$routes = include PMF_SRC_DIR . '/public-routes.php';

phpmyfaq/src/Bootstrap.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
use phpMyFAQ\Configuration\LdapConfiguration;
2727
use phpMyFAQ\Configuration\OpenSearchConfiguration;
2828
use phpMyFAQ\Core\Exception;
29+
use phpMyFAQ\Core\Exception\DatabaseConnectionException;
2930
use phpMyFAQ\Database;
3031
use phpMyFAQ\Environment;
3132
use Symfony\Component\HttpClient\HttpClient;
@@ -155,8 +156,11 @@
155156
$dbConfig->getPort(),
156157
);
157158
} catch (Exception $exception) {
158-
Database::errorPage($exception->getMessage());
159-
exit(-1);
159+
throw new DatabaseConnectionException(
160+
message: 'Database connection failed: ' . $exception->getMessage(),
161+
code: 500,
162+
previous: $exception
163+
);
160164
}
161165

162166
//
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
<?php
2+
3+
/**
4+
* Error Controller (500)
5+
*
6+
* This Source Code Form is subject to the terms of the Mozilla Public License,
7+
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
8+
* obtain one at https://mozilla.org/MPL/2.0/.
9+
*
10+
* @package phpMyFAQ
11+
* @author Thorsten Rinne <[email protected]>
12+
* @copyright 2026 phpMyFAQ Team
13+
* @license https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0
14+
* @link https://www.phpmyfaq.de
15+
* @since 2026-01-03
16+
*/
17+
18+
declare(strict_types=1);
19+
20+
namespace phpMyFAQ\Controller\Frontend;
21+
22+
use Exception;
23+
use phpMyFAQ\Translation;
24+
use Symfony\Component\HttpFoundation\Request;
25+
use Symfony\Component\HttpFoundation\Response;
26+
use Twig\Environment;
27+
use Twig\Loader\FilesystemLoader;
28+
29+
final class ErrorController extends AbstractFrontController
30+
{
31+
/**
32+
* Static fallback method for early bootstrap errors
33+
* Used when the system is not fully initialized yet
34+
*
35+
* @param string|null $errorMessage Optional error message to display
36+
* @throws Exception
37+
*/
38+
public static function renderBootstrapError(?string $errorMessage = null): Response
39+
{
40+
$loader = new FilesystemLoader(PMF_ROOT_DIR . '/assets/templates/error');
41+
$twig = new Environment($loader);
42+
$html = $twig->render('500.twig', [
43+
'errorMessage' => $errorMessage,
44+
]);
45+
46+
$response = new Response(content: $html, status: Response::HTTP_INTERNAL_SERVER_ERROR);
47+
$response->headers->set('Content-Type', 'text/html; charset=utf-8');
48+
49+
return $response;
50+
}
51+
52+
/**
53+
* Renders a 500 Internal Server Error page
54+
*
55+
* @param string|null $errorMessage Optional error message to display (shown only in debug mode)
56+
* @throws Exception
57+
*/
58+
public function internalServerError(Request $request, ?string $errorMessage = null): Response
59+
{
60+
try {
61+
$response = $this->render('500.twig', [
62+
...$this->getHeader($request),
63+
'title' => sprintf('%s - %s', Translation::get(key: 'msgError500'), $this->configuration->getTitle()),
64+
'errorMessage' => $errorMessage,
65+
]);
66+
} catch (Exception) {
67+
$response = $this->renderMinimalError($errorMessage);
68+
}
69+
70+
$response->setStatusCode(Response::HTTP_INTERNAL_SERVER_ERROR);
71+
72+
return $response;
73+
}
74+
75+
/**
76+
* Renders a minimal standalone error page without dependencies
77+
*
78+
* @param string|null $errorMessage Optional error message to display
79+
* @throws Exception
80+
*/
81+
private function renderMinimalError(?string $errorMessage = null): Response
82+
{
83+
$loader = new FilesystemLoader(PMF_ROOT_DIR . '/assets/templates/error');
84+
$twig = new Environment($loader);
85+
$html = $twig->render('500.twig', [
86+
'errorMessage' => $errorMessage,
87+
]);
88+
89+
$response = new Response(content: $html, status: Response::HTTP_INTERNAL_SERVER_ERROR);
90+
$response->headers->set('Content-Type', 'text/html; charset=utf-8');
91+
92+
return $response;
93+
}
94+
}

0 commit comments

Comments
 (0)