Upgrade or upgrade?

When you have an application built on top of an existing framework, then it is very tempting to just leave it be once the initial development is done. Never change a winning team, right? So once you delivered the project and it's running in production, the only thing you need to do is to add or change features and fix some bugs. Right?

Well, for a long time you can indeed do that and it won't hurt you. But in the long run, this will become a problem. Because while your application stands still, the world around you moves on. And that moving on includes new versions of your framework of choice. And as new versions come out, support of older versions will at some point be dropped. For big, popular frameworks there is usually a pretty clear release schedule of when new versions come out and how long old versions are being supported.

In most situations there is a phased end of life. First, active support for bugs will be dropped but security issues will be fixed. Eventually though, security issues will also not be fixed anymore.

If at that point, your application is still running on that old version, you are at risk of security issues which can lead to, amongst other things, damage to your public image or even legal action in case of security breaches. It is important to do regular maintenance and upgrades of any software you use. Whether that is the desktop software you use, frameworks and libraries and anything else.

However, because of a lot of different reasons, you can end up with an application that is years old and a lot of versions behind the current stable version. So what then?

Quite a few of the projects I've worked on in the past years have in some way or form included a situation like this (or I would see this coming in the future). There are two important approaches here, with different variations of those approaches.

Approach 1: Upgrade step by step

Let's take the example of an old Symfony project. It is stuck at version 3.4 of the framework. With Symfony 7.1 out now, one approach could be to do a step-by-step upgrade. This would mean going from 3.4 to 4, then 4.4, then 5, then 5.4, then 6, then 6.4 and then finally to 7 (either directly to 7.1 or via 7.0).

This may sound like a lot of work, and it is! So it's important to understand how to make the decision to go for this approach. What I've found is the most important factor to decide on to take this choice is the size or complexity of the project. This approach is mostly interesting for projects that are really big or very complex in terms of logic.

Requirements

Before you even start with such a massive project that is hard to estimate in terms of time and effort is to make sure that at least you can ensure your work was done correctly. So you need to have:

  • Good documentation of what the application does, preferably including developers with a lot of experience with this application
  • A good set of tests. Preferably a combination of unit tests and integration tests
  • Support of your organization and most importantly, your management

If you don't have one of the above, make sure to get it. Documentation can be obtained by talking to developers that have worked on the project, but also business stakeholders and most importantly: users. Make sure that you have a clear understanding of the expected behaviour. You can use this information not just to understand what happens, but also...

To write tests! If you don't have a comprehensive test suite of unit tests (especially for important business logic) and integration tests (at the minimum as smoke tests to quickly see where potential trouble is, but preferably that actually tests your user experience), before you even change a minor version of your framework, start writing tests. The lessons learned from requirement 1 can help you with this requirement.

Last but not least: Management really needs to support the effort. It's important that they understand why it is necessary to be running on a recent and supported version of the framework. Explain to them the risk of hacks but also the extra effort needed to build new features into such old versions. The new, modern features that may be available in newer versions, etc. Without solid support from management, at some point priorities will change and you'll be stuck with a half-upgraded project.

Go go go!

Once your requirements are met it's time to do the actual work. This will most probably take a big effort. Especially with the example I used where you go from Symfony 3.4 (which does not support PHP 8) to Symfony 7.1 (which requires PHP 8). This means that along the way, you're not just making the jump from one Symfony version to the next, but also the jump from one PHP version to the next. And you don't just do that for your framework, but also for any other libraries you're using. Adn the dependencies of dependencies might start clashing.

To make this as easy as possible, you need to really take smalls steps at a time. Usually inside a major version it's relatively easy to upgrade, so for instance from Symfony 4.0 to Symfony 4.4 is relatively easy. Once you're on the last version before a new major, it's important to check for all deprecations: In case of Symfony, the next major is usually the same as the .4 version, but without the deprecations. So work on getting rid of all deprecations before going to the next major version.

Whatever you do, use static analysis tools to help you! Running PHPStan will help you catch potential issues. Also, Rector is your friend. It can help you automate fixing issues in your code and upgrading to newer versions of your framework. There are a lot of preset rules to help you in the process of upgrading, and you can extend Rector with your own rules as well for changes specifically needed for your project.

The result

