Migration

Migration to Mezzio 1.1

Mezzio 1.1 should not result in any upgrade problems for users. However, starting in this version, we offer a few changes affecting the following that you should be aware of, and potentially update your application to adopt:

  • Deprecations
  • Original request and response messages
  • Recommendation to use programmatic pipelines
  • Error handling

Deprecations

The following classes and/or methods are deprecated with the 1.1.0 release, and will be removed for the 2.0 release:

  • Mezzio\Application::pipeErrorHandler(): Stratigility v1 error middleware are removed in the Stratigility v2 release, which Mezzio 2.0 will adopt.

  • Mezzio\Application::routeMiddleware(): routing middleware moves to a dedicated class starting in Mezzio 2.0. If you were referencing the method in order to pipe it as middleware, use pipeRoutingMiddleware() or pipe(ApplicationFactory::ROUTING_MIDDLEWARE) instead.

  • Mezzio\Application::dispatchMiddleware(): dispatch middleware moves to a dedicated class starting in Mezzio 2.0.If you were referencing the method in order to pipe it as middleware, use pipeDispatchMiddleware() or pipe(ApplicationFactory::DISPATCH_MIDDLEWARE) instead.

  • Mezzio\Application::getFinalHandler(): this method gets renamed to getDefaultDelegate() in Mezzio 2.0. We recommend retrieving the value from the application dependency injection container if you need it elsewhere.

  • Mezzio\Application::raiseThrowables(): this method becomes a no-op in Stratigility 2.0, on which Mezzio 2.0 is based; the behavior it enabled becomes the default behavior in that version.

  • Mezzio\Container\Exception\InvalidArgumentException: this exception type is thrown by ApplicationFactory; in Mezzio 2.0, it throws Mezzio\Exception\InvalidArgumentException instead.

  • Mezzio\Container\Exception\NotFoundException: this exception type is not currently used anyway.

  • Mezzio\ErrorMiddlewarePipe: Stratigility v1 error middleware are removed in the Stratigility v2 release, which Mezzio 2.0 will adopt, making this specialized middleware pipe type irrelvant.

  • Mezzio\TemplatedErrorHandler and Mezzio\WhoopsErrorHandler: The concept of "final handlers" will be removed in Mezzio 2.0, to be replaced with "default delegates" (implementations of Interop\Http\ServerMiddleware\DelegateInterface that will be called if the middleware pipeline is exhausted, and which will be guaranteed to return a response). Mezzio 2.0 will provide tooling to upgrade your dependencies to make the transition seamless; end users will only be affected if they were extending these classes.

If you were calling any of these directly, or extending or overriding them, you will need to update your code to work for version 2.0. We recommend not using these.

Original messages

Stratigility 1.3 deprecates its internal request and response decorators, Laminas\Stratigility\Http\Request and Laminas\Stratigility\Http\Response, respectively. The main utility of these instances was to provide access in inner middleware layers to the original request, original response, and original URI.

As such access may still be desired, Stratigility 1.3 introduced Laminas\Stratigility\Middleware\OriginalMessages. This middleware injects the following attributes into the request it passes to $next():

  • originalRequest is the request instance provided to the middleware.
  • originalUri is the URI instance associated with that request.
  • originalResponse is the response instance provided to the middleware.

Laminas\Stratigility\FinalHandler was updated to use these when they're available starting with version 1.0.3.

We recommend adding the OriginalMessages middleware as the outermost (first) middleware in your pipeline. Using configuration-driven middleware, that would look like this:

// config/autoload/middleware-pipeline.global.php
/* ... */
use Mezzio\Helper;
use Laminas\Stratigility\Middleware\OriginalMessages;

return [
    'dependencies' => [
        'invokables' => [
            OriginalMessages::class => OriginalMessages::class,
        ],
        /* ... */
    ],
    'middleware_pipeline' => [
        'always' => [
            'middleware' => [
                OriginalMessages::class, // <----- Add this entry
                Helper\ServerUrlMiddleware::class,
                /* ... */
            ],
            'priority' => 10000,
        ],

        /* ... */
    ],
];

If you are programmatically creating your pipeline, use the following:

$app->pipe(OriginalMessages::class);
/* all other middleware */

