Alexander-Schranz
Alexander Schranz
Core Developer – Sulu GmbH
Core developer and support king. So dedicated to his work that we couldn't find a hobby to mention.
@alex_s_
2022-07-27

Upgrade to Sulu 2.5.0 with Rector

I'd like to summarize the common pitfalls in this post to make it easier to upgrade to latest sulu version.

The right upgrade order is gold

As mentioned in the release blog post for Sulu 2.5.0 the order of upgrading Sulu to 2.5.0 should be:

  1. Upgrade your project to Symfony 5.4 
  2. Upgrade your Project to PHP 8.0 / 8.1
  3. Upgrade your database to create the new schema for 2FA
  4. Fix any conflicting return types
  5. Optional but recommended: Replace usage of Swiftmailer with Symfony Mailer
  6. Now follow our Upgrading Sulu 2.x documentation to update your project to Sulu 2.5
  7. Optional but recommended: Update to Symfony 6

This sequence is highly recommended by us and you should try to do it step by step. Trying to do everything at once can be really frustrating and could end in chaos.

In the next steps I will show you how to achieve a successful update using Rector:

Step 0: Installing Rector and PHPStan

First we need to install Rector. As any dependency we install it via composer. Since Rector is built on top of PHPstan we also need to install this including its Symfony dependencies. If you have one or both tools already installed make sure you are updating them to the latest version and install the recommended extensions shown here:

composer require rector/rector --dev
composer require phpstan/phpstan phpstan/extension-installer phpstan/phpstan-symfony phpstan/phpstan-doctrine --dev

Configure PHPStan

The recommended way of configuring Phpstan is:

parameters:
    paths:
        - src
        - tests
        - config
    level: max
    doctrine:
        objectManagerLoader: tests/phpstan/object-manager.php
    symfony:
        container_xml_path: %currentWorkingDirectory%/var/cache/website/dev/App_KernelDevDebugContainer.xml
        console_application_loader: tests/phpstan/console-application.php

The Phpstan configuration requires two additional ".php" files which we need to create in our project. You can copy the following two code snippets to create them:

<?php

// tests/phpstan/console-application.php

declare(strict_types=1);

use App\Kernel;
use Symfony\Bundle\FrameworkBundle\Console\Application;

require \dirname(__DIR__) . '/bootstrap.php';

$kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']);
$kernel->boot();

return new Application($kernel);
<?php

// test/phpstan/object-manager

declare(strict_types=1);

use App\Kernel;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Events;
use Doctrine\ORM\Tools\ResolveTargetEntityListener;
use Symfony\Component\DependencyInjection\ContainerInterface;

require \dirname(__DIR__) . '/bootstrap.php';

$kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']);
$kernel->boot();

/** @var ContainerInterface $container */
$container = $kernel->getContainer();

/** @var EntityManager $objectManager */
$objectManager = $container->get('doctrine')->getManager();

// remove ResolveTargetEntityListener from returned EntityManager to not resolve SuluPersistenceBundle classes
// this is a workaround for the following phpstan issue: https://github.com/phpstan/phpstan-doctrine/issues/98
$resolveTargetEntityListener = \current(\array_filter(
    $objectManager->getEventManager()->getListeners('loadClassMetadata'),
    static fn ($listener) => $listener instanceof ResolveTargetEntityListener,
));

if (false !== $resolveTargetEntityListener) {
    $objectManager->getEventManager()->removeEventListener([Events::loadClassMetadata], $resolveTargetEntityListener);
}

return $objectManager;

Step 1: Upgrading Symfony to 5.4

If you are already using Symfony 5.4 you can skip this. If not this is the important first step before you are updating something else.

This is part includes the most pitfalls and the most work as you are required to jump on a new major version of Symfony. But with Rector we are also trying to automate most things.

To update our code to be compatible with Symfony 5.4 we will first confiugre our rector.php the following way. We are keeping the Rector file small so we can concentrate on the Symfony upgrade:

<?php

declare(strict_types=1);

use Rector\Config\RectorConfig;
use Rector\Symfony\Set\SymfonyLevelSetList;
use Rector\Symfony\Set\SymfonySetList;

return static function (RectorConfig $rectorConfig): void {
    $rectorConfig->paths([__DIR__ . '/src', __DIR__ . '/tests']);

    $rectorConfig->phpstanConfig(__DIR__ . '/phpstan.neon');

    // basic rules
    $rectorConfig->importNames();
    $rectorConfig->importShortClasses();

    $rectorConfig->sets([
        // SymfonySetList::SYMFONY_CONSTRUCTOR_INJECTION, // optional
        SymfonyLevelSetList::UP_TO_SYMFONY_54,
    ]);
};

After configuring this you can run vendor/bin/rector process to upgrade your code.

Now we need to adjust the composer.json and change all "symfony/" dependencies from "^4.4" to " ^5.4". With running composer update all your Symfony dependencies should be updated to Symfony 5.4. Via composer info command you can then check if all Symfony dependencies where updated correctly.

There are mainly 2 changes that effect typical Symfony projects when upgrading from 4.4 to 5.4. Depending on how successfully Rector was able to analyze your code some parts have been updated already.

