轉譯 PHP 代碼的終極指南
已發表: 2021-09-22在理想情況下,我們應該為所有網站使用 PHP 8.0(撰寫本文時的最新版本),並在新版本發布後立即更新。 但是,開發人員通常需要使用以前的 PHP 版本,例如在為 WordPress 創建公共插件或使用阻礙升級網絡服務器環境的遺留代碼時。
在這些情況下,我們可以放棄使用最新的 PHP 代碼的希望。 但是還有一個更好的選擇:我們仍然可以使用 PHP 8.0 編寫我們的源代碼並將其轉換為以前的 PHP 版本——甚至是 PHP 7.1。
在本指南中,我們將教您有關轉譯 PHP 代碼的所有知識。
什麼是轉譯?
轉譯將源代碼從一種編程語言轉換為相同或不同編程語言的等效源代碼。
轉譯在 Web 開發中並不是一個新概念:客戶端開發人員很可能熟悉 Babel,這是一個 JavaScript 代碼轉譯器。
Babel 將 JavaScript 代碼從現代 ECMAScript 2015+ 版本轉換為與舊瀏覽器兼容的舊版本。 例如,給定一個 ES2015 箭頭函數:
[2, 4, 6].map((n) => n * 2);
…Babel 會將其轉換為 ES5 版本:
[2, 4, 6].map(function(n) { return n * 2; });
什麼是轉譯 PHP?
Web 開發中潛在的新事物是轉換服務器端代碼的可能性,特別是 PHP。
轉譯 PHP 的工作方式與轉譯 JavaScript 相同:將現代 PHP 版本的源代碼轉換為舊 PHP 版本的等效代碼。
按照與之前相同的示例,PHP 7.4 中的箭頭函數:
$nums = array_map(fn($n) => $n * 2, [2, 4, 6]);
…可以轉換成等效的 PHP 7.3 版本:
$nums = array_map( function ($n) { return $n * 2; }, [2, 4, 6] );
箭頭函數可以被轉譯,因為它們是語法糖,即產生現有行為的新語法。 這是唾手可得的果實。
但是,也有一些新特性會產生新的行為,因此,以前版本的 PHP 將沒有等效代碼。 PHP 8.0 中引入的聯合類型就是這種情況:
function someFunction(float|int $param): string|float|int|null { // ... }
在這些情況下,只要開發需要新功能而不是生產需要,仍然可以進行轉譯。 然後,我們可以簡單地從轉譯代碼中完全刪除該功能而不會產生嚴重後果。
一個這樣的例子是聯合類型。 此功能用於檢查輸入類型與其提供的值之間是否不匹配,這有助於防止錯誤。 如果與類型發生衝突,那麼在開發中就會出現錯誤,我們應該在代碼投入生產之前捕獲並修復它。
因此,我們有能力從生產代碼中刪除該功能:
function someFunction($param) { // ... }
如果錯誤仍然發生在生產環境中,拋出的錯誤消息將不如我們有聯合類型時那麼精確。 但是,首先能夠使用聯合類型超過了這個潛在的缺點。
轉譯 PHP 代碼的優點
轉譯使人們能夠使用最新版本的 PHP 對應用程序進行編碼,並生成一個也可以在運行舊版本 PHP 的環境中工作的版本。
這對於為遺留內容管理系統 (CMS) 創建產品的開發人員特別有用。 例如,WordPress 仍然正式支持 PHP 5.6(儘管它推薦 PHP 7.4+)。 運行 PHP 版本 5.6 到 7.2 的 WordPress 站點的百分比——這些站點都已停產(EOL),這意味著它們不再接收安全更新——佔相當大的 34.8%,並且運行在任何 PHP 版本以外的站點的百分比8.0 高達 99.5%:

因此,針對全球受眾的 WordPress 主題和插件很可能會使用舊版本的 PHP 進行編碼,以增加其可能的影響力。 多虧了轉譯,這些可以使用 PHP 8.0 進行編碼,並且仍然針對較舊的 PHP 版本發布,從而針對盡可能多的用戶。
實際上,任何需要支持除最新版本之外的任何 PHP 版本(即使在當前支持的 PHP 版本範圍內)的應用程序都可以從中受益。
Drupal 就是這種情況,它需要 PHP 7.3。 由於轉譯,開發人員可以使用 PHP 8.0 創建公開可用的 Drupal 模塊,並使用 PHP 7.3 發布它們。
另一個示例是為由於某種原因而無法在其環境中運行 PHP 8.0 的客戶創建自定義代碼時。 儘管如此,由於轉譯,開發人員仍然可以使用 PHP 8.0 對他們的可交付成果進行編碼,並在這些遺留環境中運行它們。
何時編譯 PHP
PHP 代碼總是可以被轉譯,除非它包含一些 PHP 特性在以前的 PHP 版本中沒有等效的特性。
PHP 8.0 中引入的屬性可能就是這種情況:
#[SomeAttr] function someFunc() {} #[AnotherAttr] class SomeClass {}
在前面使用箭頭函數的示例中,代碼可以被轉譯,因為箭頭函數是語法糖。 相反,屬性創造了全新的行為。 這種行為也可以在 PHP 7.4 及以下版本中重現,但只能通過手動編碼,即不能自動基於工具或流程(人工智能可以提供解決方案,但我們還沒有)。
用於開發的屬性,例如#[Deprecated]
,可以像刪除聯合類型一樣被刪除。 但是在生產環境中修改應用程序行為的屬性不能被刪除,也不能直接被轉譯。
到目前為止,沒有任何轉譯器可以獲取具有 PHP 8.0 屬性的代碼並自動生成其等效的 PHP 7.4 代碼。 因此,如果您的 PHP 代碼需要使用屬性,那麼轉譯它會很困難或不可行。
可以轉譯的 PHP 功能
這些是 PHP 7.1 及更高版本中當前可以轉譯的功能。 如果您的代碼僅使用這些功能,您可以確信您的轉譯應用程序可以正常工作。 否則,您需要評估轉譯後的代碼是否會產生故障。
PHP版本 | 特徵 |
---|---|
7.1 | 一切 |
7.2 | – object 類型– 參數類型擴大 – preg_match 中的PREG_UNMATCHED_AS_NULL 標誌 |
7.3 | – list() / 數組解構中的引用分配(除了在foreach 中 — #4376)– 靈活的 Heredoc 和 Nowdoc 語法 – 函數調用中的尾隨逗號 – set(raw)cookie 接受 $option 參數 |
7.4 | – 類型化的屬性 - 箭頭功能 – 空值合併賦值運算符 - 解包內部數組 – 數字文字分隔符 – 帶有標籤名稱數組的 strip_tags() – 協變返回類型和逆變參數類型 |
8.0 | – 聯合類型 – mixed 偽類型– static 返回類型– ::class 對像上的魔法常數– match 表達式– 僅按類型 catch 異常– 空安全運算符 – 類構造函數屬性提升 – 參數列表和閉包 use 列表中的尾隨逗號 |
PHP 轉譯器
目前,有一種轉換 PHP 代碼的工具:Rector。
Rector 是一個 PHP 重構工具,它根據可編程規則轉換 PHP 代碼。 我們輸入源代碼和要運行的規則集,Rector 將轉換代碼。
Rector 通過命令行操作,通過 Composer 安裝在項目中。 執行時,Rector 將輸出轉換前後代碼的“差異”(綠色為添加,紅色為刪除):

要轉換到哪個版本的 PHP
要跨 PHP 版本轉換代碼,必須創建相應的規則。
今天,Rector 庫包含 PHP 8.0 到 7.1 範圍內的大部分代碼轉換規則。 因此,我們可以可靠地將 PHP 代碼轉譯至 7.1 版本。
從 PHP 7.1 到 7.0 以及從 7.0 到 5.6 的轉換也有一些規則,但這些規則並不詳盡。 完成它們的工作正在進行中,因此我們最終可能會將 PHP 代碼轉換為 5.6 版本。
轉譯與反向移植
反向移植類似於轉譯,但更簡單。 向後移植代碼不一定依賴於一種語言的新特性。 相反,只需從新版本的語言複製/粘貼/改編相應的代碼,即可為舊版本的語言提供相同的功能。
例如,函數str_contains
是在 PHP 8.0 中引入的。 PHP 7.4 及以下版本的相同功能可以像這樣輕鬆實現:
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; } } }
因為反向移植比轉譯簡單,所以只要反向移植完成這項工作,我們就應該選擇這個解決方案。
關於 PHP 8.0 到 7.1 之間的範圍,我們可以使用 Symfony 的 polyfill 庫:
- Polyfill PHP 7.1
- Polyfill PHP 7.2
- Polyfill PHP 7.3
- Polyfill PHP 7.4
- Polyfill PHP 8.0
這些庫向後移植以下函數、類、常量和接口:
PHP版本 | 特徵 |
---|---|
7.2 | 職能:
常數:
|
7.3 | 職能:
例外:
|
7.4 | 職能:
|
8.0 | 接口:
課程:
常數:
職能:
|
轉譯的 PHP 示例
讓我們檢查一些已轉譯的 PHP 代碼示例,以及一些正在完全轉譯的包。
PHP 代碼
match
表達式是在 PHP 8.0 中引入的。 這個源代碼:
function getFieldValue(string $fieldName): ?string { return match($fieldName) { 'foo' => 'foofoo', 'bar' => 'barbar', 'baz' => 'bazbaz', default => null, }; }
…將使用switch
運算符轉換為等效的 PHP 7.4 版本:
function getFieldValue(string $fieldName): ?string { switch ($fieldName) { case 'foo': return 'foofoo'; case 'bar': return 'barbar'; case 'baz': return 'bazbaz'; default: return null; } }
在 PHP 8.0 中還引入了 nullsafe 運算符:
public function getValue(TypeResolverInterface $typeResolver): ?string { return $this->getResolver($typeResolver)?->getValue(); }
轉譯後的代碼需要先將操作的值賦給一個新的變量,以避免執行兩次操作:
public function getValue(TypeResolverInterface $typeResolver): ?string { return ($val = $this->getResolver($typeResolver)) ? $val->getValue() : null; }
在 PHP 8.0 中也引入了構造函數屬性提升功能,允許開發人員編寫更少的代碼:
class QueryResolver { function __construct(protected QueryFormatter $queryFormatter) { } }
為 PHP 7.4 編譯它時,會生成完整的代碼:
class QueryResolver { protected QueryFormatter $queryFormatter; function __construct(QueryFormatter $queryFormatter) { $this->queryFormatter = $queryFormatter; } }
上面的轉譯代碼包含類型屬性,它們是在 PHP 7.4 中引入的。 將該代碼轉換為 PHP 7.3 會用 docblocks 替換它們:
class QueryResolver { /** * @var QueryFormatter */ protected $queryFormatter; function __construct(QueryFormatter $queryFormatter) { $this->queryFormatter = $queryFormatter; } }
PHP 包
以下庫正在編譯以用於生產:
庫/描述 | 代碼/註釋 |
---|---|
校長 使轉譯成為可能的 PHP 重構工具 | - 源代碼 – 轉譯代碼 – 筆記 |
簡易編碼標準 使 PHP 代碼遵守一組規則的工具 | - 源代碼 – 轉譯代碼 – 筆記 |
用於 WordPress 的 GraphQL API 為 WordPress 提供 GraphQL 服務器的插件 | - 源代碼 – 轉譯代碼 – 筆記 |
轉譯 PHP 的優缺點
已經描述了轉譯 PHP 的好處:它允許源代碼使用 PHP 8.0(即 PHP 的最新版本),它將被轉換為 PHP 的較低版本,以便在遺留應用程序或環境中運行。
這有效地使我們能夠成為更好的開發人員,生成更高質量的代碼。 這是因為我們的源代碼可以使用 PHP 8.0 的聯合類型、PHP 7.4 的類型化屬性,以及添加到每個新版本 PHP(PHP 8.0 mixed
,PHP 7.2 object
)中的不同類型和偽類型,其中PHP 的其他現代特性。
使用這些功能,我們可以在開發過程中更好地捕捉錯誤並編寫更易於閱讀的代碼。
現在,讓我們來看看缺點。
它必須被編碼和維護
Rector 可以自動轉換代碼,但該過程可能需要一些手動輸入才能使其與我們的特定設置一起使用。
第三方庫也必須被轉譯
每當編譯它們產生錯誤時,這就會成為一個問題,因為我們必須深入研究它們的源代碼以找出可能的原因。 如果問題可以修復並且項目是開源的,我們將需要提交拉取請求。 如果庫不是開源的,我們可能會遇到障礙。
無法編譯代碼時,Rector 不會通知我們
如果源代碼包含 PHP 8.0 屬性或任何其他無法轉譯的特性,我們將無法繼續。 但是,Rector 不會檢查這種情況,所以我們需要手動進行。 這對於我們自己的源代碼可能不是什麼大問題,因為我們已經熟悉它,但它可能成為第三方依賴的障礙。
調試信息使用轉譯的代碼,而不是源代碼
當應用程序在生產中產生帶有堆棧跟踪的錯誤消息時,行號將指向已轉譯的代碼。 我們需要將轉譯後的代碼轉換回原始代碼,以在源代碼中找到對應的行號。
轉譯的代碼也必須加前綴
我們的轉譯項目和其他一些也安裝在生產環境中的庫可以使用相同的第三方依賴項。 此第三方依賴項將為我們的項目進行轉譯,並為其他庫保留其原始源代碼。 因此,轉譯版本必須通過 PHP-Scoper、Strauss 或其他工具添加前綴以避免潛在的衝突。
轉譯必須在持續集成 (CI) 期間進行
因為轉譯後的代碼自然會覆蓋源代碼,所以我們不應該在我們的開發計算機上運行轉譯過程,否則可能會產生副作用。 在 CI 運行期間運行該過程更合適(更多內容見下文)。
如何編譯 PHP
首先,我們需要在我們的項目中安裝Rector進行開發:
composer require rector/rector --dev
然後我們在項目的根目錄中創建一個包含所需規則集的rector.php
配置文件。 要將代碼從 PHP 8.0 降級到 7.1,我們使用以下配置:
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); };
為了確保進程按預期執行,我們可以在幹模式下運行 Rector 的process
命令,傳遞要處理的位置(在這種情況下,文件夾src/
下的所有文件):
vendor/bin/rector process src --dry-run
要執行轉譯,我們運行 Rector 的process
命令,它將修改現有位置中的文件:
vendor/bin/rector process src
請注意:如果我們在開發計算機中運行rector process
,源代碼將在src/
下就地轉換。 但是,我們希望在不同的位置生成轉換後的代碼,以免在降級代碼時覆蓋源代碼。 因此,在持續集成期間運行流程是最合適的。
優化轉譯過程
要生成用於生產的轉譯交付物,只需轉換用於生產的代碼; 可以跳過僅用於開發的代碼。 這意味著我們可以避免編譯所有測試(對於我們的項目及其依賴項)和所有開發依賴項。
關於測試,我們已經知道我們項目的測試所在的位置——例如,在文件夾tests/
下。 我們還必須找出依賴項的位置——例如,在它們的子文件夾tests/
、 test/
和Test/
下(針對不同的庫)。 然後,我們告訴 Rector 跳過處理這些文件夾:

