Updating a PHP project often feels like a high-risk operation, especially on big or sensitive codebases. We’ve all been there: something breaks, hotfixes are flying, and everyone’s hoping for the best.
But it doesn’t have to be a nightmare. With the right tools and a methodical approach, updating a Symfony project can be reliable, controlled, and even satisfying!
Your Toolbox
Think of it like a construction site. You’re the builder, and your project is the house. You’ve got a solid toolbox by your side: automated tests, static analysis, linters… and your job is simply to pick the right tool at the right moment.
Updating your project is just another renovation. Here’s how to do it step by step.
1: Test your routes
Before touching any dependencies, make sure your application is properly covered by automated tests.
We use Bruno at ekino, an API testing tool that lets us define and run test scenarios.
We chose it because it’s lightweight, Git-friendly, and works beautifully in teams, no need for complex UIs or vendor lock-in. But of course, you can use any tool you’re comfortable with; what matters is having reliable, repeatable test coverage for your endpoints.
Here’s what to do:
Use debug:router in Symfony to list all available routes.
- Identify which endpoints and workflows are critical.
- Write Bruno test scenarios that hit those routes with the right payloads and assertions.
- Set up your environments (base_url for local, staging, prod) to switch contexts easily.
- You could even integrate them into your CI pipeline so they run on every push.

The goal is to pass every test before we get into updating code.
2: Update Dependencies & Tooling
Once you’re confident in your test coverage, it’s time to modernize your stack. Start with these key upgrades:
- Bump your PHP version
Update your composer.json and local runtime (e.g. Docker, CI config) to the desired PHP version, like ^8.3.
Make sure extensions and tools like Xdebug are compatible. - Update Composer packages
Use composer outdated to review what’s lagging.
Update dependencies incrementally and commit in small batches so it’s easier to trace regressions if something breaks.
Use composer update vendor/package instead of a blind composer update. - Check for deprecations early
Deprecations are a natural part of keeping up with Symfony and modern PHP. Before upgrading major packages or PHP versions, it’s smart to identify and fix deprecations early. Symfony’s web profiler will show these during development, and running your test suite will surface them as well. - Run static analysis
Tools like PHPStan help you catch bugs and type issues before runtime. Start with a lower level (like 3 or 4) and increase it gradually.
Here’s a basic phpstan.neon.dist config:
parameters:
level: 9
paths:
- bin/
- config/
- public/
- src/
- tests/
- Apply consistent formatting
Use PHP CS Fixer to enforce consistent code style across your project. It helps reduce code review friction and ensures everyone’s writing code in the same. Run it in dry-run mode first to preview changes:
php-cs-fixer fix --dry-run --diff
Then, when you are sure about your changes, you can re-run the same command without the dry-run option.
Ideally, you shouldn’t have any deprecations, but if you have any, it is better to fix them first, you can follow Symfony’s guide if need.
Take it one step at a time. After each change, run your full test suite, including your API tests, to catch regressions early and avoid surprises later.
3: Refactor with Rector
Now that your code is stable and up to date, you can modernize it.
We use Rector, a powerful automated refactoring tool for PHP that helps migrate, clean, and improve codebases. It’s great for upgrading PHP versions, enforcing consistent code quality, and removing dead code, all with minimal manual effort.
Configure Rector with a targeted set of rules aligned with your goals. For example, if you’re aiming for PHP 8.3 compatibility, use the SetList::PHP_83 set. You can also mix in prepared sets like codeQuality, deadCode to progressively modernize legacy code without rewriting everything at once.
Apply those rules incrementally, focusing on specific folders or components. After each Rector pass, re-run your unit and API tests to ensure everything stays green.
Here’s a basic example of a rector.php configuration:
<?php
declare(strict_types=1);
use RectorConfigRectorConfig;
return RectorConfig::configure()
->withPaths([
__DIR__ . '/config',
__DIR__ . '/public',
__DIR__ . '/src',
__DIR__ . '/tests',
])
->withPhpSets(php83: true)
->withPreparedSets(
deadCode: true,
codeQuality: true
);
This allows you to refactor progressively without breaking things.
If you need to perform custom refactoring, for example renaming legacy service classes, Rector also supports custom rules.
You can define your own rules in the rules directory and register them like so:
->withRules([
AppRectorMyCustomRenameLegacyServiceRector::class,
])
Don’t hesitate to take a look at the Rector Custom Rules documentation if need.
4: Update CI and Deployments
With your updated and refactored codebase in place:
- Update your Docker image in the CI pipeline to reflect the new PHP version and its dependencies.
- Make sure your CI stages (tests, linters, analysis) still pass.
- Deploy to staging/pre-production and validate.
If you are wondering how to configure a CI, you could reuse the tests you created with Bruno or another application before running them.
Here’s an example of what it could look like if you have a bruno folder at the root of your project with all the configuration inside, using Gitlab Actions:
api-tests:
needs: tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
extensions: zip
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: build-artifacts
- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version: '18.18.2'
- name: Run Bruno API tests
run: |
npm install -g @usebruno/cli
php -S localhost:8000 -t public &
sleep 2
cd bruno && bru run --env dev
Tools like Bruno or Postman aren’t tied to Symfony — they send real HTTP requests to your app, just like a browser or mobile app would. That makes them great for testing how your API behaves from the outside, and since they’re not Symfony-specific, you can easily share test collections with frontend devs, QA teams, or anyone else.
It’s worth mentioning that on the Symfony side you’ve got tools like WebTestCase for internal testing and even Panther if you want to go further and simulate a real browser. And if you're already using Cypress for frontend tests, you can also use it to hit your backend API directly.
In the end, it’s not about picking one — it’s about using the right mix: Symfony tools for the inside, and external tools like Bruno or Cypress for the outside, as they complement each other.
If everything’s automated, the risk at this stage should be minimal.
5: Don’t Forget Frontend Testing
If your project has a frontend, tools like Cypress that we already mentioned, or Puppeteer can help you catch visual bugs and broken flows that backend testing alone won’t cover.
Even if your Symfony project is mostly backend, frontend regression testing can be a huge safety net, especially when the UI is critical to your logic.
Updating a legacy project doesn’t need to be painful, with the right preparation and tools, it becomes a routine part of maintaining quality software. Automated tests, static analysis, and gradual refactoring turn updates from something that may scare you into something predictable.
If you don’t know where to begin in your upgrade journey, you can find a Symfony upgrade pack template here, with all the tools we mentioned as well as a GitHub CI setup.
Keep in mind that all configuration files can be changed for your preferences and needs, this serves only as an example. You can also replace Symfony with your favorite framework, be it Laravel or Drupal.
Some quality framework-specific bundles (like Larastan for Laravel or Phpstan for Drupal) are available to go even further. And to top it all, why not plug in a tool similar to SonarQube to further secure your code.
Remember that you’re not just writing code, you’re building something solid. And like any good builder, you need your tools and a plan.
Be the bricoleur!
How to Confidently Update Legacy Code in Symfony was originally published in ekino-france on Medium, where people are continuing the conversation by highlighting and responding to this story.