ZF to Laminas Migration

Two months ago, after making changes to one of my websites, I encountered this message after running composer update:

No developer likes nasty surprises … but in this case it was not entirely unexpected.  The announcement to move Zend Framework (ZF) to Laminas had been made many moons ago.  So … being faced with the new reality, I decided to bite the bullet and do the migration.  I won’t bore you with too many details, but it’s important to set the proper context in order to understand why you want to migrate your ZF apps to Laminas, so let’s climb into the Wayback Machine.  Mr. Peabody, if you will …

A Little Bit of History

Zend Framework 1 was introduced in March of 2006.  It had a successful run, and is still used to this day.  However, the increasing lengthy class names, and the problem of global namespace pollution, among other issues, led to a radical restructuring, resulting in Zend Framework 2 being announced in September of 2012.  After version 2.4, a shift occurred whereby components were split out into their own repositories, and became separate Composer packages, a change that allowed for faster and parallel development, not possible in the monolithic structure imposed by ZF 2.4 and earlier.  Eventually, through a gradual evolutionary process, Zend Framework 3 was announced in June of 2016.

In the meantime … in October 2015, Rogue Wave Software acquired Zend. And then, in January of 2019, Perforce announced its acquisition of Rogue Wave!  Matching dates, you can see that ZF went from version 2 to 3 while Zend was part of Rogue Wave Software.  Understand that Rogue Wave is a software company, so it made perfect sense for them to host Zend Framework.  However, the main focus of Perforce is version control, application lifecycle management, AGILE support and statistical code analysis (all taken from their homepage).  Where does Zend Framework fit into this picture?  Short answer: it doesn’t.  So, bearing in mind that Perforce does in fact support the framework, it doesn’t fit into their long term plans, so the framework needed to find a new home, so to speak.

After much behind-the-scenes negotiation, to which us common folk are not privy, a deal was struck with the Linux Foundation, an excellent choice all told.  There was a major show-stopper in the transition, however: the name Zend is a corporate brand, meaning for legal reasons a new name had to be chosen: Laminas.  That being the case, it made sense to also convert all of the PHP namespace references from Zend to Laminas, and thus the need for a migration tool.  It should also be noted, for the record, that Zend Expressive components are now in the Mezzio namespace, and Apigility components moved from zfcampus to Laminas\ApiTools.

OK … now that we’ve dealt with the elephant in the room, the next reasonable question is why migrate?

Why Migrate At All?

First and foremost, please bear in mind that even if you choose not to migrate for some insane reason, your code will still work.  Just because ZF has transitioned to Laminas doesn’t mean the original framework and all of its components mystically stop working. So, assuming that is the case, why bother to migrate at all?  The answer is seen in the screen shot above: all of the Zend Framework components, even though they still exist as-is in their respective repositories, experience no new development of any type.  As Composer clearly tells us: the code in the legacy Zend Framework repositories is abandoned, and that means no new features, no security patches, no bug fixes … no nothing.

So, at least for now, your Zend Framework applications are “safe” … however, even as we speak, evil hackers are busy discovering vulnerabilities and figuring out ways to exploit them.  If you fail to migrate now my crystal ball foresees the eventual downfall of your Zend Framework-based websites.  To quote directly from a blog posted by Matthew Weir O’Phinney, the project lead for Zend Framework, now Laminas:

“To receive security updates, bug fixes, and new features for your Zend Framework code, you need to migrate it to the Laminas-branded packages.”

If this doesn’t light your fire, consider this: if you migrate now, your code will work as-is, unchanged.  The reason is because right now (i.e. April 2020), the source code in the Zend namespace is identical to the code in the Laminas namespace.  But if you wait too long … the Laminas source code will start to grow and evolve as new features are added, enhancements made, bugs fixed, and security patches applied.

Migration steps are well documented on the new Laminas website.  The purpose of this article is to take you through an actual migration, and to highlight potential gotchas.  OK … seatbelts fastened?  Tray tables in the upright position? (Sigh … really miss those days!) Let’s start by looking at what all is involved in preparing for a ZF to Laminas migration.

Prepare to Migrate

Before proceeding with the migration, you need to perform a minimal amount of migration preparation.  Here is a high-level summary of the steps to take:

  • Update Composer and third party software
  • Prepare version control
  • Make sure your application works OK
  • Install the laminas-migration tool

Let’s start at the top and deal with Composer.

Update

