Escaping an Abandoned Framework

Lightly edited from a version first published in php[architect] December 2019.

Over the past few years I’ve been involved with four teams whose bread-and-butter applications were built in the classic Zend 1 framework. Each team has realized the framework is no longer supported, and each has struggled to figure out their next step.

I’m the lead developer on one of these anxious-to-get-out-of-Zend teams, I have worked with the developers on two other Zend-bound teams, and the fourth team are colleagues at work who share their war stories as we discover tricks and traps. On each of these projects I’ve seen triumphs and tragedies. In this article I want to share what I’ve learned about escaping an abandoned framework.

But first a spoiler alert. As I write this article in the winter of 2019 none of the applications is completely out of Zend 1. I don’t have the joyous experience of deleting your last Zend module, but I can attest that there is a way out. There are also ways to get so tangled up you may never get out, as well as a way to comfortably stay in, but we’ll get to that in a minute.

How We Got Here

Zend 1 was arguably the best framework available a decade ago and it became the foundation for countless business applications. It reminds me of my trusty 2000 Toyota Sienna minivan. It was a well-built car that served me reliably for 356,000 miles and ran well until the end. But while the engine was purring and the car still served its purpose, it had a litany of little issues. The dash lights didn’t work reliably, the body had been beaten up by a few accidents, and in general time had taken its toll. It worked, but there was no telling how long it would keep working.

And therein lies the problem. Time marches on, and like a much-loved car, software shows its age. Zend 1 was replaced by Zend 2, and the Zend folks did not provide an easy upgrade path. As a result, many programmers indefinitely postponed upgrading, choosing to stay with their reliable old framework. Eventually, Zend 3 came along, but by then newer frameworks like Symfony and Laravel were more appealing than upgrading to another Zend.

That’s where many of us find ourselves today. Ten years ago someone started building a business application in Zend 1. Over the past decade the program has matured, capturing complicated domain knowledge that’s not recorded anywhere else. Now, because the foundational technology is outdated, we want to move the application out of Zend 1, but first, we need to find a way out.

Possible Exit Strategies

Across four teams I’ve seen three different exit strategies.

Strategy One: Why Bother?

Zend 1 works. Okay, it’s abandoned, and it doesn’t use real namespaces, and it won’t get any security upgrades, but it has worked for ten years, it still works today and it will even run on PHP 7. For some folks that’s good enough. Replacing Zend 1 will take countless programmer hours, and it can be hard to justify investing scarce resources to replace working software just because it’s old.

Based on that reasoning, one team decided to stick with the abandoned framework. Their Zend 1 application isn’t exposed to the public; it still serves its purpose reliably and doesn’t need much on-going development. Refactoring the application is not a justifiable expenditure of their limited resources, so they chose to keep it running as long as they can. One day it may be worthwhile to re-write the app, but that’s not today. Now they have more important business concerns.

While there are drawbacks to sticking with Zend 1 or any outdated platform, in software development there aren’t always “right” and “wrong” answers. Instead, there are different buckets of trouble. We have to choose the bucket we are most comfortable carrying, and for some folks that means sticking with Zend 1.

Strategy Two: Leave Frameworks Behind

In another view, our precious application is fossilized in Zend 1 and we are trying to set it free. Imagine that we make the herculean effort to move it into the latest version of Symfony. Ten years from now, will it take another heroic effort to pry our application out of Symfony? Why free ourselves from one tar pit only to get mired in another?

That rationale inspired the decision of another team. Their lead developer felt burned by framework lock-in and vowed never to put himself in that position again. Instead, he is using Composer to pull in the best components he can find and piece them together into a flexible programming platform.

Using this approach the team was able to gradually sneak the application out of Zend. They started by replacing Zend_View with Symfony templates, then replaced the Zend_Registry with PHP-DI for dependency injection. They are now at the point of replacing Zend_Db with Doctrine. By addressing one manageable chunk at a time they are slowly dismantling Zend and erecting their own menagerie of components. In a few months they will no longer be tied to any particular framework.

I have mixed feelings on this approach. It works, but it’s a very difficult path to tread. You need a deep understanding of architecture to successfully patch together a framework from component parts, and once you’ve done that the parts will not give you the same benefits of a modern, modular framework.

And while swapping out components sounds easy in theory, the reality can prove challenging. If your components become tightly coupled with your business logic, they will not be easy to replace. Instead, you’ll be locked into your own peculiar framework.

