8 Steps You Can Make Before Huge Upgrade to Make it Faster, Cheaper and More Stable

"How much will cost upgrade from Symfony 3 to Symfony 4 with Rector?"

Similar questions fill my personal and Rector email in the last 3 months. It's hard to give a reasonable answer without actually seeing the repository, so I reply with follow-up questions to get get more details.

Yet, I've discovered there are few repeated patterns, that make the upgrade easier and that most projects can do by themselves before migration starts.

These points make any code migration faster and easier. It also decreases the time required to understand the code by a person who sees the code for the very first time.

Based on my experience with 10+ legacy projects of size 100 k-800 k lines, these points can be applied generally.

1. PSR-4 Standard

What are the benefits of using PSR-4 standard? If you use it, all classes are unique, autoloaded and easy to relate to file path. We need that for effective coding - so we don't have to care about it - and thus also for effective migrations.

If you have PSR-4 standard applied, your composer.json looks like this:

    "autoload": {
        "psr-4": {
            "App\\": "src"
    "autoload-dev": {
        "psr-4": {
            "App\\Tests\\": "tests"

If you meet all these conditions... wait, you need to make sure, there is also no code like:

class Some_Fake_Namespace_Class

...nor manual file requirements...

require __DIR__ . '/libs/SomeFramework/File.php';

...nor multiple classes in one file...

class SomeException extends Exception

class SomeOtherException extends Exception

...nor incorrect namespace/class case...

# app/lowercased/SomeController.php
namespace App\Lowercased;

class Somecontroller

Should be:

-class Somecontroller
+class SomeController
If you meet all this conditions,
the migration is ~20 % cheaper.

2. Explicit PHP Version

What? Every project has a PHP version... that's obvious for many projects, but there is still that can go wrong. How?

    "require": {
        "php": "^7.1"
    "config": {
        "platform": {
            "php": "7.2"

So... which is it?

    "require": {
        "php": "7.1"

Is that locked for PHP 7.1 for some reason... is it though?

    "require": {
        "php": "^5.6|^7.2"

2 major versions... is that an open-source? Btw, you should not be at PHP 5.6 at all, it's dead.

    "require": {

Ups! Make some up your mind. It's gonna be so weird to read this: it's the most common situation.

Is it in the Docker? No way! Docker is not version control. It only runs what you allow it to. Are you sure it handles PHP 4?

If you meet one major PHP version,
the migration is ~5 % cheaper.

3. EasyCoding Standard with Basic Sets

Why are coding standards needed for the migration? The AST libraries that Rector uses aren't well-suited to make code look nice and consistent, so it's better to let coding standard tools do that.

The basic ECS setup we use looks like:


// ecs.php


use Symplify\EasyCodingStandard\Configuration\Option;use Symplify\EasyCodingStandard\ValueObject\Set\SetList;

return static function (ContainerConfigurator $containerConfigurator): void {
    $parameters = $containerConfigurator->parameters();
    $parameters->set(Option::SETS, [
        // very nice to have ↓

Run it:

composer require symplify/easy-coding-standard --dev
vendor/bin/ecs check src --fix
If you meet all the basic coding standard sets,
the migration is ~5 % cheaper.

4. PHPStan on Level 8

Coding style is one of smoke testing layers. It means it runs all over your code, without being explicitly told to. From that, the static analysis is just one step away.

composer require phpstan/phpstan
vendor/bin/phpstan analyse src --level 0

It's better to start small, then go high (like with any other drugs):

vendor/bin/phpstan analyse src --level 1
vendor/bin/phpstan analyse src --level 2
vendor/bin/phpstan analyse src --level 8

There are many Rector rules, that help you with rules jumping.

If you make PHPStan to level 8 and passing,
the migration is ~15 % cheaper.

5. Newbie Composer Install Under 2 Hours

This is how we usually run a good-quality project:

git clone your-project.git

# install backend dependencies
composer install

# install frontend dependencies
npm install
# run database migration
bin/console doctrine:migrations:migrate

# run Symfony 5 project
php -S localhost:8000 -t public

That's it. It takes 15-30 minutes to run pehapkari.cz project locally, if you see it first.

The projects we meet are often hosted on Bitbucket, Github or Gitlab. Someone needs to add your SSH key there.

If it takes more than a day, something is wrong:

One project took me 2 weeks to ask for SSH keys 3 different people by 7 mails, 4 callings, one VPN... I still can't run composer install.

Flawless install under 2 hours is a luxury.

If you make composer install under 2 hours,
the migration is ~5 % cheaper.

6. 70 % Code Coverage

When we come to a completely new project, we need instant feedback, if we break something. It would be nice to have 100 % code coverage, but even my open-source project rates as high as 75 %.

It's like having CTO who rose the project constantly at your side for any change you make.

We don't care if it's functional, integration or unit tests - we just need the coverage to be sure nothing is wrong with the code. Without tests, any change in the code is like shooting blindfolded in the dark without hands at a target that is both invisible, moving and Shrodinger's cat.

On the other hand, if you have a code coverage over 80 % percent, even change of the framework can be as fast as 80 hours.

If you make it pass ~70 % code coverage,
the migration is ~50 % cheaper.

7. Not Versioned Vendor

I know it sounds crazy again, but it's not. Many projects we get have 2 vendor directories. One is versioned by composer.json and the other is versioned... somehow.

Why use composer patches or custom forks, if you can version packages locally. It's fast, it's healthy, it's all you wish for.

But getting packages out of local vendor is the real adventure. We need to compare every file in both directories, discover guess the version, test hope it's the right one, prevent from duplications with real vendor and so on.

If you don't version your vendor,
the migration is ~10 % cheaper.

8. Solid Gitlab CI

What if you have all the items above? When is the last time you've checked them? You don't know?

If the answer is not "at every commit", it's not good enough. You need to have CI. And I don't mean Bitbucket CI.

Why? It's not that Bitbucket CI is worse than Gitlab CI or GitHub actions. It's the ecosystem support. The Gitlab CI has the longest support for CI of a private project there is.

That means:

As a side bonus, it's free for private projects with unlimited users and 2 000 build minutes per month (I've never reached that).

If you use Gitlab CI on every commit,
the migration is ~10 % cheaper.

Worth It?

Let's say your goal is to migrate the whole framework or switch a legacy framework for a modern one. If you had skills, time and money to do that, you'd probably be there. It takes the experience with many legacy migrations to there effectively without years of time and full-rewrite.

But... these steps above don't depend on such experience. You can implement in your in-house team. Such work will reduce work on our side and make your code solid on your side with not such a big overhead.

Just pick one and start slowly.

Happy coding!