Ghidul final pentru transpilarea codului PHP

Publicat: 2021-09-22

În circumstanțe ideale, ar trebui să folosim PHP 8.0 (cea mai recentă versiune la momentul scrierii acestui articol) pentru toate site-urile noastre și să o actualizăm de îndată ce este lansată o nouă versiune. Cu toate acestea, dezvoltatorii vor trebui adesea să lucreze cu versiuni anterioare PHP, cum ar fi atunci când creează un plugin public pentru WordPress sau lucrează cu cod vechi care împiedică actualizarea mediului serverului web.

În aceste situații, am putea renunța la speranța de a folosi cel mai recent cod PHP. Dar există o alternativă mai bună: putem în continuare să scriem codul sursă cu PHP 8.0 și să-l transpilăm într-o versiune PHP anterioară - chiar și la PHP 7.1.

În acest ghid, vă vom învăța tot ce trebuie să știți despre transpilarea codului PHP.

Ce este transpilarea?

Transpiling convertește codul sursă dintr-un limbaj de programare într-un cod sursă echivalent al aceluiași limbaj de programare sau al unui alt limbaj de programare.

Transpilarea nu este un concept nou în dezvoltarea web: dezvoltatorii de pe partea clientului vor fi foarte probabil familiarizați cu Babel, un transpiler pentru codul JavaScript.

Babel convertește codul JavaScript din versiunea modernă ECMAScript 2015+ într-o versiune moștenită compatibilă cu browserele mai vechi. De exemplu, având în vedere o funcție săgeată ES2015:

 [2, 4, 6].map((n) => n * 2);

…Babel îl va converti în versiunea sa ES5:

 [2, 4, 6].map(function(n) { return n * 2; });

Ce este transpilarea PHP?

Ceea ce este potențial nou în dezvoltarea web este posibilitatea transpilării codului de pe partea serverului, în special PHP.

Transpilarea PHP funcționează în același mod ca transpilarea JavaScript: codul sursă dintr-o versiune PHP modernă este convertit într-un cod echivalent pentru o versiune PHP mai veche.

Urmând același exemplu ca înainte, o funcție săgeată din PHP 7.4:

 $nums = array_map(fn($n) => $n * 2, [2, 4, 6]);

… poate fi transpilat în versiunea echivalentă PHP 7.3:

 $nums = array_map( function ($n) { return $n * 2; }, [2, 4, 6] );

Funcțiile săgeților pot fi transpilate deoarece sunt zahăr sintactic, adică o nouă sintaxă pentru a produce un comportament existent. Acesta este fructul de jos.