This approach reminds me of a custom bookshelf I made for my wife’s art supplies. It’s exactly the shape she wanted and it looks pretty good, but some of the joints aren’t entirely square, some of the shelves don’t line up quite right, and I hate to admit it, but it’s a little wobbly. I made a functional shelf, but I didn’t have the expertise to make a fine piece of furniture.

Custom framework mash-ups can be a lot like my bookshelf. Some folks can piece together components to make a decent framework, and the more experience and skill they have the better their custom solution will be. But few have the experience or expertise to cobble together a framework as good as those already available.

Many of the modern frameworks, like Symfony, are becoming more modular, allowing programmers to use only the components they need and leave the unwanted bundles out. In effect, they allow us to build our own framework out of standardized, interlocking components.

While I respect the fear of getting locked into a framework, I don’t think the result is comparable to the professional frameworks available today.

Strategy Three: Move to Another Framework

If we’re not staying in Zend 1 and we’re not creating our own framework, then our third option is to move into a new framework. That’s what the last two teams have done. Each group is significantly upgrading and expanding an older application and wanted to build in a modern framework, so each chose to start moving from Zend into another full-featured platform.

At this point it might be nice to talk about which modern framework is the “best,” to review all of the pros and cons we weighed to select our new platform. But for us the choice was pretty simple. Most of the web applications in our office are built in Symfony, which means we are surrounded by folks who have a lot of Symfony experience. It would be foolish not to take advantage of such a ready bank of wisdom, so Symfony was our logical choice.

Setting Up the Exit Route

That leads to the practical matters of migration. If we’re going to move out of Zend and into Symfony, how do we keep the old website running while we build its replacement?

We start by laying a little groundwork.

PHP 7

All the instances of Zend 1 I’ve worked with were originally running on PHP 5. However, Symfony 4.4 (the latest long-term-support version) requires at least PHP 7.1. If we’re going to move into Symfony, our first step is to upgrade the server from PHP 5 to PHP 7.

Fortunately, Zend 1.12 works with PHP 7.1. CodeSniffer’s PHPCompatibility checker identifies 75 compatibility errors between PHP 7.1 and Zend 1, which seem to be concentrated on mcrypt and SQLite calls. However, in spite of the sniffer finding incompatibilities, we have not run into any problems running Zend 1 on PHP 7.1 (perhaps because none of us were using SQLite).

In my experience the migration has required little more than upgrading the server from PHP 5 to PHP 7. It may take some tweaking to get PHP 7 configured correctly on your server, and you will need to make sure your custom code PHP 7 compatible, but Zend 1 just works in PHP 7. I guess that’s a testament to how well it was originally crafted.

Composer

Modern PHP frameworks rely on Composer to handle dependencies, but Zend 1 was written in the BC (Before Composer) era. This leads to some interesting choices when teams commit code to their CVS repository. One team was committing all of Zend 1 to their Git repository, while another didn’t have it in the repo and installed Zend separately on each server before pulling down the rest of their code. Those methods work but are a lot messier than using Composer.

If you don’t have Composer set up, that’s the next step. I won’t go into Composer installation details, but for Zend integration just:

  • Add require '../vendor/autoload.php'; to the file where you’re bootstrapping Zend.
  • Add "zendframework/zendframework1": "1.12.*" to your composer.json file

If you don’t have a composer.json file, here’s one with just the basics for Zend. In this sample, replace “MyApp” with the namespaces you’d like to assign to your Zend “library” and appropriate “application” folders.

Basic composer.json

{
  "autoload": {
    "psr-4": {
      "MyApp\\Library\\": "library/",
      "MyApp\\Modules\\": "application/modules/"
    }
  },
  "require": {
    "zendframework/zendframework1": "1.12.*",
    "zendframework/zf1-extras": "1.12.*"
  },
  "repositories": {
    "packagist": {
      "type": "composer",
      "url": "https://packagist.org",
      "allow_ssl_downgrade": false
    }
  }
}

And now, whenever you run Composer you’ll see the secret message revealed exclusively to Zend 1 users:

Package Zendframework/zendframework1 is abandoned, you should avoid using it.

Symfony

With PHP7 and Composer in place the next thing to do is set up Symfony. Each team began their migration with Symfony 3, before the Symfony team introduced Flex and completely changed the directory structure. Starting with Symfony 4 and the Flex layout will present its own challenges (especially since both Zend 1 and Symfony 4 Flex are looking for public/index.php as the base file), but in general, here’s what we’ve done that worked well.

First, we created a separate route to the Symfony controller. In the public directory we created a symbolic link, “s”, to our symfony installation folder. When users go to www.oursite.com they are routed to our Zend pages, which is what has always happened. However, www.oursite.com/s directs them to our Symfony controller. The symbolic link in the public folder allows us to have both Zend and Symfony controllers running simultaneously, and makes the transition between the two frameworks fairly seamless to the end-users.

