Kompletny przewodnik po transpilowaniu kodu PHP

Opublikowany: 2021-09-22

W idealnych okolicznościach powinniśmy używać PHP 8.0 (najnowszej wersji w momencie pisania tego) dla wszystkich naszych witryn i aktualizować je, gdy tylko pojawi się nowa wersja. Jednak programiści często będą musieli pracować z poprzednimi wersjami PHP, na przykład podczas tworzenia publicznej wtyczki do WordPressa lub pracy ze starszym kodem, który utrudnia aktualizację środowiska serwera WWW.

W takich sytuacjach moglibyśmy stracić nadzieję na wykorzystanie najnowszego kodu PHP. Ale jest lepsza alternatywa: nadal możemy pisać nasz kod źródłowy w PHP 8.0 i transpilować go do poprzedniej wersji PHP — nawet do PHP 7.1.

W tym przewodniku nauczymy Cię wszystkiego, co musisz wiedzieć o transpilowaniu kodu PHP.

Co to jest transpilacja?

Transpiling konwertuje kod źródłowy z języka programowania na równoważny kod źródłowy tego samego lub innego języka programowania.

Transpilowanie nie jest nową koncepcją w tworzeniu stron internetowych: programiści po stronie klienta prawdopodobnie będą zaznajomieni z Babel, transpilatorem kodu JavaScript.

Babel konwertuje kod JavaScript z nowoczesnej wersji ECMAScript 2015+ na starszą wersję zgodną ze starszymi przeglądarkami. Na przykład, biorąc pod uwagę funkcję strzałki ES2015:

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

… Babel przekształci go w wersję ES5:

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

Co to jest transpilacja PHP?

Potencjalną nowością w tworzeniu stron internetowych jest możliwość transpilacji kodu po stronie serwera, w szczególności PHP.

Transpilacja PHP działa tak samo jak transpilacja JavaScript: kod źródłowy z nowoczesnej wersji PHP jest konwertowany na odpowiednik dla starszej wersji PHP.

Idąc za tym samym przykładem co poprzednio, funkcja strzałki z PHP 7.4:

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

…można transpilować do jego odpowiednika w wersji PHP 7.3:

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

Funkcje strzałek można transpilować, ponieważ są one cukrem składniowym, tj. nową składnią, która powoduje istniejące zachowanie. To jest nisko wiszący owoc.

Istnieją jednak również nowe funkcje, które tworzą nowe zachowanie i jako takie nie będzie odpowiednika kodu dla poprzednich wersji PHP. Tak jest w przypadku typów związków wprowadzonych w PHP 8.0:

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

W takich sytuacjach transpilowanie można nadal wykonać, o ile nowa funkcja jest wymagana do programowania, ale nie do produkcji. Następnie możemy po prostu całkowicie usunąć tę funkcję z transpilowanego kodu bez poważnych konsekwencji.

Jednym z takich przykładów są typy związków. Ta funkcja służy do sprawdzania, czy nie ma niezgodności między typem danych wejściowych a podaną wartością, co pomaga zapobiegać błędom. Jeśli wystąpi konflikt z typami, wystąpi błąd już w fazie rozwoju i powinniśmy go złapać i naprawić, zanim kod trafi do produkcji.

Dlatego możemy sobie pozwolić na usunięcie tej funkcji z kodu do produkcji:

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

Jeśli błąd nadal występuje w środowisku produkcyjnym, zgłoszony komunikat o błędzie będzie mniej precyzyjny niż gdybyśmy mieli typy unii. Jednak ta potencjalna wada jest niwelowana przez możliwość korzystania z typów złączy w pierwszej kolejności.

W idealnym świecie powinniśmy być w stanie używać PHP 8.0 na wszystkich naszych witrynach i aktualizować je, gdy tylko pojawi się nowa wersja. Ale nie zawsze tak jest. Dowiedz się wszystkiego, co musisz wiedzieć o transpilowaniu kodu PHP tutaj Kliknij, aby Tweet

