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

Deprecate FinalHandler and ErrorMiddlewareInterface #66

Conversation

weierophinney
Copy link
Member

@weierophinney weierophinney commented Sep 26, 2016

This patch will provide the forwards compatibility features for the next minor version of Stratigility that implement the RFC on FinalHandler and ErrorMiddlewareInterface removal.

  • Create NotFoundHandler and ErrorHandler middleware
    • NotFoundHandler returns a basic 404 response, always.
    • ErrorHandler:
      • creates a PHP error handler that will throw any errors not in the current error_handler mask as ErrorExceptions.
      • Does a try/catch around $next()
      • Raises a new exception, MissingResponseException, if $next does not return a response.
      • Casts all Throwable/Exception items to error responses:
        • If a boolean true flag is passed to the constructor, it will use the stack trace in the response.
        • Triggers listeners on the instance with the exception/throwable, request, and response, for reporting purposes.
  • Create a no-op final handler that returns the provided response on invocation, ignoring all other arguments.
  • Update MiddlewarePipe to issue a deprecation notice if $out is omitted.
  • Mark Dispatch as deprecated.
  • Update Next to raise deprecation notice when receiving a non-null $err argument.
  • Mark FinalHandler as deprecated.
  • Mark ErrorMiddlewareInterface as deprecated.
  • Write migration guide.

Notes

  • In order to return a response, each new middleware type (NotFoundHandler, ErrorHandler) accepts a prototype ResponseInterface instance to the constructor; this is then used to generate the returned response in 404/error conditions. This is done to allow re-use with any PSR-7 implementation.

@weierophinney weierophinney added this to the 1.3.0 milestone Sep 26, 2016
In doing so, discovered that if we make `$out` required in v2, there's no need
to introduce the `setFinalHandler()` argument, as it can be passed directly, or
handled via `Zend\Diactoros\Server::listen()`.
First step in the journey to remove the FinalHandler.

- Bump minimum supported PHP version to 5.6 (allows calling callables without
  `call_user_func()`, and using `::class` notation).
- `NotFoundHandler` returns a basic 404 response, always.
- `ErrorHandler`:
  - creates a PHP error handler that will throw any errors not in the
    current error_handler mask as ErrorExceptions.
  - Does a try/catch around `$next()`
  - Raises a new exception, `MissingResponseException`, if `$next` did
    not return a response.
  - Casts all Throwable/Exception items to error responses:
    - If a boolean true flag is passed to the constructor, it will use
      the stack trace in the response.
    - Triggers listeners on the instance with the exception/throwable,
      request, and response, for reporting purposes.

In order to return a response, each middleware type accepts a prototype
`ResponseInterface` instance to the constructor; this is then used to generate
the returned response in 404/error conditions.
We need to notify users that `$out` cannot be omitted starting with
version 2.0.0.
@weierophinney weierophinney force-pushed the feature/final-handler-deprecation branch from f653477 to 2aa080d Compare September 26, 2016 21:30
@weierophinney
Copy link
Member Author

Marked as a BC break, as it introduces deprecation notices. These are, however, documented, and the messages themselves point to online (not-yet-published, but included in this PR) documentation to assist users in making their code forwards-compatible.

@weierophinney
Copy link
Member Author

Pinging @zendframework/community-review-team / @ezimuel / @michaelmoussa for review!

@vaclavvanik
Copy link

I think new ErrorHandler should have possibility to create response depending on content-type.

It may be optional, eg. any callable param in constructor. Thoughts?

@weierophinney
Copy link
Member Author

@vaclavvanik I like the idea, but I think that belongs more properly in zend-expressive or its skeleton, and not in Stratigility. Stratigility is a foundation, and the expectation is that it provides only the minimum functionality; in fact, the docs I wrote even recommend replacing the ErrorHandler and NotFoundHandler in your own applications in order to provide your own features.

- Created a protected method, `createErrorResponse()`, for creating and
  returning the error response to use.
