The Ultimate Php Upgrade Guide
12 hours ago

Upgrading a PHP project can be challenging and complex. Over the years, I’ve tackled numerous upgrades—sometimes on my own, other times as part of a team. Through these experiences, a clear process has emerged, and today I want to share that with you. My goal is to provide something practical, actionable, and easy to follow.

1. Analyze & Document

If you aren’t familiar with the Awareness–understanding matrix, this is a system that helps identify areas where people or systems are aware or unaware of issues and whether they understand or don't understand how to address them. This will be very useful to understand and prioritize risks when upgrading. 

Let’s have a look at at the Awareness–understanding matrix: 
Fantastic! But how is this practically relevant to a PHP upgrade? If we're aware of the risk and understand the problem, we can take proactive steps to mitigate the risk by addressing the issue. This is also a very good way in which to discuss the upgrade with your managers, you can provide a solid and well defined plan that handles the risks. 

Let’s look at a practical example in the context of upgrading: 

  • Known knowns:  You are aware the upgrade to PHP 8.2 will break each() usage, and you understand how to replace it with modern alternatives like foreach.
  • Unknown knowns: The static analysis tool (e.g., PHPStan) flags deprecations you weren’t aware of, but upon learning, you immediately understand how to fix them.
  • Known Unknowns: You are aware that upgrading to PHP 8.2 introduces readonly classes or typed constants, but you don’t fully understand how they impact your codebase.
  • Unknown Unknowns: A production bug arises after upgrading PHP, but you are unaware of its cause and don’t yet understand how to debug it.


This is a very good starting point and the more unknowns we have the more risk there is and therefore, reduced likelihood of having  a successful upgrade.  

The first step is to go through your codebase and figure out how much risk is associated with any potential change. My approach to this is to have a risk assessment for each major version change from the project’s current PHP version to the most recently released version. 

So if you have a PHP 7.2 application, there have been 6 minor versions released since then, so figure approximately out how many knowns and unknowns you have between each version. 

So from 7.2 to 7.3: 

What changed in the language?

  • Are there new features, deprecations, or backward-incompatible changes?

How heavily does the codebase rely on deprecated or changed features?

  • Are these changes localized to a few files, or do they span multiple classes, libraries, or services?

How many third-party packages are outdated or incompatible with this version?

  • Are there newer versions of dependencies available that align with PHP 7.3?

Are the current features well-tested?

  • Do you have sufficient test coverage to know if something breaks during the upgrade?

How complex is the upgrade process for the identified changes?

  • Are they quick fixes or do they require significant rewrites?

How time-consuming will it be to resolve these issues?

  • Can this upgrade step be completed incrementally or does it require a full sprint?


Do this for each version until the most recent version (even if the target version is lower) and this will either show that upgrading is a waste of time and just bin the project and start again or the result will formulate your project upgrade roadmap.

Install the following: 

  1. phpmetrics/phpmetrics - Gives you focus on where your application logic is most complex and generally where it will be most difficult to upgrade. 
  2. phpstan/phpstan - Gives you errors in your code without running it.
  3. rector/rector - Allows you to specify PHP version, find and automatically fix errors in your code. (designed for automated refactoring of PHP code)
  4. symplify/easy-coding-standard - a PHP tool that helps you automatically enforce coding standards and format code.

Figure out how each tool works (out of the scope of this article) and add your findings to a risk assessment of each version. It’s important to not get stuck just collecting data, we now need to start the upgrade and we have enough data based on the project to understand the level of risk associated with each version upgrade, which will also indicate an approximate timeframe and therefore the cost. 

2. Implement roadmap

Assuming that the data you found indicates that an upgrade is time effective and worthwhile, the next phase is implementation. 

There are some things we need to set up before beginning the upgrade:

  1. We must devise a backup and rollback strategy, in case of failure. I’d suggest multiple backups. A git branch and download the project separately before making any changes just in case. 
  2. We also need to set up our CI checks on the migration branch, so for example github actions, checking that all our code standards pass before merging, don’t just merge and think you’ll fix it later. (Let’s be honest, we rarely do).
  3. We need to consider our development environment, if we are in a large team then docker might be a logical integration. 

The process of this phrase is of course extremely dependent on the previous step. For example if you find that you don’t have much test coverage, that would be a fantastic place to start. Because if you don’t know it's broken, you can’t fix it. So a lot of the priority will be set by the first step. But the process that I’ve been using for a while now, is to incrementally go up through the versions. 

Sticking with the 7.2 version example, the first thing we need to do is update the composer.json, as we are upgrading to 7.3 we can:

  1. Create a branch “UpgradeTo7.2”
  2. Change the require php version to 7.3. 
  3. If you have a framework, change version to the version supported by PHP 7.3
  4. Run “composer update -W ”

This may chuck out “Your requirements could not be resolved to an installable set of packages.” go through each problem and resolve it manually and run “composer update -W ” again until upgrades. 

Important to note that this might break your entire application and it may no longer work. This should be expected if you have done the first step correctly. 

Ok, so now we have the application on the version we need, now we need to upgrade the broken bits in the project, have a look at what the process might look like. 

  1. Run your tests to see where in your application is broken. Figure out what is breaking it then cross reference this to your roadmap. (choose the easiest ones first and this will make you feel like your making progress)
  2. Start iterative improvements
    1. Choose a specific feature from your roadmap (For example type coverage. In our example they don’t have any) create a new feature branch called “TypeCoverage7.2Upgrade”
    2. Use Rector and other tools to upgrade this everywhere. We can do this by setting the ->withTypeCoverageLevel(1)level in the rector config.  (as of writing this int value is based on the number of rules available in the ruleset)
    3. Start with 1 then going up by tens (until nothing changes even when the level is increased) run Rector each time, check the diff after the command has run, fix anything that is wrong, run you coding standard checks after each run and then commit the changes then increase the type coverage level. 
  3. If you come across some custom or complex code that is used in many places in your code and it can’t be converted to a service then you can create a custom Rector rule. Let’s say you want to make all entity Id’s of the type Uuid and you have 200 entities, create a custom rule, add it to the config and run. 
  4. Ensure you stick to the scope of the current task, in this case focus only on type coverage. Getting distracted by other issues is how we end up going down a legacy rabbit hole and end up in  the 1990’s. 
  5. Merge the changes into the branch “UpgradeTo7.2” after the CI is passed. Then Choose another feature from your roadmap
Following this iterative process of upgrading should result in a fast and efficient upgrade.
Merge each major version into main / production, avoid having a massive upgrade branch that will be a nightmare to merge into main. Now follow the same process until you reach your desired PHP version. 

3. Clean up & Document

The upgrade is nearly complete, but now we need to tie up any loose ends and make sure everything is in order. This phase is all about cleaning up your codebase and ensuring that everything is properly documented, so you—and anyone else working on the project—can understand and maintain it going forward.

  1. Remove Unnecessary Code (use the withDeadCodeLevel() in Rector)
  2. Review and Add/Remove Tests - some feature may had changed a lot or even been completely removed, no need to test for something that doesn’t exist. 
  3. Final Regression Testing - Make sure the application is actually working, don’t just base it on unit tests. 
  4. Changelog & Version - You can use your roadmap written in the first step as the changelog between versions. Don’t forget to upgrade your project versioning and tags accordingly. 

By following this process, you’re not only increasing the likelihood of a smooth upgrade but also setting yourself up for long-term success. Clean code and clear documentation will make your life—and the life of anyone else working on the project—much easier moving forward. Good Luck!