How to setup a 404 action/view in Slim 4?

Hello,

When requesting a route that does not exist, I get the Slim 404 error page. How can I setup my own action/view ? I do not find it in the documentation of Slim 4 :frowning:

Thx, Th.

You have at least 3 options (there are more).

In Slim 4 you can register an error render per content type, e.g. html/text or application/json:

You can also register a custom default error handler with full access to the response object:

The third option is to implement a custom error handler middleware (without adding the Slim error middleware to the stack).

Thanks. So Iā€™d like to write something like this:

class HttpErrorHandler extends SlimErrorHandler {

  protected $responder;

  public function __construct(
      Responder $responder,
      CallableResolverInterface $callableResolver,
      ResponseFactoryInterface $responseFactory,
      ?LoggerInterface $logger = null) {
    parent::__construct($callableResolver, $responseFactory, $logger);
    $this->responder = $responder;
  }
  
  protected function respond(): Response {
      $exception = $this->exception;
      $errorCode = $exception->getCode();
      $response = $this->responder->createResponse();
      return $this->responder->render($response, 'myerror.twig', ['errorCode' => $errorCode]);

But I can not inject Responder this way. I get an error: Fatal error: Uncaught TypeError: Argument 1 passed to App\Handlers\HttpErrorHandler::__construct() must be an instance of App\Utils\Responder, instance of Slim\CallableResolver given,

I guest there is something I still not understand about injection.

According to your example (with Twig) you should implement your own default error handler for the Slim error middleware.

Example:

$container = $app->getContainer();
$errorMiddleware = $app->addErrorMiddleware(true, true, true);
$errorMiddleware->setDefaultErrorHandler($container->get(DefaultErrorHandler::class));

Here is an example with Twig: DefaultErrorHandler

Thank for the help. It works now.

For some reasons, in case of 404 error, it does not go throught the middlewares, even if there are not route specified.

I wrote that code:

namespace App\Handlers;

use App\Utils\ExceptionDetail;
use App\Utils\Responder;

use DomainException;
use InvalidArgumentException;
use Selective\Validation\Exception\ValidationException;
use Slim\Exception\HttpException;
use Throwable;

use Psr\Log\LoggerInterface as Logger;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;

/**
 * Default Error Renderer.
 * Inspired by https://github.com/odan/slim4-skeleton/blob/master/src/Handler/DefaultErrorHandler.php
 */
class DefaultErrorHandler {

  const TEMPLATE = '/http_error.twig';

  /**
   * @var Responder
   */
  private $responder;
  
  /**
   * @var Logger
   */
  private $logger;


  /**
    * The constructor.
    *
    * @param Responder 
    * @param Logger
    */
  public function __construct(Responder $responder, Logger $logger) {
    $this->responder = $responder;
    $this->logger = $logger;
  }

  /**
    * Invoke.
    *
    * @param Request $request The request
    * @param Throwable $exception The exception
    * @param bool $displayErrorDetails Show error details
    * @param bool $logErrors Log errors
    *
    * @return ResponseInterface The response
    */
  public function __invoke(Request $request, Throwable $exception, bool $displayErrorDetails, bool $logErrors): Response {

    // Log error
    if ($logErrors) {
      $this->logger->error(sprintf(
        'Error: [%s] %s, Method: %s, Path: %s',
        $exception->getCode(),
        ExceptionDetail::getExceptionText($exception),
        $request->getMethod(),
        $request->getUri()->getPath()
      ));
    }

    // Detect status code
    $statusCode = $this->getHttpStatusCode($exception);

    // Error message
    $genericErrorMessage = $this->getErrorMessage($exception, $statusCode, $displayErrorDetails);

    // Render twig template
    $response = $this->responder->createResponse();
    return $this->responder->render(
        $response->withStatus($statusCode), 
        Self::TEMPLATE, 
        ['statusCode' => $statusCode, 'genericErrorMessage' => $genericErrorMessage]);
  }

  /**
    * Get http status code.
    *
    * @param Throwable $exception The exception
    *
    * @return int The http code
    */
  private function getHttpStatusCode(Throwable $exception): int {
    // Detect status code
    $statusCode = 500;

    if ($exception instanceof HttpException) {
      $statusCode = (int)$exception->getCode();
    }

    if ($exception instanceof DomainException || $exception instanceof InvalidArgumentException) {
      // Bad request
      $statusCode = 400;
    }

    if ($exception instanceof ValidationException) {
      // Unprocessable Entity
      $statusCode = 422;
    }


    $file = basename($exception->getFile());
    if ($file === 'CallableResolver.php') {
      $statusCode = 404;
    }

    return $statusCode;
  }

  /**
    * Get error message.
    *
    * @param Throwable $exception The error
    * @param int $statusCode The http status code
    * @param bool $displayErrorDetails Display details
    *
    * @return string The message
    */
  private function getErrorMessage(Throwable $exception, int $statusCode, bool $displayErrorDetails): string {
    // Default error message
    $errorMessage = '500 Internal Server Error';

    if ($statusCode === 403) {
      $errorMessage = '403 Access denied. The user does not have access to this section.';
    } elseif ($statusCode === 404) {
      $errorMessage = '404 Not Found';
    } elseif ($statusCode === 405) {
      $errorMessage = '405 Method Not Allowed';
    } elseif ($statusCode >= 400 && $statusCode <= 499) {
      $errorMessage = sprintf('%s Error', $statusCode);
    }

    if ($displayErrorDetails === true) {
      $errorMessage .= ' - Error details: ' . ExceptionDetail::getExceptionText($exception);
    }

    return $errorMessage;
  }
}

Thx, Th.