diff --git a/app/Config/Exceptions.php b/app/Config/Exceptions.php
index 7cbc78a88a3a..83a588c1a387 100644
--- a/app/Config/Exceptions.php
+++ b/app/Config/Exceptions.php
@@ -3,6 +3,7 @@
namespace Config;
use CodeIgniter\Config\BaseConfig;
+use Throwable;
/**
* Setup how the exception handler works.
@@ -57,4 +58,29 @@ class Exceptions extends BaseConfig
* @var array
*/
public $sensitiveDataInTrace = [];
+
+ /**
+ * --------------------------------------------------------------------------
+ * DEFINE THE HANDLERS USED
+ * --------------------------------------------------------------------------
+ * Given the HTTP status code, returns exception handler that
+ * should be used to deal with this error. By default, it will run CodeIgniter's
+ * default handler and display the error information in the expected format
+ * for CLI, HTTP, or AJAX requests, as determined by is_cli() and the expected
+ * response format.
+ *
+ * Custom handlers can be returned if you want to handle one or more specific
+ * error codes yourself like:
+ *
+ * if (in_array($statusCode, [400, 404, 500])) {
+ * return new \App\Libraries\MyExceptionHandler();
+ * }
+ * if ($exception instanceOf PageNotFoundException) {
+ * return new \App\Libraries\MyExceptionHandler();
+ * }
+ */
+ public function handler(int $statusCode, Throwable $exception)
+ {
+ return new \CodeIgniter\Debug\ExceptionHandler();
+ }
}
diff --git a/phpstan-baseline.neon.dist b/phpstan-baseline.neon.dist
index 8d8834cb57dc..35041fba6b41 100644
--- a/phpstan-baseline.neon.dist
+++ b/phpstan-baseline.neon.dist
@@ -483,12 +483,12 @@ parameters:
-
message: "#^Expression on left side of \\?\\? is not nullable\\.$#"
count: 1
- path: system/Debug/Exceptions.php
+ path: system/Debug/BaseExceptionHandler.php
-
- message: "#^Property CodeIgniter\\\\Debug\\\\Exceptions\\:\\:\\$formatter \\(CodeIgniter\\\\Format\\\\FormatterInterface\\) in isset\\(\\) is not nullable\\.$#"
+ message: "#^Property CodeIgniter\\\\Debug\\\\BaseExceptionHandler\\:\\:\\$formatter \\(CodeIgniter\\\\Format\\\\FormatterInterface\\) in isset\\(\\) is not nullable\\.$#"
count: 1
- path: system/Debug/Exceptions.php
+ path: system/Debug/BaseExceptionHandler.php
-
message: "#^Property Config\\\\Exceptions\\:\\:\\$sensitiveDataInTrace \\(array\\) in isset\\(\\) is not nullable\\.$#"
diff --git a/system/Debug/BaseExceptionHandler.php b/system/Debug/BaseExceptionHandler.php
new file mode 100644
index 000000000000..f4c2201caa9d
--- /dev/null
+++ b/system/Debug/BaseExceptionHandler.php
@@ -0,0 +1,311 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\Debug;
+
+use CodeIgniter\API\ResponseTrait;
+use CodeIgniter\HTTP\RequestInterface;
+use CodeIgniter\HTTP\Response;
+use Throwable;
+
+/**
+ * Provides common functions for exception handlers,
+ * especially around displaying the output.
+ */
+abstract class BaseExceptionHandler
+{
+ use ResponseTrait;
+
+ /**
+ * Nesting level of the output buffering mechanism
+ */
+ protected int $obLevel;
+
+ protected int $statusCode;
+ protected Throwable $exception;
+ protected string $view;
+ protected RequestInterface $request;
+ protected Response $response;
+ protected string $viewPath;
+ protected int $exitCode;
+
+ public function __construct()
+ {
+ $this->obLevel = ob_get_level();
+ $this->viewPath = rtrim(config('Exceptions')->errorViewPath, '\\/ ') . DIRECTORY_SEPARATOR;
+ }
+
+ /**
+ * The main entry point into the handler.
+ *
+ * @return mixed
+ */
+ abstract public function handle();
+
+ /**
+ * Set the HTTP status code that is being thrown.
+ *
+ * @return $this
+ */
+ public function setStatusCode(int $statusCode)
+ {
+ $this->statusCode = $statusCode;
+
+ return $this;
+ }
+
+ /**
+ * Set the exception that was originally thrown.
+ *
+ * @return $this
+ */
+ public function setException(Throwable $exception)
+ {
+ $this->exception = $exception;
+
+ return $this;
+ }
+
+ /**
+ * Set the request used.
+ *
+ * @return $this
+ */
+ public function setRequest(RequestInterface $request)
+ {
+ $this->request = $request;
+
+ return $this;
+ }
+
+ /**
+ * Sets the response that will be used
+ *
+ * @return $this
+ */
+ public function setResponse(Response $response)
+ {
+ $this->response = $response;
+
+ return $this;
+ }
+
+ /**
+ * Sets the exit code that should be used when exiting the script.
+ *
+ * @return $this
+ */
+ public function setExitCode(int $code)
+ {
+ $this->exitCode = $code;
+
+ return $this;
+ }
+
+ /**
+ * Gathers the variables that will be made available to the view.
+ */
+ protected function collectVars(Throwable $exception, int $statusCode): array
+ {
+ $trace = $exception->getTrace();
+
+ if (config('Exceptions')->sensitiveDataInTrace !== []) {
+ $this->maskSensitiveData($trace, config('Exceptions')->sensitiveDataInTrace);
+ }
+
+ return [
+ 'title' => get_class($exception),
+ 'type' => get_class($exception),
+ 'code' => $statusCode,
+ 'message' => $exception->getMessage() ?? '(null)',
+ 'file' => $exception->getFile(),
+ 'line' => $exception->getLine(),
+ 'trace' => $trace,
+ ];
+ }
+
+ /**
+ * Mask sensitive data in the trace.
+ *
+ * @param array|object $trace
+ */
+ protected function maskSensitiveData(&$trace, array $keysToMask, string $path = '')
+ {
+ foreach ($keysToMask as $keyToMask) {
+ $explode = explode('/', $keyToMask);
+ $index = end($explode);
+
+ if (strpos(strrev($path . '/' . $index), strrev($keyToMask)) === 0) {
+ if (is_array($trace) && array_key_exists($index, $trace)) {
+ $trace[$index] = '******************';
+ } elseif (is_object($trace) && property_exists($trace, $index) && isset($trace->{$index})) {
+ $trace->{$index} = '******************';
+ }
+ }
+ }
+
+ if (is_object($trace)) {
+ $trace = get_object_vars($trace);
+ }
+
+ if (is_array($trace)) {
+ foreach ($trace as $pathKey => $subarray) {
+ $this->maskSensitiveData($subarray, $keysToMask, $path . '/' . $pathKey);
+ }
+ }
+ }
+
+ /**
+ * Describes memory usage in real-world units. Intended for use
+ * with memory_get_usage, etc.
+ */
+ protected static function describeMemory(int $bytes): string
+ {
+ if ($bytes < 1024) {
+ return $bytes . 'B';
+ }
+
+ if ($bytes < 1_048_576) {
+ return round($bytes / 1024, 2) . 'KB';
+ }
+
+ return round($bytes / 1_048_576, 2) . 'MB';
+ }
+
+ /**
+ * Creates a syntax-highlighted version of a PHP file.
+ *
+ * @return bool|string
+ */
+ protected static function highlightFile(string $file, int $lineNumber, int $lines = 15)
+ {
+ if (empty($file) || ! is_readable($file)) {
+ return false;
+ }
+
+ // Set our highlight colors:
+ if (function_exists('ini_set')) {
+ ini_set('highlight.comment', '#767a7e; font-style: italic');
+ ini_set('highlight.default', '#c7c7c7');
+ ini_set('highlight.html', '#06B');
+ ini_set('highlight.keyword', '#f1ce61;');
+ ini_set('highlight.string', '#869d6a');
+ }
+
+ try {
+ $source = file_get_contents($file);
+ } catch (Throwable $e) {
+ return false;
+ }
+
+ $source = str_replace(["\r\n", "\r"], "\n", $source);
+ $source = explode("\n", highlight_string($source, true));
+ $source = str_replace('
', "\n", $source[1]);
+ $source = explode("\n", str_replace("\r\n", "\n", $source));
+
+ // Get just the part to show
+ $start = max($lineNumber - (int) round($lines / 2), 0);
+
+ // Get just the lines we need to display, while keeping line numbers...
+ $source = array_splice($source, $start, $lines, true);
+
+ // Used to format the line number in the source
+ $format = '% ' . strlen((string) ($start + $lines)) . 'd';
+
+ $out = '';
+ // Because the highlighting may have an uneven number
+ // of open and close span tags on one line, we need
+ // to ensure we can close them all to get the lines
+ // showing correctly.
+ $spans = 1;
+
+ foreach ($source as $n => $row) {
+ $spans += substr_count($row, ']+>#', $row, $tags);
+
+ $out .= sprintf(
+ "{$format} %s\n%s",
+ $n + $start + 1,
+ strip_tags($row),
+ implode('', $tags[0])
+ );
+ } else {
+ $out .= sprintf('' . $format . ' %s', $n + $start + 1, $row) . "\n";
+ }
+ }
+
+ if ($spans > 0) {
+ $out .= str_repeat('', $spans);
+ }
+
+ return '' . $out . '
';
+ }
+
+ /**
+ * Given an exception and status code will display the error to the client.
+ *
+ * @param mixed|null $viewFile
+ */
+ protected function render($viewFile = null)
+ {
+ if (empty($viewFile) || ! is_file($viewFile)) {
+ echo 'The error view files were not found. Cannot render exception trace.';
+
+ exit(1);
+ }
+
+ if (ob_get_level() > $this->obLevel + 1) {
+ ob_end_clean();
+ }
+
+ $exception = $this->exception;
+ $statusCode = $this->statusCode;
+
+ echo(function () use ($exception, $statusCode, $viewFile): string {
+ $vars = $this->collectVars($exception, $statusCode);
+ extract($vars, EXTR_SKIP);
+
+ ob_start();
+ include $viewFile;
+
+ return ob_get_clean();
+ })();
+ }
+
+ /**
+ * This makes nicer looking paths for the error output.
+ */
+ public static function cleanPath(string $file): string
+ {
+ switch (true) {
+ case strpos($file, APPPATH) === 0:
+ $file = 'APPPATH' . DIRECTORY_SEPARATOR . substr($file, strlen(APPPATH));
+ break;
+
+ case strpos($file, SYSTEMPATH) === 0:
+ $file = 'SYSTEMPATH' . DIRECTORY_SEPARATOR . substr($file, strlen(SYSTEMPATH));
+ break;
+
+ case strpos($file, FCPATH) === 0:
+ $file = 'FCPATH' . DIRECTORY_SEPARATOR . substr($file, strlen(FCPATH));
+ break;
+
+ case defined('VENDORPATH') && strpos($file, VENDORPATH) === 0:
+ $file = 'VENDORPATH' . DIRECTORY_SEPARATOR . substr($file, strlen(VENDORPATH));
+ break;
+ }
+
+ return $file;
+ }
+}
diff --git a/system/Debug/ExceptionHandler.php b/system/Debug/ExceptionHandler.php
new file mode 100644
index 000000000000..923811c79afc
--- /dev/null
+++ b/system/Debug/ExceptionHandler.php
@@ -0,0 +1,93 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace CodeIgniter\Debug;
+
+use CodeIgniter\Exceptions\PageNotFoundException;
+use Config\Paths;
+use Throwable;
+
+class ExceptionHandler extends BaseExceptionHandler
+{
+ /**
+ * Determines the correct way to display the error.
+ *
+ * @return mixed|void
+ */
+ public function handle()
+ {
+ if (! is_cli()) {
+ $this->response->setStatusCode($this->statusCode);
+ header(sprintf('HTTP/%s %s %s', $this->request->getProtocolVersion(), $this->response->getStatusCode(), $this->response->getReasonPhrase()), true, $this->statusCode);
+
+ // Display a JSON/XML response if we weren't asked for HTML
+ if (strpos($this->request->getHeaderLine('accept'), 'text/html') === false) {
+ $this->respond(ENVIRONMENT === 'development' ? $this->collectVars($this->exception, $this->statusCode) : '', $this->statusCode)->send();
+
+ exit($this->exitCode);
+ }
+ }
+
+ // Determine possible directories of error views
+ $path = $this->viewPath;
+ $altPath = rtrim((new Paths())->viewDirectory, '\\/ ') . DIRECTORY_SEPARATOR . 'errors' . DIRECTORY_SEPARATOR;
+
+ $addPath = (is_cli() ? 'cli' : 'html') . DIRECTORY_SEPARATOR;
+ $path .= $addPath;
+ $altPath .= $addPath;
+
+ // Determine the views
+ $view = $this->determineView($this->exception, $path);
+ $altView = $this->determineView($this->exception, $altPath);
+
+ // Check if the view exists
+ $viewFile = null;
+ if (is_file($path . $view)) {
+ $viewFile = $path . $view;
+ } elseif (is_file($altPath . $altView)) {
+ $viewFile = $altPath . $altView;
+ }
+
+ // Displays the HTML or CLI error code.
+ $this->render($viewFile);
+
+ exit($this->exitCode);
+ }
+
+ /**
+ * Determines the view to display based on the exception thrown,
+ * whether an HTTP or CLI request, etc.
+ *
+ * @return string The path and filename of the view file to use
+ */
+ private function determineView(Throwable $exception, string $templatePath): string
+ {
+ // Production environments should have a custom exception file.
+ $view = 'production.php';
+ $templatePath = rtrim($templatePath, '\\/ ') . DIRECTORY_SEPARATOR;
+
+ if (str_ireplace(['off', 'none', 'no', 'false', 'null'], '', ini_get('display_errors'))) {
+ $view = 'error_exception.php';
+ }
+
+ // 404 Errors
+ if ($exception instanceof PageNotFoundException) {
+ return 'error_404.php';
+ }
+
+ // Allow for custom views based upon the status code
+ if (is_file($templatePath . 'error_' . $exception->getCode() . '.php')) {
+ return 'error_' . $exception->getCode() . '.php';
+ }
+
+ return $view;
+ }
+}
diff --git a/system/Debug/Exceptions.php b/system/Debug/Exceptions.php
index a65be7a2ad2f..0991272f3c06 100644
--- a/system/Debug/Exceptions.php
+++ b/system/Debug/Exceptions.php
@@ -11,12 +11,9 @@
namespace CodeIgniter\Debug;
-use CodeIgniter\API\ResponseTrait;
-use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\IncomingRequest;
use CodeIgniter\HTTP\Response;
use Config\Exceptions as ExceptionsConfig;
-use Config\Paths;
use ErrorException;
use Throwable;
@@ -25,23 +22,6 @@
*/
class Exceptions
{
- use ResponseTrait;
-
- /**
- * Nesting level of the output buffering mechanism
- *
- * @var int
- */
- public $ob_level;
-
- /**
- * The path to the directory containing the
- * cli and html error view directories.
- *
- * @var string
- */
- protected $viewPath;
-
/**
* Config for debug exceptions.
*
@@ -65,8 +45,6 @@ class Exceptions
public function __construct(ExceptionsConfig $config, IncomingRequest $request, Response $response)
{
- $this->ob_level = ob_get_level();
- $this->viewPath = rtrim($config->errorViewPath, '\\/ ') . DIRECTORY_SEPARATOR;
$this->config = $config;
$this->request = $request;
$this->response = $response;
@@ -107,20 +85,25 @@ public function exceptionHandler(Throwable $exception)
]);
}
- if (! is_cli()) {
- $this->response->setStatusCode($statusCode);
- header(sprintf('HTTP/%s %s %s', $this->request->getProtocolVersion(), $this->response->getStatusCode(), $this->response->getReasonPhrase()), true, $statusCode);
+ $config = config('Exceptions');
- if (strpos($this->request->getHeaderLine('accept'), 'text/html') === false) {
- $this->respond(ENVIRONMENT === 'development' ? $this->collectVars($exception, $statusCode) : '', $statusCode)->send();
-
- exit($exitCode);
- }
+ if (! method_exists($config, 'handler')) {
+ exit('Config\Exception must have a handler() method.');
}
- $this->render($exception, $statusCode);
+ $handler = config('Exceptions')->handler($statusCode, $exception);
- exit($exitCode);
+ if (! $handler instanceof BaseExceptionHandler) {
+ exit('Exception Handler not found.');
+ }
+
+ $handler
+ ->setException($exception)
+ ->setStatusCode($statusCode)
+ ->setExitCode($exitCode)
+ ->setRequest($this->request)
+ ->setResponse($this->response)
+ ->handle();
}
/**
@@ -164,132 +147,6 @@ public function shutdownHandler()
}
}
- /**
- * Determines the view to display based on the exception thrown,
- * whether an HTTP or CLI request, etc.
- *
- * @return string The path and filename of the view file to use
- */
- protected function determineView(Throwable $exception, string $templatePath): string
- {
- // Production environments should have a custom exception file.
- $view = 'production.php';
- $templatePath = rtrim($templatePath, '\\/ ') . DIRECTORY_SEPARATOR;
-
- if (str_ireplace(['off', 'none', 'no', 'false', 'null'], '', ini_get('display_errors'))) {
- $view = 'error_exception.php';
- }
-
- // 404 Errors
- if ($exception instanceof PageNotFoundException) {
- return 'error_404.php';
- }
-
- // Allow for custom views based upon the status code
- if (is_file($templatePath . 'error_' . $exception->getCode() . '.php')) {
- return 'error_' . $exception->getCode() . '.php';
- }
-
- return $view;
- }
-
- /**
- * Given an exception and status code will display the error to the client.
- */
- protected function render(Throwable $exception, int $statusCode)
- {
- // Determine possible directories of error views
- $path = $this->viewPath;
- $altPath = rtrim((new Paths())->viewDirectory, '\\/ ') . DIRECTORY_SEPARATOR . 'errors' . DIRECTORY_SEPARATOR;
-
- $path .= (is_cli() ? 'cli' : 'html') . DIRECTORY_SEPARATOR;
- $altPath .= (is_cli() ? 'cli' : 'html') . DIRECTORY_SEPARATOR;
-
- // Determine the views
- $view = $this->determineView($exception, $path);
- $altView = $this->determineView($exception, $altPath);
-
- // Check if the view exists
- if (is_file($path . $view)) {
- $viewFile = $path . $view;
- } elseif (is_file($altPath . $altView)) {
- $viewFile = $altPath . $altView;
- }
-
- if (! isset($viewFile)) {
- echo 'The error view files were not found. Cannot render exception trace.';
-
- exit(1);
- }
-
- if (ob_get_level() > $this->ob_level + 1) {
- ob_end_clean();
- }
-
- echo(function () use ($exception, $statusCode, $viewFile): string {
- $vars = $this->collectVars($exception, $statusCode);
- extract($vars, EXTR_SKIP);
-
- ob_start();
- include $viewFile;
-
- return ob_get_clean();
- })();
- }
-
- /**
- * Gathers the variables that will be made available to the view.
- */
- protected function collectVars(Throwable $exception, int $statusCode): array
- {
- $trace = $exception->getTrace();
-
- if ($this->config->sensitiveDataInTrace !== []) {
- $this->maskSensitiveData($trace, $this->config->sensitiveDataInTrace);
- }
-
- return [
- 'title' => get_class($exception),
- 'type' => get_class($exception),
- 'code' => $statusCode,
- 'message' => $exception->getMessage() ?? '(null)',
- 'file' => $exception->getFile(),
- 'line' => $exception->getLine(),
- 'trace' => $trace,
- ];
- }
-
- /**
- * Mask sensitive data in the trace.
- *
- * @param array|object $trace
- */
- protected function maskSensitiveData(&$trace, array $keysToMask, string $path = '')
- {
- foreach ($keysToMask as $keyToMask) {
- $explode = explode('/', $keyToMask);
- $index = end($explode);
-
- if (strpos(strrev($path . '/' . $index), strrev($keyToMask)) === 0) {
- if (is_array($trace) && array_key_exists($index, $trace)) {
- $trace[$index] = '******************';
- } elseif (is_object($trace) && property_exists($trace, $index) && isset($trace->{$index})) {
- $trace->{$index} = '******************';
- }
- }
- }
-
- if (is_object($trace)) {
- $trace = get_object_vars($trace);
- }
-
- if (is_array($trace)) {
- foreach ($trace as $pathKey => $subarray) {
- $this->maskSensitiveData($subarray, $keysToMask, $path . '/' . $pathKey);
- }
- }
- }
-
/**
* Determines the HTTP status code and the exit status code for this request.
*/
@@ -311,123 +168,4 @@ protected function determineCodes(Throwable $exception): array
return [$statusCode, $exitStatus];
}
-
- //--------------------------------------------------------------------
- // Display Methods
- //--------------------------------------------------------------------
-
- /**
- * This makes nicer looking paths for the error output.
- */
- public static function cleanPath(string $file): string
- {
- switch (true) {
- case strpos($file, APPPATH) === 0:
- $file = 'APPPATH' . DIRECTORY_SEPARATOR . substr($file, strlen(APPPATH));
- break;
-
- case strpos($file, SYSTEMPATH) === 0:
- $file = 'SYSTEMPATH' . DIRECTORY_SEPARATOR . substr($file, strlen(SYSTEMPATH));
- break;
-
- case strpos($file, FCPATH) === 0:
- $file = 'FCPATH' . DIRECTORY_SEPARATOR . substr($file, strlen(FCPATH));
- break;
-
- case defined('VENDORPATH') && strpos($file, VENDORPATH) === 0:
- $file = 'VENDORPATH' . DIRECTORY_SEPARATOR . substr($file, strlen(VENDORPATH));
- break;
- }
-
- return $file;
- }
-
- /**
- * Describes memory usage in real-world units. Intended for use
- * with memory_get_usage, etc.
- */
- public static function describeMemory(int $bytes): string
- {
- if ($bytes < 1024) {
- return $bytes . 'B';
- }
-
- if ($bytes < 1_048_576) {
- return round($bytes / 1024, 2) . 'KB';
- }
-
- return round($bytes / 1_048_576, 2) . 'MB';
- }
-
- /**
- * Creates a syntax-highlighted version of a PHP file.
- *
- * @return bool|string
- */
- public static function highlightFile(string $file, int $lineNumber, int $lines = 15)
- {
- if (empty($file) || ! is_readable($file)) {
- return false;
- }
-
- // Set our highlight colors:
- if (function_exists('ini_set')) {
- ini_set('highlight.comment', '#767a7e; font-style: italic');
- ini_set('highlight.default', '#c7c7c7');
- ini_set('highlight.html', '#06B');
- ini_set('highlight.keyword', '#f1ce61;');
- ini_set('highlight.string', '#869d6a');
- }
-
- try {
- $source = file_get_contents($file);
- } catch (Throwable $e) {
- return false;
- }
-
- $source = str_replace(["\r\n", "\r"], "\n", $source);
- $source = explode("\n", highlight_string($source, true));
- $source = str_replace('
', "\n", $source[1]);
- $source = explode("\n", str_replace("\r\n", "\n", $source));
-
- // Get just the part to show
- $start = max($lineNumber - (int) round($lines / 2), 0);
-
- // Get just the lines we need to display, while keeping line numbers...
- $source = array_splice($source, $start, $lines, true);
-
- // Used to format the line number in the source
- $format = '% ' . strlen((string) ($start + $lines)) . 'd';
-
- $out = '';
- // Because the highlighting may have an uneven number
- // of open and close span tags on one line, we need
- // to ensure we can close them all to get the lines
- // showing correctly.
- $spans = 1;
-
- foreach ($source as $n => $row) {
- $spans += substr_count($row, ']+>#', $row, $tags);
-
- $out .= sprintf(
- "{$format} %s\n%s",
- $n + $start + 1,
- strip_tags($row),
- implode('', $tags[0])
- );
- } else {
- $out .= sprintf('' . $format . ' %s', $n + $start + 1, $row) . "\n";
- }
- }
-
- if ($spans > 0) {
- $out .= str_repeat('', $spans);
- }
-
- return '' . $out . '
';
- }
}
diff --git a/system/Debug/Toolbar/Collectors/BaseCollector.php b/system/Debug/Toolbar/Collectors/BaseCollector.php
index 5d8d39ff169f..e550570b975f 100644
--- a/system/Debug/Toolbar/Collectors/BaseCollector.php
+++ b/system/Debug/Toolbar/Collectors/BaseCollector.php
@@ -11,7 +11,7 @@
namespace CodeIgniter\Debug\Toolbar\Collectors;
-use CodeIgniter\Debug\Exceptions;
+use CodeIgniter\Debug\BaseExceptionHandler;
/**
* Base Toolbar collector
@@ -180,7 +180,7 @@ public function display()
*/
public function cleanPath(string $file): string
{
- return Exceptions::cleanPath($file);
+ return BaseExceptionHandler::cleanPath($file);
}
/**
diff --git a/tests/system/Debug/ExceptionsTest.php b/tests/system/Debug/ExceptionsTest.php
index 08d521ede2f4..d50cac779353 100644
--- a/tests/system/Debug/ExceptionsTest.php
+++ b/tests/system/Debug/ExceptionsTest.php
@@ -26,15 +26,17 @@ final class ExceptionsTest extends CIUnitTestCase
use ReflectionHelper;
private \CodeIgniter\Debug\Exceptions $exception;
+ private ExceptionHandler $handler;
protected function setUp(): void
{
$this->exception = new Exceptions(new ExceptionsConfig(), Services::request(), Services::response());
+ $this->handler = new ExceptionHandler();
}
public function testDetermineViews(): void
{
- $determineView = $this->getPrivateMethodInvoker($this->exception, 'determineView');
+ $determineView = $this->getPrivateMethodInvoker($this->handler, 'determineView');
$this->assertSame('error_404.php', $determineView(PageNotFoundException::forControllerNotFound('Foo', 'bar'), ''));
$this->assertSame('error_exception.php', $determineView(new RuntimeException('Exception'), ''));
@@ -43,7 +45,7 @@ public function testDetermineViews(): void
public function testCollectVars(): void
{
- $vars = $this->getPrivateMethodInvoker($this->exception, 'collectVars')(new RuntimeException('This.'), 404);
+ $vars = $this->getPrivateMethodInvoker($this->handler, 'collectVars')(new RuntimeException('This.'), 404);
$this->assertIsArray($vars);
$this->assertCount(7, $vars);
@@ -67,7 +69,7 @@ public function testDetermineCodes(): void
*/
public function testCleanPaths(string $file, string $expected): void
{
- $this->assertSame($expected, Exceptions::cleanPath($file));
+ $this->assertSame($expected, BaseExceptionHandler::cleanPath($file));
}
public function dirtyPathsProvider()
diff --git a/user_guide_src/source/changelogs/v4.2.0.rst b/user_guide_src/source/changelogs/v4.2.0.rst
index 3d2a2223350f..c5dd448ec9ce 100644
--- a/user_guide_src/source/changelogs/v4.2.0.rst
+++ b/user_guide_src/source/changelogs/v4.2.0.rst
@@ -28,7 +28,8 @@ Enhancements
- Added Validation Strict Rules. See :ref:`validation-traditional-and-strict-rules`.
- Added new OCI8 driver for database.
- It can access Oracle Database and supports SQL and PL/SQL statements.
-- The ``spark routes`` command now shows closure routes, auto routtes, and filters. See :ref:`URI Routing `.
+- The ``spark routes`` command now shows closure routes, auto routes, and filters. See :ref:`URI Routing `.
+- ``Config\Exceptions`` requires a new ``handle()`` method to function. This allows for :ref:`custom exception handlers ` to be used.
Changes
*******
diff --git a/user_guide_src/source/general/errors.rst b/user_guide_src/source/general/errors.rst
index 921b1130ecf5..69967115a3cc 100644
--- a/user_guide_src/source/general/errors.rst
+++ b/user_guide_src/source/general/errors.rst
@@ -131,3 +131,68 @@ forcing a redirect to a specific route or URL::
redirect code to use instead of the default (``302``, "temporary redirect")::
throw new \CodeIgniter\Router\Exceptions\RedirectException($route, 301);
+
+.. _custom-exception-handlers:
+
+Custom Exception Handlers
+=========================
+
+If you need more control over how exceptions are displayed you can now define your own handlers and
+specify when they apply.
+
+Defining the New Handler
+------------------------
+
+The first step is to create a new class which must extend ``CodeIgniter\Debug\BaseExceptionHandler``.
+This class includes a number of utility methods that are used by the default exception handler.
+The new handler must implement a single method: ``handle()``::
+
+ use CodeIgniter\Debug\BaseExceptionHandler;
+
+ class MyExceptionHandler extends BaseExceptionHandler
+ {
+ // These are available and do not need to be specified
+ protected int $statusCode;
+ protected Throwable $exception;
+ protected RequestInterface $request;
+ protected ResponseInterface $response;
+ protected string $viewPath;
+ protected int $exitCode;
+
+ public function handle()
+ {
+ $this->render($this->viewPath ."errors/new_error_{$this->statusCode}.php");
+
+ exit($this->exitCode);
+ }
+ }
+
+This example defines the minimum amount of code typically needed - display a view and exit with the proper
+exit code. However, the ``BaseExceptionHandler`` provides a number of other helper functions and objects.
+
+Configuring the New Handler
+---------------------------
+
+Telling CodeIgniter to use your new exception handler class is done in the ``app/Config/Exceptions.php``
+configuration file's ``handler()`` method::
+
+ public function handler(int $statusCode, \Throwable $exception)
+ {
+ return new \CodeIgniter\Debug\ExceptionHandler();
+ }
+
+You can use any logic your application needs to determine whether it should handle the exception, but the
+two most common are checking on the HTTP status code or the type of exception. If your class should handle
+it then return a new instance of that class::
+
+ public function handler(int $statusCode, \Throwable $exception)
+ {
+ if (in_array($statusCode, [400, 404, 500])) {
+ return new \App\Libraries\MyExceptionHandler();
+ }
+ if ($exception instanceOf PageNotFoundException) {
+ return new \App\Libraries\MyExceptionHandler();
+ }
+
+ return new \CodeIgniter\Debug\ExceptionHandler();
+ }