Internetagentur

Migration from Zend Framework 1 to Zend Expressive


Today I will show my approach for successively migrating a Zend Framework 1 application to Zend Expressive. As the project is quite big, a complete rewrite is especially under economic aspects not tenable. Our target was to successively migrate those components to the new architecture when they anyways need to be changed due to new feature requests. 

Of course this will be an ongoing effort over several years until everything will be migrated. Thus one requirement is, that existing legacy services and classes can also be used in the new architecture (e.g. by facades). Basically we can use everything from the legacy system except the MVC (Controllers, views etc.) which does not hurt.

The steps to get there were pretty easy:

Eleminate ZF1 autoloading and migrate to composer

The first step was to migrate all ZF1 style autoloading to composer. The application was build with the typical ZF1 like folder structure (e.g. Backend_) which is not PSR-0 compatible:

application
|- controllers (e.g. Backend_Controller)
|- db
|- ....

To make that easily PSR-0 compatible I introduced a virtual directory "Backend" with subdirectories as symlinks linking to the equivalent real directories

application
|- Backend_
    |- Controller (symlink to application/controllers)
    |- Db (symlink to application/db)
|- controllers (e.g. Backend_Controller)
|- db (e.g. Backend_Db)
|- ....

After that we were easily able to load the classes through composer

"psr-0": {
    "Backend_": "./"
},

Decide which application to load upon routes in the central index.php bootstrap file

As I wanted to have the old an the new system clearly devided, I decided to keep them standalone. E.g. requests which can be served from the legacy system will be still served from there without firing up Zend Expressive.

Therefore in the central index.php bootstrap file I consumed the Zend Expressive routes from the new system. If the new system can´t serve the request, the legacy application is used as is. This required to use configuration driven routes in Zend Expressive.

index.php

$router = new \Zend\Expressive\Router\ZendRouter();
$config = (new ConfigProvider())();

foreach($config['routes'] AS $spec){
    $methods = \Zend\Expressive\Router\Route::HTTP_METHOD_ANY;
    if (isset($spec['allowed_methods'])) {
        $methods = $spec['allowed_methods'];
        if (! is_array($methods)) {
            throw new Exception\InvalidArgumentException(sprintf('
                'Allowed HTTP methods for a route must be in form of an array; received "%s"',
                gettype($methods)'
            ));
        }

    }

    $name  = isset($spec['name']) ? $spec['name'] : null;
    $route = new \Zend\Expressive\Router\Route($spec['path'], $spec['middleware'], $methods, $name);

    if (isset($spec['options'])) {
        $options = $spec['options'];
        if (! is_array($options)) {
            throw new Exception\InvalidArgumentException(sprintf(
                'Route options must be an array; received "%s"',
                gettype($options)
            ));
        }

        $route->setOptions($options);
    }
    $router->addRoute($route);
}

$result = $router->match(\Zend\Diactoros\ServerRequestFactory::fromGlobals());

if($result->isSuccess()){
    include_once('indexExpressive.php');
}else{
    include_once('indexLegacy.php');
}

Configure Expressive for configuration driven routes:

pipeline.php
....

$app->injectRoutesFromConfig((new ConfigProvider())());
.....

Consuming ZF1 configuration and classes in Zend Expressive

To be able to consume the legacy services and classes in Zend Expressive I had to take two steps:

  1. Consume the ZF1 configuration (as the legacy layer needs it to get setup) and I did not want to configure the same things in two places
  2. Bootstrap the ZF1 Zend_Registry services used through the static calls to Zend_Registry::get() over and over in the legacy services and classes. Therefore I introduced a LegacyMiddleware and a BootstrapTrait class which bootstrapped the same services for the legacy system (typically found in Bootstrap.php) and the new application.

The ZF1 configuration was loaded within the Expressive config aggregator through a LegacyConfigProvider:

class LegacyConfigProvider
{
    use \BootstrapTrait;

    /**
     * Returns the configuration array
     *
     * To add a bit of a structure, each section is defined in a separate
     * method which returns an array with its configuration.
     *
     * @return array
     */

    public function __invoke()
    {
        $path = '...../application.ini';
        $configZf1 = new \Zend_Config_Ini(.../application.ini,APPLICATION_ENV,array('allowModifications' => true));'
    }

        return [
            'legacy' => $configZf1->toArray() //only used for setting up ZF1 services in the LegacyMiddleware
        ];
    }
}

The ZF1 Zend_Registry services are bootstrapped within a LegacyMiddleware. Once the migration is done I just need to remove the Legacy Middleware and all connections to the legacy application are dropped.

pipeline.php
....
$app->pipe(LegacyMiddleware::class); 
....

class LegacyMiddleware implements MiddlewareInterface

