Updated with Symplify packages and local links.
It would take us 3 full-time months to rewrite this code in 2017. In February 2019, we did it in less than 3 weeks with the help of automated tools. Why and how?
Similar post was originally published in Czech on Zdrojak.cz, where it got colossal attention of the PHP community and hit a record of 56 comments. But when I talk about this migration with my English speaking PHP friends, it seems crazy to them, and they want to hear details - who, how, when, what exactly?
This post is for you (and for you of course, if you haven't read it on Zdroják).
Backend of Entry.do project - API application built on controllers, routing, Kdyby integrations of Symfony, Doctrine, and a few Latte templates. The application has been running in production for the last 4 years. We migrated from Nette 2.4 to Symfony 4.2.
How big is it? If we don't count tests, migration, fixtures, etc., the application has 270 PHP files in the length of 54 357 lines (using phploc).
How many unique routes does it have? 20...? 50...? 151! Just to have an idea, the pehapkari.cz website has 35 routes.
The application used framework Nette, which worked and met the technical requirements. The primary motivation for the transcription was the dying ecosystem and the over-integration of Symfony. What does the "dying ecosystem" mean? Nette released just 1 minor version since July 2016, while Symfony had 6 releases during the same period.
Why use unmaintained integrations of Kdyby and Zenify, that only integrate Symfony to Nette\DI, if Symfony is already there? The last new minor version of Nette was published 3 years ago. Symfony releases a new minor version every 6 months with new features that will make your work easier.
I offered Honza Mikes deal he couldn't refuse:
"We will give it a week, and if we get stuck, we'll give up".
On January 27th, we met with his Nette application, and on February 13th, the Symfony application went to the staging server. In less than 17 days, we finished migration, and on February 14th, we celebrated a new production application in addition to Valentine's Day.
We talked about migration at the beginning of 2017 because the Nette ecosystem wasn't developing, and Symfony was technologically skipping it. At that time, however, the transition would last at least 80-90 days for full-time, which is insane, so we didn't go into it.
In 2019 we already had a lot of tools to do the work for you:
The first is Rector, a tool I made that can change any code that runs at least on PHP 5.3 from pattern A to pattern B. It can instantly update the code from PHP 5.3, 5.4, 5.5, 5.6... to 7.4, Symfony from 2.8 to 4.2, Laravel from static code to constructor injection, and more. You can add your own rules tailored to migrate your specific code, that can handle anything that PHP programmer can do (A → B) in a fraction of the time.
The second is NeonToYamlConverter - as you can guess, it converts NEON syntax to YAML
The third assistant is LatteToTwigConverter - it migrates Latte files to TWIG syntax
During those 17 days, we put in 80 hours of work for both of us together (= 40 hours each).
Although we do not like it, we had to do 20 % of the migration manually.
One of the first steps was to move from config programming to PHP programming. Both frameworks try to promote their sugar syntax for Neon or YAML. It sounds cool to new programmers to write less code. Still, it's confusing, framework-specific, can be done in plain PHP anyway, and most importantly, static analysis and instant refactoring won't deal with it.
How does "config programming" look like?
services:
- FirstService(@secondService::someMethod())
Or also:
services:
-
class: 'Entrydo\Infrastructure\Payment\GoPay\NotifyUrlFactory'
arguments:
- '@http.request::getUrl()::getHostUrl()'
What typical PHP pattern, that is framework-agnostic and almost everyone knows, can we use?
Factory!
<?php
final class FirstServiceFactory
{
/**
* @var SecondService
*/
private $secondService;
public function __construct(SecondService $secondService)
{
$this->secondService = $secondService;
}
public function create()
{
return new SomeService($this->secondService);
}
}
In Nette and Symfony, several things were different:
Automatic tools did another 80% of the pull-request you saw above. The first one was enough to write, the other one to set it up.
Neon and YAML are de facto fields with minor differences in syntax, but when it comes to services, each framework writes a little differently. Config with services had 316 lines in the services section. You don't want to migrate it manually, the Neon entities. Besides, just one error in related migration, and you can do it all over again.
I took few hours and wrote Symplify/NeonToYamlConverter. Just pass the path to the *.neon
file, and it will convert into a beautiful *.yaml
file.
Again to the factory pattern - there were several custom Response classes in the code that inherited from Nette Response and added extra logic. We could edit them manually, but it was easier to extract them into the factory method:
<?php
class SomePresenter
{
+ /**
+ * @var ResponseFactory
+ */
+ private $responseFactory;
+
+ public function __construct(ResponseFactory $responseFactory)
+ {
+ $this->responseFactory = $responseFactory;
+ }
+
public function someAction()
{
- return new OKResponse($response);
+ return $this->responseFactory->createJsonResponse($response);
}
}
Honza created new NewObjectToFactoryCreateRector
rule that handled this.
RouterFactory
to particular Controller actionsRequest
and Response
classes + including their codes (POST
, GET
, 200
...)Nette\DI\Container
, Nette\Configurator
, Nette\Application\IPresenter
etc,*Controller
(they use "Presenter" naming in Nette)App\Presenter
to App\Controller
The most changes were in controllers:
<?php declare (strict_types = 1);
-namespace App\Presenter;
+namespace App\Controller;
-use Nette\Application\AI\Presenter;
+use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
-use Nette\Http\Request;
+use Symfony\Component\HttpFoundation\Request;
-final class SomePresenter extends Presenter
+final class SomeController extends AbstractController
{
- public static function someAction()
+ public static function someAction(Request $request)
{
- $header = $this-> httpRequest-> getHeader('x');
+ $header = $request-> headers-> get('x');
- $method = Request::POST;
+ $method = Request::METHOD_HPOST
}
}
For a while, Kdyby\Translation screwed us with "syntax sugar". In the Nette application, the listing of variables (Tom) worked for us:
But in Symfony magically added %%
:
WTF? After 15 minutes we figured it out - Kdyby\Translation wrapped the variable name in "%%" for you - and fixed it:
<?php
class SomePresenter
{
public function someAction()
{
// Kdyby/Translation difference to native Symfony/Translation
$this->translations->translate('Hi, my name is %name%', [
- 'name' => 'Tom',
+ '%name%' => 'Tom'
]);
}
}
Pretty cool, huh?
We also cannot forget the rename of events from Contribute\Events to Symfony KernelEvents:
RouterFactory
to Controller @Route
annotationRouteFactory is single class in Nette to define all routes for all controllers and their actions. In Symfony, this is quite the opposite. You define the routes directly at the Controller action. And to make matters worse, it uses annotation.
What with this? Well, one option is to move one route at a time - all 151. To make it even more challenging, we had our own RestRoute
and our own RouteList
, including POST/GET/..., which Nette doesn't have.
How does one change look like?
<?php
namespace App;
use Entrydo\RestRouteList;
use Entrydo\Restart;
final class RouterFactory
{
- private const PAYMENT_RESPONSE_ROUTE = '/ payment / process';
// 150 more!
public function create()
{
$router = new RestRouteList();
- $router[] = RestRoute::get(self::PAYMENT_RESPONSE_ROUTE, ProcessGPWebPayResponsePresenter::class);
// 150 more!
return $router;
}
}
namespace App Presenter;
+use Symfony\Component\Routing\Annotation\Route;
final class ProcessGPWebPayResponsePresenter
{
+ /**
+ * @Route(path = "/payments/gpwebpay/process-response", methods="GET"})
+ */
public function __invoke()
{
// ...
}
}
Now do this 151 times... and make rebase-proof. When we first talked about the migration in 2017, we would make all these changes manually. Too lazy to work.
And in 2019? For a few days, we were preparing the nette-to-symfony
Rector set and then run it on the entire code base:
composer require rector/rector -dev
vendor/bin/rector process app src --level nette-to-symfony
And it is done :)
Everything we've learned during the 17-day migration is in this set and this post. Just download Rector, and you can use the set straight away.
From Valentine's Day to the nette-to-symfony set, a complete migration from Nette Tester to PHPUnit and the migration of Nette Forms to Symfony Forms and Component to Controllers have been added.
After a lot of static content changes, the code worked, and the tests went through, but it looked messy. Spaces were missing, fully qualified class names were not imported, etc.
You can use your own PHP_CodeSniffer and PHP-CS-Fixer set. We used the [Rector-prepared set] with ECS:
vendor/bin/ecs check app src --config vendor/rector/rector/ecs-after-rector.php
And so we migrated a 4-years old Nette application of 54 357 lines under 80 hours to Symfony and put it into production. Most of the time took us debugging events and writing migration rules and tools. Now the same application would take us (or you) 10 hours top to migrate.
As you can see, any application can be migrated from one framework to another under a month. Dare us!
Happy coding!
Do you learn from my contents or use open-souce packages like Rector every day?
Consider supporting it on GitHub Sponsors.
I'd really appreciate it!