How we Upgraded Pehapkari.cz from Symfony 4 to 5 in 25 days

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.

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:

vendor/bin/rector process bin src packages --set symfony41

Verify, check that CI passes and then continue with following Symfony minor versions:

vendor/bin/rector process bin src packages --set symfony42
vendor/bin/rector process bin src packages --set symfony43
vendor/bin/rector process bin src packages --set symfony44
vendor/bin/rector process bin src packages --set symfony50

3. Update composer.json before composer update

Easy Admin Bundle

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

Doctrine

 {
     "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"
     }
 }

Sentry

 {
     "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.

How?

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
 framework:
     ...
-    templating:
-        engines: ["twig"]

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

-config/packages/stof_doctrine_extensions.yaml
-config/packages/swiftmailer.yaml
-config/packages/dev/swiftmailer.yaml
-config/packages/test/swiftmailer.yaml
-config/packages/twig_extensions.yaml
-config/routes/dev/twig.yaml

Small update of the EasyAdmin bundle:

 # config/routes/easy_admin.yaml
 easy_admin_bundle:
-    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:

Feel free to explore it, ask, read comments or share your problems.
  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!