Modernising ZF1 applications: Wrapping legacy application in a PSR-7 middleware

by Mateusz Tymek — on PHP, Zend Framework

Just as presented in the previous article, wrapping legacy application in a PSR-7 compatible middleware seems to be the best option for gradual modernisation process. In this post I will show how to do it, starting from Zend Framework 1 skeleton application.

Before jumping into the code, let’s consider some challenges we’ll be facing.

Challenges

ZF1 was written with a different design approach in mind than modern frameworks. Back in the days, we didn't have service containers, we had to rely on a fragile global state, initialised in a Bootstrap class and shared between services using the static Zend_Registry object. Good old times.

As a consequence, ZF1 added heavy footprint on application bootstrap. We don’t want to drag this complexity along to our modern codebase - it should remain fast. Anything legacy should be bootstrapped directly from the middleware when we are sure that given request should not be handled by Zend Expressive.

Another challenge also comes from using global state - ZF1 apps are relying on header() and exit() functions, making it impossible to convert PSR-7 requests and responses to their ZF1 equivalents.

Finally, both frameworks have their own ways of dealing with configuration and handling errors. I will address it in another article.

I prepared a simple skeleton of ZF1 application that you can use to test everything from this article. Otherwise, you can start with your own legacy application as a base.

Optional: install ZF1 skeleton

Install Zend Expressive Skeleton and merge it with the new app Somewhere on the side, create an application based on Zend Expressive installer:

$ composer create-project zendframework/zend-expressive-skeleton expressive-skeleton

You can choose any options, but in order to stay compatible with this tutorial, I recommend selecting “Modular structure”, “zend-servicemanager”, “FastRoute”, “zend-view” and “Whoops”.

Now you have to merge the two apps together. You can simply move the following directories from Zend Expressive skeleton to legacy ZF1 app: bin, config and src and replace main entry point (index.php) with the Expressive one:

$ mv expressive-skeleton/{bin,config,src} zf1-to-expressive/
$ mv expressive-skeleton/public/index.php zf1-to-expressive/public/

Now merge composer.json file from the legacy repository with the Zend Expressive one. The ultimate contents will depend on what you had there previously. For example, it could look like this:

{
    "require": {
        "php": "^7.1",
        "zendframework/zend-component-installer": "^2.1.1",
        "zendframework/zend-config-aggregator": "^1.0",
        "zendframework/zend-diactoros": "^1.7.1",
        "zendframework/zend-expressive": "^3.0.1",
        "zendframework/zend-expressive-helpers": "^5.0",
        "zendframework/zend-stdlib": "^3.1",
        "zendframework/zend-servicemanager": "^3.3",
        "zendframework/zend-expressive-fastroute": "^3.0",
        "zendframework/zend-expressive-zendviewrenderer": "^2.0",
        "zendframework/zendframework1": "^1.12"
    },
    "require-dev": {
        "phpunit/phpunit": "^7.0.1",
        "roave/security-advisories": "dev-master",
        "squizlabs/php_codesniffer": "^2.9.1",
        "zendframework/zend-expressive-tooling": "^1.0",
        "zfcampus/zf-development-mode": "^3.1",
        "filp/whoops": "^2.1.12"
    },
    "autoload": {
        "psr-4": {
            "App\\": "src/App/src/"
        }
    }
}

Run composer install to ensure all packages are in place. This should leave you with the following directory layout:

Build ZF1 Middleware

Finally, we are getting to the magic part and the main point of this article. We want to build a middleware that bootstraps and runs ZF1 application. Let’s put it in src/App/src/Integration/Zf1Middleware.php file:

<?php

declare(strict_types=1);

namespace App\Integration;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Zend_Application;

class Zf1Middleware implements MiddlewareInterface
{
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        // Default ZF1 bootstrap - copied from legacy skeleton's index.php
        defined('APPLICATION_PATH')
            || define('APPLICATION_PATH', realpath('application'));
        defined('APPLICATION_ENV')
            || define('APPLICATION_ENV', (getenv('APPLICATION_ENV') ? getenv('APPLICATION_ENV') : 'production'));
        set_include_path(implode(PATH_SEPARATOR, array(
            realpath(APPLICATION_PATH . '/../vendor/zendframework/zendframework1/library'),
            get_include_path(),
        )));
        $application = new Zend_Application(
            APPLICATION_ENV,
            APPLICATION_PATH . '/configs/application.ini'
        );

