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_

Upgrade to Sulu 2.5.0 with Rector

In this post I'd like to summarize some common pitfalls to make it easier to upgrade to Sulu version 2.5.0.

The right upgrade sequence 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. Like 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 sulu/sulu-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 to be 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.php

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 step. If not, this is the important first step before you update anything else.

This step requires the most work and includes the most pitfalls because 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 configure 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". By running composer update, all your Symfony dependencies should be updated to Symfony 5.4. You can then check if all Symfony dependencies were updated correctly via the composer info command.

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

The first change is that Symfony removed the "Symfony\Bundle\FrameworkBundle\Controller\Controller" class, and you will need to replace it 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 for details. 

A short example of the upgrade might 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;
    }
}

The second change is that there is a required update of your custom commands. Symfony removed the "Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand" class, and you will need to replace it 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 to see if everything works as expected. Then eventually update other breaking changes of Symfony 4 - 5 update. Only continue after fixing all additional incompatibilities to Symfony 5.4.

Step 2: Upgrading PHP to 8.1

If you are already on PHP 8.0 or 8.1 you can skip this step. If not,  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. In your composer.json, update the "php" requirement to "8.1.*". If required, update 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 gradually update them to your requirements.

Step 3: Upgrade Database schema

To upgrade your project database schema you can use manual migration via SQLs, as documented in the UPGRADE.md. Alternatively, you can use the DoctrineMigrationBundle, or 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 projects 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 used an 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 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 use the Swiftmailer in your project you should migrate all instances of it to 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 follow best practice, 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 its JavaScript build via the bin/console sulu:admin:update-build command. For all projects with a custom JavaScript build, we recommend testing it first with the shipped build and then add your customization after you verify that Sulu 2.5 works as 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 also  upgrading to Symfony 6. Depending on that all your other dependencies are also uptodate. Here again Rector will help you, depending if you had skiped step 1 and 2 you need to create or adjust or the rector.php in your project root directory 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,
    ]);
};

For future upgrades, just adjust 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 the sequence recommended here, we are keeping the update simple and focused on a single task. To me this is the most important thing when doing major upgrades.

Projects that keep Symfony and PHP up-to-date benefit here as they can simply skip the first two steps. And most of the work necessary will probably be in 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.

Any problems?

The blog should give you step by step tutorial for upgrading. It still can happen that not all of your project changes are targetted. If you run into any problems make sure to read the UPGRADE.md of Sulu which lists all changes. A project mostly includes other dependencies then Sulu make sure if your composer update between any steps did a major version jump or you run into any issue to read the changelog and upgrade files of that specific library.

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.