Zalety transpilacji kodu PHP

Transpiling umożliwia kodowanie aplikacji przy użyciu najnowszej wersji PHP i tworzenie wydania, które działa również w środowiskach ze starszymi wersjami PHP.

Może to być szczególnie przydatne dla programistów tworzących produkty dla starszych systemów zarządzania treścią (CMS). Na przykład WordPress nadal oficjalnie obsługuje PHP 5.6 (mimo że zaleca PHP 7.4+). Odsetek witryn WordPress korzystających z PHP w wersjach od 5.6 do 7.2 — z których wszystkie są wycofane z eksploatacji (EOL), co oznacza, że ​​nie otrzymują już aktualizacji zabezpieczeń — wynosi 34,8%, a witryn korzystających z dowolnej wersji PHP innej niż 8.0 to aż 99,5%:

Wykorzystanie WordPressa według wersji
Statystyki użytkowania WordPress według wersji. Źródło obrazu: WordPress

W związku z tym motywy i wtyczki WordPress skierowane do odbiorców na całym świecie będą prawdopodobnie kodowane za pomocą starej wersji PHP, aby zwiększyć ich możliwy zasięg. Dzięki transpilowaniu mogły one zostać zakodowane przy użyciu PHP 8.0 i nadal być wydawane dla starszej wersji PHP, w ten sposób skierowane do jak największej liczby użytkowników.

Rzeczywiście, każda aplikacja, która musi obsługiwać dowolną wersję PHP inną niż najnowsza (nawet w zakresie obecnie obsługiwanych wersji PHP) może na tym skorzystać.

Tak jest w przypadku Drupala, który wymaga PHP 7.3. Dzięki transpilingowi programiści mogą tworzyć publicznie dostępne moduły Drupala w PHP 8.0 i wydawać je w PHP 7.3.

Innym przykładem jest tworzenie niestandardowego kodu dla klientów, którzy nie mogą uruchomić PHP 8.0 w swoich środowiskach z tego czy innego powodu. Niemniej jednak, dzięki transpilowaniu, programiści mogą nadal kodować swoje wyniki przy użyciu PHP 8.0 i uruchamiać je w tych starszych środowiskach.

Kiedy transpilować PHP

Kod PHP można zawsze transpilować, chyba że zawiera jakąś funkcję PHP, która nie ma odpowiednika w poprzedniej wersji PHP.

Tak jest prawdopodobnie w przypadku atrybutów wprowadzonych w PHP 8.0:

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

We wcześniejszym przykładzie z funkcjami strzałek kod mógł zostać transpilowany, ponieważ funkcje strzałek są cukrem składniowym. Atrybuty natomiast tworzą zupełnie nowe zachowanie. To zachowanie może być również odtworzone w PHP 7.4 i niższych, ale tylko przez ręczne kodowanie, tj. nie oparte na automatycznym narzędziu lub procesie (AI może zapewnić rozwiązanie, ale jeszcze nas tam nie ma).

Atrybuty przeznaczone do użytku programistycznego, takie jak #[Deprecated] , można usunąć w taki sam sposób, jak usuwane są typy unii. Ale atrybutów, które modyfikują zachowanie aplikacji w środowisku produkcyjnym, nie można usunąć, a także nie można ich bezpośrednio transpilować.

Na dzień dzisiejszy żaden transpiler nie może wziąć kodu z atrybutami PHP 8.0 i automatycznie wygenerować jego odpowiednik w PHP 7.4. W związku z tym, jeśli Twój kod PHP musi używać atrybutów, transpilacja będzie trudna lub niewykonalna.

Funkcje PHP, które można transpilować

Są to funkcje z PHP 7.1 i nowszych, które można obecnie transpilować. Jeśli Twój kod korzysta tylko z tych funkcji, możesz cieszyć się pewnością, że Twoja przetranspilowana aplikacja będzie działać. W przeciwnym razie musisz ocenić, czy transpilowany kod spowoduje błędy.

