Skip to content

Commit a3b9214

Browse files
committed
add support for PSR-3 logger
1 parent 895c084 commit a3b9214

File tree

5 files changed

+191
-4
lines changed

5 files changed

+191
-4
lines changed

CHANGELOG.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,56 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](http://keepachangelog.com/)
55
and this project adheres to [Semantic Versioning](http://semver.org/).
66

7+
## [3.2.0] - 2025-04-22
8+
### Added
9+
- Support for [PSR-3 Logger](https://github.com/php-fig/log). Example using [Monolog](https://github.com/Seldaek/monolog):
10+
11+
With default logging:
12+
```php
13+
use Monolog\Level;
14+
use Monolog\Logger;
15+
use Monolog\Handler\StreamHandler;
16+
17+
$logger = new Logger('app');
18+
$logger->pushHandler(new StreamHandler('path/to/your.log', Level::Warning));
19+
20+
$response = Dispatcher::run([
21+
new ErrorHandler(null, $logger),
22+
function ($request) {
23+
throw new Exception('Something went wrong');
24+
},
25+
]);
26+
27+
```
28+
With a custom log callback:
29+
```php
30+
use Monolog\Level;
31+
use Monolog\Logger;
32+
use Monolog\Handler\StreamHandler;
33+
34+
$logger = new Logger('app');
35+
$logger->pushHandler(new StreamHandler('path/to/your.log', Level::Warning));
36+
37+
$response = Dispatcher::run([
38+
(new ErrorHandler(null, $logger))
39+
->logCallback(function (
40+
LoggerInterface $logger,
41+
Throwable $error,
42+
ServerRequestInterface $request
43+
): void {
44+
$logger->critical('Uncaught exception', [
45+
'message' => $error->getMessage(),
46+
'request' => [
47+
'uri' => $request->getUri()->getPath(),
48+
]
49+
]);
50+
}),
51+
function ($request) {
52+
throw new Exception('Something went wrong');
53+
},
54+
]);
55+
```
56+
757
## [3.1.0] - 2025-03-21
858
### Fixed
959
- Support for PHP typing.
@@ -117,6 +167,7 @@ First version
117167

118168
[#9]: https://github.com/middlewares/error-handler/issues/9
119169

170+
[3.2.0]: https://github.com/middlewares/error-handler/compare/v3.1.0...v3.2.0
120171
[3.1.0]: https://github.com/middlewares/error-handler/compare/v3.0.2...v3.1.0
121172
[3.0.2]: https://github.com/middlewares/error-handler/compare/v3.0.1...v3.0.2
122173
[3.0.1]: https://github.com/middlewares/error-handler/compare/v3.0.0...v3.0.1

README.md

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ composer require middlewares/error-handler
2727
use Middlewares\ErrorFormatter;
2828
use Middlewares\ErrorHandler;
2929
use Middlewares\Utils\Dispatcher;
30+
use Monolog\Level;
31+
use Monolog\Logger;
32+
use Monolog\Handler\StreamHandler;
3033

3134
// Create a new ErrorHandler instance
3235
// Any number of formatters can be added. One will be picked based on the Accept
@@ -41,14 +44,18 @@ $errorHandler = new ErrorHandler([
4144
new ErrorFormatter\XmlFormatter(),
4245
]);
4346

47+
// Create logger (optional)
48+
$logger = new Logger('app');
49+
$logger->pushHandler(new StreamHandler('path/to/your.log', Level::Warning));
50+
4451
// ErrorHandler should always be the first middleware in the stack!
4552
$dispatcher = new Dispatcher([
4653
$errorHandler,
4754
// ...
4855
function ($request) {
4956
throw HttpErrorException::create(404);
5057
}
51-
]);
58+
], $logger);
5259

5360
$request = $serverRequestFactory->createServerRequest('GET', '/');
5461
$response = $dispatcher->dispatch($request);
@@ -67,6 +74,40 @@ $errorHandler = new ErrorHandler([
6774

6875
**Note:** If no formatter is found, the first value of the array will be used. In the example above, `HtmlFormatter`.
6976

77+
### How to setup a custom log callback
78+
79+
This allows you to fully customize how you log (level, message, context, etc.) with access to values such as `Throwable` and `ServerRequestInterface` instances.
80+
81+
Example using Monolog:
82+
83+
```php
84+
use Monolog\Level;
85+
use Monolog\Logger;
86+
use Monolog\Handler\StreamHandler;
87+
88+
$logger = new Logger('app');
89+
$logger->pushHandler(new StreamHandler('path/to/your.log', Level::Warning));
90+
91+
$response = Dispatcher::run([
92+
(new ErrorHandler(null, $logger))
93+
->logCallback(function (
94+
LoggerInterface $logger,
95+
Throwable $error,
96+
ServerRequestInterface $request
97+
): void {
98+
$logger->critical('Uncaught exception', [
99+
'message' => $error->getMessage(),
100+
'request' => [
101+
'uri' => $request->getUri()->getPath(),
102+
]
103+
]);
104+
}),
105+
function ($request) {
106+
throw new Exception('Something went wrong');
107+
},
108+
]);
109+
```
110+
70111
### How to use a custom response for Production
71112

72113
```php

composer.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@
3030
"squizlabs/php_codesniffer": "^3",
3131
"oscarotero/php-cs-fixer-config": "^2",
3232
"phpstan/phpstan": "^1 || ^2",
33-
"laminas/laminas-diactoros": "^2 || ^3"
33+
"laminas/laminas-diactoros": "^2 || ^3",
34+
"filisko/fake-psr3-logger": "^1.0"
3435
},
3536
"autoload": {
3637
"psr-4": {
@@ -50,4 +51,4 @@
5051
"coverage": "phpunit --coverage-text",
5152
"coverage-html": "phpunit --coverage-html=coverage"
5253
}
53-
}
54+
}

src/ErrorHandler.php

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,29 @@
1414
use Psr\Http\Message\ServerRequestInterface;
1515
use Psr\Http\Server\MiddlewareInterface;
1616
use Psr\Http\Server\RequestHandlerInterface;
17+
use Psr\Log\LoggerInterface;
1718
use Throwable;
1819

1920
class ErrorHandler implements MiddlewareInterface
2021
{
2122
/** @var FormatterInterface[] */
2223
private $formatters = [];
2324

25+
/** @var LoggerInterface|null */
26+
private $logger = null;
27+
28+
/** @var callable|null */
29+
private $logCallback = null;
30+
2431
/**
2532
* Configure the error formatters
2633
*
2734
* @param FormatterInterface[] $formatters
2835
*/
29-
public function __construct(?array $formatters = null)
36+
public function __construct(?array $formatters = null, ?LoggerInterface $logger = null)
3037
{
38+
$this->logger = $logger;
39+
3140
if (empty($formatters)) {
3241
$formatters = [
3342
new PlainFormatter(),
@@ -54,11 +63,33 @@ public function addFormatters(FormatterInterface ...$formatters): self
5463
return $this;
5564
}
5665

66+
/**
67+
* @param callable $callback
68+
*/
69+
public function logCallback(callable $callback): self
70+
{
71+
$this->logCallback = $callback;
72+
73+
return $this;
74+
}
75+
5776
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
5877
{
5978
try {
6079
return $handler->handle($request);
6180
} catch (Throwable $error) {
81+
if ($this->logger) {
82+
if ($this->logCallback) {
83+
($this->logCallback)($this->logger, $error, $request);
84+
} else {
85+
$this->logger->critical('Uncaught exception', [
86+
'message' => $error->getMessage(),
87+
'file' => $error->getFile(),
88+
'line' => $error->getLine(),
89+
]);
90+
}
91+
}
92+
6293
foreach ($this->formatters as $formatter) {
6394
if ($formatter->isValid($error, $request)) {
6495
return $formatter->handle($error, $request);

tests/ErrorHandlerTest.php

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
namespace Middlewares\Tests;
55

66
use Exception;
7+
use Filisko\FakeLogger;
78
use Middlewares\ErrorFormatter\HtmlFormatter;
89
use Middlewares\ErrorFormatter\ImageFormatter;
910
use Middlewares\ErrorFormatter\JsonFormatter;
@@ -15,6 +16,9 @@
1516
use Middlewares\Utils\Factory;
1617
use Middlewares\Utils\HttpErrorException;
1718
use PHPUnit\Framework\TestCase;
19+
use Psr\Http\Message\ServerRequestInterface;
20+
use Psr\Log\LoggerInterface;
21+
use Throwable;
1822

1923
class ErrorHandlerTest extends TestCase
2024
{
@@ -61,6 +65,65 @@ public function getStatusCode(): int
6165
$this->assertEquals(418, $response->getStatusCode());
6266
}
6367

68+
public function testLoggerException(): void
69+
{
70+
$logger = new FakeLogger();
71+
72+
$response = Dispatcher::run([
73+
new ErrorHandler(null, $logger),
74+
function ($request) {
75+
throw new Exception('Something went wrong');
76+
},
77+
]);
78+
79+
$this->assertEquals(500, $response->getStatusCode());
80+
$this->assertEquals([[
81+
'level' => 'critical',
82+
'message' => 'Uncaught exception',
83+
'context' => [
84+
'message' => 'Something went wrong',
85+
'file' => __FILE__,
86+
'line' => 75,
87+
]
88+
]], $logger->logs());
89+
}
90+
91+
public function testLoggerExceptionWithCustomCallback(): void
92+
{
93+
$logger = new FakeLogger();
94+
95+
$response = Dispatcher::run([
96+
(new ErrorHandler(null, $logger))
97+
->logCallback(function (
98+
LoggerInterface $logger,
99+
Throwable $error,
100+
ServerRequestInterface $request
101+
): void {
102+
$logger->critical('Uncaught exception', [
103+
'message' => $error->getMessage(),
104+
'request' => [
105+
'uri' => $request->getUri()->getPath(),
106+
]
107+
]);
108+
}),
109+
function ($request) {
110+
throw new Exception('Something went wrong');
111+
},
112+
]);
113+
114+
$this->assertEquals(500, $response->getStatusCode());
115+
$this->assertEquals([[
116+
'level' => 'critical',
117+
'message' => 'Uncaught exception',
118+
'context' => [
119+
'message' => 'Something went wrong',
120+
'request' => [
121+
'uri' => '/',
122+
]
123+
]
124+
]], $logger->logs());
125+
}
126+
64127
public function testGifFormatter(): void
65128
{
66129
$request = Factory::createServerRequest('GET', '/');

0 commit comments

Comments
 (0)