From 59a1eac506453791fcce2d987ad3a73f207522ce Mon Sep 17 00:00:00 2001 From: Andrey Pyzhikov <5071@mail.ru> Date: Thu, 18 Nov 2021 17:05:35 +0800 Subject: [PATCH 1/4] Custom exception handler --- system/CodeIgniter.php | 31 +++++++++++-------- .../CustomExceptionHandlerInterface.php | 23 ++++++++++++++ tests/_support/Controllers/Popcorn.php | 17 ++++++++++ tests/system/CodeIgniterTest.php | 23 ++++++++++++++ user_guide_src/source/general/errors.rst | 29 +++++++++++++++++ 5 files changed, 110 insertions(+), 13 deletions(-) create mode 100644 system/Exceptions/CustomExceptionHandlerInterface.php 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..a7c779c73c17 100644 --- a/tests/_support/Controllers/Popcorn.php +++ b/tests/_support/Controllers/Popcorn.php @@ -11,8 +11,13 @@ namespace Tests\Support\Controllers; +use Exception; use CodeIgniter\API\ResponseTrait; +use CodeIgniter\Config\Services; use CodeIgniter\Controller; +use CodeIgniter\Exceptions\CustomExceptionHandlerInterface; +use CodeIgniter\HTTP\RequestInterface; +use CodeIgniter\HTTP\ResponseInterface; 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..35804089fab3 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(); + $routes->add('exception', '\Tests\Support\Controllers\Popcorn::customException'); + + $router = Services::router($routes, Services::request()); + 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..85a0dc6c526b 100644 --- a/user_guide_src/source/general/errors.rst +++ b/user_guide_src/source/general/errors.rst @@ -131,3 +131,32 @@ 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 From 818003613d9e5088871c47d6c1e30371c19e9583 Mon Sep 17 00:00:00 2001 From: Andrey Pyzhikov <5071@mail.ru> Date: Thu, 18 Nov 2021 18:42:00 +0800 Subject: [PATCH 2/4] fixed. --- tests/_support/Controllers/Popcorn.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/_support/Controllers/Popcorn.php b/tests/_support/Controllers/Popcorn.php index a7c779c73c17..dd85c7cb7ef4 100644 --- a/tests/_support/Controllers/Popcorn.php +++ b/tests/_support/Controllers/Popcorn.php @@ -11,13 +11,13 @@ namespace Tests\Support\Controllers; -use Exception; 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; /** From 08c137ffb367b302f8f8db67c0f89c6e1ac3393b Mon Sep 17 00:00:00 2001 From: Andrey Pyzhikov <5071@mail.ru> Date: Fri, 19 Nov 2021 10:58:14 +0800 Subject: [PATCH 3/4] New instances --- tests/system/CodeIgniterTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/system/CodeIgniterTest.php b/tests/system/CodeIgniterTest.php index 35804089fab3..833fd8773988 100644 --- a/tests/system/CodeIgniterTest.php +++ b/tests/system/CodeIgniterTest.php @@ -432,10 +432,10 @@ public function testCustomExceptionHandler() $_SERVER['REQUEST_URI'] = '/exception'; // Inject mock router. - $routes = Services::routes(); + $routes = Services::routes(false); $routes->add('exception', '\Tests\Support\Controllers\Popcorn::customException'); - $router = Services::router($routes, Services::request()); + $router = Services::router($routes, Services::request(), false); Services::injectMock('router', $router); ob_start(); From ea007c2f116f172fec6614aa3a5dc192bb3b1277 Mon Sep 17 00:00:00 2001 From: Andrey Pyzhikov <5071@mail.ru> Date: Sat, 20 Nov 2021 11:22:06 +0800 Subject: [PATCH 4/4] doc --- user_guide_src/source/general/errors.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/user_guide_src/source/general/errors.rst b/user_guide_src/source/general/errors.rst index 85a0dc6c526b..9abe5269c652 100644 --- a/user_guide_src/source/general/errors.rst +++ b/user_guide_src/source/general/errors.rst @@ -134,13 +134,14 @@ redirect code to use instead of the default (``302``, "temporary redirect"):: 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;