Wersja PHP Cechy
7,1 Wszystko
7,2 – typ object
– poszerzenie typu parametru
– Flaga PREG_UNMATCHED_AS_NULL w preg_match
7,3 – Przypisania referencyjne w list() / destrukturyzacja tablicy ( z wyjątkiem wewnątrz foreach — #4376)
– Elastyczna składnia Heredoc i Nowdoc
– Przecinki końcowe w wywołaniach funkcji
set(raw)cookie przyjmuje argument $option
7,4 – Wpisane właściwości
– Funkcje strzałek
– Zerowy operator przypisania koalescencyjnego
– Rozpakowywanie wewnątrz tablic
– Separator liczbowo-literowy
strip_tags() z tablicą nazw tagów
– kowariantne typy zwracane i kontrawariantne typy param
8,0 – Typy Unii
mixed pseudotyp
– zwrotny typ static
::class na przedmiotach
match wyrażenia
catch wyjątki tylko według typu
– Null-safe operator
– Promocja właściwości Konstruktora klas
– Przecinki końcowe w listach parametrów i listach use zamknięcia

Transpilatory PHP

Obecnie istnieje jedno narzędzie do transpilacji kodu PHP: Rector.

Rector to narzędzie do rekonstrukcji PHP, które konwertuje kod PHP na podstawie programowalnych reguł. Wprowadzamy kod źródłowy i zestaw reguł do uruchomienia, a Rector przekształci kod.

Rektor obsługiwany jest za pomocą wiersza poleceń, instalowanego w projekcie za pomocą Composera. Po wykonaniu, Rector wypisze „różnicę” (dodatki na zielono, usunięcia na czerwono) kodu przed i po konwersji:

Wyjście "różnicowe" od Rektora
Wyjście „różnic” od Rektora

Którą wersję PHP przetranspilować do

Aby transpilować kod między wersjami PHP, należy utworzyć odpowiednie reguły.

Obecnie biblioteka Rector zawiera większość reguł transpilacji kodu w zakresie PHP 8.0 do 7.1. Dzięki temu możemy niezawodnie transpilować nasz kod PHP aż do wersji 7.1.

Istnieją również zasady transpilacji z PHP 7.1 na 7.0 iz 7.0 na 5.6, ale nie są one wyczerpujące. Trwają prace nad ich ukończeniem, więc możemy ostatecznie przetranspilować kod PHP do wersji 5.6.

Transpilowanie a Backporting

Backporting jest podobny do transpilowania, ale prostszy. Backporting kodu niekoniecznie opiera się na nowych funkcjach z języka. Zamiast tego tę samą funkcjonalność można zapewnić w starszej wersji języka, po prostu kopiując/wklejając/dostosowując odpowiedni kod z nowej wersji języka.

Na przykład funkcja str_contains została wprowadzona w PHP 8.0. Ta sama funkcja dla PHP 7.4 i niższych może być łatwo zaimplementowana w następujący sposób:

 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; } } }

Ponieważ backporting jest prostszy niż transpilowanie, powinniśmy wybierać to rozwiązanie zawsze, gdy backporting działa.

Jeśli chodzi o zakres od PHP 8.0 do 7.1, możemy użyć bibliotek polyfill Symfony:

  • Polyfill PHP 7.1
  • Polyfill PHP 7,2
  • Polyfill PHP 7,3
  • Polifill PHP 7,4
  • Polifill PHP 8.0

Te biblioteki wspierają następujące funkcje, klasy, stałe i interfejsy:

Wersja PHP Cechy
7,2 Funkcje:
  • spl_object_id
  • utf8_encode
  • utf8_decode

Stałe:

  • PHP_FLOAT_*
  • PHP_OS_FAMILY
7,3 Funkcje:
  • array_key_first
  • array_key_last
  • hrtime
  • is_countable

Wyjątki:

  • JsonException
