One of the painful but most often useful things you have to do as a software developer or team is upgrading your software to the latest version of a framework it relies on.
Most often you can delay the need to upgrade if you are running on a Long Term Support (LTS) version of the framework. Eventually, though, you will have to upgrade as the end of support or life date approaches.
When does an upgrade become inevitable?
A couple of months ago I had the task of updating an old but very much in-use app from Laravel version 5.2 to 7.0.
The first line of code was written about 7 years ago and Laravel 5.2 had long passed its LTS.
PHP is advancing at an incredible pace, so we can expect our entire tech stack to become obsolete soon. This further reinforced our decision to upgrade.
Needless to say, an upgrade was long overdue.
To get started with the migration I needed to answer a few questions:
How do I sync the migration with continuous development?
Since the project is an active one, it meant we worked on the code regularly. The main question is how do I make sure I performed the upgrade whiles incorporating all the new changes we made along the way?
Well if you consider the upgrade itself the same as any new commit then it can be handled just like all other changes to the codebase.
Thus I can pull in the work of my colleagues into the upgraded version as I go along and when I have a complete body of work they can incorporate it into their work as well. So as and when I upgraded to one version I tested and then committed to the main branch that’s about it.
With limited test coverage, how do I ensure everything works after the migration?
One other big problem was that the application had very limited test coverage and I needed to have some reliable way to know all existing functionalities worked after each version shift.
I could start by writing tests to cover as many parts as I could, but I decided to do this once the upgrade was finished. My reason? Well, the way laravel handles tests has changed across versions which means it too might break and need fixing, not too useful.
So I decided to go with acceptance testing. The app is split into two parts an SPA frontend and an API-based Laravel backend. We agreed to upgrade the backend first. So I performed a whole bunch of API requests then exported them as CURL statements then dumped all of that into Postman.
I slowly sorted the output and wrote a basic Postman test for each one. This model served me very well throughout the entire upgrade.
And the biggest question is what if you are several versions behind?
At first, I just merged the Laravel branch 7.0 into our 5.1 app. This led to a lot of merge conflicts that needed fixing. It was not a good move because we had mutated the original Laravel folder structure soo much that there were a lot of things that needed changing and new files the new version brought that we wouldn’t need.
So I resorted to using diffs to compare what had changed between the newer version and our current version. I did this using GitHub's built-in compare feature eg: https://github.com/laravel/laravel/compare/5.1...5.2.
With this, I got to see all the different changes that were made file by file and this allowed me to know what the change was for and how to translate that into our codebase. Sometimes this was a simple copy and paste, other times it was a rewrite to reflect what the improvement was for but then I couldn’t just copy and paste it.
Once that was done I ran composer to pull in all the other updated dependencies and then run through my tests.
What about Deprecated packages?
There were also times when I found out that a package we relied on had been deprecated and not available for later versions of Laravel, in this case, I forked the original package and made any required updates, then referenced the forked repo in the composer file. I have currently started to replace these packages with newer ones as well but during the upgrade, the goal was to get everything working as is.
It’s not just about updating source files
Between versions, some behaviors in Laravel changed. You won’t have a problem with most of these, but your output may not be what you expect. For example, the version of Carbon Library which Laravel uses to handle dates and time changed between version 1 and 2 and brought with it subtle changes that affected our DateTime formats. To help understand what behavioral changes each version had, the Laravel upgrade guide came in handy, this documented every piece of info regarding important behavior changes between versions.
Following this approach, I was able to get our app to version 7 of Laravel which was the latest at the time. Today we have new policies in place to migrate our tech stack frequently to prevent having to deal with an undertaking like I did.
Here is another article you might like 😊 "How to render a string as a blade template (Laravel 9)"