{
    use \BootstrapTrait;
     private $entityManager;
     private $config;
     /**
     * LegacyMiddleware constructor.
     * @param $config
     */
    public function __construct($config, $entityManager)
    {
        $this->config = $config;
        $this->entityManager = $entityManager;
    }
     public function process(ServerRequestInterface $request, DelegateInterface $delegate)
    {
        $legacyConfig = $this->getLegacyConfig();
         $this->initConfig($this->getLegacyConfigObj());
        $this->_initDb();
        $this->_initSystemLog();
        $this->_initI18();
        $this->initTimezone($legacyConfig['system']);
         $this->_initSystemSession();
        $this->_initAuth();
        $this->_initDoctrine();
         return $delegate->process($request);
    }
      private function _initDb(){
        $legacyConfig = $this->getLegacyConfig();
         $plugin = new \Zend_Application_Resource_Db();
        $plugin->setAdapter($legacyConfig['resources']['db']['adapter']);
        $plugin->setParams($legacyConfig['resources']['db']['params']);
        $adapter = $plugin->getDbAdapter();
        $this->initDb($adapter);
    }
     private function _initSystemLog(){
         $this->initSystemLog($this->getLegacyConfigObj());
    }
     private function _initI18(){
        $legacyConfig = $this->getLegacyConfig();
         $plugin = new \Zend_Application_Resource_Locale();
        $plugin->setOptions($legacyConfig['resources']['locale']);
         $currentLocale = Locale::getDefault();
         $locale = $plugin->getLocale();
        $locale->setLocale($currentLocale); // Set the current request locale which is determined in SetLocaleMiddleware
         $this->initI18($locale);
     }
     private function _initSystemSession(){
        $legacyConfig = $this->getLegacyConfig();
         $plugin = new \Zend_Application_Resource_Session();
        $plugin->setOptions($legacyConfig['resources']['session']);
        $plugin->init();
         $this->initSystemSession();
    }
      private function _initAuth(){
        $legacyConfig = $this->getLegacyConfig();
        $plugin = new \Zend_Application_Resource_Session($legacyConfig['resources']['session']);
        $plugin->init();
    }
     private function _initDoctrine(){
        \Zend_Registry::set('Doctrine/ORM/EntityManager', $this->getEntityManager());
    }
     /**
     * @return mixed
     */
    public function getEntityManager()
    {
        return $this->entityManager;
    }
      /**
     * @return mixed
     */
    private function getConfig()
    {
        return $this->config;
    }
     /**
     * @return mixed
     */
    private function getLegacyConfig()
    {
        $config = $this->config;
        return $config['legacy'];
    }
     private function getLegacyConfigObj(){
        return new \Zend_Config($this->getLegacyConfig());
    }
 }

Both - the legacy Bootstrap.php file as well as the expressive legacy middleware register the Zend_Registry services through the same trait:

trait BootstrapTrait
{
 public function initTimeZone($systemConfig)
    {
        $timezone = $systemConfig['timezone'];
        if ($timezone === null || empty($timezone)) {
            $timezone = 'Europe/Berlin';
        }
        date_default_timezone_set($timezone);
    }
     public function initConfig($config){
     \Zend_Registry::set('Zend_Config', $config);
    }
     public function initDb($dbAdapter){
        \Zend_Registry::set('db', $dbAdapter);
    }
     public function initSystemLog($config): void
    {
        try {
             $dataPath = $config->data->path;
             $logger = new \Zend_Log();
             //Do not log debug messages
            $filter = new \Zend_Log_Filter_Priority(Zend_Log::DEBUG, '<');
            $logger->addFilter($filter);
             $writer = new \Zend_Log_Writer_Stream(
                $dataPath . '/logs/error_' . date("Y-m-d") . '.log'
            );
            $filter = new \Zend_Log_Filter_Priority(Zend_Log::WARN, '<');
            $writer->addFilter($filter);
            $logger->addWriter($writer);
             $writer1 = new \Zend_Log_Writer_Stream(
                $dataPath . '/logs/info_' . date("Y-m-d") . '.log'
            );
            $filter = new \Zend_Log_Filter_Priority(Zend_Log::INFO, '>=');
            $writer1->addFilter($filter);
            $logger->addWriter($writer1);
             $writer2 = new \Zend_Log_Writer_Stream(
                $dataPath . '/logs/warn_' . date("Y-m-d") . '.log'
            );
            $filter = new \Zend_Log_Filter_Priority(Zend_Log::WARN, '>=');
            $writer2->addFilter($filter);
            $filter1 = new \Zend_Log_Filter_Priority(Zend_Log::INFO, '<');
            $writer2->addFilter($filter1);
            $logger->addWriter($writer2);
              $this->_log = $logger;
            \Zend_Registry::set('Zend_Log', $this->_log);
        } catch (Exception $e) {
            error_log($e->getMessage());
            error_log($e->getTraceAsString());
            die('Log file is not writable. Please fix!');
        }
    }
     /**
     * @param $locale
     */
    protected function initI18($locale): void
    {
        $lang = $locale->getLanguage();
         if (empty($lang)) {
            $lang = 'en';
        }
         $validationPath = APPLICATION_PATH . '/vendor/zendframework/zendframework1/resources/languages/'.$lang.'/Zend_Validate.php';
         $translate = new \Zend_Translate('Csv', APPLICATION_PATH . "/configs/languages/".$lang.".csv", $lang);
        $translate->setLocale($lang);
         $translate_i2 = new \Zend_Translate('Array', $validationPath, $lang);
        $translate_i2->setLocale($lang);
        $translate->addTranslation($translate_i2);
         \Zend_Registry::set('Zend_Translate', $translate);
    }
     public function initSystemSession(): void
    {
        $defaultNamespace = new \Zend_Session_Namespace();
        \Zend_Registry::set('Zend_Session', $defaultNamespace);
    }
 }