Rector 0.18 - From Symfony Container to Laravel and How to Upgrade your Extensions

Since the first Rector version, we used Symfony container to inject the services. It worked very well. The new PHP 8.0 came with attributes, and Symfony started to use them extensively.

We're downgrading Rector down to PHP 7.2, and this forced us to lock with unmaintained Symfony 6.1. We needed a hacky patch to make Rector config work...

This made us think: Is there a better way?

The container started to cost more maintenance than the features it provided, so we tried an experimental switch to Laravel. To our surprise, this helped us to make our tests run 7x faster.


It's all about the Downgrade

How is this possible? Apart from the 3 changes we mentioned in the article regarding downgrading and scoping - the size matters.


During downgrade and scoping, all the /vendor is shipped with the Rector project. That means every dependency is scoped and downgraded - line by line.


What about features? Rector is a CLI app, and it needs a simple dependency injection container:

  • autowire service constructor,
  • pass tagged services,
  • callback to avoid circular dependencies.

If two packages are compatible, the more lines mean only more features we will never use.


Downgrading is a challenging process and sometimes it's a dead end - e.g., downgrading PHP code that uses PHP 8.0 attributes and doesn't provide a fallback feature on PHP 7.2 cannot be automated. Fewer lines mean fewer pitfalls to worry about.


Smaller is Better

Let's compare container package sizes without knowing the package name.

Guess which is which ↓

Filesystem                                         count
Directories ......................................... 14
Files .............................................. 186

Lines of code                           count / relative
Code ................................... 17 694 /   79 %
Comments ................................ 4 698 /   21 %
Total .................................. 22 392 /  100 %
  • 17 694 lines of code

Then the other:

Filesystem                                         count
Directories .......................................... 0
Files ................................................ 6

Lines of code                           count / relative
Code .................................... 1 091 / 56.1 %
Comments .................................. 855 / 43.9 %
Total ................................... 1 946 /  100 %
  • 1 091 lines of code

That means one is 16 x size greater than the other.


Results revealed:

  • the symfony/dependency-injection 6.3 ..... 17 694 lines
  • the illuminate/container 10.20 ..................... 1 081 lines

We also used symfony/http-kernel for the kernel test case to run, but we skipped transitional packages for the sake of comparison simplicity.


How to Upgrade your Extensions?

Rector 0.18 is the first release with a Laravel container. Now, we're testing in the wild.


If you've used bare RectorConfig class to set up your configuration, no upgrade is needed.

Few changes are required if you've used Symfony internal methods or maintain a custom Rector extension.


The way services are registered:

  • Before, you had to register every single service, even if fully autowired.
  • Now, you only register the services requiring a scalar parameter, factory, or tagged services.
 use Rector\Config\RectorConfig;

 return static function (RectorConfig $rectorConfig): void {
-    $services = $rectorConfig->services();
-    $services->set(App\SomeService::class);

+    $rectorConfig->singleton(App\SomeService::class, function () {
+        return new SomeService('some parameter');
+    });
};

Tag service:

 use Rector\Config\RectorConfig;

 return static function (RectorConfig $rectorConfig): void {
-    $services = $rectorConfig->services();
-    $services->set(App\SomeService::class)
-        ->tag(SomeInterface::class);

+    $rectorConfig->singleton(SomeService::class);
+    $rectorConfig->tag(SomeService::class, SomeInterface::class);
};

You can drop the $services->defaults() calls ultimately, as this is now included in the core:

-    $services = $rectorConfig->services();
-    $services->defaults()
-        ->public()
-        ->autowire()
-        ->autoctonfigure();

Also, PSR-4 autodiscovery is not needed anymore, as services are created for you:

-    $services->load('Rector\\', __DIR__ . '/../packages')
-        ->exclude([...]);

That's it! Find more in Laravel documentation and pull-request with container switch in Rector.

Is there something missing? Let us know to update this post. Thank you!


Happy coding!