7,4 Funkcje:
  • get_mangled_object_vars
  • mb_str_split
  • password_algos
8,0 Interfejsy:
  • Stringable

Zajęcia:

  • ValueError
  • UnhandledMatchError

Stałe:

  • FILTER_VALIDATE_BOOL

Funkcje:

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

Przykłady transpilowanego PHP

Przyjrzyjmy się kilku przykładom transpilowanego kodu PHP i kilku pakietom, które są w pełni transpilowane.

Kod PHP

Wyrażenie match zostało wprowadzone w PHP 8.0. Ten kod źródłowy:

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

…zostanie transpilowany do jego odpowiednika w wersji PHP 7.4, przy użyciu operatora switch :

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

Operator nullsafe został również wprowadzony w PHP 8.0:

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

Transpilowany kod musi najpierw przypisać wartość operacji do nowej zmiennej, aby uniknąć dwukrotnego wykonania operacji:

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

Funkcja promocji właściwości konstruktora, również wprowadzona w PHP 8.0, pozwala programistom pisać mniej kodu:

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

Podczas transpilacji do PHP 7.4 tworzony jest pełny fragment kodu:

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

Powyższy transpilowany kod zawiera wpisane właściwości, które zostały wprowadzone w PHP 7.4. Transpilacja tego kodu do PHP 7.3 zastępuje go blokami docblock:

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

Pakiety PHP

Następujące biblioteki są transpilowane do produkcji:

Biblioteka/opis Kod/uwagi
Rektor
Narzędzie do rekonstrukcji PHP, które umożliwia transpilację
- Kod źródłowy
– Kod transpilowany
– Notatki
Proste standardy kodowania
Narzędzie, dzięki któremu kod PHP jest zgodny z zestawem reguł
- Kod źródłowy
– Kod transpilowany
– Notatki
GraphQL API dla WordPress
Wtyczka udostępniająca serwer GraphQL dla WordPress
- Kod źródłowy
– Kod transpilowany
– Notatki

Plusy i minusy transpilacji PHP

Korzyść z transpilacji PHP została już opisana: umożliwia kodowi źródłowemu korzystanie z PHP 8.0 (tj. najnowszej wersji PHP), które zostanie przekształcone do niższej wersji PHP, aby można ją było uruchomić produkcyjnie w starszej aplikacji lub środowisku.

To skutecznie pozwala nam stać się lepszymi programistami, produkując kod o wyższej jakości. Dzieje się tak, ponieważ nasz kod źródłowy może używać typów unii PHP 8.0, typowanych właściwości PHP 7.4 oraz różnych typów i pseudotypów dodawanych do każdej nowej wersji PHP ( mixed z PHP 8.0, object z PHP 7.2), wśród inne nowoczesne funkcje PHP.

Korzystając z tych funkcji, możemy lepiej wyłapywać błędy podczas programowania i pisać kod, który jest łatwiejszy do odczytania.

Przyjrzyjmy się teraz wadom.

Musi być zakodowany i utrzymany

Rector może transpilować kod automatycznie, ale proces prawdopodobnie będzie wymagał ręcznego wprowadzenia, aby działał z naszą konkretną konfiguracją.

Biblioteki innych firm również muszą być transpilowane

Staje się to problemem, gdy ich transpilacja powoduje błędy, ponieważ musimy wtedy zagłębić się w ich kod źródłowy, aby znaleźć możliwą przyczynę. Jeśli problem można rozwiązać, a projekt jest typu open source, będziemy musieli przesłać żądanie ściągnięcia. Jeśli biblioteka nie jest open source, możemy trafić na blokadę.

Rektor nie informuje nas, gdy kod nie może być transpilowany

Jeśli kod źródłowy zawiera atrybuty PHP 8.0 lub jakąkolwiek inną funkcję, której nie można transpilować, nie możemy kontynuować. Rektor nie sprawdzi jednak tego warunku, więc musimy to zrobić ręcznie. Może nie jest to duży problem dotyczący naszego własnego kodu źródłowego, ponieważ już go znamy, ale może stać się przeszkodą w zależności od stron trzecich.

