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

Commit 25af1dd

Browse files
committed
Merge branch 'hotfix/29-middleware-listeners' into release-1.0.0
Forward port #29 Updates `ProblemDetailsMiddlewareTest::testErrorHandlingTriggersListeners` to mock the http-interop/http-server-handlers `RequestHandlerInterface` instead of the polyfill interface. Conflicts: CHANGELOG.md
2 parents 778e09a + 6829417 commit 25af1dd

File tree

4 files changed

+189
-1
lines changed

4 files changed

+189
-1
lines changed

CHANGELOG.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,41 @@ Versions 0.3.0 and prior were released as "weierophinney/problem-details".
3737

3838
- Nothing.
3939

40+
## 0.5.2 - 2018-01-10
41+
42+
### Added
43+
44+
- [#29](https://github.com/zendframework/zend-problem-details/pull/29) adds
45+
the ability for the `ProblemDetailsMiddleware` to trigger listeners when
46+
it catches a `Throwable` to produce a response. Listeners are PHP callables
47+
and receive the following arguments, in the following order:
48+
49+
- `Throwable $error`: the throwable/exception caught by the
50+
`ProblemDetailsMiddleware`.
51+
- `ServerRequestInterface $request`: the request handled by the
52+
`ProblemDetailsMiddleware`.
53+
- `ResponseInterface $response`: the response generated by the
54+
`ProblemDetailsMiddleware`.
55+
56+
Attach listeners using the `ProblemDetailsMiddleware::attachListeners()`
57+
instance method.
58+
59+
### Changed
60+
61+
- Nothing.
62+
63+
### Deprecated
64+
65+
- Nothing.
66+
67+
### Removed
68+
69+
- Nothing.
70+
71+
### Fixed
72+
73+
- Nothing.
74+
4075
## 0.5.1 - 2017-12-07
4176

4277
### Added

docs/book/middleware.md

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,50 @@ This latter approach ensures that you are only providing problem details for
5353
specific API endpoints, which can be useful when you have a mix of APIs and
5454
traditional web content in your application.
5555

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

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

src/ProblemDetailsMiddleware.php

Lines changed: 45 additions & 0 deletions
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
*/
@@ -48,13 +53,38 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface
4853
$response = $handler->handle($request);
4954
} catch (Throwable $e) {
5055
$response = $this->responseFactory->createResponseFromThrowable($request, $e);
56+
$this->triggerListeners($e, $request, $response);
5157
} finally {
5258
restore_error_handler();
5359
}
5460

5561
return $response;
5662
}
5763

64+
/**
65+
* Attach an error listener.
66+
*
67+
* Each listener receives the following three arguments:
68+
*
69+
* - Throwable $error
70+
* - ServerRequestInterface $request
71+
* - ResponseInterface $response
72+
*
73+
* These instances are all immutable, and the return values of
74+
* listeners are ignored; use listeners for reporting purposes
75+
* only.
76+
*
77+
* @param callable $listener
78+
*/
79+
public function attachListener(callable $listener)
80+
{
81+
if (\in_array($listener, $this->listeners, true)) {
82+
return;
83+
}
84+
85+
$this->listeners[] = $listener;
86+
}
87+
5888
/**
5989
* Can the middleware act as an error handler?
6090
*
@@ -94,4 +124,19 @@ private function createErrorHandler() : callable
94124
throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
95125
};
96126
}
127+
128+
/**
129+
* Trigger all error listeners.
130+
*
131+
* @param Throwable $error
132+
* @param ServerRequestInterface $request
133+
* @param ResponseInterface $response
134+
* @return void
135+
*/
136+
private function triggerListeners($error, ServerRequestInterface $request, ResponseInterface $response) : void
137+
{
138+
array_walk($this->listeners, function ($listener) use ($error, $request, $response) {
139+
$listener($error, $request, $response);
140+
});
141+
}
97142
}

test/ProblemDetailsMiddlewareTest.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,4 +125,37 @@ public function testRethrowsCaughtExceptionIfUnableToNegotiateAcceptHeader() : v
125125
$this->expectExceptionCode(507);
126126
$middleware->process($this->request->reveal(), $handler->reveal());
127127
}
128+
129+
/**
130+
* @dataProvider acceptHeaders
131+
*/
132+
public function testErrorHandlingTriggersListeners(string $accept) : void
133+
{
134+
$this->request->getHeaderLine('Accept')->willReturn($accept);
135+
136+
$exception = new TestAsset\RuntimeException('Thrown!', 507);
137+
138+
$handler = $this->prophesize(RequestHandlerInterface::class);
139+
$handler
140+
->handle(Argument::that([$this->request, 'reveal']))
141+
->willThrow($exception);
142+
143+
$expected = $this->prophesize(ResponseInterface::class)->reveal();
144+
$this->responseFactory
145+
->createResponseFromThrowable($this->request->reveal(), $exception)
146+
->willReturn($expected);
147+
148+
$listener = function ($error, $request, $response) use ($exception, $expected) {
149+
$this->assertSame($exception, $error, 'Listener did not receive same exception as was raised');
150+
$this->assertSame($this->request->reveal(), $request, 'Listener did not receive same request');
151+
$this->assertSame($expected, $response, 'Listener did not receive same response');
152+
};
153+
$listener2 = clone $listener;
154+
$this->middleware->attachListener($listener);
155+
$this->middleware->attachListener($listener2);
156+
157+
$result = $this->middleware->process($this->request->reveal(), $handler->reveal());
158+
159+
$this->assertSame($expected, $result);
160+
}
128161
}

0 commit comments

Comments
 (0)