Identifying and fixing getOriginal calls

To help you identify and update calls in your own code to the getOriginal*() methods, we provide a tool via the mezzio/mezzio-tooling package, vendor/bin/mezzio-migrate-original-messages.

First, install the tooling package; since the tooling it provides is only useful during development, install it as a development requirement:

$ composer require --dev mezzio/mezzio-tooling

Once installed, you can execute the tool using:

$ ./vendor/bin/mezzio-migrate-original-messages

Passing the arguments help, --help, or -h will provide usage information; in most cases, it will assume sane defaults in order to run its scans.

The tool updates calls to getOriginalRequest() and getOriginalUri() to instead use the new request attributes that the OriginalMessages middleware injects:

  • getOriginalRequest() becomes getAttribute('originalRequest', $request)
  • getOriginalUri() becomes getAttribute('originalUri', $request->getUri())

In both cases, $request will be replaced with whatever variable name you used for the request instance.

For getOriginalResponse() calls, which happen on the response instance, the tool will instead tell you what files had such calls, and detail how you can update those calls to use the originalResponse request attribute.

Programmatic middleware pipelines

With Mezzio 1.0, we recommended creating middleware pipelines and routing via configuration. Starting with 1.1, we recommend programmatic creation of pipelines and routing.

Programmatic pipelines exercise the existing Mezzio API. Methods include:

  • pipe() allows you to pipe middleware for the pipeline; this can optionally take a $path argument. (If one argument is present, it is assumed to be middleware; with two arguments, the first argument is the $path.) Paths are literal URI path segments. If the incoming request matches that segment, the middleware will execute; otherwise, it will not. These can be used to provide sub-applications with their own routing.

  • pipeRoutingMiddleware() is used to pipe the internal routing middleware into the pipeline.

  • pipeDispatchMiddleware() is used to pipe the internal dispatch middleware into the pipeline.

  • pipeErrorMiddleware() is used to pipe the legacy Stratigility error middleware into the pipeline. We recommend NOT using this method, and instead adapting your application to use standard middleware for error handling. Otherwise, it acts just like pipe(). Starting in Mezzio 1.1, this method will emit a deprecation notice.

As an example pipeline:

$app->pipe(OriginalMessages::class);
$app->pipe(Helper\ServerUrlMiddleware::class);
$app->pipe(ErrorHandler::class);
$app->pipeRoutingMiddleware();
$app->pipe(Helper\UrlHelperMiddleware::class);
$app->pipeDispatchMiddleware();
$app->pipe(NotFoundHandler::class);

Mezzio also provides methods for specifying routed middleware. These include:

  • get($path, $middleware, $name = null)
  • post($path, $middleware, $name = null)
  • put($path, $middleware, $name = null)
  • patch($path, $middleware, $name = null)
  • delete($path, $middleware, $name = null)
  • route($path, $middleware, array $methods = null, $name = null)

Each returns a Mezzio\Router\Route instance; this is useful if you wish to provide additional options to your route:

$app->get('/api/ping', Ping::class)
    ->setOptions([
        'timestamp' => date(),
    ]);

As an example, the default routes defined in the skeleton application can be written as follows:

$app->get('/', \App\Action\HomePageAction::class, 'home');
$app->get('/api/ping', \App\Action\PingAction::class, 'api.ping');

We recommend rewriting your middleware pipeline and routing configuration into programmatic/declarative statements. Specifically:

  • We recommend putting the pipeline declarations into config/pipeline.php.
  • We recommend putting the routing declarations into config/routes.php.