- Made `$isDevelopmentMode` protected, to allow extending classes to
  query its value.
@weierophinney
Copy link
Member Author

@vaclavvanik I've updated the implementation to provide an extension point in ErrorHandler, allowing developers to extend the class and provide a protected createErrorResponse() method. This allows a common workflow of:

  • creating a PHP error handler that casts to ErrorException
  • using a try/catch block to handle all exceptions/throwables and respond to them
  • generating an error response
  • providing listeners that can act on the final error, request, and response

This will provide a better starting point with flexibility for end-users, while still being minimalist.

@vaclavvanik
Copy link

@weierophinney Imo custom error response factory could be injected as optional callable into ErrorHandler constructor. This allow great flexibility when pulling ErrorHandler from container and createErrorResponse method can be still private.

class ErrorHandler
{
    private function createErrorResponse($e, ServerRequestInterface $request, ResponseInterface $response)
    {
        if ($this->errorResponseCallback) {
            return call_user_func($this->errorResponseCallback, $e, $request, $response);
        }

        $response = $response->withStatus(Utils::getStatusCode($e, $response));
        $body = $response->getBody();

        if ($this->isDevelopmentMode) {
             $body->write($this->createDevelopmentErrorMessage($e));
             return $response;
        }

        $body->write($response->getReasonPhrase() ?: 'Unknown Error');
        return $response;
    }
}

$errorResponse = function ($e, ServerRequestInterface $request, ResponseInterface $response) {
  // do content negotiation and other things
  return $response;
};

$errorHandler = new ErrorHandler(new Response(), $isDevelopmentMode,  $errorResponse);

Ummm... thinking maybe whole error response factory could be moved to another class. So ErrorHandler constructor could look:

class ErrorHandlerResponseFactory
{
    public function __construct(ResponseInterface $responsePrototype, $isDevelopmentMode = false);
    public function __invoke($e, ServerRequestInterface $request, ResponseInterface $response);
}

class ErrorHandler
{
    public function __construct(callable $errorResponseFactory)
    {
        $this->errorResponseFactory = $errorResponseFactory;
    } 
}

Thanks for your opinions.

Per @vaclavvanik, this patch:

- Moves the error generation aspects to a new class,
  `ErrorResponseGenerator`.
- Updates the `ErrorHandler`:
  - to allow passing an optional, callable response generator via the
    constructor, instead of the `$isDevelopmentMode` flag.
  - to specify `ErrorResponseGenerator` in production mode if no
    generator is provided during instantiation.
  - to trigger the response generator from `handleThrowable()` in order
    to get an error response, passing the exception, request, and
    response prototype.
  - Marks the class final.

This allows flexibility in generating error responses without requiring
extension, by composing the generator in the handler itself.
* update and return when returning an error response.
* @param bool $isDevelopmentMode
*/
public function __construct(ResponseInterface $responsePrototype, $isDevelopmentMode = false)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@weierophinney what are your thoughts on a more granular configuration $options parameter with an is_development_mode setting rather than a simple flag? Or would further configurability be something that should be accomplished by extending the handler?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've actually just pushed changes based on the feedback from @vaclavvanik that shoves that stuff into the response generator. As such, the ErrorHandler now has only the following responsibilities:

  • compose a response prototype, a response generator, and listeners
  • create and enable a PHP error handler that casts errors to exceptions
  • detect when no inner middleware returns a response, and raise an exception
  • catch all exceptions, and pass them, the request, and the prototype response to the response generator
  • notifiy all listeners of the error, the request, and the final response

As such, no "optional" types of behavior are present, and those would be handled by the response generator and/or listeners, which fall out of scope of the class itself.

