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.

Celý článek

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.

80 % rozšíření jsou jen integrace Symfony a Doctrine

Z Nette byly jen prestenery, routing a dependency-injection

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.

Takhle velký byl pull-request s migrací

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ě:

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.

Nahoru
Tento web používá k poskytování služeb a analýze návštěvnosti soubory cookie. Používáním tohoto webu s tímto souhlasíte. Další informace