Once you've written these, you will then need to make the following changes to your application:

  • First, enable the mezzio.programmatic_pipeline configuration flag. This can be done in any config/autoload/*.global.php file:
return [
    'mezzio' => [
        'programmatic_pipeline' => true,
    ],
];

Once enabled, any middleware_pipeline or routes configuration will be ignored when creating the Application instance.

  • Second, update your public/index.php to add the following lines immediately prior to calling $app->run();:
require 'config/pipeline.php';
require 'config/routes.php';

Once this has been done, the application will use your new programmatic pipelines instead of configuration. You can remove the middleware_pipeline and routes configuration after verifying your application continues to work.

For programmatic pipelines to work properly, you will also need to provide error handling middleware, which is discussed in the next section.

Error handling

Prior to version 1.1, error handling was accomplished via two mechanisms:

  • Stratigility "error middleware" (middleware with the signature function ($error, ServerRequestInterface $request, ResponseInterface $response, callable $next)). This middleware would be invoked when calling $next() with a third argument indicating an error, and would be expected to handle it or delegate to the next error middleware.

Internally, Stratigility would execute each middleware within a try/catch block; if an exception were caught, it would then delegate to the next error middleware using the caught exception as the $err argument.

  • The "Final Handler". This is a handler invoked when the middleware pipeline is exhausted without returning a response, and has the signature function (ServerRequestInterface $request, ResponseInterface $response, $err = null); it is provided to the middleware pipeline when invoking the outermost middleware; in the case of Mezzio, it is composed in the Application instance, and passed to the application middleware when it executes run(). When invoked, it needs to decide if invocation is due to no middleware executing (HTTP 404 status), middleware calling $next() with an altered response (response is then returned), or due to invocation of error middleware (calling $next() with the third, error, argument) with no error middleware returning a response.

Mezzio 1.1 updates the minimum supported Stratigility version to 1.3, which deprecates the concept of error middleware, and recommends a "final handler" that does no error handling, but instead returns a canned response (typically a 404). Additionally, it deprecates the practice of wrapping middleware execution in a try/catch block, and provides a flag for disabling that behavior entirely, raise_throwables.

Starting in Mezzio 1.1, you can set the raise_throwables flag in your configuration:

return [
    'mezzio' => [
        'raise_throwables' => true,
    ],
];

When enabled, the internal dispatcher will no longer catch exceptions.

This both allows you to, and requires you to, write your own error handling middleware. This will require two things:

  • Middleware with a try/catch block that operates as the outermost (or close to outermost) layer of your application, and which can provide error pages or details to your end users.
  • Middleware at the innermost layer that is guaranteed to return a response; generally, reaching this means no middleware was able to route the request, and thus a 404 condition.

The below sections detail approaches to each.

Error handling middleware

Error handling middleware generally will look something like this:

function (
    ServerRequestInterface $request,
    ResponseInterface $response,
    callable $next
) {
    try {
        $response = $next($request, $response);
        return $response;
    } catch (\Throwable $exception) {
        // caught PHP 7 throwable
    } catch (\Exception $exception) {
        // caught PHP 5 exception
    }

    // ...
    // do something with $exception and generate a response
    // ...

    return $response;
}

Stratigility 1.3 provides such an implementation via its Laminas\Stratigility\Middleware\ErrorHandler. In addition to the try/catch block, it also sets up a PHP error handler that will catch any PHP error types in the current error_reporting mask; the error handler will raise exceptions of the type ErrorException with the PHP error details.

Stratigility's ErrorHandler allows injection of an "error response generator", which allows you to alter how the error response is generated based on the current environment. Error response generators are callables with the signature:

function (
    Throwable|Exception $e,
    ServerRequestInterface $request,
    ResponseInterface $response
) : ResponseInterface

We recommend using the Stratigility ErrorHandler and writing and attaching a custom error response generator. As a simple example, the following details a generator that will use a template to display an error page:

namespace Acme;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Mezzio\Template\TemplateRendererInterface;

class TemplatedErrorResponseGenerator
{
    const TEMPLATE_DEFAULT = 'error::error';

    private $renderer;

    private $template;

    public function __construct(
        TemplateRendererInterface $renderer,
        $template = TEMPLATE_DEFAULT
    ) {
        $this->renderer = $renderer;
        $this->template = $template;
    }

    public function __invoke(
        $e,
        ServerRequestInterface $request,
        ResponseInterface $response
    ) {
        $response->write($this->renderer->render($this->template, [
            'exception' => $e,
            'request'   => $request,
        ]));
        return $response;
    }
}

You might then create a factory for generating the ErrorHandler and attaching this response generator as follows:

namespace Acme\Container;

use Acme\TemplatedErrorResponseGenerator;
use Interop\Container\ContainerInterface;
use Laminas\Diactoros\Response;
use Mezzio\Template\TemplateRendererInterface;
use Laminas\Stratigility\Middleware\ErrorHandler;

class ErrorHandlerFactory
{
    public function __invoke(ContainerInterface $container)
    {
        $generator = new TemplatedErrorResponseGenerator(
            $container->get(TemplateRendererInterface::class)
        );

        return new ErrorHandler(new Response(), $generator);
    }
}

Once that is created you can tell your middleware configuration about it:

// in config/autoload/middleware-pipeline.global.php
use Acme\Container\ErrorHandlerFactory;
use Laminas\Stratigility\Middleware\ErrorHandler;

return [
    'dependencies' => [
        /* ... */
        'factories' => [
            ErrorHandler::class => ErrorHandlerFactory::class,
            /* ... */
        ],
        /* ... */
    ],
    'middleware_pipeline' => [
        'always' => [
            'middleware' => [
                ErrorHandler::class,
                /* ... */
            ],
            'priority' => 10000,
        ],
        /* ... */
    ],
];