The first one is that Symfony removed the "Symfony\Bundle\FrameworkBundle\Controller\Controller" class, which you need to replace with "Symfony\Bundle\FrameworkBundle\Controller\AbstractController". For this you need to define your services via the getSubscribedService method. Take a look at the official Symfony documentation here for details. A short example of the upgrade can look like this:

<?php

// before

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;

class YourController extends Controller {
    public function indexAction()
    {
          $this->get('logger')->info('test');
    }
}

// after

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use PSR\Log\LoggerInterface;

class YourController extends AbstractController {
    public function indexAction()
    {
          $this->container->get('logger')->info('test');
    }

    public function getSubscribedServices(): array
    {
           $subscribedServices = parent::getSubscribedServices();
           $subscribedServices['logger'] = LoggerInterface::class;

           return $subscribedServices;
    }
}

Secondly, there is the required update of your custom commands. Symfony removed the "Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand" class which need to be replaced with the "Symfony\Component\Console\Command\Command". Also if "autoconfigure" is not activated the command services need to be tagged with "console.command". Here's an example for this:

<?php

// before

namespace App\Command;

use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;

class MyCommand extends ContainerAwareCommand {
     public function execute(/* ... */)
     {
           $this->container->get('logger')->info('Test');
     }
}

// after

namespace App\Command;

use Symfony\Component\Console\Command;

class MyCommand extends Command {
     public function __construct(LoggerInterface $logger)
     {
           $this->logger = $logger;
     }

     public function execute(/* ... */): int
     {
            $this->logger->info('Test');

            return 0;
     }
}

Now test your project if everything works like expected and eventually update other breaking changes of Symfony 4 - 5 update. Continue only after fixing all this additional incompatibilities to Symfony 5.4.

Step 2: Upgrading PHP to 8.1

If you are on PHP 8.0 or 8.1 already you can skip this step. Else we are now upgrading our code to be compatible with the latest PHP version. For this make sure you have PHP 8.1 installed on your System. Upgrade in your "composer.json" the "php" requirement to "8.1.*". If configured, the "config.platform.php" version to "8.1.8". After this change you should run "composer update" to update all your dependencies to a compatible PHP 8.1 version. In the next step we will update our PHP code to be compatible with PHP 8.1. We can achieve this via the following rector config:

<?php

declare(strict_types=1);

use Rector\Config\RectorConfig;
use Rector\Set\ValueObject\LevelSetList;
use Rector\PHPUnit\Set\PHPUnitLevelSetList;
use Rector\PHPUnit\Set\PHPUnitSetList;

return static function (RectorConfig $rectorConfig): void {
    $rectorConfig->paths([__DIR__ . '/src', __DIR__ . '/tests']);

    $rectorConfig->phpstanConfig(__DIR__ . '/phpstan.neon');

    // basic rules
    $rectorConfig->importNames();
    $rectorConfig->importShortClasses();

    // optional:
    // $rectorConfig->sets([
    //     PHPUnitLevelSetList::UP_TO_PHPUNIT_90,
    //     PHPUnitSetList::PHPUNIT_91,
    // ]);

    $rectorConfig->sets([
        LevelSetList::UP_TO_PHP_81,
    ]);
};

After running the vendor/bin/rector process command your code should be updated. Re-check all changes and eventually update them to your requirements.

Step 3: Upgrade Database schema

To upgrade your project database schema you can use manual migration via SQLs, just as documented in the UPGRADE.md. Alternative you can use the DoctrineMigrationBundle. Alternatively the recommended bin/console doctrine:schema:update command to update the Schema of your database. 

