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

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
Merge develop to master in preparation for 1.1.0.
  • Loading branch information
weierophinney committed Jun 25, 2015
2 parents 63b0645 + e418bce commit 47a8cd2
Show file tree
Hide file tree
Showing 7 changed files with 260 additions and 58 deletions.
14 changes: 11 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,15 @@ Versions prior to 1.0 were originally released as `phly/conduit`; please visit
its [CHANGELOG](https://github.com/phly/conduit/blob/master/CHANGELOG.md) for
details.

## 1.0.3 - TBD
## 1.1.0 - 2015-06-25

### Added

- Nothing.
- [#13](https://github.com/zendframework/zend-stratigility/pull/13) adds
`Utils::getStatusCode($error, ResponseInterface $response)`; this static
method will attempt to use an exception code as an HTTP status code, if it
falls in a valid HTTP error status range. If the error is not an exception, it
ensures that the status code is an error status.

### Deprecated

Expand All @@ -22,7 +26,11 @@ details.

### Fixed

- Nothing.
- [#12](https://github.com/zendframework/zend-stratigility/pull/12) updates
`FinalHandler` such that it will return the response provided at invocation
if it differs from the response at initialization (i.e., a new response
instance, or if the body size has changed). This allows you to safely call
`$next()` from all middleware in order to allow post-processing.

## 1.0.2 - 2015-06-24

Expand Down
39 changes: 30 additions & 9 deletions doc/book/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,11 @@ executed for that path and any subpaths.
Middleware is executed in the order in which it is piped to the `MiddlewarePipe` instance.

`__invoke()` is itself middleware. If `$out` is not provided, an instance of
`Zend\Stratigility\FinalHandler` will be created, and used in the event that the pipe
stack is exhausted. The callable should use the same signature as `Next()`:
`Zend\Stratigility\FinalHandler` will be created, and used in the event that the pipe stack is
exhausted (`MiddlewarePipe` passes the `$response` instance it receives to `FinalHandler` as well,
so that the latter can determine if the response it receives is new).

The callable should use the same signature as `Next()`:

```php
function (
Expand Down Expand Up @@ -151,6 +154,10 @@ And, if not calling `$next()`, returning the response instance:
return $response;
```

The `FinalHandler` implementation will check the `$response` instance passed when invoking it
against the instance passed during instantiation, and, if different, return it. As such, `return
$next(/* ... */)` is the recommended workflow.

### Raising an error condition

To raise an error condition, pass a non-null value as the third argument to `$next()`:
Expand All @@ -172,13 +179,27 @@ function ($request, $response, $next)
exhausts itself. It expects three arguments when invoked: a request instance, a response instance,
and an error condition (or `null` for no error). It returns a response.

`FinalHandler` allows an optional argument during instantiation, `$options`, an array of options
with which to configure itself. These options currently include:

- `env`, the application environment. If set to "production", no stack traces will be provided.
- `onerror`, a callable to execute if an error is passed when `FinalHandler` is invoked. The
callable is invoked with the error (which will be `null` in the absence of an error), the request,
and the response, in that order.
`FinalHandler` allows two optional arguments during instantiation

- `$options`, an array of options with which to configure itself. These options currently include:
- `env`, the application environment. If set to "production", no stack traces will be provided.
- `onerror`, a callable to execute if an error is passed when `FinalHandler` is invoked. The
callable is invoked with the error (which will be `null` in the absence of an error), the request,
and the response, in that order.
- `Psr\Http\Message\ResponseInterface $response`; if passed, it will compare the response passed
during invocation against this instance; if they are different, it will return the response from
the invocation, as this indicates that one or more middleware provided a new response instance.

Internally, `FinalHandler` does the following on invocation:

- If `$error` is non-`null`, it creates an error response from the response provided at invocation,
ensuring a 400 or 500 series response is returned.
- If the response at invocation matches the response provided at instantiation, it returns it
without further changes. This is an indication that some middleware at some point in the execution
chain called `$next()` with a new response instance.
- If the response at invocation does not match the response provided at instantiation, or if no
response was provided at instantiation, it creates a 404 response, as the assumption is that no
middleware was capable of handling the request.

## HTTP Messages

Expand Down
172 changes: 127 additions & 45 deletions src/FinalHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,46 +10,90 @@
namespace Zend\Stratigility;

use Exception;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Zend\Escaper\Escaper;

/**
* Handle incomplete requests
*/
class FinalHandler
{
/**
* Original response body size.
*
* @var int
*/
private $bodySize = 0;

/**
* @var array
*/
private $options;

/**
* Original response provided to the middleware.
*
* @var null|ResponseInterface
*/
private $response;

/**
* @param array $options Options that change default override behavior.
* @param null|ResponseInterface $response Original response, if any.
*/
public function __construct(array $options = [])
public function __construct(array $options = [], ResponseInterface $response = null)
{
$this->options = $options;
$this->options = $options;
$this->response = $response;

if ($response) {
$this->bodySize = $response->getBody()->getSize();
}
}

/**
* Handle incomplete requests
*
* This handler should only ever be invoked if Next exhausts its stack.
* When that happens, we determine if an $err is present, and, if so,
* create a 500 status with error details.
*
* Otherwise, a 404 status is created.
* When that happens, one of three possibilities exists:
*
* @param Http\Request $request Request instance.
* @param Http\Response $response Response instance.
* - If an $err is present, create a 500 status with error details.
* - If the instance composes a response, and it differs from the response
* passed during invocation, return the invocation response; this is
* indicative of middleware calling $next to allow post-processing of
* a populated response.
* - Otherwise, a 404 status is created.
*
* @param RequestInterface $request Request instance.
* @param ResponseInterface $response Response instance.
* @param mixed $err
* @return Http\Response
* @return ResponseInterface
*/
public function __invoke(Http\Request $request, Http\Response $response, $err = null)
public function __invoke(RequestInterface $request, ResponseInterface $response, $err = null)
{
if ($err) {
return $this->handleError($err, $request, $response);
}

// Return provided response if it does not match the one provided at
// instantiation; this is an indication of calling `$next` in the fina
// registered middleware and providing a new response instance.
if ($this->response && $this->response !== $response) {
return $response;
}

// If the response passed is the same as the one at instantiation,
// check to see if the body size has changed; if it has, return
// the response, as the message body has been written to.
if ($this->response
&& $this->response === $response
&& $this->bodySize !== $response->getBody()->getSize()
) {
return $response;
}

return $this->create404($request, $response);
}

Expand All @@ -59,14 +103,14 @@ public function __invoke(Http\Request $request, Http\Response $response, $err =
* Use the $error to create details for the response.
*
* @param mixed $error
* @param Http\Request $request Request instance.
* @param Http\Response $response Response instance.
* @param RequestInterface $request Request instance.
* @param ResponseInterface $response Response instance.
* @return Http\Response
*/
private function handleError($error, Http\Request $request, Http\Response $response)
private function handleError($error, RequestInterface $request, ResponseInterface $response)
{
$response = $response->withStatus(
$this->getStatusCode($error, $response)
Utils::getStatusCode($error, $response)
);

$message = $response->getReasonPhrase() ?: 'Unknown Error';
Expand All @@ -76,7 +120,7 @@ private function handleError($error, Http\Request $request, Http\Response $respo
$message = $this->createDevelopmentErrorMessage($error);
}

$response = $response->end($message);
$response = $this->completeResponse($response, $message);

$this->triggerError($error, $request, $response);

Expand All @@ -86,52 +130,41 @@ private function handleError($error, Http\Request $request, Http\Response $respo
/**
* Create a 404 status in the response
*
* @param Http\Request $request Request instance.
* @param Http\Response $response Response instance.
* @param RequestInterface $request Request instance.
* @param ResponseInterface $response Response instance.
* @return Http\Response
*/
private function create404(Http\Request $request, Http\Response $response)
private function create404(RequestInterface $request, ResponseInterface $response)
{
$response = $response->withStatus(404);
$originalRequest = $request->getOriginalRequest();
$uri = $originalRequest->getUri();
$uri = $this->getUriFromRequest($request);
$escaper = new Escaper();
$message = sprintf(
"Cannot %s %s\n",
$escaper->escapeHtml($request->getMethod()),
$escaper->escapeHtml((string) $uri)
);
return $response->end($message);

return $this->completeResponse($response, $message);
}

/**
* Determine status code
* Create a complete error message for development purposes.
*
* Creates an error message with full error details:
*
* If the error is an exception with a code between 400 and 599, returns
* the exception code.
* - If the error is an exception, creates a message that includes the full
* stack trace.
* - If the error is an object that defines `__toString()`, creates a
* message by casting the error to a string.
* - If the error is not an object, casts the error to a string.
* - Otherwise, cerates a generic error message indicating the class type.
*
* Otherwise, retrieves the code from the response; if not present, or
* less than 400 or greater than 599, returns 500; otherwise, returns it.
* In all cases, the error message is escaped for use in HTML.
*
* @param mixed $error
* @param Http\Response $response
* @return int
* @return string
*/
private function getStatusCode($error, Http\Response $response)
{
if ($error instanceof Exception
&& ($error->getCode() >= 400 && $error->getCode() < 600)
) {
return $error->getCode();
}

$status = $response->getStatusCode();
if (! $status || $status < 400 || $status >= 600) {
$status = 500;
}
return $status;
}

private function createDevelopmentErrorMessage($error)
{
if ($error instanceof Exception) {
Expand All @@ -150,11 +183,17 @@ private function createDevelopmentErrorMessage($error)
/**
* Trigger the error listener, if present
*
* If no `onerror` option is present, or if it is not callable, does
* nothing.
*
* If the request is not an Http\Request, casts it to one prior to invoking
* the error handler.
*
* @param mixed $error
* @param Http\Request $request
* @param RequestInterface $request
* @param Http\Response $response
*/
private function triggerError($error, Http\Request $request, Http\Response $response)
private function triggerError($error, RequestInterface $request, Http\Response $response)
{
if (! isset($this->options['onerror'])
|| ! is_callable($this->options['onerror'])
Expand All @@ -163,6 +202,49 @@ private function triggerError($error, Http\Request $request, Http\Response $resp
}

$onError = $this->options['onerror'];
$onError($error, $request, $response);
$onError(
$error,
($request instanceof Http\Request) ? $request : new Http\Request($request),
$response
);
}

/**
* Retrieve the URI from the request.
*
* If the request instance is a Stratigility decorator, pull the URI from
* the original request; otherwise, pull it directly.
*
* @param RequestInterface $request
* @return \Psr\Http\Message\UriInterface
*/
private function getUriFromRequest(RequestInterface $request)
{
if ($request instanceof Http\Request) {
$original = $request->getOriginalRequest();
return $original->getUri();
}

return $request->getUri();
}

/**
* Write the given message to the response and mark it complete.
*
* If the message is an Http\Response decorator, call and return its
* `end()` method; otherwise, decorate the response and `end()` it.
*
* @param ResponseInterface $response
* @param string $message
* @return Http\Response
*/
private function completeResponse(ResponseInterface $response, $message)
{
if ($response instanceof Http\Response) {
return $response->end($message);
}

$response = new Http\Response($response);
return $response->end($message);
}
}
2 changes: 1 addition & 1 deletion src/MiddlewarePipe.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ public function __invoke(Request $request, Response $response, callable $out = n
$request = $this->decorateRequest($request);
$response = $this->decorateResponse($response);

$done = $out ?: new FinalHandler();
$done = $out ?: new FinalHandler([], $response);
$next = new Next($this->pipeline, $done);
$result = $next($request, $response);

Expand Down
Loading

0 comments on commit 47a8cd2

Please sign in to comment.