Informacje debugowania wykorzystują przetranspilowany kod, a nie kod źródłowy

Gdy aplikacja wygeneruje komunikat o błędzie ze śladem stosu w środowisku produkcyjnym, numer wiersza będzie wskazywał na transpilowany kod. Musimy przekonwertować z powrotem z transpilowanego na oryginalny kod, aby znaleźć odpowiedni numer wiersza w kodzie źródłowym.

Transpilowany kod musi również być poprzedzony prefiksem

Nasz przetranspilowany projekt i niektóre inne biblioteki również zainstalowane w środowisku produkcyjnym mogą korzystać z tej samej zależności innej firmy. Ta zależność innej firmy zostanie przeniesiona do naszego projektu i zachowa oryginalny kod źródłowy dla innej biblioteki. Dlatego transpilowana wersja musi być poprzedzona prefiksem za pomocą PHP-Scopera, Straussa lub innego narzędzia, aby uniknąć potencjalnych konfliktów.

Transpilowanie musi mieć miejsce podczas ciągłej integracji (CI)

Ponieważ transpilowany kod w naturalny sposób zastąpi kod źródłowy, nie powinniśmy uruchamiać procesu transpilacji na naszych komputerach programistycznych, ponieważ grozi to powstaniem efektów ubocznych. Bardziej odpowiednie jest uruchomienie procesu podczas przebiegu CI (więcej na ten temat poniżej).

Jak transpilować PHP

Najpierw musimy zainstalować Rector w naszym projekcie dla rozwoju:

 composer require rector/rector --dev

Następnie tworzymy plik konfiguracyjny rector.php w katalogu głównym projektu, zawierający wymagane zestawy reguł. Aby obniżyć kod z PHP 8.0 do 7.1, używamy tej konfiguracji:

 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); };

Aby upewnić się, że proces działa zgodnie z oczekiwaniami, możemy uruchomić polecenie process Rectora w trybie suchym, przekazując lokalizację(e) do procesu (w tym przypadku wszystkie pliki w folderze src/ ):

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

Aby wykonać transpilację, uruchamiamy polecenie Rector's process , które zmodyfikuje pliki w ich istniejącej lokalizacji:

 vendor/bin/rector process src

Uwaga: jeśli uruchomimy rector process na naszych komputerach deweloperskich, kod źródłowy zostanie przekonwertowany w miejscu, pod src/ . Jednak chcemy utworzyć przekonwertowany kod w innej lokalizacji, aby nie zastępować kodu źródłowego podczas obniżania wersji kodu. Z tego powodu prowadzenie procesu jest najbardziej odpowiednie podczas ciągłej integracji.

Optymalizacja procesu transpilacji

Aby wygenerować transpilowany dokument do produkcji, tylko kod do produkcji musi zostać przekonwertowany; kod potrzebny tylko do rozwoju można pominąć. Oznacza to, że możemy uniknąć transpilacji wszystkich testów (zarówno naszego projektu, jak i jego zależności) oraz wszystkich zależności na potrzeby rozwoju.

Jeśli chodzi o testy, będziemy już wiedzieć, gdzie znajdują się te dla naszego projektu — na przykład w folderze tests/ . Musimy również dowiedzieć się, gdzie znajdują się te dla zależności — na przykład pod ich podfolderami tests/ , test/ i Test/ (dla różnych bibliotek). Następnie mówimy Rektorowi, aby pominął przetwarzanie tych folderów:

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

Jeśli chodzi o zależności, Composer wie, które z nich są przeznaczone do rozwoju (te we wpisie require-dev w composer.json ), a które są przeznaczone do produkcji (te we wpisie require ).

Aby pobrać z Composera ścieżki wszystkich zależności dla produkcji, uruchamiamy:

 composer info --path --no-dev