*/
private function handleThrowable($e, ServerRequestInterface $request)
{
$response = $this->createErrorResponse($e, $request, $this->responsePrototype);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@weierophinney I'm not clear on why the responsePrototype is needed in the ErrorHandler. Could we not use the $response that's passed to __invoke and treat that as the prototype?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm using the response prototype in part to prepare for PSR-15, where the response is not passed to middleware.

use Zend\Stratigility\Exception\MissingResponseException;
use Zend\Stratigility\Utils;

class ErrorHandler
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@weierophinney shouldn't this implement MiddlewareInterface?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possibly, but if MiddlewareInterface is replaced in v2 with the PSR-15 versions, it'd be an extra change.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated to make this implement MiddlewareInterface now. We can change if/when PSR-15 is ratified.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. I'll look at the remainder of modifications later this afternoon.

In general, should we tend to approach solutions based on what works best for Expressive in the present? At least until PSR-15 is close to ratification or the decision to not include the Response in the signature is as close to a sure thing as possible?

I figure that no matter what happens, there will need to be changes, so perhaps sticking to the conventions we have now will make it easier for people to continue upgrading until it's time for a major migration.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the decision to not include the Response in the signature is as close to a sure thing as possible

This is a sure thing at this point; all those working on http-interop have agreed it should not be present.

In terms of upgrading, in the case of these two middleware, it's an implementation detail unless they choose to write their own implementations. If they do, they can do so using whatever conventions make sense to them. We'd just be providing minimalist barebones implementations (though the ErrorHandler can likely be used verbatim everywhere, due to the ability to inject a response generator).

use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface;

class NotFoundHandler
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@weierophinney I'm a little unclear on $responsePrototype here as well, in addition to the different __invoke signature. If we add the response param to __invoke, then that could be used as the prototype, and if we add $next, then the NoopFinalHandler will complete the pipe and return the response back, while still giving the user the option to execute some additional middleware after the NotFoundHandler without having to extend it (perhaps logging or metrics gathering of some sort following a 404). I think in that case the NotFoundHandler would also be able to implement MiddlewareInterface.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please see my response above regarding the ErrorHandler.

Essentially, you should only manipulate the response when traversing outwards; i.e., the response returned by $next().

Since this particular middleware does not call on $next(), I omitted that argument entirely. However, to be useful in other stacks, it would need to implement the full interface. I'll update both of these middleware to do so shortly.

@weierophinney
Copy link
Member Author

Incorporated feedback from @vaclavvanik and @michaelmoussa .

@weierophinney
Copy link
Member Author

@vaclavvanik I've adopted the idea of injecting a response generator via the ErrorHandler constructor; if none is provided, the default ErrorResponseGenerator is used, in production mode. Thanks for the suggestion!


```php
// setup error handling
$app->pipe(new ErrorHandler(new Response(), new ErrorHandler($isDevelopmentMode));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should'nt be $app->pipe(new ErrorHandler(new Response(), new ErrorResponseGenerator($isDevelopmentMode));?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! fixed!

* Listeners are attached using the attachListener() method, and triggered
* in the order attached.
*/
final class ErrorHandler implements MiddlewareInterface
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@weierophinney should this be final if we have the extra attachListener public method w/no interface to cover it? (i'm sure you've seen Marco's blog post on the subject). Perhaps final class ErrorHandler implements ErrorHandlingMiddleware instead, with this interface?

interface ErrorHandlerMiddleware extends MiddlewareInterface
{
    public function attachListener(callable $listener);
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OTOH it actually might not matter in this case since nothing typehints specifically on ErrorHandler, so it shouldn't affect anyone's ability to test or reuse this component.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@michaelmoussa Exactly. 😄

Essentially, if they need to extend it, it's small enough they can copy/paste. In terms of testing a system, they can provide a stub implementing MiddlewareInterface against which to perform assertions.

@weierophinney weierophinney self-assigned this Sep 28, 2016
@weierophinney weierophinney merged commit d413d5d into zendframework:develop Sep 28, 2016
weierophinney added a commit that referenced this pull request Sep 28, 2016
@weierophinney weierophinney deleted the feature/final-handler-deprecation branch September 28, 2016 21:39
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
v2
Complete
Development

Successfully merging this pull request may close these issues.

None yet

3 participants