From 895c084ce40242de7fb63456c29fc247185e6594 Mon Sep 17 00:00:00 2001 From: Filis Futsarov Date: Tue, 22 Apr 2025 21:02:34 +0200 Subject: [PATCH 1/3] update --- .gitattributes | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitattributes b/.gitattributes index b67c7ca..12e8e4b 100644 --- a/.gitattributes +++ b/.gitattributes @@ -5,6 +5,6 @@ .gitattributes export-ignore .gitignore export-ignore .php-cs-fixer.php export-ignore -phpcs.xml.dist export-ignore -phpunit.xml.dist export-ignore +phpcs.xml export-ignore +phpunit.xml export-ignore .phpstan.neon export-ignore From a3b921440a1eb12f8298e439cb7fb186142fc0ed Mon Sep 17 00:00:00 2001 From: Filis Futsarov Date: Wed, 23 Apr 2025 00:10:12 +0200 Subject: [PATCH 2/3] add support for PSR-3 logger --- CHANGELOG.md | 51 ++++++++++++++++++++++++++++++ README.md | 43 +++++++++++++++++++++++++- composer.json | 5 +-- src/ErrorHandler.php | 33 +++++++++++++++++++- tests/ErrorHandlerTest.php | 63 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 191 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e174c4e..bd68b7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,56 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [3.2.0] - 2025-04-22 +### Added +- Support for [PSR-3 Logger](https://github.com/php-fig/log). Example using [Monolog](https://github.com/Seldaek/monolog): + +With default logging: +```php +use Monolog\Level; +use Monolog\Logger; +use Monolog\Handler\StreamHandler; + +$logger = new Logger('app'); +$logger->pushHandler(new StreamHandler('path/to/your.log', Level::Warning)); + +$response = Dispatcher::run([ + new ErrorHandler(null, $logger), + function ($request) { + throw new Exception('Something went wrong'); + }, +]); + +``` +With a custom log callback: +```php +use Monolog\Level; +use Monolog\Logger; +use Monolog\Handler\StreamHandler; + +$logger = new Logger('app'); +$logger->pushHandler(new StreamHandler('path/to/your.log', Level::Warning)); + +$response = Dispatcher::run([ + (new ErrorHandler(null, $logger)) + ->logCallback(function ( + LoggerInterface $logger, + Throwable $error, + ServerRequestInterface $request + ): void { + $logger->critical('Uncaught exception', [ + 'message' => $error->getMessage(), + 'request' => [ + 'uri' => $request->getUri()->getPath(), + ] + ]); + }), + function ($request) { + throw new Exception('Something went wrong'); + }, +]); +``` + ## [3.1.0] - 2025-03-21 ### Fixed - Support for PHP typing. @@ -117,6 +167,7 @@ First version [#9]: https://github.com/middlewares/error-handler/issues/9 +[3.2.0]: https://github.com/middlewares/error-handler/compare/v3.1.0...v3.2.0 [3.1.0]: https://github.com/middlewares/error-handler/compare/v3.0.2...v3.1.0 [3.0.2]: https://github.com/middlewares/error-handler/compare/v3.0.1...v3.0.2 [3.0.1]: https://github.com/middlewares/error-handler/compare/v3.0.0...v3.0.1 diff --git a/README.md b/README.md index 59350d8..c69e4c5 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,9 @@ composer require middlewares/error-handler use Middlewares\ErrorFormatter; use Middlewares\ErrorHandler; use Middlewares\Utils\Dispatcher; +use Monolog\Level; +use Monolog\Logger; +use Monolog\Handler\StreamHandler; // Create a new ErrorHandler instance // Any number of formatters can be added. One will be picked based on the Accept @@ -41,6 +44,10 @@ $errorHandler = new ErrorHandler([ new ErrorFormatter\XmlFormatter(), ]); +// Create logger (optional) +$logger = new Logger('app'); +$logger->pushHandler(new StreamHandler('path/to/your.log', Level::Warning)); + // ErrorHandler should always be the first middleware in the stack! $dispatcher = new Dispatcher([ $errorHandler, @@ -48,7 +55,7 @@ $dispatcher = new Dispatcher([ function ($request) { throw HttpErrorException::create(404); } -]); +], $logger); $request = $serverRequestFactory->createServerRequest('GET', '/'); $response = $dispatcher->dispatch($request); @@ -67,6 +74,40 @@ $errorHandler = new ErrorHandler([ **Note:** If no formatter is found, the first value of the array will be used. In the example above, `HtmlFormatter`. +### How to setup a custom log callback + +This allows you to fully customize how you log (level, message, context, etc.) with access to values such as `Throwable` and `ServerRequestInterface` instances. + +Example using Monolog: + +```php +use Monolog\Level; +use Monolog\Logger; +use Monolog\Handler\StreamHandler; + +$logger = new Logger('app'); +$logger->pushHandler(new StreamHandler('path/to/your.log', Level::Warning)); + +$response = Dispatcher::run([ + (new ErrorHandler(null, $logger)) + ->logCallback(function ( + LoggerInterface $logger, + Throwable $error, + ServerRequestInterface $request + ): void { + $logger->critical('Uncaught exception', [ + 'message' => $error->getMessage(), + 'request' => [ + 'uri' => $request->getUri()->getPath(), + ] + ]); + }), + function ($request) { + throw new Exception('Something went wrong'); + }, +]); +``` + ### How to use a custom response for Production ```php diff --git a/composer.json b/composer.json index b8d4dfd..5c6f6db 100644 --- a/composer.json +++ b/composer.json @@ -30,7 +30,8 @@ "squizlabs/php_codesniffer": "^3", "oscarotero/php-cs-fixer-config": "^2", "phpstan/phpstan": "^1 || ^2", - "laminas/laminas-diactoros": "^2 || ^3" + "laminas/laminas-diactoros": "^2 || ^3", + "filisko/fake-psr3-logger": "^1.0" }, "autoload": { "psr-4": { @@ -50,4 +51,4 @@ "coverage": "phpunit --coverage-text", "coverage-html": "phpunit --coverage-html=coverage" } -} \ No newline at end of file +} diff --git a/src/ErrorHandler.php b/src/ErrorHandler.php index 8d6afd4..2c016e9 100644 --- a/src/ErrorHandler.php +++ b/src/ErrorHandler.php @@ -14,6 +14,7 @@ use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; +use Psr\Log\LoggerInterface; use Throwable; class ErrorHandler implements MiddlewareInterface @@ -21,13 +22,21 @@ class ErrorHandler implements MiddlewareInterface /** @var FormatterInterface[] */ private $formatters = []; + /** @var LoggerInterface|null */ + private $logger = null; + + /** @var callable|null */ + private $logCallback = null; + /** * Configure the error formatters * * @param FormatterInterface[] $formatters */ - public function __construct(?array $formatters = null) + public function __construct(?array $formatters = null, ?LoggerInterface $logger = null) { + $this->logger = $logger; + if (empty($formatters)) { $formatters = [ new PlainFormatter(), @@ -54,11 +63,33 @@ public function addFormatters(FormatterInterface ...$formatters): self return $this; } + /** + * @param callable $callback + */ + public function logCallback(callable $callback): self + { + $this->logCallback = $callback; + + return $this; + } + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { try { return $handler->handle($request); } catch (Throwable $error) { + if ($this->logger) { + if ($this->logCallback) { + ($this->logCallback)($this->logger, $error, $request); + } else { + $this->logger->critical('Uncaught exception', [ + 'message' => $error->getMessage(), + 'file' => $error->getFile(), + 'line' => $error->getLine(), + ]); + } + } + foreach ($this->formatters as $formatter) { if ($formatter->isValid($error, $request)) { return $formatter->handle($error, $request); diff --git a/tests/ErrorHandlerTest.php b/tests/ErrorHandlerTest.php index d250385..8a6aadf 100644 --- a/tests/ErrorHandlerTest.php +++ b/tests/ErrorHandlerTest.php @@ -4,6 +4,7 @@ namespace Middlewares\Tests; use Exception; +use Filisko\FakeLogger; use Middlewares\ErrorFormatter\HtmlFormatter; use Middlewares\ErrorFormatter\ImageFormatter; use Middlewares\ErrorFormatter\JsonFormatter; @@ -15,6 +16,9 @@ use Middlewares\Utils\Factory; use Middlewares\Utils\HttpErrorException; use PHPUnit\Framework\TestCase; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Log\LoggerInterface; +use Throwable; class ErrorHandlerTest extends TestCase { @@ -61,6 +65,65 @@ public function getStatusCode(): int $this->assertEquals(418, $response->getStatusCode()); } + public function testLoggerException(): void + { + $logger = new FakeLogger(); + + $response = Dispatcher::run([ + new ErrorHandler(null, $logger), + function ($request) { + throw new Exception('Something went wrong'); + }, + ]); + + $this->assertEquals(500, $response->getStatusCode()); + $this->assertEquals([[ + 'level' => 'critical', + 'message' => 'Uncaught exception', + 'context' => [ + 'message' => 'Something went wrong', + 'file' => __FILE__, + 'line' => 75, + ] + ]], $logger->logs()); + } + + public function testLoggerExceptionWithCustomCallback(): void + { + $logger = new FakeLogger(); + + $response = Dispatcher::run([ + (new ErrorHandler(null, $logger)) + ->logCallback(function ( + LoggerInterface $logger, + Throwable $error, + ServerRequestInterface $request + ): void { + $logger->critical('Uncaught exception', [ + 'message' => $error->getMessage(), + 'request' => [ + 'uri' => $request->getUri()->getPath(), + ] + ]); + }), + function ($request) { + throw new Exception('Something went wrong'); + }, + ]); + + $this->assertEquals(500, $response->getStatusCode()); + $this->assertEquals([[ + 'level' => 'critical', + 'message' => 'Uncaught exception', + 'context' => [ + 'message' => 'Something went wrong', + 'request' => [ + 'uri' => '/', + ] + ] + ]], $logger->logs()); + } + public function testGifFormatter(): void { $request = Factory::createServerRequest('GET', '/'); From 5f8a29ba68c594abf9649362f5ad29498f8e1ba5 Mon Sep 17 00:00:00 2001 From: Filis Futsarov Date: Wed, 23 Apr 2025 00:10:12 +0200 Subject: [PATCH 3/3] add support for PSR-3 logger --- CHANGELOG.md | 51 ++++++++++++++++++++++++++++++ README.md | 43 +++++++++++++++++++++++++- composer.json | 8 +++-- src/ErrorHandler.php | 33 +++++++++++++++++++- tests/ErrorHandlerTest.php | 63 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 193 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e174c4e..bd68b7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,56 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [3.2.0] - 2025-04-22 +### Added +- Support for [PSR-3 Logger](https://github.com/php-fig/log). Example using [Monolog](https://github.com/Seldaek/monolog): + +With default logging: +```php +use Monolog\Level; +use Monolog\Logger; +use Monolog\Handler\StreamHandler; + +$logger = new Logger('app'); +$logger->pushHandler(new StreamHandler('path/to/your.log', Level::Warning)); + +$response = Dispatcher::run([ + new ErrorHandler(null, $logger), + function ($request) { + throw new Exception('Something went wrong'); + }, +]); + +``` +With a custom log callback: +```php +use Monolog\Level; +use Monolog\Logger; +use Monolog\Handler\StreamHandler; + +$logger = new Logger('app'); +$logger->pushHandler(new StreamHandler('path/to/your.log', Level::Warning)); + +$response = Dispatcher::run([ + (new ErrorHandler(null, $logger)) + ->logCallback(function ( + LoggerInterface $logger, + Throwable $error, + ServerRequestInterface $request + ): void { + $logger->critical('Uncaught exception', [ + 'message' => $error->getMessage(), + 'request' => [ + 'uri' => $request->getUri()->getPath(), + ] + ]); + }), + function ($request) { + throw new Exception('Something went wrong'); + }, +]); +``` + ## [3.1.0] - 2025-03-21 ### Fixed - Support for PHP typing. @@ -117,6 +167,7 @@ First version [#9]: https://github.com/middlewares/error-handler/issues/9 +[3.2.0]: https://github.com/middlewares/error-handler/compare/v3.1.0...v3.2.0 [3.1.0]: https://github.com/middlewares/error-handler/compare/v3.0.2...v3.1.0 [3.0.2]: https://github.com/middlewares/error-handler/compare/v3.0.1...v3.0.2 [3.0.1]: https://github.com/middlewares/error-handler/compare/v3.0.0...v3.0.1 diff --git a/README.md b/README.md index 59350d8..c69e4c5 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,9 @@ composer require middlewares/error-handler use Middlewares\ErrorFormatter; use Middlewares\ErrorHandler; use Middlewares\Utils\Dispatcher; +use Monolog\Level; +use Monolog\Logger; +use Monolog\Handler\StreamHandler; // Create a new ErrorHandler instance // Any number of formatters can be added. One will be picked based on the Accept @@ -41,6 +44,10 @@ $errorHandler = new ErrorHandler([ new ErrorFormatter\XmlFormatter(), ]); +// Create logger (optional) +$logger = new Logger('app'); +$logger->pushHandler(new StreamHandler('path/to/your.log', Level::Warning)); + // ErrorHandler should always be the first middleware in the stack! $dispatcher = new Dispatcher([ $errorHandler, @@ -48,7 +55,7 @@ $dispatcher = new Dispatcher([ function ($request) { throw HttpErrorException::create(404); } -]); +], $logger); $request = $serverRequestFactory->createServerRequest('GET', '/'); $response = $dispatcher->dispatch($request); @@ -67,6 +74,40 @@ $errorHandler = new ErrorHandler([ **Note:** If no formatter is found, the first value of the array will be used. In the example above, `HtmlFormatter`. +### How to setup a custom log callback + +This allows you to fully customize how you log (level, message, context, etc.) with access to values such as `Throwable` and `ServerRequestInterface` instances. + +Example using Monolog: + +```php +use Monolog\Level; +use Monolog\Logger; +use Monolog\Handler\StreamHandler; + +$logger = new Logger('app'); +$logger->pushHandler(new StreamHandler('path/to/your.log', Level::Warning)); + +$response = Dispatcher::run([ + (new ErrorHandler(null, $logger)) + ->logCallback(function ( + LoggerInterface $logger, + Throwable $error, + ServerRequestInterface $request + ): void { + $logger->critical('Uncaught exception', [ + 'message' => $error->getMessage(), + 'request' => [ + 'uri' => $request->getUri()->getPath(), + ] + ]); + }), + function ($request) { + throw new Exception('Something went wrong'); + }, +]); +``` + ### How to use a custom response for Production ```php diff --git a/composer.json b/composer.json index b8d4dfd..b63befa 100644 --- a/composer.json +++ b/composer.json @@ -20,9 +20,10 @@ }, "require": { "php": "^7.2 || ^8.0", + "ext-gd": "*", "middlewares/utils": "^2 || ^3 || ^4", "psr/http-server-middleware": "^1", - "ext-gd": "*" + "psr/log": "^1 || ^2 || ^3" }, "require-dev": { "phpunit/phpunit": "^8 || ^9", @@ -30,7 +31,8 @@ "squizlabs/php_codesniffer": "^3", "oscarotero/php-cs-fixer-config": "^2", "phpstan/phpstan": "^1 || ^2", - "laminas/laminas-diactoros": "^2 || ^3" + "laminas/laminas-diactoros": "^2 || ^3", + "filisko/fake-psr3-logger": "^1.0" }, "autoload": { "psr-4": { @@ -50,4 +52,4 @@ "coverage": "phpunit --coverage-text", "coverage-html": "phpunit --coverage-html=coverage" } -} \ No newline at end of file +} diff --git a/src/ErrorHandler.php b/src/ErrorHandler.php index 8d6afd4..2c016e9 100644 --- a/src/ErrorHandler.php +++ b/src/ErrorHandler.php @@ -14,6 +14,7 @@ use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; +use Psr\Log\LoggerInterface; use Throwable; class ErrorHandler implements MiddlewareInterface @@ -21,13 +22,21 @@ class ErrorHandler implements MiddlewareInterface /** @var FormatterInterface[] */ private $formatters = []; + /** @var LoggerInterface|null */ + private $logger = null; + + /** @var callable|null */ + private $logCallback = null; + /** * Configure the error formatters * * @param FormatterInterface[] $formatters */ - public function __construct(?array $formatters = null) + public function __construct(?array $formatters = null, ?LoggerInterface $logger = null) { + $this->logger = $logger; + if (empty($formatters)) { $formatters = [ new PlainFormatter(), @@ -54,11 +63,33 @@ public function addFormatters(FormatterInterface ...$formatters): self return $this; } + /** + * @param callable $callback + */ + public function logCallback(callable $callback): self + { + $this->logCallback = $callback; + + return $this; + } + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { try { return $handler->handle($request); } catch (Throwable $error) { + if ($this->logger) { + if ($this->logCallback) { + ($this->logCallback)($this->logger, $error, $request); + } else { + $this->logger->critical('Uncaught exception', [ + 'message' => $error->getMessage(), + 'file' => $error->getFile(), + 'line' => $error->getLine(), + ]); + } + } + foreach ($this->formatters as $formatter) { if ($formatter->isValid($error, $request)) { return $formatter->handle($error, $request); diff --git a/tests/ErrorHandlerTest.php b/tests/ErrorHandlerTest.php index d250385..8a6aadf 100644 --- a/tests/ErrorHandlerTest.php +++ b/tests/ErrorHandlerTest.php @@ -4,6 +4,7 @@ namespace Middlewares\Tests; use Exception; +use Filisko\FakeLogger; use Middlewares\ErrorFormatter\HtmlFormatter; use Middlewares\ErrorFormatter\ImageFormatter; use Middlewares\ErrorFormatter\JsonFormatter; @@ -15,6 +16,9 @@ use Middlewares\Utils\Factory; use Middlewares\Utils\HttpErrorException; use PHPUnit\Framework\TestCase; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Log\LoggerInterface; +use Throwable; class ErrorHandlerTest extends TestCase { @@ -61,6 +65,65 @@ public function getStatusCode(): int $this->assertEquals(418, $response->getStatusCode()); } + public function testLoggerException(): void + { + $logger = new FakeLogger(); + + $response = Dispatcher::run([ + new ErrorHandler(null, $logger), + function ($request) { + throw new Exception('Something went wrong'); + }, + ]); + + $this->assertEquals(500, $response->getStatusCode()); + $this->assertEquals([[ + 'level' => 'critical', + 'message' => 'Uncaught exception', + 'context' => [ + 'message' => 'Something went wrong', + 'file' => __FILE__, + 'line' => 75, + ] + ]], $logger->logs()); + } + + public function testLoggerExceptionWithCustomCallback(): void + { + $logger = new FakeLogger(); + + $response = Dispatcher::run([ + (new ErrorHandler(null, $logger)) + ->logCallback(function ( + LoggerInterface $logger, + Throwable $error, + ServerRequestInterface $request + ): void { + $logger->critical('Uncaught exception', [ + 'message' => $error->getMessage(), + 'request' => [ + 'uri' => $request->getUri()->getPath(), + ] + ]); + }), + function ($request) { + throw new Exception('Something went wrong'); + }, + ]); + + $this->assertEquals(500, $response->getStatusCode()); + $this->assertEquals([[ + 'level' => 'critical', + 'message' => 'Uncaught exception', + 'context' => [ + 'message' => 'Something went wrong', + 'request' => [ + 'uri' => '/', + ] + ] + ]], $logger->logs()); + } + public function testGifFormatter(): void { $request = Factory::createServerRequest('GET', '/');