diff --git a/system/CodeIgniter.php b/system/CodeIgniter.php index a8ebeef1b5cf..1a5469213937 100644 --- a/system/CodeIgniter.php +++ b/system/CodeIgniter.php @@ -14,6 +14,7 @@ use Closure; use CodeIgniter\Debug\Timer; use CodeIgniter\Events\Events; +use CodeIgniter\Exceptions\CustomExceptionHandlerInterface; use CodeIgniter\Exceptions\FrameworkException; use CodeIgniter\Exceptions\PageNotFoundException; use CodeIgniter\HTTP\CLIRequest; @@ -394,23 +395,27 @@ protected function handleRequest(?RouteCollectionInterface $routes, Cache $cache } } - $returned = $this->startController(); + try { + $returned = $this->startController(); - // Closure controller has run in startController(). - if (! is_callable($this->controller)) { - $controller = $this->createController(); + // Closure controller has run in startController(). + if (! is_callable($this->controller)) { + $controller = $this->createController(); - if (! method_exists($controller, '_remap') && ! is_callable([$controller, $this->method], false)) { - throw PageNotFoundException::forMethodNotFound($this->method); - } + if (! method_exists($controller, '_remap') && ! is_callable([$controller, $this->method], false)) { + throw PageNotFoundException::forMethodNotFound($this->method); + } - // Is there a "post_controller_constructor" event? - Events::trigger('post_controller_constructor'); + // Is there a "post_controller_constructor" event? + Events::trigger('post_controller_constructor'); - $returned = $this->runController($controller); - } else { - $this->benchmark->stop('controller_constructor'); - $this->benchmark->stop('controller'); + $returned = $this->runController($controller); + } else { + $this->benchmark->stop('controller_constructor'); + $this->benchmark->stop('controller'); + } + } catch (CustomExceptionHandlerInterface $e) { + $returned = $e->renderResponse($this->request); } // If $returned is a string, then the controller output something, diff --git a/system/Exceptions/CustomExceptionHandlerInterface.php b/system/Exceptions/CustomExceptionHandlerInterface.php new file mode 100644 index 000000000000..50cbd9e8779a --- /dev/null +++ b/system/Exceptions/CustomExceptionHandlerInterface.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Exceptions; + +use CodeIgniter\HTTP\RequestInterface; +use CodeIgniter\HTTP\ResponseInterface; + +/** + * Interface for implementing a global custom exception handler + */ +interface CustomExceptionHandlerInterface +{ + public function renderResponse(RequestInterface $request): ResponseInterface; +} diff --git a/tests/_support/Controllers/Popcorn.php b/tests/_support/Controllers/Popcorn.php index 71a316685a32..dd85c7cb7ef4 100644 --- a/tests/_support/Controllers/Popcorn.php +++ b/tests/_support/Controllers/Popcorn.php @@ -12,7 +12,12 @@ namespace Tests\Support\Controllers; use CodeIgniter\API\ResponseTrait; +use CodeIgniter\Config\Services; use CodeIgniter\Controller; +use CodeIgniter\Exceptions\CustomExceptionHandlerInterface; +use CodeIgniter\HTTP\RequestInterface; +use CodeIgniter\HTTP\ResponseInterface; +use Exception; use RuntimeException; /** @@ -89,4 +94,16 @@ public function echoJson() { return $this->response->setJSON($this->request->getJSON()); } + + public function customException() + { + throw new class () extends Exception implements CustomExceptionHandlerInterface { + public function renderResponse(RequestInterface $request): ResponseInterface + { + return Services::response() + ->setStatusCode(400) + ->setBody('an exception thrown'); + } + }; + } } diff --git a/tests/system/CodeIgniterTest.php b/tests/system/CodeIgniterTest.php index f7838105af4d..833fd8773988 100644 --- a/tests/system/CodeIgniterTest.php +++ b/tests/system/CodeIgniterTest.php @@ -12,6 +12,7 @@ namespace CodeIgniter; use CodeIgniter\Config\Services; +use CodeIgniter\HTTP\ResponseInterface; use CodeIgniter\Router\RouteCollection; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\Mock\MockCodeIgniter; @@ -425,4 +426,26 @@ public function testRunDefaultRoute() $this->assertStringContainsString('Welcome to CodeIgniter', $output); } + + public function testCustomExceptionHandler() + { + $_SERVER['REQUEST_URI'] = '/exception'; + + // Inject mock router. + $routes = Services::routes(false); + $routes->add('exception', '\Tests\Support\Controllers\Popcorn::customException'); + + $router = Services::router($routes, Services::request(), false); + Services::injectMock('router', $router); + + ob_start(); + $this->codeigniter->useSafeOutput(true)->run(); + ob_get_clean(); + + /** @var ResponseInterface $response */ + $response = $this->getPrivateProperty($this->codeigniter, 'response'); + + $this->assertSame('an exception thrown', $response->getBody()); + $this->assertSame(400, $response->getStatusCode()); + } } diff --git a/user_guide_src/source/general/errors.rst b/user_guide_src/source/general/errors.rst index b7863b03010d..9abe5269c652 100644 --- a/user_guide_src/source/general/errors.rst +++ b/user_guide_src/source/general/errors.rst @@ -131,3 +131,33 @@ 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 Handler +======================== + +You can use your own exception handler which will be called globally. + +Your exception must implement ``\CodeIgniter\Exception\CustomExceptionHandlerInterface``:: + + namespace App\Exceptions; + + use App\Config\Services; + use CodeIgniter\Exception\CustomExceptionHandlerInterface + use Exception; + + class MyException extends Exception implements CustomExceptionHandlerInterface + { + public function renderResponse(RequestInterface $request): ResponseInterface + { + return Services::response()->setBody($this->getMessage()); + } + } + +Now, if an exception is thrown in any part of the application, it will be handled.:: + + if (someCondition) { + throw new MyException('Something wrong') + } + +.. note:: Of course, if not caught by the try..catch block defined earlier. \ No newline at end of file