Alternately, if using a programmatic pipeline, as detailed in the previous section, you can use the following:

use Laminas\Stratigility\Middleware\ErrorHandler;

$app->pipe(ErrorHandler::class);
// add all other middleware after it

Not Found middleware

At the innermost layer of your application, you need middleware guaranteed to return a response; typically, this indicates a failure to route the request, and, as such, an HTTP 404 response. Laminas\Stratigility\Middleware\NotFoundHandler provides an implementation, but is written such that the response body remains empty. As such, you might write a custom, templated handler:

namespace Acme;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Laminas\Diactoros\Response;
use Mezzio\Template\TemplateRendererInterface;

class TemplatedNotFoundHandler
{
    const TEMPLATE_DEFAULT = 'error::404';

    private $renderer;

    private $template;

    public function __construct(
        TemplateRendererInterface $renderer,
        $template = self::TEMPLATE_DEFAULT
    ) {
        $this->renderer = $renderer;
        $this->template = $template;
    }

    public function __invoke(
        ServerRequestInterface $request,
        ResponseInterface $response,
        callable $next
    ) {
        $response = new Response();
        $response->write($this->renderer->render($this->template));
        return $response->withStatus(404);
    }
}

Similar to the discussion of the ErrorHandler above, we'll create a factory for this:

namespace Acme\Container;

use Acme\TemplatedNotFoundHandler;
use Interop\Container\ContainerInterface;
use Mezzio\Template\TemplateRendererInterface;

class TemplatedNotFoundHandlerFactory
{
    public function __invoke(ContainerInterface $container)
    {
        return new TemplatedNotFoundHandler(
            $container->get(TemplateRendererInterface::class)
        );
    }
}

We can then register it in our pipeline:

// in config/autoload/middleware-pipeline.global.php
use Acme\Container\NotFoundHandlerFactory;
use Acme\TemplatedNotFoundHandler;

return [
    'dependencies' => [
        /* ... */
        'factories' => [
            TemplatedNotFoundHandler::class => TemplatedNotFoundHandlerFactory::class,
            /* ... */
        ],
        /* ... */
    ],
    'middleware_pipeline' => [
        /* ... */

        // After 'routing', but before 'error';
        // alternately as last item in 'routing' middleware list.
        'not-found' => [
            'middleware' => TemplatedNotFoundHandler::class,
            'priority' => 0,
        ],

        /* ... */
    ],
];

If you are using programmatic pipelines, as described in the previous section:

use Acme\TemplatedNotFoundHandler;

// all other pipeline directives, and then:
$app->pipe(TemplatedNotFoundHandler::class);

Detecting error middleware usage

If you use the new error handling paradigm, we recommend that you also audit your application for legacy Stratigility error middleware, as well as invocation of error middleware. To do this, we provide a tool via the mezzio/mezzio-tooling package, vendor/bin/mezzio-scan-for-error-middleware.

First, install the tooling as a development requirement:

$ composer require --dev mezzio/mezzio-tooling

The tool will scan the src/ directory by default, but allows you to scan other directories via the --dir flag. It will detect and report files with any of the following:

  • Classes implementing Laminas\Stratigility\ErrorMiddlewareInterface.
  • Invokable classes implementing the error middleware signature.
  • Methods accepting $next that invoke it with an error argument.

