Skip to content
This repository was archived by the owner on Jan 29, 2020. It is now read-only.

Commit ebf5fb5

Browse files
committed
Merge branch 'hotfix/29-middleware-listeners'
Close #29 Fixes #28
2 parents 0e78109 + 6829417 commit ebf5fb5

File tree

4 files changed

+189
-1
lines changed

4 files changed

+189
-1
lines changed

CHANGELOG.md

+35
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,41 @@ All notable changes to this project will be documented in this file, in reverse
44

55
Versions 0.3.0 and prior were released as "weierophinney/problem-details".
66

7+
## 0.5.2 - 2018-01-10
8+
9+
### Added
10+
11+
- [#29](https://github.com/zendframework/zend-problem-details/pull/29) adds
12+
the ability for the `ProblemDetailsMiddleware` to trigger listeners when
13+
it catches a `Throwable` to produce a response. Listeners are PHP callables
14+
and receive the following arguments, in the following order:
15+
16+
- `Throwable $error`: the throwable/exception caught by the
17+
`ProblemDetailsMiddleware`.
18+
- `ServerRequestInterface $request`: the request handled by the
19+
`ProblemDetailsMiddleware`.
20+
- `ResponseInterface $response`: the response generated by the
21+
`ProblemDetailsMiddleware`.
22+
23+
Attach listeners using the `ProblemDetailsMiddleware::attachListeners()`
24+
instance method.
25+
26+
### Changed
27+
28+
- Nothing.
29+
30+
### Deprecated
31+
32+
- Nothing.
33+
34+
### Removed
35+
36+
- Nothing.
37+
38+
### Fixed
39+
40+
- Nothing.
41+
742
## 0.5.1 - 2017-12-07
843

944
### Added

docs/book/middleware.md

+76-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,50 @@ This latter approach ensures that you are only providing problem details for
5555
specific API endpoints, which can be useful when you have a mix of APIs and
5656
traditional web content in your application.
5757

58-
### Factory
58+
## Listeners
59+
60+
- Since 0.5.2
61+
62+
The `ProblemDetailsMiddleware` allows you to register _listeners_ to trigger
63+
when it handles a `Throwable`. Listeners are PHP callables, and the middleware
64+
triggers them with the following arguments, in the following order:
65+
66+
- `Throwable $error`: the throwable/exception caught by the middleware.
67+
- `ServerRequestInterface $request`: the request as provided to the
68+
`ProblemDetailsMiddleware`.
69+
- `ResponseInterface $response`: the response the `ProblemDetailsMiddleware`
70+
generated based on the `$error`.
71+
72+
Note that each of these arguments are immutable; you cannot change the state in
73+
a way that that state will propagate meaningfully. As such, you should use
74+
listeners for reporting purposes only (e.g., logging).
75+
76+
As an example:
77+
78+
```php
79+
// Where $logger is a PSR-3 logger implementation
80+
$listener = function (
81+
Throwable $error,
82+
ServerRequestInterface $request,
83+
ResponseInterface $response
84+
) use ($logger) {
85+
$logger->error('[{status}] {method} {uri}: {message}', [
86+
'status' => $response->getStatusCode(),
87+
'method' => $request->getMethod(),
88+
'uri' => (string) $request->getUri(),
89+
'message' => $error->getMessage(),
90+
]);
91+
};
92+
```
93+
94+
Attach listeners to the `ProblemDetailsMiddleware` instance using its
95+
`attachListener()` method:
96+
97+
```php
98+
$middleware->attachListener($listener);
99+
```
100+
101+
## Factory
59102

60103
The `ProblemDetailsMiddleware` ships with a corresponding PSR-11 compatible factory,
61104
`ProblemDetailsMiddlewareFactory`. This factory looks for a service named
@@ -65,3 +108,35 @@ to instantiate the middleware.
65108
For Expressive 2 users, this middleware should be registered automatically with
66109
your application on install, assuming you have the zend-component-installer
67110
plugin in place (it's shipped by default with the Expressive skeleton).
111+
112+
### Registering listeners
113+
114+
- Since 0.5.2
115+
116+
In order to register listeners, we recommend using a
117+
[delegator factory](https://docs.zendframework.com/zend-expressive/features/container/delegator-factories/)
118+
on the `Zend\ProblemDetails\ProblemDetailsMiddleware` service.
119+
120+
As an example:
121+
122+
```php
123+
class LoggerProblemDetailsListenerDelegator
124+
{
125+
public function __construct(ContainerInterface $container, $serviceName, callable $callback)
126+
{
127+
$middleware = $callback();
128+
$middleware->attachListener($container->get(LoggerProblemDetailsListener::class));
129+
return $middleware;
130+
}
131+
}
132+
```
133+
134+
You would then register this as a delegator factory in your configuration:
135+
136+
```php
137+
'delegators' => [
138+
ProblemDetailsMiddleware::class => [
139+
LoggerProblemDetailsListenerDelegator::class,
140+
],
141+
],
142+
```

src/ProblemDetailsMiddleware.php

+45
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@
2323
*/
2424
class ProblemDetailsMiddleware implements MiddlewareInterface
2525
{
26+
/**
27+
* @var callable[]
28+
*/
29+
private $listeners = [];
30+
2631
/**
2732
* @var ProblemDetailsResponseFactory
2833
*/
@@ -52,13 +57,38 @@ public function process(ServerRequestInterface $request, DelegateInterface $dele
5257
}
5358
} catch (Throwable $e) {
5459
$response = $this->responseFactory->createResponseFromThrowable($request, $e);
60+
$this->triggerListeners($e, $request, $response);
5561
} finally {
5662
restore_error_handler();
5763
}
5864

5965
return $response;
6066
}
6167

68+
/**
69+
* Attach an error listener.
70+
*
71+
* Each listener receives the following three arguments:
72+
*
73+
* - Throwable $error
74+
* - ServerRequestInterface $request
75+
* - ResponseInterface $response
76+
*
77+
* These instances are all immutable, and the return values of
78+
* listeners are ignored; use listeners for reporting purposes
79+
* only.
80+
*
81+
* @param callable $listener
82+
*/
83+
public function attachListener(callable $listener)
84+
{
85+
if (\in_array($listener, $this->listeners, true)) {
86+
return;
87+
}
88+
89+
$this->listeners[] = $listener;
90+
}
91+
6292
/**
6393
* Can the middleware act as an error handler?
6494
*
@@ -98,4 +128,19 @@ private function createErrorHandler() : callable
98128
throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
99129
};
100130
}
131+
132+
/**
133+
* Trigger all error listeners.
134+
*
135+
* @param Throwable $error
136+
* @param ServerRequestInterface $request
137+
* @param ResponseInterface $response
138+
* @return void
139+
*/
140+
private function triggerListeners($error, ServerRequestInterface $request, ResponseInterface $response) : void
141+
{
142+
array_walk($this->listeners, function ($listener) use ($error, $request, $response) {
143+
$listener($error, $request, $response);
144+
});
145+
}
101146
}

test/ProblemDetailsMiddlewareTest.php

+33
Original file line numberDiff line numberDiff line change
@@ -148,4 +148,37 @@ public function testRethrowsCaughtExceptionIfUnableToNegotiateAcceptHeader() : v
148148
$this->expectExceptionCode(507);
149149
$middleware->process($this->request->reveal(), $delegate->reveal());
150150
}
151+
152+
/**
153+
* @dataProvider acceptHeaders
154+
*/
155+
public function testErrorHandlingTriggersListeners(string $accept) : void
156+
{
157+
$this->request->getHeaderLine('Accept')->willReturn($accept);
158+
159+
$exception = new TestAsset\RuntimeException('Thrown!', 507);
160+
161+
$delegate = $this->prophesize(DelegateInterface::class);
162+
$delegate
163+
->{HANDLER_METHOD}(Argument::that([$this->request, 'reveal']))
164+
->willThrow($exception);
165+
166+
$expected = $this->prophesize(ResponseInterface::class)->reveal();
167+
$this->responseFactory
168+
->createResponseFromThrowable($this->request->reveal(), $exception)
169+
->willReturn($expected);
170+
171+
$listener = function ($error, $request, $response) use ($exception, $expected) {
172+
$this->assertSame($exception, $error, 'Listener did not receive same exception as was raised');
173+
$this->assertSame($this->request->reveal(), $request, 'Listener did not receive same request');
174+
$this->assertSame($expected, $response, 'Listener did not receive same response');
175+
};
176+
$listener2 = clone $listener;
177+
$this->middleware->attachListener($listener);
178+
$this->middleware->attachListener($listener2);
179+
180+
$result = $this->middleware->process($this->request->reveal(), $delegate->reveal());
181+
182+
$this->assertSame($expected, $result);
183+
}
151184
}

0 commit comments

Comments
 (0)