To polecenie wygeneruje listę zależności z ich nazwą i ścieżką, tak jak poniżej:

 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

Możemy wyodrębnić wszystkie ścieżki i wprowadzić je do polecenia Rector, które następnie przetworzy folder src/ naszego projektu oraz foldery zawierające wszystkie zależności produkcyjne:

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

Dalsze ulepszenie może uniemożliwić Rectorowi przetwarzanie tych zależności już przy użyciu docelowej wersji PHP. Jeśli biblioteka została zakodowana w PHP 7.1 (lub w dowolnej wersji poniżej), nie ma potrzeby transpilacji jej do PHP 7.1.

Aby to osiągnąć, możemy uzyskać listę bibliotek wymagających PHP 7.2 lub nowszego i przetwarzać tylko te. Nazwy wszystkich tych bibliotek uzyskamy za pomocą polecenia why-not w Composer, tak jak poniżej:

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

Ponieważ to polecenie nie działa z flagą --no-dev , aby uwzględnić tylko zależności produkcyjne, najpierw musimy usunąć zależności na potrzeby programowania i ponownie wygenerować autoloader, wykonać polecenie, a następnie dodać je ponownie:

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

Komenda Composer's info --path pobiera ścieżkę do pakietu w następującym formacie:

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

Wykonujemy to polecenie dla wszystkich pozycji na naszej liście, aby uzyskać wszystkie ścieżki do transpilacji:

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

Na koniec przekazujemy tę listę Rektorowi (plus folder src/ projektu):

Potrzebujesz rozwiązania hostingowego, które zapewni Ci przewagę nad konkurencją? Kinsta zapewnia niesamowitą szybkość, najnowocześniejsze zabezpieczenia i automatyczne skalowanie. Sprawdź nasze plany

 vendor/bin/rector process src $paths

Pułapki, których należy unikać podczas transpilacji kodu

Transpilowanie kodu można uznać za sztukę, często wymagającą poprawek specyficznych dla projektu. Zobaczmy kilka problemów, które możemy napotkać.

Powiązane reguły nie zawsze są przetwarzane

Reguła łańcuchowa ma miejsce, gdy reguła musi przekonwertować kod utworzony przez poprzednią regułę.

Na przykład biblioteka symfony/cache zawiera następujący kod:

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

Podczas transpilacji z PHP 7.4 do 7.3 tag funkcji musi zostać poddany dwóm modyfikacjom:

  • Zwracany typ ItemInterface musi zostać najpierw przekonwertowany na self , ze względu na regułę DowngradeCovariantReturnTypeRector
  • Zwracany typ self musi zostać następnie usunięty ze względu na regułę DowngradeSelfTypeDeclarationRector

Wynik końcowy powinien wyglądać następująco:

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

Jednak Rektor wyprowadza tylko etap pośredni:

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

Problem w tym, że Rektor nie zawsze może kontrolować kolejność stosowania zasad.

Rozwiązaniem jest zidentyfikowanie, które powiązane reguły pozostały nieprzetworzone, i wykonanie nowego uruchomienia Rectora w celu ich zastosowania.

Aby zidentyfikować powiązane reguły, uruchamiamy Rector dwukrotnie na kodzie źródłowym, tak jak to:

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

Za pierwszym razem uruchamiamy Rektora zgodnie z oczekiwaniami, aby wykonać transpilację. Za drugim razem używamy flagi --dry-run , aby sprawdzić, czy nadal trzeba wprowadzić zmiany. Jeśli tak, polecenie zakończy się z kodem błędu, a wynik „różnic” wskaże, które reguły można nadal zastosować. Oznaczałoby to, że pierwszy przebieg nie był kompletny, a jakaś reguła łańcuchowa nie została przetworzona.

Biegnący Rektor z flagą --dry-run
Biegnący Rektor z flagą „suchobiegu”

