How we Upgraded from Symfony 4 to 5 in 25 days

This post was updated at November 2020 with fresh know-how.
What is new?

Switch from deprecated --set option to rector.php config.

A month ago, Symfony 5 has been released. Upgrading of such a small web as our community website must be easy, right?

Well, that's what we thought. Were we right or wrong?

This post is based on the real problems we faced when we upgraded our website. It is full of experience with pieces of explanation, real code snippets in diffs, painful frustration of Symfony ecosystem and bright light at the end of the tunnel.

Are you ready? Let's dive in ↓

1. Easy picks

Twig 2 → 3

Before, you could connect for with if like this:

{% for post in posts if post.isPublic() %}
    {{ post.title }}
{% endfor %}

Now the filter has to be used:

{% for post in posts|filter(post => post.isPublic()) %}
    {{ post.title }}
{% endfor %}

Thanks Patrik for the tip

2. Rector Helps You with PHP

Do you want to know, what is needed for the upgrade to Symfony 5? Just read upgrade notes in Symfony repository.

  1. For PHP stuff, use Rector:
# install Rector
composer require rector/rector --dev

# or in case of conflicts
composer require rector/rector-prefixed --dev

Rector has minimal sets, meaning each minor version is standalone and independent. What does that mean? For upgrading from Symfony 4 to 5, you need to run all the minor version sets, one by one.

  1. Update rector.php config
use Rector\Symfony\Set\SymfonySetList;
use Rector\Config\RectorConfig;