The first thing you need to do is to update Composer itself.  This step is critical as, later in the process, you could very well see an error such as this:

Loading composer repositories with package information
Installing dependencies (including require-dev) from lock file
Package operations: 73 installs, 0 updates, 0 removals
  - Installing laminas/laminas-dependency-plugin (1.0.3): Loading from cache
PHP Fatal error:  Uncaught Error: Undefined class constant 'PRE_COMMAND_RUN' 
    in vendor/laminas/laminas-dependency-plugin/src/DependencyRewriterPlugin.php:63

To be on the safe side I would recommend the Composer version should be 1.9 or above.  To update Composer, if you have it installed as a phar file, use this syntax:

$ php composer.phar self-update

Otherwise, if you have it installed “globally” as a command, use this command:

$ composer self-update

Please note that if you have Composer installed globally, it’s probably in a restricted directory, so when you update it, be sure you are logged in as a user with sufficient rights.  If issuing this command on a Linux server, if you have the ability, you might also be able to prepend either of the above commands with sudo to temporarily acquire root privileges.  This would also be a great time to update all the third party software in your application’s vendor folder.  A simple composer update will do the trick.

Next we look at what needs to be done with regards to your version control software.

Preparing Version Control

Before starting the actual migration, it’s not a bad idea to backup your application software.  If you are using version control (you are using version control, aren’t you?), commit and push any changes still lingering around.  You should then create and checkout a new branch.  Stay on this branch while performing the actual update.  If something goes horribly wrong for some reason, you can always wipe out the branch and start all over again.

Here is a screenshot of how this might appear:

Next step: make sure your application works as-is.

Pre-Migration Application Test

Before migrating … before making any major changes for that matter, it’s a good idea to test your application to make sure that it works OK.  This might be a good time to put all those unit tests you’ve spent hours developing to work.  (Uh … you are writing unit tests for your applications, correct??) Also, if you are set up in this manner, you could also run tests using Selenium, or Apache JMeter as the tools are available.  Here is an example of a pre-migration unit test result:

Next: install the migration tool.

Install the Migration Tool

The final step in migration preparation is to install the Laminas Migration Tool.  The tool itself is in the form of a Composer package laminas/laminas-migration, so naturally you could use Composer to install the tool.  Alternatively, you could clone its repository, located at https://github.com/laminas/laminas-migration.  Now … here’s the trick: do not install the tool inside the directory structure of your project!  Theoretically this is possible, but personally I would not recommend this.  The best approach is to install the migration tool in a separate directory, well away from your application source code.

Once you have installed the tool using whatever means you prefer, you will see that the actual migration tool itself resides in the laminas-migration/bin folder. There is an executable shell script in that folder named laminas-migration that invokes PHP code.  Add laminas-migration/bin/laminas-migration to the operating system path on your server.  Alternatively, you could create a symlink from this file to a directory already in your path.  Here is an example of how that might appear:

jed@jed:~/Repos/unlikelysource.com$ echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
jed@jed:~/Repos/unlikelysource.com$ ls -l /usr/local/bin/laminas-migration 
lrwxrwxrwx 1 root root 55 Jan 27 13:19 /usr/local/bin/laminas-migration -> /home/jed/Repos/laminas-migration/bin/laminas-migration
jed@jed:~/Repos/unlikelysource.com$

Now we’re ready to proceed with the migration itself.

Performing the Migration

Once you complete the preparation outline above, the actual migration process looks something like this:

  • Perform the migration using the migration tool
  • Confirm differences in the code base
  • Use Composer to install the new Laminas components
  • Address any configuration provider injection issues
  • Test and debug

Let’s start with the main show, actually performing the migration.

Using the Migration Tool

The migration tool is extremely simple to use.  There are two main options migrate and nested-deps, as seen here:

The migrate option is designed for Zend Framework, Apigility or Zend Expressive project migration.  The nested-deps option is designed for projects that use Zend Framework components, but are not strictly Zend Framework projects themselves.  An example of the latter would be the LightMVC framework.  In either case, all references to the Zend namespace are replaced with Laminas.  Zend Expressive specific components end up as Mezzio, and any Apigility code formerly residing in the zf-campus namespace (simply ZF) ends up in Laminas\ApiTools.

Now … before you get all fired-up and rush off to run the migration tool, please don’t forget to change to the application project directory!  You need to be at the same directory level where your Zend Framework vendor, config, public, etc. folders are housed.