Po zidentyfikowaniu niezastosowanej reguły (lub reguł) połączonej łańcuchem możemy utworzyć kolejny plik konfiguracyjny Rectora — na przykład, rector-chained-rule.php wykona brakującą regułę. Zamiast przetwarzać pełny zestaw reguł dla wszystkich plików w src/ , tym razem możemy uruchomić konkretną brakującą regułę na konkretnym pliku, w którym ma być zastosowana:

 // 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', ]); };

Na koniec mówimy Rectorowi podczas drugiego przebiegu, aby użył nowego pliku konfiguracyjnego przez wejście --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

Zależności kompozytora mogą być niespójne

Biblioteki mogą deklarować zależność, która ma być przeznaczona do rozwoju (tj. pod require-dev w composer.json ), ale nadal odwoływać się do ich kodu do produkcji (na przykład w niektórych plikach w src/ , a nie tests/ ).

Zwykle nie stanowi to problemu, ponieważ ten kod może nie być ładowany na produkcji, więc nigdy nie wystąpi błąd w aplikacji. Jednak gdy Rector przetwarza kod źródłowy i jego zależności, sprawdza, czy można załadować cały kod, do którego się odwołuje. Rector zgłosi błąd, jeśli jakikolwiek plik odwołuje się do jakiegoś fragmentu kodu z niezainstalowanej biblioteki (ponieważ zadeklarowano, że jest potrzebny tylko do programowania).

