-
-
Notifications
You must be signed in to change notification settings - Fork 6
Error Handling
PHP is an "exception-light" programming language, meaning that it doesn't throw exceptions for every little thing that goes wrong unlike other languages such as Python.
The chapter Errors and Exceptions from PHP The Right Way explains in great detail how PHP handles errors.
The following does not throw an exception but a warning:
Warning: Undefined variable $foo
.
"Hello world" is still printed.
<?php
echo $foo;
echo 'Hello world';
PHP categorizes errors into different levels of severity, with three primary types of
messages: errors, notices, and warnings.
Each type corresponds to a specific constant, namely E_ERROR
, E_NOTICE
, and E_WARNING
.
The list of all error constants can be found here.
Errors with the severity E_ERROR
are fatal and result in the termination of the script execution.
They occur when there is a critical error, for e.g. a syntax error or an undefined function.
Notices (E_NOTICE
) and warnings (E_WARNING
) don't halt the script execution.
They produce a message and continue processing.
Notices are advisory messages that occur when PHP encounters something that might be an error, and warnings are non-fatal errors.
Displaying error details to the user exposes information about the structure of the application. In development, it's useful to see the errors to debug the application, but in production, it would present a big security risk.
Error configuration is done in the php.ini
file on the webserver and
in the application itself if it implements an error handler.
If the error handler from the application picks up the error, the application configuration
overrides the php.ini
settings but if the error happens before the error handler is initialized,
the server configuration is used.
This means that it's important to configure the php.ini
file correctly even if the
application error handler is configured separately.
By default, (apache) errors are logged in the error.log
file on the webserver.
Here is a summary of the error configuration options for a PHP server:
- error_reporting: What levels of errors get triggered.
- display_errors: Whether to show triggered errors in script output.
- display_startup_errors: Whether to show errors that were triggered during PHP's startup sequence.
- log_errors: Whether to write triggered errors to a log file.
error_reporting
should always be set to E_ALL
except if certain error severities should be
ignored.
To show every possible error during development, the php.ini
file should be configured like this:
display_errors = On
display_startup_errors = On
error_reporting = E_ALL
log_errors = On
This will show the error details to the user either by the default PHP error message, the framework default error handler, the debugging tool (e.g. xdebug) or a custom error handler.
In production, no error detail should ever be made visible to the user.
File: php.ini
display_errors = Off
display_startup_errors = Off
error_reporting = E_ALL
log_errors = On
This will take into account the errors, log them and display a generic error page but not show the details.
The error configurations that are used by the Slim app are initialized in the
defaults.php
file and then modified in the
environment configuration files.
File: config/defaults.php
$settings['error'] = [
// Must be set to false in production
'display_error_details' => false,
// Whether to log errors or not
'log_errors' => true,
];
All errors, warnings and notices should be displayed in the browser while developing.
Therefore, display_error_details
should be true
in the development config file.
File: config/env/env.dev.php
// Display error details in browser and throw ErrorException for notices and warnings
$settings['error']['display_error_details'] = true;
Errors, warnings and notices should be logged, but details shouldn't be shown to the client in production.
File: config/env/env.prod.php
// Display generic error page without details
$settings['error']['display_error_details'] = false;
During testing, every notice and warning should bring the test to a halt, so
display_error_details
should be true
.
File: config/env/env.test.php
// Enable display_error_details for testing as this will throw an ErrorException for notices and warnings
$settings['error']['display_error_details'] = true;
The default way to handle errors in Slim is by adding the ErrorMiddleware
with the addErrorMiddleware
method or when a bit more control over the configuration is required,
the ErrorMiddleware
can be defined in the container and then added to the stack.
This is required to take the environment configuration into account.
The ErrorMiddleware
is instantiated in the
container
with the config values and
logger.
File: config/container.php
use Slim\Middleware\ErrorMiddleware;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
use Slim\App;
return [
// ...
ErrorMiddleware::class => function (ContainerInterface $container) {
$config = $container->get('settings')['error'];
$app = $container->get(App::class);
$errorMiddleware = new ErrorMiddleware(
$app->getCallableResolver(),
$app->getResponseFactory(),
(bool)$config['display_error_details'],
(bool)$config['log_errors'],
true, // log error details
$container->get(LoggerInterface::class)
);
return $errorMiddleware;
},
];
The ErrorMiddleware
must be added at the very end of the middleware stack.
File: config/middleware.php
use Slim\App;
use Slim\Middleware\ErrorMiddleware;
return function (App $app) {
// ...
// Last middleware in the stack
$app->add(ErrorMiddleware::class);
};
This is how to add the default error handling middleware to the Slim application.
This setup might be sufficient for some applications, but in my opinion, lacks some crucial
features to make the error handling more user-friendly and informative.
Thankfully, with Slim and PHP, it's straightforward to use a custom error handler.
The way errors are logged and displayed to the user can be customized with a custom error handler.
Symfony has a great error page that displays the error details in a clear and pretty format. Laravel uses the whoops, which also displays error details and code.
For my projects, I wanted an error handler that renders a lean error details page that looks clean and highlights the important files in an uncluttered stack trace.
I've extracted the error handling into a small library
slim-error-renderer
that can be used with Slim and provides a custom error handling middleware and renderer as well as a
middleware to promote notices and warnings to exceptions.
The documentation on how to configure middlewares can be found in the
README
of the package. It's not more complicated than the default error handling in Slim.
Below is a guide on the key elements of the slim-error-renderer
library which can be used to
create an own custom error handler or to understand how the error handling works in Slim.
The core of the
ExceptionHandlingMiddlware
is a try
catch
block that catches all exceptions and invokes the custom error handler.
It's essential that ExceptionHandlingMiddlware
processed first, because it should catch as
many errors as possible.
It is essential that it's added last in the stack because that means it will be the first one called
on a request due to the LIFO
order of execution.
Everything that happens before the process
function of ExceptionHandlingMiddlware
will not be caught
and handled by the custom error handler. In that case it falls back to the default error handler
from the web server.
The handleException
function is our custom error handler that logs the error and renders the error page.
File: vendor/samuelgfeller/slim-error-renderer/src/Middleware/ExceptionHandlingMiddleware.php
// ...
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
try {
return $handler->handle($request);
} catch (Throwable $exception) {
return $this->handleException($request, $exception);
}
}
// ...
The first thing the DefaultErrorHandler
does is log the error except if the exception is an
instance of ErrorException
because that would mean it's a warning / notice, and
it's already logged in the NonFatalErrorHandlerMiddleware
.
Then, it checks if the script is called via the command line. If it's the case, the error probably occurred during testing, and PHPUnit has its own error handler that is more suitable for the console as it supports syntax highlighting and a stack trace with hyperlinks. Therefore, the exception that's caught is thrown so that the PHPUnit error handler can handle it. The logger should be disabled for the entire application in the testing configuration, so the error isn't logged.
After that, the response is created and returned as JSON or rendered as an HTML page.
If the request is of type JSON, the error details are returned in the JSON format. Else, the error details are rendered in an HTML page.
When the configuration value display_error_details
is true
, the error details
(exact error message and stack trace) are rendered in the HTML template or included in the JSON response.
If $displayErrorDetails
is false
, a generic error page is displayed
or added to the JSON response containing only the status code
and reason phrase (e.g. 500 Internal Error or 404 Not found).
File: src/Application/ErrorHandler/DefaultErrorHandler.php
During development, notices and warnings should be addressed the same way as fatal errors.
The script should stop the execution, the error logged and an error details page with stack
trace displayed in the browser.
Symfony and Laravel are "exception-heavy" in debug mode for a long time already.
To archive this, notices and warnings are transformed into exceptions that can be caught by the error handling middleware.
The library slim-error-renderer
provides the NonFatalErrorHandlingMiddlware
that
throws an ErrorException
if display_error_details
is true
.
This middleware is also in charge of logging the non-fatal errors, as in production (when
display_error_details
is false
),
no exception is thrown and the error handling middleware is not called.
The PHP function set_error_handler
enables the custom handling of the non-fatal errors.
File: src/Application/Middleware/NonFatalErrorHandlerMiddleware.php
<?php
namespace App\Application\Middleware;
use ErrorException;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\Log\LoggerInterface;
final readonly class NonFatalErrorHandlerMiddleware implements MiddlewareInterface
{
private bool $displayErrorDetails;
private bool $logErrors;
public function __construct(bool $displayErrorDetails, bool $logErrors, private LoggerInterface $logger)
{
$this->displayErrorDetails = $displayErrorDetails;
$this->logErrors = $logErrors;
}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
// set_error_handler only handles non-fatal errors. The function callback is not called by fatal errors.
set_error_handler(
function ($severity, $message, $file, $line) {
// Don't throw exception if error reporting is turned off.
// '&' checks if a particular error level is included in the result of error_reporting().
if (error_reporting() & $severity) {
// Log non fatal errors if logging is enabled
if ($this->logErrors) {
// If error is warning
if ($severity === E_WARNING | E_CORE_WARNING | E_COMPILE_WARNING | E_USER_WARNING) {
$this->logger->warning("Warning [$severity] $message on line $line in file $file");
} // If error is non-fatal and is not a warning
else {
$this->logger->notice("Notice [$severity] $message on line $line in file $file");
}
}
if ($this->displayErrorDetails === true) {
// Throw ErrorException to stop script execution and have access to more error details
// Logging for fatal errors happens in DefaultErrorHandler.php
throw new ErrorException($message, 0, $severity, $file, $line);
}
}
return true;
}
);
$response = $handler->handle($request);
// Restore previous error handler in post-processing to satisfy PHPUnit 11 that checks for any
// leftover error handlers https://github.com/sebastianbergmann/phpunit/pull/5619
restore_error_handler();
return $response;
}
}
The NonFatalErrorHandlerMiddleware
is instantiated in the
DI-container
with the display_error_details
and log_errors
config values as well as the logger instance.
File: config/container.php
use App\Application\Middleware\NonFatalErrorHandlerMiddleware;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
return [
// ...
NonFatalErrorHandlerMiddleware::class => function (ContainerInterface $container) {
$config = $container->get('settings')['error'];
$logger = $container->get(LoggerInterface::class);
return new NonFatalErrorHandlerMiddleware(
(bool)$config['display_error_details'],
(bool)$config['log_errors'],
$logger,
);
},
];
This middleware is the second last in the middleware stack.
File: config/middleware.php
use Slim\App;
use App\Application\Middleware\NonFatalErrorHandlerMiddleware;
return function (App $app) {
// ...
$app->add(NonFatalErrorHandlerMiddleware::class);
// ErrorMiddleware is the only middleware that comes after the NonFatalErrorHandlerMiddleware
};
The template error/error-details.html.php
shows the error message
and stack trace in a responsive table.
The function html()
is used to escape
the values because we never know.
File: templates/error/error-details.html.php
<?php
/**
* @var string $basePath
* @var string $severityCssClassName 'error' fatal errors or 'warning' for notices and warnings
* @var int|null $statusCode http status code e.g. 404
* @var string|null $reasonPhrase http reason phrase e.g. 'Not Found'
* @var string|null $exceptionClassName e.g. 'HttpNotFoundException'
* @var string|null $exceptionMessage e.g. 'Page not found.'
* @var string|null $pathToMainErrorFile e.g. 'src\Application\Action\'
* @var string|null $mainErrorFile e.g. 'UserAction.php'
* @var int|null $errorLineNumber e.g. 123
* @var array $traceEntries contains keys 'args' (function arguments),
* 'nonVendorClass' (empty or 'non-vendor' to indicate that string should be highlighted),
* 'nonVendorFunctionCallClass' (empty or 'non-vendor' to indicate that string should be highlighted),
* 'classAndFunction' (class and function that was called in stack trace entry),
* 'fileName' (name of the file in the stack trace entry), 'line' (line number)
*/
// Remove layout if there was a default
$this->setLayout('');
?>
<!DOCTYPE html>
<html lang="en">
<head>
<base href="<?= $basePath ?>/"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="assets/error/error-details.css">
<title>Error</title>
</head>
<body class="<?= html($severityCssClassName) ?>">
<div id="title-div" class="<?= html($severityCssClassName) ?>">
<p><span><?= html($statusCode) ?> | <?= html($reasonPhrase) ?></span>
<span id="exception-name"><?= html($exceptionClassName) ?></span>
</p>
<h1><?= html($exceptionMessage) ?> in <span id="first-path-chunk"><?= html($pathToMainErrorFile) ?></span>
<?= html($mainErrorFile) ?>
on line <?= html($errorLineNumber) ?>.
</h1>
</div>
<div id="trace-div" class="<?= html($severityCssClassName) ?>">
<table>
<tr class="non-vendor">
<th id="num-th">#</th>
<th>Function</th>
<th>Location</th>
</tr>
<?php
foreach ($traceEntries as $key => $entry) { ?>
<tr>
<td class="<?= html($entry['nonVendorClass']) ?>"><?= html($key) ?></td>
<td class="function-td <?= html($entry['nonVendorFunctionCallClass']) ?>">
<?= html($entry['classAndFunction']) ?>(
<?php
foreach ($entry['args'] as $argument) { ?>
<span class="args-span" data-full-details="<?= html($argument['detailed']) ?>">
<?= html($argument['truncated']) ?></span>,
<?php
} ?>
)
</td>
<td class="stack-trace-file-name <?= html($entry['nonVendorClass']) ?>">
<?= html($entry['fileName']) ?>:<span class="lineSpan"><?= html($entry['line']) ?></span>
</td>
</tr>
<?php
}
?>
</table>
</div>
<script src="assets/error/error-details.js"></script>
</body>
</html>
File: public/assets/error/error-details.css
@font-face {
font-family: Poppins;
src: url(Poppins-Regular.ttf);
font-weight: normal;
}
/* mobile first min-width sets base and content is adapted to computers. */
@media (min-width: 100px) {
* {
overflow-wrap: anywhere;
}
body {
margin: 0;
background: #ffd9d0;
font-family: Poppins, Geneva, AppleGothic, sans-serif;
}
body.warning {
background: #ffead0;
}
body.error {
background: #ffd9d0;
}
#title-div {
padding: 5px 10%;
color: black;
background: tomato;
border-radius: 0 35px;
box-shadow: 0 0 17px tomato;
box-sizing: border-box;
margin: 30px 0;
font-size: 0.8em;
}
#title-div h1 {
margin-top: 4px;
}
#title-div.warning {
background: orange;
box-shadow: 0 0 17px orange;
}
#title-div.error {
background: tomato;
box-shadow: 0 0 17px tomato;
}
#first-path-chunk {
font-size: 0.7em;
}
#trace-div {
font-size: 0.8em;
margin: auto auto 40px;
min-width: 350px;
padding: 20px;
background: #ff9e88;
border-radius: 0 35px;
box-shadow: 0 0 10px #ff856e;
width: 90%;
}
#trace-div.warning {
background: #ffc588;
box-shadow: 0 0 10px #ffad6e;
}
#trace-div.error {
background: #ff9e88;
box-shadow: 0 0 10px #ff856e;
}
#trace-div h2 {
margin-top: 0;
padding-top: 19px;
text-align: center;
}
#trace-div table {
border-collapse: collapse;
width: 100%;
overflow-x: auto;
}
#trace-div table td, #trace-div table th { /*border-top: 6px solid red;*/
padding: 8px;
text-align: left;
}
#trace-div table tr td:nth-child(3) {
min-width: 100px;
}
#num-th {
font-size: 2em;
color: #a46856;
margin-right: 50px;
}
.non-vendor {
font-weight: bold;
font-size: 1.2em;
}
.non-vendor .lineSpan {
font-weight: bold;
color: #b00000;
font-size: 1.1em;
}
.is-vendor {
font-weight: normal;
}
.args-span {
color: #395186;
cursor: pointer;
}
#exception-name {
float: right
}
.function-td {
font-size: 0.9em;
}
}
@media (min-width: 641px) {
#trace-div {
width: 80%;
}
}
@media (min-width: 810px) {
#title-div {
margin: 30px;
}
#trace-div table tr td:first-child, #trace-div table tr th:first-child {
padding-left: 20px;
}
#title-div {
font-size: 1em;
}
}
@media (min-width: 1000px) {
#trace-div {
font-size: 1em;
}
}
The following script makes that the stack trace file names are camel-wrapped and that the full details of the function arguments are displayed when clicking on them.
File: public/assets/error/error-details.js
window.onload = function () {
// Camel-wrap all stack trace file names
let elements = document.querySelectorAll('.stack-trace-file-name');
elements.forEach(function (element) {
camelWrap(element);
});
// Show full details when clicking on an argument
// Select all spans with the class 'args-span'
var spans = document.querySelectorAll('.args-span');
// Add a click event listener to each span
spans.forEach(function (span) {
let spanExpanded = false;
let formatted;
span.addEventListener('click', function () {
// Get the full details from the data attribute
let fullDetails = this.getAttribute('data-full-details');
// Display the full details and store the formatted text
if (!spanExpanded) {
formatted = this.innerText;
span.innerText = fullDetails;
} else {
span.innerText = formatted;
}
spanExpanded = !spanExpanded;
});
});
}
/**
* This function is used to apply the camelWrapUnicode function to a given DOM node
* and then replace all zero-width spaces in the node's innerHTML with <wbr> elements.
* The <wbr> element represents a word break opportunity—a position within text where
* the browser may optionally break a line, though its line-breaking rules would not
* otherwise create a break at that location.
*
* @param {Node} node - The DOM node to which the camelWrapUnicode function should
* be applied and in whose innerHTML the zero-width spaces should be replaced
* with <wbr> elements.
*/
function camelWrap(node) {
camelWrapUnicode(node);
node.innerHTML = node.innerHTML.replace(/\u200B/g, "<wbr>");
}
/**
* This function is used to insert a zero-width space before each uppercase letter in
* a camelCase string.
* It does this by recursively traversing the DOM tree starting from the given node.
* For each text node it finds, it replaces the node's value with a new string where
* a zero-width space has been inserted before each uppercase letter.
*
* Source: http://heap.ch/blog/2016/01/19/camelwrap/
* @param {Node} node - The node from where to start the DOM traversal.
*/
function camelWrapUnicode(node) {
// Start from the first child of the given node and continue to the next sibling until there are no more siblings.
for (node = node.firstChild; node; node = node.nextSibling) {
// If the current node is a text node, replace its value.
if (node.nodeType === Node.TEXT_NODE) {
// Replace the node's value with a new string where a zero-width space has been inserted before each uppercase letter.
// This is done by first matching any word character or colon that is repeated 18 or more times, and for each match,
// a new string is returned where a zero-width space has been inserted before each uppercase letter.
// The same is done by matching any dot character, but without the repetition requirement.
node.nodeValue = node.nodeValue.replace(/[\w:]{18,}/g, function (str) {
return str.replace(/([a-z])([A-Z])/g, "$1\u200B$2");
});
} else {
// If the current node is not a text node, continue the traversal from this node.
camelWrapUnicode(node);
}
}
}
The error page for a production environment displays the status code, the reason phrase,
an error message depending on the status code and optionally a server message.
There is also a button to report the issue with a partially pre-written email and
another one to go back home.
The navigation bar stays accessible for the user to navigate.
The default layout is loaded to show the navigation bar, footer and potentially other elements.
File: templates/error/error-page.html.php
<?php
/**
* @var \Slim\Interfaces\RouteParserInterface $route
* @var array $errorMessage containing (int) statusCode; (string) reasonPhrase; (string) exceptionMessage
* @var string|null $statusCode e.g. 403
* @var string|null $reasonPhrase e.g. Forbidden
* @var string|null $exceptionMessage e.g. You are not allowed to access this page.
* @var \Slim\Views\PhpRenderer $this
* @var array $config public config values
*/
$this->setLayout('layout.html.php');
?>
<?php
// Define assets that should be included
$this->addAttribute('css', ['assets/error/prod-error-page.css']);
$this->addAttribute('js', ['assets/error/prod-error-page.js']);
?>
<section id="error-inner-section">
<h1 id="error-status-code"><?= html($statusCode) ?></h1>
<section id="error-description-section">
<?php
switch ($statusCode) {
case 404:
$title = 'Page not found';
$message = __("Looks like you've ventured into uncharted territory. Please report the issue!");
break;
case 403:
$title = 'Access forbidden';
$message = __(
'You are not allowed to access this page. Please report the issue if you think this is
an error.'
);
break;
case 400:
$title = 'The request is invalid';
$message = __('There is something wrong with the request syntax. Please report the issue.');
break;
case 422:
$title = 'Validation failed.';
$message = __(
'The server could not interpret the data it received. Please try again with valid data and
report the issue if it persists.'
);
break;
case 500:
$title = 'Internal Server Error.';
$message = __(
'It\'s not your fault! The server has an internal error. <br> Please try again and
report the issue if the problem persists.'
);
break;
default:
$title = 'An error occurred.';
$message = __(
'While it\'s unfortunate that an error exists, the silver lining is that it can be rectified!
<br>Please try again and then contact us.'
);
break;
}
$emailSubject = strip_tags(str_replace('"', '', $exceptionMessage))
?? $statusCode . ' ' . $title;
$emailBody = __('This is what I did before the error happened:');
?>
<h2 id="error-reason-phrase">OOPS! <?= html($title) ?></h2>
<p id="error-message"><?=
/* Not escape with html() because $message is safe as it is created above and has html tags */
$message ?></p>
<?= $exceptionMessage !== null ?
'<p id="server-message">Server message: ' . html($exceptionMessage) . '</p>' : '' ?>
</section>
<section id="error-btn-section">
<a href="<?= $route?->urlFor('home-page') ?>" class="btn"><?= __('Go back home') ?></a>
<a href="mailto:<?= ($config['email']['main_contact_address'] ?? 'contact@samuel-gfeller.ch')
. '?subject=' . html($emailSubject) . '&body=' . html($emailBody) ?>" target="_blank" class="btn">
<?= __('Report the issue') ?></a>
</section>
</section>
@font-face {
font-family: Poppins;
src: url(Poppins-Bold.ttf);
font-weight: 700;
}
@font-face {
font-family: Poppins;
src: url(Poppins-Regular.ttf);
}
:root {
--error-body-gradient-color-1: #49d2ff;
--error-body-gradient-color-2: #ea9bc2;
--error-inner-section-background: rgba(255, 255, 255, 0.4);
--error-reason-phrase-color: #535353;
--error-status-code-gradient-color-1: #00c1ff;
--error-status-code-gradient-color-2: #ff6bb4;
}
[data-theme="dark"] {
--error-body-gradient-color-1: rgb(102, 93, 182);
--error-body-gradient-color-2: rgb(64, 148, 157);
--error-inner-section-background: rgba(0, 0, 0, 0.4);
--error-reason-phrase-color: #a9a9a9;
}
@media (min-width: 100px) {
body {
background: linear-gradient(to bottom right, var(--error-body-gradient-color-1) 0%, var(--error-body-gradient-color-2) 100%);
}
main {
display: flex;
align-items: center;
justify-content: center;
/*background: lightblue;*/
margin-left: 0;
margin-top: 0;
border-radius: 0 0 0 0;
background: transparent;
}
footer {
background: var(--error-inner-section-background);
margin-top: 0;
/*border-radius: 0;*/
}
#error-inner-section {
width: fit-content;
max-width: 92%;
height: fit-content;
padding: 40px 30px;
/*border: 1px solid #ccc;*/
/*margin-left: 50px;*/
text-align: center;
border-radius: 30px;
background: var(--error-inner-section-background);
/*backdrop-filter: blur(50px);*/
box-shadow: 0 5px 10px rgba(0, 0, 0, 0.1);
}
/* #error-status-code */
#error-inner-section h1 {
font-size: clamp(100px, 18vw, 200px);
font-family: Poppins, Helvetica, sans-serif;
line-height: 1em;
margin-bottom: 0;
margin-top: 0px;
position: relative;
background: linear-gradient(to bottom right, var(--error-status-code-gradient-color-1) 0%,
var(--error-status-code-gradient-color-2) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
#error-inner-section h2 {
text-transform: uppercase;
font-family: Poppins, Helvetica, sans-serif;
color: var(--error-reason-phrase-color);
/*font-family: SF-Pro Display, Helvetica, sans-serif;*/
}
#error-inner-section p {
font-family: Poppins, Helvetica, sans-serif;
font-weight: 400;
font-size: 1.2em;
}
p#server-message {
font-size: 1em;
}
#error-btn-section {
margin-top: 30px;
display: flex;
flex-wrap: wrap;
justify-content: space-evenly;
}
#error-btn-section .btn {
background: rgba(255, 255, 255, 0.4);
margin: 10px;
}
}
@media (min-width: 641px) {
#error-inner-section {
padding: 40px 50px;
max-width: 80%;
}
}
@media (min-width: 961px) {
#error-inner-section {
padding: 40px 100px;
}
}
In an effort to make the error page a little less boring, the linear gradient direction of the status code follows the cursor.
File: public/assets/error/prod-error-page.js
const statusCode = document.getElementById('error-status-code');
// Make the linear gradient direction of the status code follow the cursor
document.documentElement.addEventListener("mousemove", function (event) {
// Retrieve the bounding rectangle of the "statusCode" element
const {left, top, width, height} = statusCode.getBoundingClientRect();
// Calculate the center coordinates of the "statusCode" element
const centerX = left + width / 2;
const centerY = top + height / 2;
// Calculate the angle (in radians) between the cursor position and the center of the element
const radians = Math.atan2(event.clientY - centerY, event.clientX - centerX);
// Convert the angle from radians to degrees
const degrees = radians * (180 / Math.PI);
// Add 90 degrees to shift the range from [-180, 180] to [0, 360] degrees
const gradientDirection = degrees + 90;
// Apply the linear gradient background to the "statusCode" element
const style = getComputedStyle(document.body);
const color1 = style.getPropertyValue('--error-status-code-gradient-color-1');
const color2 = style.getPropertyValue('--error-status-code-gradient-color-2');
setTimeout(() => {
statusCode.style.backgroundImage = `linear-gradient(${gradientDirection}deg, ${color1} 0%, ${color2} 100%)`;
}, 300
)
});
Slim app basics
- Composer
- Web Server config and Bootstrapping
- Dependency Injection
- Configuration
- Routing
- Middleware
- Architecture
- Single Responsibility Principle
- Action
- Domain
- Repository and Query Builder
Features
- Logging
- Validation
- Session and Flash
- Authentication
- Authorization
- Translations
- Mailing
- Console commands
- Database migrations
- Error handling
- Security
- API endpoint
- GitHub Actions
- Scrutinizer
- Coding standards fixer
- PHPStan static code analysis
Testing
Frontend
Other