Next, we tackled authorization. Zend and Symfony each have their own authentication systems, but we need them to work together. We didn’t want a user to authenticate in our Zend application and then have to log in again when they hit a Symfony page. To avoid that scenario, we let one framework handle the authentication and pointed the other framework to this authoritative login system.

On our teams we continued using Zend for authorization and had Symfony generate a new session with a valid user token based on the Zend login. In retrospect, it may have been better to do it the other way around (make the new Symfony system authoritative), but the main idea is to configure your system to have one single authorization.

Finally, there are few other specific details to get Symfony and Zend playing well together. These specifics will vary based on which version of Symfony you install and how you configure it, but just know it can be done and you can find most of the answers online.

Now, skipping ahead in a fast-forward montage, we have Symfony and Zend standing side-by-side and sharing a login. All that’s left is to completely rewrite the whole application in Symfony. What could go wrong?

Lessons Learned

Updating your server and installing Symfony was the easy part. The real chore is moving ten years of complicated and fragile stuff into the new framework. Do you know how much fun it is to rent a truck and move all of your worldly possessions from one house to another? Moving into a new framework is almost as much fun, and gives you just as many opportunities to damage your precious cargo.

Here are a few hard lessons we learned.

Don’t Look Back

In the Hebrew Bible there is a story about Lot and his family fleeing a doomed city. As fire rains on their old home an angel tells Lot and his family, “don’t look back.” Lot and his daughters keep running forward and are saved from the catastrophe, but Lot’s wife looks back and is turned into a pillar of salt.

It’s not a happy story but it carries an important message—don’t look back.

One of our teams started escaping Zend and moving into a new framework, but then they looked back. They wrote code in Symfony that relied on their old Zend code, and their project turned into a pillar of salt.

Well, not really a pillar of salt, but it turned into a mess, which is close enough. They needed the new code to send an email, but they had not yet created an email service in Symfony. In a hurry to get this code into production, they didn’t have time to build an email component in Symfony. Since they already had a full-featured email system in Zend, they pointed the shiny new Symfony class to the mature Zend email component. It seemed like a nice shortcut at the time; they figured they would replace this little hack soon enough.

But they didn’t replace it because there were a lot of other pressing things to do. Instead, they built more Symfony features that relied on the Zend email system. Then, emboldened by the apparent success of this pattern, they built other Symfony code that relied on other Zend code. Now the two systems are so intertwined that replacing the old Zend code becomes more difficult and less likely every day. They’re not really getting out of Zend, they’re building a Symfony program with a Zend foundation.

They’re stuck.

Don’t look back to Zend.

Don’t build new modules relying on code you plan to replace. It will take time to build new components in Symfony and it will always be more convenient to fall back on the old code. But if you’re ever going to get out of Zend you have to take the time to get out of Zend. Keep moving forward.

This is a hard bit of advice to follow sometimes but it’s crucial. On another team we look for opportunities to move code into Symfony. If we need to update a page generated by a Zend controller, we create a new controller entry and move the page into Symfony. If some business logic changes dramatically, we rebuild the logic in Symfony instead of updating Zend.

By building bits in Symfony whenever we can, we make it easier to move other logic into Symfony later on. Step by step we get ourselves out of Zend, and we have a pleasant zealot who looks at every pull request to make sure nobody introduces a Zend dependency into the Symfony code.

When you’re building new code in Symfony and fire is raining down all around you it’s tempting to look back to Zend, but remember Lot’s wife.

Let the New Code Mature

One thing which pushes teams to look back is they want the new code to be fully grown right now. They don’t have the patience to let it gradually mature. And since it can’t quickly do everything the old code can do, they look back to Zend to get the work done.

But, like children, applications need time to mature. When our kids were born they couldn’t do anything useful—except look cute, which they didn’t do consistently. It took a long time for them to learn to talk, then walk, control their bladders, read, think useful thoughts, use tools, and eventually climb up in the attic and install CAT 6 cabling in our home office. They didn’t start as cable monkeys, that took years of training.

The same is true of the code you write in the new framework. Your old code is mature and can do all sorts of important things (after all, it’s ten years old, which is like 40 in human years). The code in the new framework, by comparison, is a toddler. It’s cute and has loads of potential, but it still has a lot to learn.

Let your Symfony-based application mature gradually.