Na przykład klasa EarlyExpirationHandler z komponentu Symfony Cache implementuje interfejs MessageHandlerInterface z komponentu Messenger:

 class EarlyExpirationHandler implements MessageHandlerInterface { //... }

Jednak symfony/cache deklaruje symfony/messenger jako zależność dla rozwoju. Następnie, uruchamiając Rector w projekcie zależnym od symfony/cache , wygeneruje błąd:

 [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".

Istnieją trzy rozwiązania tego problemu:

  1. W konfiguracji Rector pomiń przetwarzanie pliku, który odwołuje się do tego fragmentu kodu:
 return static function (ContainerConfigurator $containerConfigurator): void { // ... $parameters->set(Option::SKIP, [ __DIR__ . '/vendor/symfony/cache/Messenger/EarlyExpirationHandler.php', ]); };
  1. Pobierz brakującą bibliotekę i dodaj jej ścieżkę do automatycznego załadowania przez Rektora:
 return static function (ContainerConfigurator $containerConfigurator): void { // ... $parameters->set(Option::AUTOLOAD_PATHS, [ __DIR__ . '/vendor/symfony/messenger', ]); };
  1. Niech Twój projekt będzie zależny od brakującej biblioteki do produkcji:
 composer require symfony/messenger

Transpilowanie i ciągła integracja

Jak wspomniano wcześniej, w naszych komputerach programistycznych musimy użyć flagi --dry-run podczas uruchamiania Rectora, w przeciwnym razie kod źródłowy zostanie zastąpiony kodem transpilowanym. Z tego powodu bardziej odpowiednie jest uruchomienie rzeczywistego procesu transpilacji podczas ciągłej integracji (CI), gdzie możemy uruchomić tymczasowe programy uruchamiające, aby wykonać proces.

Idealnym momentem na przeprowadzenie procesu transpilacji jest generowanie wersji dla naszego projektu. Na przykład poniższy kod to przepływ pracy dla akcji GitHub, który tworzy wydanie wtyczki 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 }}

Ten przepływ pracy zawiera standardową procedurę wydawania wtyczki WordPress za pośrednictwem akcji GitHub. Nowy dodatek, aby transpilować kod wtyczki z PHP 7.4 do 7.1, odbywa się w tym kroku:

 - 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

Podsumowując, ten przepływ pracy wykonuje teraz następujące kroki:

  1. Sprawdza kod źródłowy wtyczki WordPress ze swojego repozytorium, napisany w PHP 7.4
  2. Instaluje swoje zależności Composer
  3. Transpiluje swój kod z PHP 7.4 do 7.1
  4. Zmienia wpis „Wymaga PHP” w nagłówku głównego pliku wtyczki z "7.4" na "7.1"
  5. Usuwa zależności potrzebne do rozwoju
  6. Tworzy plik .zip wtyczki, wyłączając wszystkie niepotrzebne pliki
  7. Przesyła plik .zip jako zasób wersji (i dodatkowo jako artefakt do akcji GitHub)

Testowanie transpilowanego kodu

Po transpilacji kodu do PHP 7.1, skąd mamy wiedzieć, że działa dobrze? Innymi słowy, skąd wiemy, że został on gruntownie przekonwertowany i nie pozostały żadne resztki wyższych wersji kodu PHP?

Podobnie jak w przypadku transpilacji kodu, możemy zaimplementować rozwiązanie w ramach procesu CI. Pomysł polega na skonfigurowaniu środowiska runnera z PHP 7.1 i uruchomieniu lintera na transpilowanym kodzie. Jeśli jakikolwiek fragment kodu nie jest zgodny z PHP 7.1 (np. wpisana właściwość z PHP 7.4, która nie została przekonwertowana), linter zgłosi błąd.

Linter dla PHP, który działa dobrze, to PHP Parallel Lint. Możemy zainstalować tę bibliotekę jako zależność do rozwoju w naszym projekcie lub zlecić zainstalowanie jej przez proces CI jako samodzielny projekt Composer:

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

Ilekroć kod zawiera PHP 7.2 i nowsze, PHP Parallel Lint zgłosi błąd podobny do tego:

 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.

Dodajmy linter do przepływu pracy naszego CI. Kroki, które należy wykonać w celu transpilacji kodu z PHP 8.0 do 7.1 i przetestowania to:

  1. Sprawdź kod źródłowy
  2. Uruchom środowisko PHP 8.0, aby Rector mógł zinterpretować kod źródłowy
  3. Transpiluj kod do PHP 7.1
  4. Zainstaluj narzędzie PHP linter
  5. Zmień wersję PHP środowiska na 7.1
  6. Uruchom linter na przetranspilowanym kodzie

Ten przepływ pracy GitHub Action wykonuje zadanie:

 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

Proszę zauważyć, że kilka plików bootstrap80.php z bibliotek polyfill Symfony (które nie muszą być transpilowane) musi być wykluczonych z lintera. Pliki te zawierają PHP 8.0, więc linter będzie generował błędy podczas ich przetwarzania. Jednak wykluczenie tych plików jest bezpieczne, ponieważ będą one ładowane w środowisku produkcyjnym tylko w przypadku uruchomienia PHP 8.0 lub nowszego:

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

Niezależnie od tego, czy tworzysz publiczną wtyczkę do WordPressa, czy aktualizujesz starszy kod, istnieje wiele powodów, dla których korzystanie z najnowszej wersji PHP może być niemożliwe Dowiedz się, jak transpilowanie może pomóc, z tego przewodnika Kliknij, aby Tweet

Streszczenie

Ten artykuł nauczył nas, jak transpilować nasz kod PHP, co pozwoliło nam użyć PHP 8.0 w kodzie źródłowym i stworzyć wersję, która działa w PHP 7.1. Transpilowanie odbywa się za pomocą Rectora, narzędzia do rekonstrukcji PHP.

Transpilacja naszego kodu czyni nas lepszymi programistami, ponieważ możemy lepiej wyłapywać błędy w rozwoju i tworzyć kod, który jest naturalnie łatwiejszy do odczytania i zrozumienia.

Transpiling umożliwia nam również oddzielenie naszego kodu od konkretnych wymagań PHP z CMS. Teraz możemy to zrobić, jeśli chcemy użyć najnowszej wersji PHP do stworzenia publicznie dostępnej wtyczki WordPress lub modułu Drupal bez poważnego ograniczania naszej bazy użytkowników.

Czy masz jakieś pytania dotyczące transpilacji PHP? Daj nam znać w sekcji komentarzy!