        // Run ZF1 application
        $application->bootstrap()
            ->run();

        // Exit in order to avoid conflicts with Zend Expressive
        exit;
    }
}

As you see, I pretty much copied and pasted what was previously in the original index.php file. You may need to alter it, depending on your legacy application's specific requirements. Note that we have to stop the application with exit() function, in order to solve the problem with ZF1 application using the global state. Without this function, you would get an exception:

There are alternative ways of dealing with this problem - if you wanted, you could extend HttpHandlerRunner and prevent it from rising exception if ZF1 code is being processed. It should be good enough to start with though.

Let’s enable our new middleware, by updating config/pipeline.php file. Go to the end of this file and replace the default “not found” handler:

// replace this:
$app->pipe(NotFoundHandler::class);

// with this:
$app->pipe(\App\Integration\Zf1Middleware::class);

In order to test it, go to config/routes.php file and remove the home route by commenting it out:

// $app->get('/', App\Handler\HomePageHandler::class, 'home');
$app->get('/api/ping', App\Handler\PingHandler::class, 'api.ping’);

Now, open your app in the browser. Et voilà! You will see old starting page. If you browse to /app/ping, you should see the Expressive part.

Bonus: keeping the code clean

The middleware from above looks simple, but by stopping here, we risk that it will grow in the future and become yet another chunk of legacy code. Before going forward, let's refactor it a bit. What's the best way of doing so?

At a first glance, our middleware has two responsibilities: creating application object and invoking it. Let's extract that first responsibility into a factory:

<?php

declare(strict_types=1);

namespace App\Integration;

use Psr\Container\ContainerInterface;
use Zend_Application;

class Zf1ApplicationFactory
{
    public function __invoke(ContainerInterface $container): Zend_Application
    {
        defined('APPLICATION_PATH')
            || define('APPLICATION_PATH', realpath('application'));
        defined('APPLICATION_ENV')
            || define('APPLICATION_ENV', (getenv('APPLICATION_ENV') ? getenv('APPLICATION_ENV') : 'production'));
        set_include_path(implode(PATH_SEPARATOR, array(
            realpath(APPLICATION_PATH . '/../vendor/zendframework/zendframework1/library'),
            get_include_path(),
        )));
        $application = new Zend_Application(
            APPLICATION_ENV,
            APPLICATION_PATH . '/configs/application.ini'
        );

        return $application;
    }
}

With this change, our middleware will look much nicer:

<?php

declare(strict_types=1);

namespace App\Integration;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Zend_Application;

class Zf1Middleware implements MiddlewareInterface
{
    /** @var Zend_Application */
    private $zf1Application;

    public function __construct(Zend_Application $zf1Application)
    {
        $this->zf1Application = $zf1Application;
    }

    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        $this->zf1Application->bootstrap()
            ->run();

        // Exit in order to avoid conflicts with Zend Expressive
        exit;
    }
}

We will also need a new factory for our middleware:

<?php

declare(strict_types=1);

namespace App\Integration;

use Psr\Container\ContainerInterface;
use Zend_Application;

class Zf1MiddlewareFactory
{
    public function __invoke(ContainerInterface $container): Zf1Middleware
    {
        return new Zf1Middleware(
            $container->get(Zend_Application::class)
        );
    }
}

Lastly, let's wire all pieces together by editing App\ConfigProvider class. New getDependencies method should look like this:

public function getDependencies() : array
    {
        return [
            'invokables' => [
                Handler\PingHandler::class => Handler\PingHandler::class,
            ],
            'factories'  => [
                Handler\HomePageHandler::class => Handler\HomePageHandlerFactory::class,
                Zf1Middleware::class => Zf1MiddlewareFactory::class,
                Zend_Application::class => Zf1ApplicationFactory::class,
            ],
        ];
    }

Conclusion

You now have two applications working alongside each other. Whenever your team is working on something, they can now take one ZF1 action at a time and refactor it as an Expressive request handler. That’s great, but we’re not finished yet - you could achieve the same result by configuring your web server. However, this is an investment - the first step towards deeper integration between legacy and modern.

In the next post in the series, I will show how to share services between legacy and modern application.

You can check out the working example on Github, by cloning this repository and checking out part2-expressive-integration branch.


comments powered by Disqus