Cu toate acestea, există și caracteristici noi care creează un comportament nou și, ca atare, nu va exista un cod echivalent pentru versiunile anterioare de PHP. Acesta este cazul tipurilor de uniuni, introduse în PHP 8.0:

 function someFunction(float|int $param): string|float|int|null { // ... }

În aceste situații, transpilarea se poate face în continuare atâta timp cât noua caracteristică este necesară pentru dezvoltare, dar nu pentru producție. Apoi, putem elimina pur și simplu funcția din codul transpilat fără consecințe grave.

Un astfel de exemplu este tipurile de sindicate. Această caracteristică este utilizată pentru a verifica dacă nu există nepotrivire între tipul de intrare și valoarea furnizată, ceea ce ajută la prevenirea erorilor. Dacă există un conflict cu tipurile, va exista o eroare deja în dezvoltare și ar trebui să o luăm și să o reparăm înainte ca codul să ajungă la producție.

Prin urmare, ne putem permite să eliminăm caracteristica din cod pentru producție:

 function someFunction($param) { // ... }

Dacă eroarea se întâmplă în continuare în producție, mesajul de eroare aruncat va fi mai puțin precis decât dacă am avea tipuri de uniuni. Cu toate acestea, acest dezavantaj potențial este depășit de posibilitatea de a utiliza tipuri de uniuni în primul rând.

Într-o lume perfectă, ar trebui să putem folosi PHP 8.0 pe toate site-urile noastre și să-l actualizăm de îndată ce o nouă versiune este lansată. Dar nu este întotdeauna cazul. Aflați tot ce trebuie să știți despre transpilarea codului PHP aici Click to Tweet

Avantajele transpilării codului PHP

Transpilarea permite codificarea unei aplicații folosind cea mai recentă versiune de PHP și producerea unei versiuni care funcționează și în medii care rulează versiuni mai vechi de PHP.

Acest lucru poate fi deosebit de util pentru dezvoltatorii care creează produse pentru sistemele de management al conținutului (CMS) moștenite. WordPress, de exemplu, încă acceptă oficial PHP 5.6 (chiar dacă recomandă PHP 7.4+). Procentul de site-uri WordPress care rulează versiunile PHP 5.6 până la 7.2 – care sunt toate la sfârşitul duratei de viaţă (EOL), ceea ce înseamnă că nu mai primesc actualizări de securitate – este de 34,8%, iar cele care rulează pe orice versiune PHP, alta decât 8.0 se ridică la 99,5%:

Utilizarea WordPress în funcție de versiune
Statistici de utilizare WordPress în funcție de versiune. Sursa imagine: WordPress

În consecință, temele și pluginurile WordPress destinate unui public global vor fi foarte probabil codificate cu o versiune veche de PHP pentru a le crește posibila acoperire. Datorită transpilării, acestea ar putea fi codificate folosind PHP 8.0 și pot fi încă lansate pentru o versiune PHP mai veche, vizând astfel cât mai mulți utilizatori posibil.

Într-adevăr, orice aplicație care trebuie să suporte orice versiune PHP, alta decât cea mai recentă (chiar și în intervalul versiunilor PHP suportate în prezent) poate beneficia.

Acesta este cazul Drupal, care necesită PHP 7.3. Datorită transpilării, dezvoltatorii pot crea module Drupal disponibile public folosind PHP 8.0 și le pot lansa cu PHP 7.3.

Un alt exemplu este atunci când se creează cod personalizat pentru clienții care nu pot rula PHP 8.0 în mediile lor dintr-un motiv sau altul. Cu toate acestea, datorită transpilării, dezvoltatorii își pot codifica livrabilele folosind PHP 8.0 și le pot rula în acele medii vechi.

Când să transpilați PHP

Codul PHP poate fi întotdeauna transpilat, cu excepția cazului în care conține o caracteristică PHP care nu are echivalent în versiunea anterioară a PHP.

Acesta este posibil și cazul atributelor, introduse în PHP 8.0:

 #[SomeAttr] function someFunc() {} #[AnotherAttr] class SomeClass {}

În exemplul anterior, folosind funcții de săgeată, codul ar putea fi transpilat deoarece funcțiile de săgeată sunt zahăr sintactic. Atributele, în schimb, creează un comportament complet nou. Acest comportament ar putea fi reprodus și cu PHP 7.4 și mai jos, dar doar prin codificarea manuală, adică nu automat pe baza unui instrument sau proces (AI ar putea oferi o soluție, dar nu suntem încă acolo).

Atributele destinate utilizării în dezvoltare, cum ar fi #[Deprecated] , pot fi eliminate în același mod în care sunt eliminate tipurile de uniuni. Dar atributele care modifică comportamentul aplicației în producție nu pot fi eliminate și nici nu pot fi transpilate direct.

Începând de astăzi, niciun transpiler nu poate prelua cod cu atribute PHP 8.0 și poate produce automat codul său echivalent PHP 7.4. În consecință, dacă codul dvs. PHP trebuie să utilizeze atribute, transpilarea acestuia va fi dificilă sau imposibilă.

Caracteristici PHP care pot fi transpilate

Acestea sunt caracteristicile de la PHP 7.1 și mai sus, care pot fi transpilate în prezent. Dacă codul dvs. folosește doar aceste funcții, vă puteți bucura de certitudinea că aplicația dvs. transpilată va funcționa. În caz contrar, va trebui să evaluați dacă codul transpilat va produce erori.

Versiunea PHP Caracteristici
7.1 Tot
7.2 – tipul object
– lărgirea tipului de parametru
– flag PREG_UNMATCHED_AS_NULL în preg_match
7.3 – Atribuții de referință în destructurarea list() / array (cu excepția interiorului foreach - #4376)
– Sintaxă flexibilă Heredoc și Nowdoc
– Virgulele în urmă în apelurile de funcții
set(raw)cookie acceptă argumentul $opțiune
7.4 – Proprietăți tipizate
– Funcții săgeți
– Operator de atribuire de coalescere nulă
– Dezambalarea în interiorul matricelor
– Separator numeric literal
strip_tags() cu matrice de nume de etichete
– tipuri de returnare covariantă și tipuri de parametri contravariante
8.0 – Tipuri de uniuni
– pseudotip mixed
– tip de returnare static
::class pe obiecte
match expresii
catch excepții numai după tip
– Operator cu siguranță nulă
– Promovarea proprietății constructorului de clasă
– Virgulele în urmă în listele de parametri și listele de use de închidere

PHP Transpilers

În prezent, există un singur instrument pentru transpilarea codului PHP: Rector.

Rector este un instrument de reconstrucție PHP, care convertește codul PHP pe baza unor reguli programabile. Introducem codul sursă și setul de reguli pentru a rula, iar Rector va transforma codul.

Rector este operat prin linie de comandă, instalat în proiect prin Composer. Când este executat, Rector va scoate o „diferență” (adăugiri în verde, eliminări în roșu) a codului înainte și după conversie:

ieșire „diferență” de la Rector
ieșire „difer” de la Rector

În ce versiune de PHP să transpuneți

Pentru a transpila codul în versiunile PHP, trebuie create regulile corespunzătoare.

Astăzi, biblioteca Rector include majoritatea regulilor pentru transpilarea codului în intervalul PHP 8.0 până la 7.1. Prin urmare, putem transpila în mod fiabil codul nostru PHP până la versiunea 7.1.

Există și reguli pentru transpilarea de la PHP 7.1 la 7.0 și de la 7.0 la 5.6, dar acestea nu sunt exhaustive. Se lucrează pentru a le finaliza, așa că în cele din urmă este posibil să transpilăm codul PHP până la versiunea 5.6.

Transpiling vs Backporting

Backporting este similar cu transpiling, dar mai simplu. Codul de backporting nu se bazează neapărat pe funcții noi dintr-o limbă. În schimb, aceeași funcționalitate poate fi furnizată unei versiuni mai vechi a limbii pur și simplu prin copierea/lipirea/adaptarea codului corespunzător din noua versiune a limbii.

De exemplu, funcția str_contains a fost introdusă în PHP 8.0. Aceeași funcție pentru PHP 7.4 și mai jos poate fi implementată cu ușurință astfel:

 if (!defined('PHP_VERSION_ID') || (defined('PHP_VERSION_ID') && PHP_VERSION_ID < 80000)) { if (!function_exists('str_contains')) { /** * Checks if a string contains another * * @param string $haystack The string to search in * @param string $needle The string to search * @return boolean Returns TRUE if the needle was found in haystack, FALSE otherwise. */ function str_contains(string $haystack, string $needle): bool { return strpos($haystack, $needle) !== false; } } }

Deoarece backporting este mai simplu decât transpiling, ar trebui să optăm pentru această soluție ori de câte ori backporting face treaba.

În ceea ce privește intervalul dintre PHP 8.0 și 7.1, putem folosi bibliotecile polyfill ale Symfony:

  • Polyfill PHP 7.1
  • Polyfill PHP 7.2
  • Polyfill PHP 7.3
  • Polyfill PHP 7.4
  • Polyfill PHP 8.0

Aceste biblioteci backport următoarele funcții, clase, constante și interfețe:

Versiunea PHP Caracteristici
7.2 Functii:
  • spl_object_id
  • utf8_encode
  • utf8_decode

constante:

  • PHP_FLOAT_*
  • PHP_OS_FAMILY
7.3 Functii:
  • array_key_first
  • array_key_last
  • hrtime
  • is_countable

Excepții:

  • JsonException
7.4 Functii:
  • get_mangled_object_vars
  • mb_str_split
  • password_algos
8.0 Interfete:
  • Stringable

Clase:

  • ValueError
  • UnhandledMatchError

constante:

  • FILTER_VALIDATE_BOOL

Functii:

  • fdiv
  • get_debug_type
  • preg_last_error_msg
  • str_contains
  • str_starts_with
  • str_ends_with
  • get_resource_id

Exemple de PHP Transpiled

Să inspectăm câteva exemple de cod PHP transpilat și câteva pachete care sunt complet transpilate.

Cod PHP

Expresia match a fost introdusă în PHP 8.0. Acest cod sursă:

 function getFieldValue(string $fieldName): ?string { return match($fieldName) { 'foo' => 'foofoo', 'bar' => 'barbar', 'baz' => 'bazbaz', default => null, }; }

…va fi transpilat în versiunea echivalentă PHP 7.4, folosind operatorul de switch :

 function getFieldValue(string $fieldName): ?string { switch ($fieldName) { case 'foo': return 'foofoo'; case 'bar': return 'barbar'; case 'baz': return 'bazbaz'; default: return null; } }

Operatorul nullsafe a fost introdus și în PHP 8.0:

 public function getValue(TypeResolverInterface $typeResolver): ?string { return $this->getResolver($typeResolver)?->getValue(); }

Codul transpilat trebuie să atribuie mai întâi valoarea operației unei variabile noi, pentru a evita executarea operației de două ori:

 public function getValue(TypeResolverInterface $typeResolver): ?string { return ($val = $this->getResolver($typeResolver)) ? $val->getValue() : null; }

Caracteristica de promovare a proprietății constructorului, introdusă și în PHP 8.0, permite dezvoltatorilor să scrie mai puțin cod:

 class QueryResolver { function __construct(protected QueryFormatter $queryFormatter) { } }

La transpilarea lui pentru PHP 7.4, este produsă întreaga bucată de cod:

 class QueryResolver { protected QueryFormatter $queryFormatter; function __construct(QueryFormatter $queryFormatter) { $this->queryFormatter = $queryFormatter; } }

Codul transpilat de mai sus conține proprietăți tipizate, care au fost introduse în PHP 7.4. Transpilarea codului la PHP 7.3 le înlocuiește cu docblocks:

 class QueryResolver { /** * @var QueryFormatter */ protected $queryFormatter; function __construct(QueryFormatter $queryFormatter) { $this->queryFormatter = $queryFormatter; } }

Pachete PHP

Următoarele biblioteci sunt transpilate pentru producție:

Bibliotecă/descriere Cod/note
Rector
Instrument de reconstrucție PHP care face posibilă transpilarea
- Cod sursa
– Cod transpilat
– Note
Standarde de codare simplă
Instrument pentru ca codul PHP să respecte un set de reguli
- Cod sursa
– Cod transpilat
– Note
API-ul GraphQL pentru WordPress
Plugin care oferă un server GraphQL pentru WordPress
- Cod sursa
– Cod transpilat
– Note

Avantaje și dezavantaje ale transpilării PHP

Avantajul transpilării PHP a fost deja descris: permite codului sursă să folosească PHP 8.0 (adică cea mai recentă versiune de PHP), care va fi transformată într-o versiune inferioară pentru PHP pentru ca producția să ruleze într-o aplicație sau mediu moștenit.

Acest lucru ne permite efectiv să devenim dezvoltatori mai buni, producând cod cu o calitate superioară. Acest lucru se datorează faptului că codul nostru sursă poate folosi tipurile de uniuni ale PHP 8.0, proprietățile tipizate ale PHP 7.4 și diferitele tipuri și pseudo-tipuri adăugate la fiecare nouă versiune de PHP ( mixed din PHP 8.0, object din PHP 7.2), printre alte caracteristici moderne ale PHP.

Folosind aceste funcții, putem detecta mai bine erorile în timpul dezvoltării și putem scrie cod care este mai ușor de citit.

Acum, să aruncăm o privire asupra dezavantajelor.

Trebuie să fie codificat și întreținut

Rector poate transpila codul automat, dar procesul va necesita probabil o introducere manuală pentru ca acesta să funcționeze cu configurația noastră specifică.

Bibliotecile terțe trebuie, de asemenea, să fie transpilate

Aceasta devine o problemă ori de câte ori transpilarea lor produce erori, deoarece trebuie apoi să ne adâncim în codul lor sursă pentru a afla posibilul motiv. Dacă problema poate fi remediată și proiectul este open source, va trebui să trimitem o cerere de extragere. Dacă biblioteca nu este open source, este posibil să ajungem la un obstacol.

Rectorul nu ne anunță când codul nu poate fi transpus

Dacă codul sursă conține atribute PHP 8.0 sau orice altă caracteristică care nu poate fi transpilată, nu putem continua. Cu toate acestea, Rector nu va verifica această condiție, așa că trebuie să o facem manual. Aceasta poate să nu fie o problemă mare în ceea ce privește propriul cod sursă, deoarece suntem deja familiarizați cu acesta, dar ar putea deveni un obstacol în ceea ce privește dependențele de la terți.

Informațiile de depanare utilizează codul transpilat, nu codul sursă

Când aplicația produce un mesaj de eroare cu o urmă de stivă în producție, numărul de linie va indica codul transpilat. Trebuie să convertim înapoi din codul transpilat în codul original pentru a găsi numărul de linie corespunzător în codul sursă.

Codul transpilat trebuie, de asemenea, să fie prefixat

Proiectul nostru transpilat și o altă bibliotecă instalată, de asemenea, în mediul de producție ar putea folosi aceeași dependență de la terți. Această dependență terță parte va fi transpilată pentru proiectul nostru și va păstra codul sursă original pentru cealaltă bibliotecă. Prin urmare, versiunea transpilată trebuie să fie prefixată prin PHP-Scoper, Strauss sau orice alt instrument pentru a evita potențiale conflicte.

Transpilarea trebuie să aibă loc în timpul integrării continue (CI)

Deoarece codul transpilat va suprascrie în mod natural codul sursă, nu ar trebui să rulăm procesul de transpilare pe computerele noastre de dezvoltare, altfel riscăm să creăm efecte secundare. Rularea procesului în timpul unei rulări CI este mai potrivită (mai multe despre asta mai jos).

Cum se transpilează PHP

Mai întâi, trebuie să instalăm Rector în proiectul nostru pentru dezvoltare:

 composer require rector/rector --dev

Creăm apoi un fișier de configurare rector.php în directorul rădăcină al proiectului, care conține seturile necesare de reguli. Pentru a downgrade codul de la PHP 8.0 la 7.1, folosim această configurație:

 use Rector\Set\ValueObject\DowngradeSetList; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; return static function (ContainerConfigurator $containerConfigurator): void { $containerConfigurator->import(DowngradeSetList::PHP_80); $containerConfigurator->import(DowngradeSetList::PHP_74); $containerConfigurator->import(DowngradeSetList::PHP_73); $containerConfigurator->import(DowngradeSetList::PHP_72); };

Pentru a ne asigura că procesul se execută conform așteptărilor, putem rula comanda de process a lui Rector în modul uscat, trecând locația(e) de procesat (în acest caz, toate fișierele din folderul src/ ):

 vendor/bin/rector process src --dry-run

Pentru a efectua transpilarea, rulăm comanda process Rector, care va modifica fișierele din locația lor existentă:

 vendor/bin/rector process src

Vă rugăm să rețineți: dacă rulăm rector process în computerele noastre de dezvoltare, codul sursă va fi convertit în loc, sub src/ . Cu toate acestea, dorim să producem codul convertit într-o locație diferită pentru a nu suprascrie codul sursă atunci când facem downgrade. Din acest motiv, rularea procesului este cea mai potrivită în timpul integrării continue.

Optimizarea procesului de transpilare

Pentru a genera un livrabil transpilat pentru producție, trebuie convertit doar codul pentru producție; codul necesar doar pentru dezvoltare poate fi omis. Asta înseamnă că putem evita transpilarea tuturor testelor (atât pentru proiectul nostru, cât și pentru dependențele sale) și toate dependențele pentru dezvoltare.

În ceea ce privește testele, vom ști deja unde se află cele pentru proiectul nostru — de exemplu, în folderul tests/ . De asemenea, trebuie să aflăm unde sunt cele pentru dependențe — de exemplu, sub folderele lor tests/ , test/ și Test/ (pentru diferite biblioteci). Apoi, îi spunem lui Rector să omite procesarea acestor foldere:

 return static function (ContainerConfigurator $containerConfigurator): void { // ... $parameters->set(Option::SKIP, [ // Skip tests '*/tests/*', '*/test/*', '*/Test/*', ]); };

În ceea ce privește dependențele, Composer știe care sunt pentru dezvoltare (cele sub intrarea require-dev în composer.json ) și care sunt pentru producție (cele sub intrarea require ).

Pentru a prelua din Composer căile tuturor dependențelor pentru producție, rulăm:

 composer info --path --no-dev

Această comandă va produce o listă de dependențe cu numele și calea lor, astfel:

 brain/cortex /Users/leo/GitHub/leoloso/PoP/vendor/brain/cortex composer/installers /Users/leo/GitHub/leoloso/PoP/vendor/composer/installers composer/semver /Users/leo/GitHub/leoloso/PoP/vendor/composer/semver guzzlehttp/guzzle /Users/leo/GitHub/leoloso/PoP/vendor/guzzlehttp/guzzle league/pipeline /Users/leo/GitHub/leoloso/PoP/vendor/league/pipeline

Putem extrage toate căile și le putem alimenta în comanda Rector, care va procesa apoi folderul src/ al proiectului nostru plus acele foldere care conțin toate dependențele pentru producție:

 $ paths="$(composer info --path --no-dev | cut -d' ' -f2- | sed 's/ //g' | tr '\n' ' ')" $ vendor/bin/rector process src $paths

O îmbunătățire suplimentară poate împiedica Rector să proceseze acele dependențe folosind deja versiunea PHP țintă. Dacă o bibliotecă a fost codificată cu PHP 7.1 (sau orice versiune de mai jos), atunci nu este nevoie să o transpilați în PHP 7.1.

Pentru a realiza acest lucru, putem obține lista de biblioteci care necesită PHP 7.2 și mai sus și să le procesăm numai pe acelea. Vom obține numele tuturor acestor biblioteci prin comanda de why-not a lui Composer, astfel:

 composer why-not php "7.1.*" | grep -o "\S*\/\S*"

Deoarece această comandă nu funcționează cu --no-dev , pentru a include numai dependențe pentru producție, trebuie mai întâi să eliminăm dependențele pentru dezvoltare și să regenerăm încărcătorul automat, să executăm comanda și apoi să le adăugăm din nou:

 $ composer install --no-dev $ packages=$(composer why-not php "7.1.*" | grep -o "\S*\/\S*") $ composer install

Comanda info --path de la Composer preia calea pentru un pachet, cu acest format:

 # Executing this command $ composer info psr/cache --path # Produces this response: psr/cache /Users/leo/GitHub/leoloso/PoP/vendor/psr/cache

Executăm această comandă pentru toate elementele din lista noastră pentru a obține toate căile de transpilat:

 for package in $packages do path=$(composer info $package --path | cut -d' ' -f2-) paths="$paths $path" done

În cele din urmă, oferim rectorului această listă (plus folderul src/ al proiectului):

Aveți nevoie de o soluție de găzduire care să vă ofere un avantaj competitiv? Kinsta v-a acoperit cu o viteză incredibilă, securitate de ultimă generație și scalare automată. Verificați planurile noastre

 vendor/bin/rector process src $paths

Capcanele de evitat la transpilarea codului

Transpilarea codului ar putea fi considerată o artă, necesitând adesea ajustări specifice proiectului. Să vedem câteva probleme în care putem veni.

Regulile înlănțuite nu sunt întotdeauna procesate

O regulă înlănțuită este atunci când o regulă trebuie să convertească codul produs de o regulă anterioară.

De exemplu, biblioteca symfony/cache conține acest cod:

 final class CacheItem implements ItemInterface { public function tag($tags): ItemInterface { // ... return $this; } }

La transpilarea de la PHP 7.4 la 7.3, tag funcției trebuie să sufere două modificări:

  • Tipul de returnare ItemInterface trebuie mai întâi convertit în self , datorită regulii DowngradeCovariantReturnTypeRector
  • Tipul de returnare self trebuie apoi eliminat, din cauza regulii DowngradeSelfTypeDeclarationRector

Rezultatul final ar trebui să fie acesta:

 final class CacheItem implements ItemInterface { public function tag($tags) { // ... return $this; } }

Cu toate acestea, Rector produce doar etapa intermediară:

 final class CacheItem implements ItemInterface { public function tag($tags): self { // ... return $this; } }

Problema este că rectorul nu poate controla întotdeauna ordinea în care sunt aplicate regulile.

Soluția este de a identifica regulile înlănțuite care au fost lăsate neprocesate și de a executa o nouă rulare Rector pentru a le aplica.

Pentru a identifica regulile înlănțuite, rulăm Rector de două ori pe codul sursă, astfel:

 $ vendor/bin/rector process src $ vendor/bin/rector process src --dry-run

Prima dată, rulăm Rector așa cum era de așteptat, pentru a executa transpilarea. A doua oară, folosim --dry-run pentru a descoperi dacă mai sunt modificări de făcut. Dacă există, comanda va ieși cu un cod de eroare, iar ieșirea „dif” va indica ce regulă(e) pot fi încă aplicate. Asta ar însemna că prima rulare nu a fost completă, cu o regulă înlănțuită nefiind procesată.

Rector în exercițiu cu steag --dry-run
Rector în exercițiu cu –drapel de rulare uscată

Odată ce am identificat regula (sau regulile) înlănțuită neaplicată, putem crea apoi un alt fișier de configurare Rector - de exemplu, rector-chained-rule.php va executa regula lipsă. În loc să procesăm un set complet de reguli pentru toate fișierele sub src/ , de data aceasta, putem rula regula specifică lipsă pe fișierul specific în care trebuie aplicată:

 // rector-chained-rule.php use Rector\Core\Configuration\Option; use Rector\DowngradePhp74\Rector\ClassMethod\DowngradeSelfTypeDeclarationRector; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; return static function (ContainerConfigurator $containerConfigurator): void { $services = $containerConfigurator->services(); $services->set(DowngradeSelfTypeDeclarationRector::class); $parameters = $containerConfigurator->parameters(); $parameters->set(Option::PATHS, [ __DIR__ . '/vendor/symfony/cache/CacheItem.php', ]); };

În cele din urmă, îi spunem lui Rector la a doua trecere să folosească noul fișier de configurare prin intrarea --config :

 # First pass with all modifications $ vendor/bin/rector process src # Second pass to fix a specific problem $ vendor/bin/rector process --config=rector-chained-rule.php

Dependența compozitorului poate fi inconsecventă

Bibliotecile ar putea declara o dependență care urmează să fie programată pentru dezvoltare (adică sub require-dev în composer.json ), totuși, să facă referire la un cod din ele pentru producție (cum ar fi pe unele fișiere sub src/ , nu tests/ ).

De obicei, aceasta nu este o problemă, deoarece codul respectiv nu poate fi încărcat în producție, deci nu va exista niciodată o eroare în aplicație. Cu toate acestea, atunci când Rector procesează codul sursă și dependențele acestuia, validează că tot codul la care se face referire poate fi încărcat. Rector va arunca o eroare dacă orice fișier face referire la o bucată de cod dintr-o bibliotecă neinstalată (deoarece a fost declarat că este necesar doar pentru dezvoltare).

De exemplu, clasa EarlyExpirationHandler din componenta Cache a Symfony implementează interfața MessageHandlerInterface din componenta Messenger:

 class EarlyExpirationHandler implements MessageHandlerInterface { //... }

Cu toate acestea, symfony/cache declară symfony/messenger ca fiind o dependență pentru dezvoltare. Apoi, când rulați Rector pe un proiect care depinde de symfony/cache , va genera o eroare:

 [ERROR] Could not process "vendor/symfony/cache/Messenger/EarlyExpirationHandler.php" file, due to: "Analyze error: "Class Symfony\Component\Messenger\Handler\MessageHandlerInterface not found.". Include your files in "$parameters->set(Option::AUTOLOAD_PATHS, [...]);" in "rector.php" config. See https://github.com/rectorphp/rector#configuration".

Există trei soluții la această problemă:

  1. În configurația Rector, omiteți procesarea fișierului care face referire la acea bucată de cod:
 return static function (ContainerConfigurator $containerConfigurator): void { // ... $parameters->set(Option::SKIP, [ __DIR__ . '/vendor/symfony/cache/Messenger/EarlyExpirationHandler.php', ]); };
  1. Descărcați biblioteca lipsă și adăugați calea acesteia pentru a fi încărcată automat de către Rector:
 return static function (ContainerConfigurator $containerConfigurator): void { // ... $parameters->set(Option::AUTOLOAD_PATHS, [ __DIR__ . '/vendor/symfony/messenger', ]); };
  1. Puneți proiectul să depindă de biblioteca lipsă pentru producție:
 composer require symfony/messenger

Transpilare și integrare continuă

După cum am menționat mai devreme, în computerele noastre de dezvoltare trebuie să folosim steag-ul --dry-run atunci când rulăm Rector, sau în caz contrar, codul sursă va fi suprascris cu codul transpilat. Din acest motiv, este mai potrivit să rulăm procesul de transpilare propriu-zis în timpul integrării continue (CI), unde putem învârti alergători temporari pentru a executa procesul.

Un moment ideal pentru a executa procesul de transpilare este atunci când se generează versiunea pentru proiectul nostru. De exemplu, codul de mai jos este un flux de lucru pentru GitHub Actions, care creează lansarea unui plugin WordPress:

 name: Generate Installable Plugin and Upload as Release Asset on: release: types: [published] jobs: build: name: Build, Downgrade and Upload Release runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/[email protected] - name: Downgrade code for production (to PHP 7.1) run: | composer install vendor/bin/rector process sed -i 's/Requires PHP: 7.4/Requires PHP: 7.1/' graphql-api.php - name: Build project for production run: | composer install --no-dev --optimize-autoloader mkdir build - name: Create artifact uses: montudor/[email protected] with: args: zip -X -r build/graphql-api.zip . -x *.git* node_modules/\* .* "*/\.*" CODE_OF_CONDUCT.md CONTRIBUTING.md ISSUE_TEMPLATE.md PULL_REQUEST_TEMPLATE.md rector.php *.dist composer.* dev-helpers** build** - name: Upload artifact uses: actions/[email protected] with: name: graphql-api path: build/graphql-api.zip - name: Upload to release uses: JasonEtco/[email protected] with: args: build/graphql-api.zip application/zip env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Acest flux de lucru conține o procedură standard pentru a lansa un plugin WordPress prin GitHub Actions. Noua adăugare, pentru a transpila codul pluginului de la PHP 7.4 la 7.1, are loc în acest pas:

 - name: Downgrade code for production (to PHP 7.1) run: | vendor/bin/rector process sed -i 's/Requires PHP: 7.4/Requires PHP: 7.1/' graphql-api.php

Luat împreună, acest flux de lucru realizează acum următorii pași:

  1. Verifică codul sursă pentru un plugin WordPress din depozitul său, scris cu PHP 7.4
  2. Își instalează dependențele Composer
  3. Transpilează codul de la PHP 7.4 la 7.1
  4. Modifică intrarea „Necesită PHP” din antetul fișierului principal al pluginului de la "7.4" la "7.1"
  5. Îndepărtează dependențele necesare dezvoltării
  6. Creează fișierul .zip al pluginului, excluzând toate fișierele care nu sunt necesare
  7. Încarcă fișierul .zip ca material de lansare (și, în plus, ca artefact al acțiunii GitHub)

Testarea codului transpilat

Odată ce codul a fost transpilat în PHP 7.1, de unde știm că funcționează bine? Sau, cu alte cuvinte, de unde știm că a fost convertit complet și că nu au fost lăsate rămășițe ale versiunilor superioare de cod PHP?

Similar cu transpilarea codului, putem implementa soluția într-un proces CI. Ideea este să configurați mediul alergătorului cu PHP 7.1 și să rulați un linter pe codul transpilat. Dacă vreo bucată de cod nu este compatibilă cu PHP 7.1 (cum ar fi o proprietate tipizată din PHP 7.4 care nu a fost convertită), atunci linter va genera o eroare.

Un linter pentru PHP care funcționează bine este PHP Parallel Lint. Putem instala această bibliotecă ca dependență pentru dezvoltare în proiectul nostru sau putem pune procesul CI să o instaleze ca proiect Composer independent:

 composer create-project php-parallel-lint/php-parallel-lint

Ori de câte ori codul conține PHP 7.2 și mai sus, PHP Parallel Lint va genera o eroare ca aceasta:

 Run php-parallel-lint/parallel-lint layers/ vendor/ --exclude vendor/symfony/polyfill-ctype/bootstrap80.php --exclude vendor/symfony/polyfill-intl-grapheme/bootstrap80.php --exclude vendor/symfony/polyfill-intl-idn/bootstrap80.php --exclude vendor/symfony/polyfill-intl-normalizer/bootstrap80.php --exclude vendor/symfony/polyfill-mbstring/bootstrap80.php PHP 7.1.33 | 10 parallel jobs ............................................................ 60/2870 (2 %) ............................................................ 120/2870 (4 %) ... ............................................................ 660/2870 (22 %) .............X.............................................. 720/2870 (25 %) ............................................................ 780/2870 (27 %) ... ............................................................ 2820/2870 (98 %) .................................................. 2870/2870 (100 %) Checked 2870 files in 15.4 seconds Syntax error found in 1 file ------------------------------------------------------------ Parse error: layers/GraphQLAPIForWP/plugins/graphql-api-for-wp/graphql-api.php:55 53| '0.8.0', 54| \__('GraphQL API for WordPress', 'graphql-api'), > 55| ))) { 56| $plugin->setup(); 57| } Unexpected ')' in layers/GraphQLAPIForWP/plugins/graphql-api-for-wp/graphql-api.php on line 55 Error: Process completed with exit code 1.

Să adăugăm linter-ul în fluxul de lucru al CI. Pașii de executat pentru a transpila codul de la PHP 8.0 la 7.1 și a-l testa sunt:

  1. Verificați codul sursă
  2. Rulați mediul PHP 8.0, astfel încât Rector să poată interpreta codul sursă
  3. Transpilați codul în PHP 7.1
  4. Instalați instrumentul PHP linter
  5. Comutați versiunea PHP a mediului la 7.1
  6. Rulați linter pe codul transpiled

Acest flux de lucru GitHub Action face treaba:

 name: Downgrade PHP tests jobs: main: name: Downgrade code to PHP 7.1 via Rector, and execute tests runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/[email protected] - name: Set-up PHP uses: shivammathur/[email protected] with: php-version: 8.0 coverage: none - name: Local packages - Downgrade PHP code via Rector run: | composer install vendor/bin/rector process # Prepare for testing on PHP 7.1 - name: Install PHP Parallel Lint run: composer create-project php-parallel-lint/php-parallel-lint --ansi - name: Switch to PHP 7.1 uses: shivammathur/[email protected] with: php-version: 7.1 coverage: none # Lint the transpiled code - name: Run PHP Parallel Lint on PHP 7.1 run: php-parallel-lint/parallel-lint src/ vendor/ --exclude vendor/symfony/polyfill-ctype/bootstrap80.php --exclude vendor/symfony/polyfill-intl-grapheme/bootstrap80.php --exclude vendor/symfony/polyfill-intl-idn/bootstrap80.php --exclude vendor/symfony/polyfill-intl-normalizer/bootstrap80.php --exclude vendor/symfony/polyfill-mbstring/bootstrap80.php

Vă rugăm să rețineți că mai multe fișiere bootstrap80.php din bibliotecile polyfill Symfony (care nu trebuie să fie transpilate) trebuie excluse din linter. Aceste fișiere conțin PHP 8.0, așa că linter-ul ar arunca erori la procesarea lor. Cu toate acestea, excluderea acestor fișiere este sigură, deoarece acestea vor fi încărcate în producție numai când rulați PHP 8.0 sau o versiune ulterioară:

 if (\PHP_VERSION_ID >= 80000) { return require __DIR__.'/bootstrap80.php'; }

Indiferent dacă creați un plugin public pentru WordPress sau actualizați codul vechi, există multe motive pentru care utilizarea celei mai recente versiuni PHP poate fi imposibilă. Aflați cum poate ajuta transpilarea în acest ghid Faceți clic pentru a Tweet

rezumat

Acest articol ne-a învățat cum să transpilăm codul nostru PHP, permițându-ne să folosim PHP 8.0 în codul sursă și să creăm o versiune care să funcționeze pe PHP 7.1. Transpilarea se face prin Rector, un instrument de reconstrucție PHP.

Transpilarea codului nostru ne face dezvoltatori mai buni, deoarece putem detecta mai bine erorile în dezvoltare și putem produce cod care este în mod natural mai ușor de citit și de înțeles.

Transpilarea ne permite, de asemenea, să ne decuplăm codul cu cerințe PHP specifice din CMS. Acum putem face acest lucru dacă dorim să folosim cea mai recentă versiune de PHP pentru a crea un plugin WordPress disponibil public sau un modul Drupal, fără a restricționa sever baza noastră de utilizatori.

Mai aveți întrebări despre transpilarea PHP? Anunțați-ne în secțiunea de comentarii!