For example, when you’re anxious to build out that new email component in Symfony but are pressed for time, it’s okay to only build out the features you need right now. If you just need to send a plain text email to a single verified user today, then just build that bit of the email module on the Symfony side. Later, when you’re moving over some code which requires more advanced features in the email system, add those features. You don’t have to replicate all of the old Zend code in Symfony from the very beginning; you can incrementally build it as you need it.

And while you’re at it, recognize that you will need to mature as well. Just because you had a baby yesterday doesn’t mean you understand how to be a parent. Similarly, if this is your first time using Symfony there are a lot of concepts that will take you a while to become comfortable with. You may start out putting all sorts of logic in a repository and later think services would be a better place for that code. It took a while to learn the Zend 1 paradigms, and it will take a while to become comfortable in Symfony. Be patient with yourself, and know it will take time (and a few mistakes) for you to learn and grow as well.

Sometimes we get stuck because re-building full-featured components in Symfony is too daunting. But they don’t have to start that way. It took a long time for your Zend baby to grow up and it’s going to take a while for your Symfony child to grow as well. If you take just one step at time and keep moving forward, one day that little monkey will be all grown up.

Plug Zend Into Symfony With an Adapter

While we never want Symfony to rely on Zend code, letting Symfony gradually take over Zend is a great way to move forward.

Going back to our email module example, let’s say you finally get your Symfony email component fully built. It’s now even better than your old Zend email system, but there are still a lot of places in Zend that rely on your original Zend email service. Since the new Symfony class has the same functionality, wouldn’t it be nice to point all of those Zend references to the new code and completely delete the old Zend email module? Well, you can do just that, and yes, it is really nice.

To accomplish this one team created a SymfonyAdapter class which provides Zend access to selected Symfony services. We don’t fire up a full Symfony instance; instead, we just inject the necessary dependencies to get the appropriate Symfony class into Zend. There were a few tricky bits to set up, like instances of Twig and the TokenStorage, but once we had those figured out injecting Symfony classes into Zend was a breeze.

Below is a simplified version of the SymfonyAdapter we’re using. This example shows how we make our email service available in Zend. To do this, we are taking advantage of Symfony’s auto-wiring feature to inject the dependencies we need to run the class. For this email service, we have to create instances of Twig, TokenStorage and SwiftMail which are then passed as parameters to the EmailService.

public static function getEmailService(): EmailService
    {
        $twig = self::getTwig();
        $token = self::getTokenStorage();
        $swiftMail = self::getSwiftmail();
        return new EmailService($swiftMail, $twig, $token);
    }


public static function getTwig(): TwigEnv
    {
        $twigLoader = new TwigLoader(['../symfony/app/Resources/views']);
        return new TwigEnv($twigLoader);
    }

  private static function getSwiftmail(): \Swift_Mailer
    {
        $transport = new \Swift_SendmailTransport('/usr/sbin/sendmail -bs');        
        return new \Swift_Mailer($transport);
    }

    private static function getTokenStorage(): TokenStorage
    {
        $tokenStorage = new TokenStorage();
        // getCurrentUser is a custom method in this adapter that gets the current User entity
        $user = self::getCurrentUser();
        $userToken = new UsernamePasswordToken($user, null, 'account', $user->getRoles());
        $tokenStorage->setToken($userToken);
        return $tokenStorage;
    }

In our real SymfonyAdapter we have a couple dozen methods shared with Zend, all of them following the same format as the getEmaillService() method here.

Once we could use the Symfony email system in Zend we refactored all of Zend code to use the new Symfony class. When that refactoring was finished, we deleted the old Zend email class and had a grand little party. Thanks to this SymfonyAdapter, we have been able to migrate services out of Zend bit by bit, a process which we will keep repeating until one day we won’t need the adapter anymore.

Share Entities

None of the Zend projects I’ve worked on were equipped with Doctrine, the object-relational mapper Symfony uses to communicate with your database. Instead, one team used the standard Zend 1 Database module, and the others had unique, custom-built ORMs.

When you’re working on a growing application the database is constantly changing. Having a custom ORM on the Zend side and Doctrine entities on the Symfony side means keeping two separate systems constantly in sync with the database. That’s a lot of duplicated effort.

One of our early goals was to get Doctrine set up in Zend. We figured if we could get the two frameworks sharing the same ORM it would be so much easier to refactor Zend code.

Having never used Doctrine before it was daunting to set it up from scratch. It took a bit of research and a couple of false starts to get it going, but it was well worth the aggravation.

Now we have a static Doctrine class in Zend which makes the entity manager, entities, and related repositories available to all of the Zend code. As we refactor Zend modules, we can replace the custom ORM calls with calls to the Doctrine entities. And, as the entities and repositories mature in Symfony, that new code is automatically available to Zend, making it easier to replace and remove old code.