If you have the time, the result should be an application that is running on the latest stable version of the framework, including completely adhering to modern code structures etc. However, since this is usually a very big time investment, I see it happening a lot that the upgrade of the project is done, but also refactoring to modern structures and approaches is not done. Which may make economic sense, but may still make the codebase feel a bit clunky and old-fashioned, even if it works and it on the latest and greatest.

Approach 2: The big bang upgrade

This second approach is, in my experience, mostly useful when your project isn't very big or complex. With this approach, you will have to touch most of the code in your project in one way or another. In this approach, instead of doing all the individual steps of the upgrade, you basically start over, but with the code you already have. So, let's use the same example of a project built on top of Symfony 3.4. Instead of doing the whole flow of Symfony 4, 4.4, 5, 5.4 etc, you just create a fresh new project on Symfony 7.1, then start moving the code from the old project bit by bit into the new project.

Requirements

Not surprisingly, the list of requirements for this approach is similar as the previous approach:

  • Good documentation of what the application does, preferably including developers with a lot of experience with this application
  • A good set of tests. Integration tests are the most important in this approach
  • Support of your organization and most importantly, your management

The good documentation is perhaps even more important in this approach, since the developers working on this upgrade will actually touch all the code in the project. In the first approach, they will mostly touch the integration between custom code and framework. Here, all the code will have to be touched.

For tests, the most important one may be integration tests. When using this approach you usually not just copy over code, but actually also refactor it to modern code structures and apply newly learned patterns and architecture, unit tests may or may not be very useful. There's a good chance that the changes in code structure require you to write new unit tests or highly alter existing unit tests. The integration tests, however, are there to test if the application still does what it is supposed to do. So those are the most important in this approach.

Support from the organization and especially management is actually the same as in approach one, but it is worth mentioning since I've really seen projects like this fail because this support wasn't obtained before starting the project.

Here we go!

So... now to get started. You start with a fresh Symfony 7.1 project. In my example, we started with Symfony 3.4 which still had a structure of bundles inside the application code. A good approach is to migrate the bundles to non-bundle src/ code. Be critical of code placement and naming. In Symfony 3.4 times a lot of developers had the inclination to use very technical terms when naming namespaces and classes, while these days even if you don't do full Domain-Driven Design, it is still common to use domain-based names for namespaces and classes. You don't have to make these changes but since you're starting from scratch, it may be worth the little extra effort while you're copying over code anyway.

Just like in approach one, PHPStan is your friend. Rector maybe less so although it can still help you with your tasks, but PHPStan will be there to ensure the newly written/copied code is built on modern standards and methodologies.

Think about what you do. Don't mindlessly copy over code and adapt it to fit into Symfony 7.1. Focus on building things the way you'd do on a fresh new project. This will make your code a lot more future-proof.

The result

When taking this approach, the chances are a lot bigger that at completion of the project, the code is not just "working" but also built according to the latest best practices and standards and is ready for the future.

Upgraded, and now what?

So you've done the upgrade, whichever approach you took, and now what? The most important is to not get into this situation again. Make sure to stay up-to-date by doing regular upgrades. Make this part of your regular maintenance work. Because small steps are a lot easier than a big jump. And the smaller the step, the easier it is. Since the release schedule of a lot of frameworks and libraries is openly available, you can even schedule most of the work.

Alternatives

If you want to stick with the same framework (in this example: Symfony), the above two approaches can work well. But there are alternatives. You can for instance start a new project, run that parallel to your existing application and then carve out specific sets of functionality. This approach can be complex when code hooks into other part of the code. But it will allow you to spread out the effort of upgrading/rewriting over a longer period, which may make it easier for a business to deal with the cost of the project. There is a risk though: I've worked for companies that took this approach several times and ended up with 3 or 4 different codebases that each still handled part of the logic.

Another approach could be... but this will sound scary, to literally throw away everything you've done so far and start from scratch. Be really careful when you're considering this. While this approach will probably deliver the best, most high-quality product at the end of the project, there is a lot of risk involved. Hidden functionality that no-one knows about may be forgotten, and the project may end up being so big that management is not willing to invest any more money into it, resulting in having an even older codebase in production and a half-finished new codebase that will never be used.

You mention Symfony, but...

... I use Laravel, CakePHP, Drupal, or another foundation for my software. Well, good news, while I used Symfony as an example here, you can apply these approaches to basically any software project. You don't even have to use PHP to apply the above.