Proiecte · 01

De pe un stack mixt PHP și Node.js pe Symfony 7, cu paritate comportamentală

Un produs folosit zilnic de companii mari a fost mutat pe o fundație modernă, bucată cu bucată, fără ca utilizatorii să simtă ceva și fără ca echipa să se oprească din livrat.

Migrare · SaaS enterprise

3 lunide pe un stack mixt PHP și Node.js pe Symfony 7
Context
O platformă multi-tenant folosită de clienți enterprise și din sectorul public în UE, UK, Canada și SUA. Stack-ul crescuse timp de aproape un deceniu: PHP pe Zend Framework, alături de servicii pe o versiune veche de Node.js, ambele ieșite din mentenanță. Facturare, API-uri publice, consumatori de cozi, cron-uri și SSO, toate pe cod care trebuia să meargă în continuare pe durata mutării.
Ce am făcut
Am ținut mutarea la paritate comportamentală strictă, 1:1. Noul cod Symfony 7 a rulat ca sistem shadow, în mod dry-run, lângă codul legacy și pe același input, iar outputul celor două a fost comparat până a devenit identic. Apoi clienții au fost mutați treptat, sub monitorizare, iar codul legacy și cel shadow au fost șterse. Facturarea, mai multe suprafețe de API public, consumatorii de cozi, cron-urile și SAML SSO au fost mutate toate așa, în timp ce produsul livra în continuare.
Rezultat
Migrare făcută în 3 luni, cu zero incidente în producție cauzate de o comutare și sub 15 buguri după aceea, toate minore. O bază de cod pe Symfony 7, cu 11 reguli PHPStan proprii și praguri de acoperire impuse în CI, pe care se angajează mai ușor și se livrează mai repede.
Stack
PHP 8, Symfony 7, Node.js, MySQL, Redis, SAML

De unde am pornit

Platforma era în producție de aproape un deceniu. Nucleul rula pe PHP cu Zend Framework, iar un set de servicii rula pe o versiune veche de Node.js. Ambele ajunseseră la capătul mentenanței: biblioteci care renunțau la suport de la un release la altul și tot mai puțini ingineri dispuși să lucreze pe ele.

Nimic nu ardea. Asta e partea incomodă la un stack nementenat: riscul crește în liniște și e cel mai ieftin de tratat cât timp produsul e sănătos și echipa nu e sub presiune.

De ce migrare și nu rescriere

Scopul a fost îngust intenționat: aducerea stack-ului la zi și eliminarea riscului de a rula pe tehnologie nementenată, fără să oprim roadmap-ul. O rescriere ar fi oprit livrările și ar fi pus în pericol un deceniu de comportament pe care se bazează clienții, inclusiv părțile pe care nu-și mai amintește nimeni că le-a scris.

Așa că regula a fost paritate 1:1. Fără redesign, fără îmbunătățiri "dacă tot suntem aici". Îmbunătățirile așteaptă până dispare codul vechi.

Un sistem shadow în loc de un salt în gol

Pe un sistem atât de vechi, nu ne-am bazat doar pe o suită de teste ca să dovedim paritatea. Am construit un sistem shadow: noul cod Symfony 7 rula lângă codul legacy, în mod dry-run, pe același input. Codul legacy rămânea la comandă și făcea munca reală. Codul nou calcula ce ar fi făcut, fără efecte secundare, iar outputul celor două era monitorizat și comparat.

Fiecare diferență era unul din două lucruri: un bug în codul nou sau un comportament al codului vechi pe care nu-l notase nimeni. Pe primele le-am reparat, pe celelalte le-am reprodus, și am repetat până când outputul celor două sisteme a fost complet identic.

Abia atunci au fost mutați clienții, treptat, cu ambele sisteme monitorizate. Când ultimul client a ajuns pe Symfony 7, codul legacy și sistemul shadow au fost șterse.

Garduri care rămân după migrare

O bază de cod migrată alunecă înapoi dacă arhitectura trăiește doar în capul oamenilor. Așa că deciziile sunt impuse de unelte, nu de memorie: 11 reguli PHPStan proprii rulează în CI alături de pragurile de acoperire. Ce impun:

  • Injectarea dependențelor: fără injectarea întregului container de servicii, fără injectarea claselor excluse din el și un singur stil de injectare, consecvent.
  • Accesul la baza de date: query-urile Doctrine stau în repository-uri și nicăieri altundeva, cu excepții înguste, scrise, pentru hook-uri de vendor, iar repository-urile întorc tipuri de domeniu, nu obiecte de query.
  • O singură cale de scriere pentru entitatea centrală: salvarea directă e interzisă în afara unei liste scurte în care fiecare intrare își are motivul notat, astfel încât regulile de dry-run, aprobare și evenimente să nu poată fi ocolite din greșeală.
  • Controllere și securitate: fiecare argument de controller declară cum este rezolvat și fiecare endpoint poartă un atribut de securitate cunoscut, deci nimic nu ajunge în producție fără o decizie explicită de autorizare.
  • Igiena entităților: fără accesori de umplutură și fără apeluri către accesori care nu mai există.

Aceleași porți se aplică și codului scris de oameni, și codului scris cu AI. Asta face ca asistența AI să poată fi folosită în viteză, în siguranță, pe această bază de cod.

Ce a ieșit

Migrarea a durat 3 luni, în timp ce produsul livra în continuare. Zero incidente în producție cauzate de o comutare. Singura surpriză a fost cât de puțin s-a întors după aceea: sub 15 buguri, toate minore.

Efectul de durată este viteza. Pe un framework la zi, cu analiză statică și porți în CI, echipa livrează funcționalități mai repede și iterează cu mai multă încredere, mai ales cu AI în buclă.

Contact

Hai să discutăm

Scrie-ne ce ai, în cuvintele tale. Nu trebuie să știi ce tehnologie folosește sistemul; ne dăm seama noi. Primești înapoi întrebări și o propunere scrisă.

Scrie-nesau direct la [email protected]