Here is an example using the migrate option:

The tool operates quite rapidly.  In my experience, even migrating a Zend Expressive based application with over 100,000 lines of code, the actual migration only took seconds.

Another thing the tool does is to install and inject a reference to the laminas-zendframework-bridge component.  A reference to this is added to the traditional config/modules.config.php file, or, in the case of Zend Expressive, at the end of config/config.php.  The purpose of this component is to provide a fallback autoloader that maps legacy Zend Framework classes to their Laminas equivalent.

Next up: confirming code differences.

Confirming Code Differences

Humans being are naturally curious (er … in most cases anyhow!): we want to know what changed post-migration.  Here’s where version control comes back to save the day.  In most version control systems, there is a way to view differences between what has been committed and what has been changed.  In the screenshot below you can see a simple git diff command that shows the old code in red, and the changes in green:

Next: install the new components.

Install Laminas Components

Another thing you might (or might not!) have noticed is that the migration tool deletes the vendor folder.  Oops!  Did I forget to mention that?  Sorry!  Anyhow, all silliness aside, when you step back to consider, this makes absolute sense.  The migration tool rewrites the composer.json file, replacing references from zendframework/* packages to laminas/* or mezzio/* packages in their place.  Everything else in the composer.json file is left alone.  Accordingly, there’s no need to retain the contents of the original vendor folder.

With this in mind, the next step is to install the new Laminas infrastructure by running the composer install command.  Here is an example of how that might appear:

As with a legacy Zend Framework application, Laminas includes certain components that need to be added to the appropriate configuration file.  Select the correct option, and away you go.  In most cases the installation should complete successfully.  In the case of Zend Expressive applications, you might see this error appear:

If you see something like this, just re-run the composer install command, select Do not inject, and manually perform ZendFrameworkBridge injections as needed afterwards.  In the case shown above, the reason for the error was due to the fact that the migrate process had already injected a reference to Laminas\ZendFrameworkBridge\ConfigPostProcessor in config/config.php as seen here:

<?php
use Laminas\ConfigAggregator\ArrayProvider;
use Laminas\ConfigAggregator\ConfigAggregator;
use Laminas\ConfigAggregator\PhpFileProvider;
$aggregator = new ConfigAggregator([
    \Mezzio\LaminasView\ConfigProvider::class,
    \Laminas\HttpHandlerRunner\ConfigProvider::class,
    \Mezzio\Router\LaminasRouter\ConfigProvider::class,
    \Laminas\Router\ConfigProvider::class,
    \Laminas\Validator\ConfigProvider::class,
    // not all config shown ...
    new PhpFileProvider(realpath(__DIR__) . '/development.config.php'),
], $cacheConfig['config_cache_path'], [\Laminas\ZendFrameworkBridge\ConfigPostProcessor::class]);
return $aggregator->getMergedConfig();

There is an excellent FAQ that includes a discussion of ConfigPostProcessor in the Laminas Migration documentation.  Next up: post migration testing.

Post Migration: Test and Merge Branches

Now that the hard work is pretty much over, you should return to the original tests you performed prior to migration.  If the tests do not return the same results, you need to go back to the top, and carefully review all steps to make sure you got things right.  Also, if worse comes to worse, you can always pull the plug: destroy the branch and start over again.

If all tests complete successfully, you can consider merging the new branch back down into your master branch.  You might consider doing this by way of a pull request just to make the process formal and documented.  Here are the commands to merge the branch created above:

git checkout master
git pull origin master
git merge update_laminas
git push origin master

And, as the saying goes, that’s all she wrote.

Summary

There a number of key takeaways from this article.  First and foremost: planning and preparation are vital to the success of your migration.  As you noticed, proper use of version control software can help you not only identify changes that took place, but also serves as a safety valve in case of a problem.  Another key point it to update everything managed by Composer, as well as Composer itself before you start.

The actual process of migration is quite trivial, as you learned.  What is more important is that you come up with a solid set of tests, preferably automated, that can be run both before and after the migration.  Ideally the test results should be identical.  If not, review your migration checklist and possibly start over again.

Now for the mandatory sales pitch!  At PHP-CL we offer a Mini-Course on ZF to Laminas migration.  We go over pretty much the same material, but with a live instructor.  Having read this article, you now have the opportunity to prepare a list of questions to ask the instructor as you attend.  Thanks for reading and stay safe everyone!