As an example running it:

$ ./vendor/bin/mezzio-scan-for-error-middleware scan
# or, with a directory argument:
$ ./vendor/bin/mezzio-scan-for-error-middleware scan --dir ./lib

You may also call the tool using its help command, or either of the --help or -h flags to get full usage information.

Use this tool to identify potential problem areas in your application, and update your code to use the new error handling facilities as outlined above.

Full example

Putting all of the above together — original message memoizing, programmatic pipelines, and middleware-based error handling — might look like the following examples.

First, we'll tell Mezzio to use programmatic pipelines, and to enable the new error handling (by telling it to "raise throwables", instead of catching them):

// In config/autoload/mezzio.global.php:
return [
    /* ... */
    'mezzio' => [
        'programmatic_pipeline' => true,
        'raise_throwables' => true,
        /* ... */
    ],
];

Next, we'll update config/autoload/middleware-pipeline.global.php to list only dependencies:

use Acme\Container;
use Acme\TemplatedNotFoundHandler;
use Laminas\Stratigility\Middleware\ErrorHandler;
use Laminas\Stratigility\Middleware\OriginalMessages;
use Mezzio\Helper;

return [
    'dependencies' => [
        'invokables' => [
            OriginalMessages::class => OriginalMesssages::class,
        ],
        'factories' => [
            ErrorHandler::class => Container\ErrorHandlerFactory::class,
            Helper\ServerUrlMiddleware::class => Helper\ServerUrlMiddlewareFactory::class,
            Helper\UrlHelperMiddleware::class => Helper\UrlHelperMiddlewareFactory::class,
            TemplatedNotFoundHandler::class => Container\TemplatedNotFoundHandlerFactory::class,
        ],
    ],
];

We'll also update config/autoload/routes.global.php to only list dependencies; in the following example, we list only the middleware shipped by default with the skeleton application:

use App\Action;
use Mezzio\Router\FastRouteRouter;
use Mezzio\Router\RouterInterface;

return [
    'dependencies' => [
        'invokables' => [
            RouterInterface::class => FastRouteRouter::class,
            Action\PingAction::class => Action\PingAction::class,
        ],
        'factories' => [
            Action\HomePageAction::class => Action\HomePageFactory::class,
        ],
    ],
];

To create our pipeline, we will create the file config/pipeline.php:

use Acme\TemplatedNotFoundHandler;
use Laminas\Stratigility\Middleware\ErrorHandler;
use Laminas\Stratigility\Middleware\OriginalMessages;
use Mezzio\Container\ApplicationFactory;
use Mezzio\Helper;

$app->pipe(OriginalMessages::class);
$app->pipe(ErrorHandler::class);
$app->pipe(Helper\ServerUrlMiddleware::class);
$app->pipe([
    ApplicationFactory::ROUTING_MIDDLEWARE,
    Helper\UrlHelperMiddleware::class,
    ApplicationFactory::DISPATCH_MIDDLEWARE,
]);
$app->pipe(TemplatedNotFoundHandler::class);

Note that you can use arrays of middleware just like you did in the configuration; this allows you to separate middleware into logical groups if desired!

To provide our routed middleware, we will create the file config/pipeline.php:

use App\Action;

$app->get('/', Action\HomePageAction::class, 'home');
$app->get('/api/ping', Action\PingAction::class, 'api.ping');

The above exercises the various routing methods of the Application class.

Finally, we will need to update our public/index.php, to tell it to require our new pipeline and routing files; we'll do that between retrieving the application from the container, and running the application:

$app = $container->get(\Mezzio\Application::class);
require 'config/pipeline.php';
require 'config/routes.php';
$app->run();

With these changes in place, your application should continue to run as it did previously!

Looking forward

Mezzio 2.0 will ship error handling middleware and "not found" middleware, as well as tools to convert your application to a programmatic pipeline in such a way as to utilize these shipped implementations. In the meantime, however, you can adopt programmatic pipelines and the new error handling paradigm within the version 1 series using the configuration flags and guidelines listed above in order to make your application forwards-compatible.