CREATE TABLE se_user_two_factors (id INT AUTO_INCREMENT NOT NULL, method VARCHAR(12) DEFAULT NULL, options LONGTEXT DEFAULT NULL, idUsers INT NOT NULL, UNIQUE INDEX UNIQ_732E8321347E6F4 (idUsers), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB;
ALTER TABLE se_user_two_factors ADD CONSTRAINT FK_732E8321347E6F4 FOREIGN KEY (idUsers) REFERENCES se_users (id) ON DELETE CASCADE;

// most project are also effected by the SQL upgrades from Sulu Version 2.4.3 so have a look also at the UPGRADE.md if your project did use a old version previously

Step 4: Fix conflicting return types

Some return types has been changed from Sulu 2.4 to Sulu 2.5 so have a look at the list in the UPGRADE.md about this. The breaking changes here can again be updated via a Rector rule. We are now again adjusting our rector.php to the following:

<?php

declare(strict_types=1);

use Rector\Config\RectorConfig;
use Sulu\Rector\Set\SuluLevelSetList;

return static function (RectorConfig $rectorConfig): void {
    $rectorConfig->paths([__DIR__ . '/src', __DIR__ . '/tests']);

    $rectorConfig->phpstanConfig(__DIR__ . '/phpstan.neon');

    // basic rules
    $rectorConfig->importNames();
    $rectorConfig->importShortClasses();

    // sulu rules
    $rectorConfig->sets([
        SuluLevelSetList::UP_TO_SULU_25,
    ]);
};

Step 5: Replace Swiftmailer with Symfony Mailer

This step is optional but recommend for future upgrades of your project. If you did use the Swiftmailer inside your project you should migrate all appeareance of it with the Symfony Mailer component. For this have a look at the official Symfony Mailer documentation.

Step 6: Upgrade Sulu to 2.5

In the next step you should upgrade Sulu to the latest 2.5 version by following the upgrade documentation. As documented the first step is to update the composer json via composer require sulu/sulu:"~2.5.0" --no-update and composer update command. Most skeleton changes are not required but if you want to keep your project with best practices have a look at the changes from 2.4.4 -> to 2.5.0 via Github.
Most of the breaking changes have already been fixed in Step 4 via Rector. So the last step to Sulu 2.5 is to update it's JavaScript build via the bin/console sulu:admin:update-build command. For all projects having a custom JavaScript build we recommend testing it first with the shipped build and then add your customization after you tested if Sulu 2.5 works like expected.

If you run into any errors, check the UPGRADE.md file. If you are upgrading from a version before 2.4.x, you also need to follow the upgrade steps of the previous versions.

Step 7: Upgrade to Symfony 6.1

This step is optional but recommended. After you have upgraded successfully to Sulu 2.5 you should have no problem to upgrade your Project to Symfony 6 also. Here again Rector will help you by adjusting the rector.php file to the following:

<?php

declare(strict_types=1);

use Rector\Config\RectorConfig;
use Rector\Symfony\Set\SymfonyLevelSetList;
use Rector\Symfony\Set\SymfonySetList;

return static function (RectorConfig $rectorConfig): void {
    $rectorConfig->paths([__DIR__ . '/src', __DIR__ . '/tests']);

    $rectorConfig->phpstanConfig(__DIR__ . '/phpstan.neon');

    // basic rules
    $rectorConfig->importNames();
    $rectorConfig->importShortClasses();

    $rectorConfig->sets([
        // SymfonySetList::SYMFONY_CONSTRUCTOR_INJECTION, // optional
        SymfonyLevelSetList::UP_TO_SYMFONY_61,
    ]);
};

After this you are on the latest version of supported dependencies of Sulu. I recommend now to update your rector.php with the following configuration to activate the code quality checks of Rector:

<?php

declare(strict_types=1);

use Rector\Config\RectorConfig;
use Rector\Doctrine\Set\DoctrineSetList;
use Rector\PHPUnit\Set\PHPUnitLevelSetList;
use Rector\PHPUnit\Set\PHPUnitSetList;
use Rector\Set\ValueObject\LevelSetList;
use Rector\Set\ValueObject\SetList;
use Rector\Symfony\Set\SymfonyLevelSetList;
use Rector\Symfony\Set\SymfonySetList;
use Sulu\Rector\Set\SuluLevelSetList;

return static function (RectorConfig $rectorConfig): void {
    $rectorConfig->paths([__DIR__ . '/src', __DIR__ . '/tests']);

    $rectorConfig->phpstanConfig(__DIR__ . '/phpstan.neon');

    // basic rules
    $rectorConfig->importNames();
    $rectorConfig->importShortClasses();

    $rectorConfig->sets([
        SetList::CODE_QUALITY,
        LevelSetList::UP_TO_PHP_81,
    ]);

    // symfony rules
    $rectorConfig->symfonyContainerPhp(__DIR__ . '/var/cache/website/dev/App_KernelDevDebugContainer.xml');

    $rectorConfig->sets([
        SymfonySetList::SYMFONY_CODE_QUALITY,
        SymfonySetList::SYMFONY_CONSTRUCTOR_INJECTION,
        SymfonyLevelSetList::UP_TO_SYMFONY_60,
    ]);

    // doctrine rules
    $rectorConfig->sets([
        DoctrineSetList::DOCTRINE_CODE_QUALITY,
    ]);

    // phpunit rules
    $rectorConfig->sets([
        PHPUnitLevelSetList::UP_TO_PHPUNIT_90,
        PHPUnitSetList::PHPUNIT_91,
    ]);

    // sulu rules
    $rectorConfig->sets([
        SuluLevelSetList::UP_TO_SULU_25,
    ]);
};

In future upgrades adjust just your rector.php to the desired version and Rector will do 90% of the work for you. Still, don't forget to follow the official upgrade docs and upgrade.md files of your dependencies.

Conclusion

With Rector and my recommended sequence we are keeping the update simple and focused on a single task. To me this is the most important when doing this upgrades.
Projects that keep Symfony and PHP up to date benefit here as they can simply skip the first 2 steps. And most of the work necessary will probably be doing the Symfony upgrade from 4.4 to 5.4.

As always, we are happy to hear your feedback about this new release of Sulu CMS. 

Feel free to create an issue or a discussion on GitHub for bugs and feature requests. You can also contact us via Slack or our website anytime.

What is coming next?

We are currently working on bringing Symfony 6 to all our Sulu Bundles and to update our Demo to the latest Sulu and Symfony versions.

Finally, if you have not done it yet, don't hesitate to spread some love and leave Sulu a ⭐ on GitHub.