return static function (ContainerConfigurator $containerConfigurator): void { // ... $parameters->set(Option::SKIP, [ // Skip tests '*/tests/*', '*/test/*', '*/Test/*', ]); };
關於依賴關係,Composer 知道哪些是用於開發的(那些在composer.json
中的 entry require-dev
下),哪些是用於生產的(那些在 entry require
下的)。
為了從 Composer 中檢索所有生產依賴項的路徑,我們運行:
composer info --path --no-dev
此命令將生成一個包含名稱和路徑的依賴項列表,如下所示:
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
我們可以提取所有路徑並將它們提供給 Rector 命令,然後它將處理我們項目的src/
文件夾以及包含所有生產依賴項的文件夾:
$ paths="$(composer info --path --no-dev | cut -d' ' -f2- | sed 's/ //g' | tr '\n' ' ')" $ vendor/bin/rector process src $paths
進一步的改進可以防止 Rector 處理那些已經使用目標 PHP 版本的依賴項。 如果一個庫是用 PHP 7.1(或任何以下版本)編碼的,那麼就沒有必要將它轉換成 PHP 7.1。
為此,我們可以獲取需要 PHP 7.2 及更高版本的庫列表並僅處理這些庫。 我們將通過 Composer 的why-not
命令獲取所有這些庫的名稱,如下所示:
composer why-not php "7.1.*" | grep -o "\S*\/\S*"
因為此命令不能與--no-dev
標誌一起使用,要僅包含生產依賴項,我們首先需要刪除開發依賴項並重新生成自動加載器,執行命令,然後再次添加它們:
$ composer install --no-dev $ packages=$(composer why-not php "7.1.*" | grep -o "\S*\/\S*") $ composer install
Composer 的info --path
命令檢索包的路徑,格式如下:
# Executing this command $ composer info psr/cache --path # Produces this response: psr/cache /Users/leo/GitHub/leoloso/PoP/vendor/psr/cache
我們對列表中的所有項目執行此命令以獲取所有要轉換的路徑:
for package in $packages do path=$(composer info $package --path | cut -d' ' -f2-) paths="$paths $path" done
最後,我們將這個列表提供給 Rector(加上項目的src/
文件夾):
需要一個可以為您提供競爭優勢的託管解決方案? Kinsta 為您提供令人難以置信的速度、最先進的安全性和自動縮放功能。 查看我們的計劃
vendor/bin/rector process src $paths
轉譯代碼時要避免的陷阱
轉譯代碼可以被認為是一門藝術,通常需要針對項目進行調整。 讓我們看看我們可能會遇到的一些問題。
不總是處理鍊式規則
鍊式規則是指規則需要轉換前一條規則生成的代碼。
例如,庫symfony/cache
包含以下代碼:
final class CacheItem implements ItemInterface { public function tag($tags): ItemInterface { // ... return $this; } }
從 PHP 7.4 編譯到 7.3 時,函數tag
必須經過兩次修改:
- 由於規則
DowngradeCovariantReturnTypeRector
,返回類型ItemInterface
必須首先轉換為self
- 由於規則
DowngradeSelfTypeDeclarationRector
,返回類型self
然後必須被刪除
最終結果應該是這個:
final class CacheItem implements ItemInterface { public function tag($tags) { // ... return $this; } }
但是,Rector 只輸出中間階段:
final class CacheItem implements ItemInterface { public function tag($tags): self { // ... return $this; } }
問題是 Rector 不能總是控制應用規則的順序。
解決方案是確定哪些鍊式規則未處理,並執行新的 Rector 運行以應用它們。
為了識別鍊式規則,我們在源代碼上運行了兩次 Rector,如下所示:
$ vendor/bin/rector process src $ vendor/bin/rector process src --dry-run
第一次,我們按預期運行 Rector 來執行轉譯。 第二次,我們使用--dry-run
標誌來發現是否還有要進行的更改。 如果有,該命令將退出並顯示錯誤代碼,並且“diff”輸出將指示哪些規則仍然可以應用。 這意味著第一次運行沒有完成,一些鍊式規則沒有被處理。

一旦我們確定了未應用的鍊式規則(或多個規則),我們就可以創建另一個 Rector 配置文件——例如, rector-chained-rule.php
將執行缺失的規則。 這次我們可以在需要應用的特定文件上運行特定的缺失規則,而不是為src/
下的所有文件處理一整套規則:
// 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', ]); };
最後,我們通過輸入--config
告訴 Rector 在第二次通過時使用新的配置文件:
# 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
Composer 依賴項可能不一致
庫可以聲明要開發的依賴項(即在composer.json
中的require-dev
下),但仍然可以從它們中引用一些代碼用於生產(例如在src/
下的某些文件上,而不是在tests/
下)。
通常,這不是問題,因為該代碼可能不會在生產環境中加載,因此應用程序永遠不會出錯。 但是,當 Rector 處理源代碼及其依賴項時,它會驗證是否可以加載所有引用的代碼。 如果任何文件引用了未安裝庫中的某些代碼,Rector 將拋出錯誤(因為它被聲明為僅用於開發)。
例如,來自 Symfony 的 Cache 組件的類EarlyExpirationHandler
實現了來自 Messenger 組件的接口MessageHandlerInterface
:
class EarlyExpirationHandler implements MessageHandlerInterface { //... }
但是, symfony/cache
聲明symfony/messenger
是開發的依賴項。 然後,在依賴symfony/cache
的項目上運行 Rector 時,會拋出錯誤:
[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".
這個問題有三種解決方案:
- 在 Rector 配置中,跳過處理引用該代碼的文件:
return static function (ContainerConfigurator $containerConfigurator): void { // ... $parameters->set(Option::SKIP, [ __DIR__ . '/vendor/symfony/cache/Messenger/EarlyExpirationHandler.php', ]); };
- 下載缺失的庫並添加其路徑以由 Rector 自動加載:
return static function (ContainerConfigurator $containerConfigurator): void { // ... $parameters->set(Option::AUTOLOAD_PATHS, [ __DIR__ . '/vendor/symfony/messenger', ]); };
- 讓您的項目依賴於缺少的庫進行生產:
composer require symfony/messenger
轉譯和持續集成
如前所述,在我們的開發計算機中,我們必須在運行 Rector 時使用--dry-run
標誌,否則,源代碼將被轉譯代碼覆蓋。 出於這個原因,在持續集成(CI)期間運行實際的轉譯過程更合適,我們可以在其中啟動臨時運行器來執行該過程。
執行轉譯過程的理想時間是在為我們的項目生成版本時。 例如,下面的代碼是 GitHub Actions 的工作流程,它創建了 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 }}
此工作流程包含通過 GitHub Actions 發布 WordPress 插件的標準程序。 將插件代碼從 PHP 7.4 轉換為 7.1 的新增功能發生在此步驟中:
- 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
總之,此工作流程現在執行以下步驟:
- 從其存儲庫中查看 WordPress 插件的源代碼,使用 PHP 7.4 編寫
- 安裝其 Composer 依賴項
- 將其代碼從 PHP 7.4 轉換為 7.1
- 將插件主文件頭中的“Requires PHP”條目從
"7.4"
修改為"7.1"
- 刪除開發所需的依賴項
- 創建插件的 .zip 文件,排除所有不需要的文件
- 將 .zip 文件作為發布資產上傳(此外,作為工件上傳到 GitHub 操作)
測試轉譯的代碼
一旦代碼被轉譯為 PHP 7.1,我們怎麼知道它運行良好? 或者,換句話說,我們怎麼知道它已經被徹底轉換,並且沒有留下更高版本的 PHP 代碼?
與轉譯代碼類似,我們可以在 CI 流程中實現解決方案。 這個想法是用 PHP 7.1 設置運行器的環境,並在轉譯的代碼上運行 linter。 如果任何一段代碼與 PHP 7.1 不兼容(例如 PHP 7.4 中的類型化屬性未轉換),則 linter 將拋出錯誤。
一個運行良好的 PHP linter 是 PHP Parallel Lint。 我們可以將此庫安裝為項目中的開發依賴項,或者讓 CI 過程將其安裝為獨立的 Composer 項目:
composer create-project php-parallel-lint/php-parallel-lint
每當代碼包含 PHP 7.2 及更高版本時,PHP Parallel Lint 都會拋出如下錯誤:
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.
讓我們將 linter 添加到 CI 的工作流程中。 將代碼從 PHP 8.0 轉換為 7.1 並對其進行測試的步驟如下:
- 查看源代碼
- 讓環境運行 PHP 8.0,以便 Rector 可以解釋源代碼
- 將代碼轉換為 PHP 7.1
- 安裝 PHP linter 工具
- 將環境的PHP版本切換到7.1
- 在轉譯的代碼上運行 linter
這個 GitHub Action 工作流程完成了這項工作:
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
請注意,來自 Symfony 的 polyfill 庫的幾個bootstrap80.php
文件(不需要轉譯)必須從 linter 中排除。 這些文件包含 PHP 8.0,因此 linter 在處理它們時會拋出錯誤。 但是,排除這些文件是安全的,因為它們僅在運行 PHP 8.0 或更高版本時才會在生產環境中加載:
if (\PHP_VERSION_ID >= 80000) { return require __DIR__.'/bootstrap80.php'; }
特
概括
本文教我們如何編譯 PHP 代碼,允許我們在源代碼中使用 PHP 8.0 並創建適用於 PHP 7.1 的版本。 轉譯是通過 PHP 重構工具 Rector 完成的。
轉譯我們的代碼使我們成為更好的開發人員,因為我們可以更好地捕捉開發中的錯誤並生成自然更易於閱讀和理解的代碼。
轉譯還使我們能夠將我們的代碼與來自 CMS 的特定 PHP 要求解耦。 如果我們希望使用最新版本的 PHP 來創建公開可用的 WordPress 插件或 Drupal 模塊,我們現在可以這樣做,而不會嚴重限制我們的用戶群。
您對轉譯 PHP 有任何疑問嗎? 請在評價部分留下您的意見!