Jak jsme zmigrovali 50k řádků kódu z Nette do Symfony za 17 dní ve 2 lidech
Publikováno: 25.3.2019
Kód, který bychom před dvěma lety přepisovali dobré 3 měsíce, jsme dnes za pomoci automatických nástrojů zvládli přepsat za necelé 3 týdny. Ukážeme vám, jak jsme na to šli.
Text vychází z anglické verze, která vyšla na autorově blogu jako3-dílnýseriál.
Co jsme migrovali?
Backend projektu Entry.do – API aplikace ostavená na presenterech, routingu, Kdyby integracích Symfony, Doctrine a pár Latte šablonách, která už 4 roky běží na produkci. Z Nette 2.4 na Symfony 4.2.
Jak je velká? Když nepočítáme testy, migrace, fixtures apod., aplikace má 270 PHP souborů v délce 54 357 řádků.
Říkáte se, kolik má taková aplikace rout? 20…? 50…? 151!
Proč?
Aplikace byla v Nette, které fungovalo a splňovalo technické požadavky. Hlavní motivací na přepis byl umírající ekosystém a integrace Symfony.
Proč používat neudržované integrace Kdyby a Zenify, které stejně dělají jen integraci do Nette/DI, když je tu přímo Symfony? V Symfony vyjde každých 6 měsíců nová verze a nové featury je usnadní práci.
Jak jsme to udělali?
Přesvědčil jsem Honzu Mikeše že to: “Dáme týden a když to bude drhnout, tak to zabalíme”. 27. 1. jsme se potkali nad Nette aplikací a 13. 2. šla na staging server už aplikace Symfony. Za 17 dní jsme měli hotovo a na 14. 2. jsme kromě Valentýna slavili novou aplikaci na produkci.
Ve skutečnosti jsme se o migraci bavili už na začátku roku 2017, protože Nette ekosystém se pořádně nerozvíjel a Symfony jej technologicky přeskočilo. Tenkrát by ovšem přechod trval minimálně 80-90 dní na full-time, což je šílenost, takže jsme do toho nakonec nešli.
V roce 2019 už máme spoustu nástrojů, které práci udělají za vás:
- Prvním je Rector, který dokáže změnit jakýkoliv kód který jede aspoň na PHP 5.3 z A na B. V základu umí instantně aktualizovat kód z PHP 5.3 na na 7.4, Symfony z 2.8 na 4.2, Laravel ze statického kódu na constructor injection apod. Navíc si do něj můžete dopsat vlastní pravidla, která dělají cokoliv zvládne PHP programátor (A → B), jen za zlomek času.
- Druhým je NeonToYamlConverter – ten jak název napovídá převádí NEON syntaxi na YAML.
- Třetím pomocníkem je LatteToTwigConverter – ten převádí zase Latte do Twigu.
Takže těch 17 dní bylo nakonec dohromady pohodových ~80 hodin (za nás oba dohromady).
20 % ruční práce
I když neradi, 20 % práce jsme museli odpracovat ručně.
Jedním z prvních kroků byl přesun od programování v configu do programování PHP. Oba frameworky se snaží ukázat vlastní syntax sugar pro Neon nebo Yaml. Programátorům to zní sice cool, psát méně kódu, je to ale nepřehledné, framework-specific, a hlavně – statická analýza, ani instantní refaktoring si s tím neporadí.
Jak to “programování v configu“ vypadá?
services:
FirstService(@secondService::someMethod())
Nebo taky:
services:
-
class: Entrydo\Infrastructure\Payment\GoPay\NotifyUrlFactory
arguments:
- @http.request::getUrl()::getHostUrl()
Jaký normální PHP pattern, kterému skoro každý rozumí i když nedělá s žadným frameworkem, můžete použít?
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);
}
}
Co jsme refaktoringem získali?
- Constructor injection!
- Nezávislost na frameworku – když za 3 roky budeme migrovat na jiný framework, tenhle soubor už nemusíme řešit.
- Statická analýza funguje
- Testovatelnější kód, díky PHP kódu místo configu
- Rector funguje
- Je to jasný PHP kód
V Nette a Symfony se několik věcí dělalo výrazně různě:
- ErrorPresenter → ExceptionSubscriber
- Použití SymfonyTestBundle na testy
- Přesouvání souborů do Symfony 4 jednoúrovňové struktury
- Výměna služeb v testech za mocky
- V Nette je Request/Reponse služba, ale v Symfony object
- Přepis extension configů na flex configy
80 % automaticky
Dalších 80 % práce z pull-requestu, který jste viděli nahoře, za nás udělali automatické nástroje. Ten první stačilo napsat, ten druhý prakticky nastavit.
Neon do Yaml
Neon i Yaml jsou de facto pole s drobnými rozdíly v syntaxi, když jde ale o služby, každý framework je zapisuje trochu jinak. Config se službami měl 316 řádků v sekci services. To nechcete migrovat ručně, tím spíš Neon entity. Navíc stačí jedna chyba v migraci souvisejícího a můžete to dělat celé znovu.
Tak jsem vzal napsal Symplify/NeonToYamlConverter. Stačí předat cestu k *.neon souboru a na výstupu bude krásně převedený *.yaml.
Migrace PHP
Ještě jednou k factory patternu – v kódu bylo několik vlastních Response tříd, které dědily od Nette Response a přidávaly extra logiku. Mohli bychom je upravovat jednu po druhé ručně, jednodušší ale bylo extrahovat je do factory metody:
<?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);
}
}
S tím nám pomohl Rector a NewObjectToFactoryCreateRector pravidlo, které Honza vytvořil.
Co ještě zbývalo?
- Přesun rout z RouterFactory k jednotlivým akcím Controllerům
- Přejmenování Request a Response tříd + včetně jejich kódů (POST, GET, 200…)
- Přejmenování tříd a metod na Nette\DI\Container, Nette\Configurator, Nette\Application\IPresenter atd.
- Změna parent tříd na Presenterech, přejmenování na *Controller
- Přesun namespacu z App\Presenter to App\Controller
Nejvíc by nám dali zabrat presentery.
<?php declare(strict_types=1);
-namespace App\Presenter;
+namespace App\Controller;
-use Nette\Application\UI\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
}
}
Syntax Sugar? Syntax Hell
Na chvíli nás taky vypekl Kdyby “syntax sugar”, resp. Kdyby\Translation. V Nette aplikaci nám výpis proměnných (Tom) ještě fungoval:
- “Hi, my name is Tom”
Ale v Symfony magicky přibyli %%:
- “Hi, my name is %Tom%”
WTF? Po 15 minutách jsem na to přišli a opravili:
<?php
class SomePresenter
{
public function someAction()
{
// Kdyby/Translation differnce to natvie Symfony/Translation
$this->translations->translate('Hi, my name is %name%', [
- 'name' => 'Tom',
+ '%name%' => 'Tom',
]);
}
}
Nesmíme zapomenou na přejmenování eventů z Contribute\Events na Symfony KernelEvents:
Z RouterFactory na Controller @Route annotation
RouteFactory je v Nette jedna třída, kde obvykle definujete všechny routy na všechny presentery a jejich akce. V Symfony je tohle úplně naopak. Routy definujete přímo na akci v Controlleru. A aby toho nebylo málo, tak pomocí anotace.
Co s tím? No přesunout jednu routu po druhé – všech 151. Aby toho nebylo málo, měli jsme vlastní RestRoute a vlastní RouteList, včetně rozlišení POST/GET/…, které Nette v základu nemá.
Jak taková 1 změna vypadá?
<?php
namespace App;
use Entrydo\RestRouteList;
use Entrydo\RestRoute;
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()
{
// ...
}
}
Rector
V roce 2017 bychom všechny tyhle změny dělali ručně. Teď už je naštěstí rok 2019, a my jsme líně použili nástroj Rector.
Pár dní jsme připravovali nette-to-symfony set a ten pak pustili nad celou code base:
composer require rector/rector --dev
vendor/bin/rector process app src --level nette-to-symfony
A je to :)
Všechno, co jsme se při migraci za těch 17 dní naučili, je v tomto setu. Vy si jen stáhnete Rectora a set rovnou můžete použít.
Od Valentýna do nette-to-symfony setu přibyla kompletní migrace z Nette\Tester na PHPUnit a počátek migrace Nette\Forms na Symfony\Forms a Component na Controllery.
Na závěr už jen učesat
Po spoustě změn, které se týkaly statického obsahu, sice kód fungoval a testy procházely, ale vypadal trochu rozcuchaně. Tu mezera navíc, jinde zase chyběla.
Můžete použít vlastní PHP_CodeSniffer set nebo PHP-CS-Fixer . Na to jsme použili připravený set 7 pravidel pro EasyCodingStandard:
vendor/bin/ecs check app src vendor/rector/rector/ecs-after-rector.yaml
A tak jsme zmigrovali čtyřletou Nette aplikaci o 54 357 řádcích za 17 dní do Symfony a nasadili ji do produkce.