return function (RectorConfig $rectorConfig): void {

    // take it 1 set at a time, so next set works with output of the previous set; I do 1 set per pull-request
    // $rectorConfig->import(SymfonySetList::SYMFONY_42);
    // $rectorConfig->import(SymfonySetList::SYMFONY_43);
    // $rectorConfig->import(SymfonySetList::SYMFONY_44);
    // $rectorConfig->import(SymfonySetList::SYMFONY_50);
  1. Run Rector
vendor/bin/rector process app src tests

Verify, check that CI passes and then continue with next Symfony minor version.

3. Update composer.json before composer update

Easy Admin Bundle

     "require": {
-        "alterphp/easyadmin-extension-bundle": "^2.1",
+        "alterphp/easyadmin-extension-bundle": "^3.0",


     "require": {
-        "doctrine/cache": "^1.8",
+        "doctrine/cache": "^1.10",
-        "doctrine/doctrine-bundle": "^1.11",
+        "doctrine/doctrine-bundle": "^2.0",
-        "doctrine/orm": "^2.6",
+        "doctrine/orm": "^2.7",

Doctrine Behaviors

     "require": {
-        "stof/doctrine-extensions-bundle": "^1.3",
-        "knplabs/doctrine-behaviors": "^1.6"
+        "knplabs/doctrine-behaviors": "^2.0"


     "require": {
-        "sentry/sentry-symfony": "^3.2",
+        "sentry/sentry-symfony": "^3.4",

Twig Extensions were Removed

     "require": {
-        "twig/extensions": "^1.5"

This might be scary at first, depends on how many of those functions have you used.

Look at the README on Github to find out more:

4. Symfony Packages in composer.json

Do you use Flex and config * version?

    "require": {
        "symfony/console": "*",
        "symfony/event-disptacher": "*"
    "extra": {
        "symfony": {
            "require": "^4.4"

Not sure why, but in some cases, it failed and blocked from the upgrading. I had to switch to explicit version per package, to resolve it:

     "require": {
-        "symfony/console": "*",
+        "symfony/console": "^4.4",
-        "symfony/event-disptacher": "*"
+        "symfony/event-disptacher": "^4.4"
-    },
+    }
-    "extra": {
-        "symfony": {
-            "require": "^4.4"
-        }
-    }

Then switch to Symfony 5:

-"symfony/asset": "^4.4",
+"symfony/asset": "^5.0",
-"symfony/console": "^4.4",
+"symfony/console": "^5.0",

// etc.

But some packages are released out of monorepo cycle:

-"symfony/maker-bundle": "^1.14",
+"symfony/maker-bundle": "^1.13",

All right, now you run...

composer update

...and get new packages with Symfony 5... or probably a lot of composer version conflicts.

Symfony Packages WTFs in

In Symfony 5, some packages were removed:

-"symfony/error-renderer": "^4.4",
-"symfony/web-server-bundle": "^4.4",

Some packages were replaced by new ones:

-"symfony/error-debug": "^4.4",
+"symfony/error-handler": "^5.0",

And some package were split into more smaller ones:

-"symfony/security": "^4.4",
+"symfony/security-core": "^5.0",
+"symfony/security-http": "^5.0",
+"symfony/security-csrf": "^5.0",
+"symfony/security-guard": "^5.0",

5. Rector, ECS, and PHPStan

These were production dependencies, but what about dev ones? Both have the same rules - they need to allow Symfony 5 installation.

The safest way is to use prefixed versions, which don't care about a Symfony version:

-"phpstan/phpstan": "^0.11",
+"phpstan/phpstan": "^0.12",
-"rector/rector": "^0.5",
+"rector/rector-prefixed": "^0.6",

Easy Coding Standard:

-"symplify/easy-coding-standard": "^0.11",
+"symplify/easy-coding-standard": "^0.12",

Update your composer.json to include a package that you need.

Then run:

composer update

Still conflicts?

6. And The Biggest Symfony Upgrade Blocker is...

If you don't do open-source, you probably don't use the git tag feature. It seems that the tagging of a package is a very difficult process. Even packages with million downloads/month had the latest 15 months ago.

What are Tags For?

Let's say you want to use symplify/easy-coding-standard that supports Symfony 5. Here is the deal:

    "require-dev": {
        "symplify/easy-coding-standard": "dev-master"
    "prefer-stable": true,
    "minimum-stability": "dev"

Now imagine one of your package you require requires some other package, that requires another package, that doesn't allow Symfony 5 in tagged version, but in master. Well, you've just finished.

That's why it's very important to know to tag a package regularly:

git tag v2.0.0
git push --tags

That's all! Still, many packages support Symfony 5 in the master but are not tagged yet... to be true, not once for the last 2 years. Why? The human factor, maintainers are afraid of negative feedback, of back-compatibility breaks, lack of test coverage takes their confidence, etc.

Tagging Cancer of PHP Ecosystem

These packages block Symfony 5 upgrade for months:

United We Stand

This will be resolved in the future by an open-source monorepo approach, but we're not there yet.

In the meantime, please complain at issues, ask for help and offer to become maintainer until it changes (or until somebody forks it and tags the fork).

One good example for all - I complained and offered help at knplabs/doctrine-behaviors, got maintainer rights in 3 hours and made + merged 30 pull-request in the last month.

You see, it works :)

7. Still Conflicts?

Ok, so you have the right version of packages, everything is stable and allows Symfony 5. Yet still, the composer says "conflicts, cannot install x/y".

To my surprise, the composer is very bad at solving complex conflicts. Composer reports false positive and blocks your install, because of installing packages in /vendor or overly strict composer.lock. I spent 30-60 minutes trying to figure out what the heck is conflicting on every Symfony training I had in the last 2 months. Now I'm able to do it in 3 minutes.


It works so well I do it more often than resolving conflicts manually.

8. Cleanup bundles.php

 return [
-    Doctrine\Bundle\DoctrineCacheBundle\DoctrineCacheBundle::class => ['all' => true],

Switch the dead gedmo/stof doctrine extensions for the maintained KnpLabs/DoctrineBehaviors. I'll write a standalone post about this migration, once a stable version is out (check me, pls :)).

 return [
-    Stof\DoctrineExtensionsBundle\StofDoctrineExtensionsBundle::class => ['all' => true],

We also had some troubles with Switfmailer Bundle:

 return [
-    Symfony\Bundle\SwiftmailerBundle\SwiftmailerBundle::class => ['all' => true],

The Mailer component will take over Swiftmailer in the future, so this is just a start.

9. Clear config/packages

 # config/packages/framework.yaml
-    templating:
-        engines: ["twig"]

Don't forget to remove all extension configs. In our case it was:


Small update of the EasyAdmin bundle:

 # config/routes/easy_admin.yaml
-    resource: '@EasyAdminBundle/Controller/AdminController.php'
+    resource: '@EasyAdminBundle/Controller/EasyAdminController.php'
And that's all folks!
Got any questions?
All the know-how is taken from practical pull-request, that was under strict Travis control:
<img src="/assets/images/posts/2019/symfony5_pr.png" class="img-thumbnail">

Feel free to explore it, ask, read comments or share your problems.

<a href="" class="btn btn-dark mb-5 mt-3">
    Check the PR on Github

Have we Missed Something?

Of course, we did! Every application has a different set of blocking dependencies and different sets of used Symfony features that might have changed.

Share your issues in comments or edit this post on Github to make list complete!

Happy coding!

Do you learn from my contents or use open-souce packages like Rector every day?
Consider supporting it on GitHub Sponsors. I'd really appreciate it!