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ă.