Take the time to get Doctrine in Zend. Here I’ve included a basic outline of the code we used to make this happen. You’ll notice we call a couple of special classes, Credentials and Environment, to get the database credentials and to identify whether we are in production mode or not. You can easily re-write this class without those calls.

use Doctrine\Common\Annotations\AnnotationReader;
use Doctrine\Common\Annotations\AnnotationRegistry;
use Doctrine\Common\EventManager;
use Doctrine\DBAL\Event\Listeners\OracleSessionInit;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Events;
use Doctrine\ORM\Mapping\Driver\AnnotationDriver;
use Doctrine\ORM\Tools\Setup;
use MyApp\Credentials;
use MyApp\Environment;
 
final class Doctrine
{
    private static $instance = null;
    private static $entityManager = null;
 
    private function __construct()
    {
        $dbCreds = Credentials::getDatabaseCredentials();  // Or however you get your dbase credentials
        $dbParams = [
            'driver' => $dbCreds['database_driver'],
            'host' => $dbCreds['database_host'],
            'port' => $dbCreds['database_port'],
            'service' => $dbCreds['database_service'],
            'user' => $dbCreds['database_user'],
            'password' => $dbCreds['database_password'],
            'dbname' => $dbCreds['database_name'],
            'charset' => $dbCreds['database_charset']
        ];
        $isDevMode = Environment::isDevMode(); // Or however you get your dev mode
        $paths = [__DIR__ . '/../symfony/src/AppBundle/Entity'];
        $cachePath = __DIR__ . ‘../cache/data/doctrine';
        $config = Setup::createConfiguration($isDevMode, $cachePath);
        $driver = new AnnotationDriver(new AnnotationReader(), $paths);
        AnnotationRegistry::registerLoader('class_exists');
        $config->setMetadataDriverImpl($driver);
 
        $eventManager = new EventManager();
        $eventManager->addEventListener(
            [Events::prePersist, Events::preUpdate],
            new DoctrineEventListener()
        );
 
        self::$entityManager = EntityManager::create($dbParams, $config, $eventManager);
    }
 
    public static function getEntityManager()
    {
        if (!isset(self::$instance)) {
            self::$instance = new self();
        }
        return self::$entityManager;
    }
}

Don’t Build on Deprecated Code

Earlier I mentioned my trusty old Toyota minivan that lasted 20 years and 356,000 miles. Toward the end, the dash lights stopped working reliably, the passenger seat was being held upright by zip ties, and the radio knob had a tendency to fall off. As the car crumbled to pieces, I knew I would have to replace it one day, yet I kept putting more money into it. I installed new wheels and tires when the car had 330,000 miles, bought new shocks at 340,000 miles, did major engine work at about 350,000, then sold the car for scrap 6,000 miles later. I spent thousands of dollars in the last couple of years of owning that car, money that would have been better invested in a replacement vehicle.

There comes a point when you need to let go of the past, and the same is true with your old Zend code. You can patch it up and it will keep doing what you need it to do, but if you keep investing in the old code, you’re not only squandering your resources, you’re sabotaging your efforts to get out of Zend.

Every new line of code you write in Zend will have to be rewritten in Symfony. So, not only are you investing the time writing the code in Zend, but you are obligating yourself to re-write the code in Symfony one day. It’s like a fool buying high-end tires for a car that’s barely road-worthy; it’s not a good investment. In the long run, wouldn’t it be better to write the code once in Symfony?

When you need to upgrade your application or create a new module, it may seem easier to get it done in Zend. But keep in mind you’re leaving Zend, and do the future you a favor. Take the time now to write the code in Symfony instead.

The Light at the End of the Tunnel

Many of us have faced the daunting challenge of migrating a complex application from Zend into a new framework. Some chose to keep the original app running in Zend 1 because they can’t justify the expense of a migration. Others decided to roll their own framework, never to be locked in again. And some are moving into new frameworks, like Symfony. Three out of four teams can see a light at the end of the tunnel, the hope that someday they will be out of Zend.

Moving from Zend into Symfony is a massive undertaking, but it can be done. It requires setting up a new environment, constantly moving forward instead of looking back, and giving yourself and the new software time to mature.

It can be overwhelming to consider how much time such a migration will take. It took ten years to build these applications in Zend, and it’s likely to take a few years to migrate the application into the new framework. But the years are going to go by whether you start moving out of Zend or not. If you get moving now, maybe in a couple of years you’ll be able to see the light at the end of the tunnel too.