WordPress 抽象:最佳實踐和 WordPress 抽象插件

WordPress 是一個古老的 CMS,但也是最常用的。由於它支持過時的 PHP 版本和遺留代碼的歷史,它仍然缺乏實現現代編碼實踐——WordPress 抽象就是一個例子。

嘗試免費演示

例如,將 WordPress 核心代碼庫拆分為 Composer 管理的包會好得多。或者,從文件路徑自動載入 WordPress 類。

本文將教你如何手動抽象 WordPress 代碼並使用抽象的 WordPress 插件功能。

集成 WordPress 和 PHP 工具的問題

由於其古老的架構,我們在將 WordPress 與 PHP 代碼庫工具集成時偶爾會遇到問題,例如靜態分析器 PHPStan、單元測試庫 PHPUnit 和命名空間範圍庫 PHP-Scoper。例如,考慮以下情況:

  • 在支持 PHP 8.0 的 WordPress 5.6 之前,Yoast 的一份報告描述了在 WordPress 核心上運行 PHPStan 會產生數千個問題。
  • 由於仍然支持 PHP 5.6,WordPress 測試套件目前只支持 PHPUnit 到 7.5 版本,該版本已經結束。
  • 通過 PHP-Scoper 確定 WordPress 插件的範圍非常具有挑戰性。

我們項目中的 WordPress 代碼只是總數的一小部分;該項目還將包含與底層 CMS 無關的業務代碼。然而,僅僅通過一些 WordPress 代碼,項目可能無法正確地與工具集成。

因此,將項目拆分為多個包可能是有意義的,其中一些包包含 WordPress 代碼,而其他包僅包含使用「vanilla」PHP 的業務代碼而沒有 WordPress 代碼。這樣,後面的這些包不會受到上述問題的影響,但可以與工具完美集成。

什麼是代碼抽象?

代碼抽象從代碼中刪除了固定的依賴關係,產生了通過契約相互交互的包。然後可以將這些包添加到具有不同堆棧的不同應用程序中,從而最大限度地提高它們的可用性。代碼抽象的結果是一個基於以下支柱的完全解耦的代碼庫:

  1. 針對介面的代碼,而不是實現。
  2. 創建包並通過 Composer 分發它們。
  3. 通過依賴注入將所有部分粘合在一起。

#008cc4}想了解更多關於 WordPress 代碼抽象的信息嗎?👩‍💻 從最佳實踐到推薦的插件,您需要知道的一切只需點擊一下⬇️點擊推文

針對介面而不是實現編碼

針對介面編碼是使用契約讓代碼片段相互交互的做法。合約只是一個 PHP 介面(或任何不同的語言),它定義了哪些函數可用及其簽名,即它們接收哪些輸入和輸出。

介面聲明了功能的意圖,而不解釋功能將如何實現。通過介面訪問功能,我們的應用程序可以依賴自主代碼段來完成特定目標,而無需知道或關心它們是如何實現的。通過這種方式,應用程序不需要進行調整以切換到實現相同目標的另一段代碼——例如,來自不同的提供者。

合同示例

以下代碼使用 Symfony 的合約 CacheInterface 和 PHP 標準推薦(PSR)合約 CacheItemInterface 來實現緩存功能:

使用 PsrCacheCacheItemInterface;
使用 SymfonyContractsCacheCacheInterface;

$value = $cache->get(‘my_cache_key’, function (CacheItemInterface $item) {
$item->expiresAfter(3600);
return ‘foobar’;
});

$cache 實現了 CacheInterface,它定義了從緩存中檢索對象的方法 get。通過合約訪問此功能,應用程序可以不知道緩存在哪裡。無論是在內存、磁碟、資料庫、網路還是其他任何地方。儘管如此,它必須執行該功能。CacheItemInterface 定義了 expiresAfter 方法來聲明項目必須在緩存中保留多長時間。應用程序可以調用這個方法而不用關心緩存的對象是什麼;它只關心必須緩存多長時間。

針對 WordPress 中的介面編碼

因為我們正在抽象 WordPress 代碼,結果將是應用程序不會直接引用 WordPress 代碼,而是始終通過介面引用。例如,WordPress 函數 get_posts 具有以下簽名:

/**
* @param array $args
* @return WP_Post[]|int[] 帖子對象或帖子 ID 的數組。
*/
函數 get_posts( $args = null )

我們可以通過合約 OwnerMyAppContractsPostsAPIInterface 訪問它,而不是直接調用此方法:

命名空間 OwnerMyAppContracts;

interface PostAPIInterface
{
public function get_posts(array $args = null): PostInterface[]|int[];
}

請注意,WordPress 函數 get_posts 可以返回特定於 WordPress 的 WP_Post 類的對象。在抽象代碼的時候,我們需要去掉這種固定的依賴。合約中的 get_posts 方法返回 PostInterface 類型的對象,允許您引用類 WP_Post 而無需對其進行明確說明。PostInterface 類需要提供對 WP_Post 的所有方法和屬性的訪問:

命名空間 OwnerMyAppContracts;

interface PostInterface
{
public function get_ID(): int;
公共函數 get_post_author(): 字元串;
公共函數 get_post_date(): 字元串;
// …
}

執行此策略可以改變我們對 WordPress 在我們的堆棧中的位置的理解。與其將 WordPress 視為應用程序本身(我們在其上安裝主題和插件),我們可以將其簡單地視為應用程序中的另一個依賴項,可以像任何其他組件一樣替換。(儘管我們不會在實踐中替換 WordPress,但從概念的角度來看,它是可以替換的。)

創建和分發包

Composer 是 PHP 的包管理器。它允許 PHP 應用程序從存儲庫中檢索包(即代碼段)並將它們安裝為依賴項。要將應用程序與 WordPress 分離,我們必須將其代碼分發到兩種不同類型的包中:一種包含 WordPress 代碼,另一種包含業務邏輯(即沒有 WordPress 代碼)。

最後,我們將所有包作為依賴項添加到應用程序中,並通過 Composer 安裝它們。由於工具將應用於業務代碼包,因此它們必須包含應用程序的大部分代碼;百分比越高越好。讓他們管理大約 90% 的整體代碼是一個很好的目標。

將 WordPress 代碼提取到包中

按照前面的例子,合約 PostAPIInterface 和 PostInterface 將被添加到包含業務代碼的包中,另一個包將包含這些合約的 WordPress 實現。為了滿足 PostInterface,我們創建了一個 PostWrapper 類,它將從 WP_Post 對象中檢索所有屬性:

命名空間 OwnerMyAppForWPContractImplementations;

使用 OwnerMyAppContractsPostInterface;
使用 WP_Post;

類 PostWrapper 實現 PostInterface
{
private WP_Post $post;

公共函數 __construct(WP_Post $post)
{
$this->post = $post;
}

公共函數 get_ID(): int
{
return $this->post->ID;
}

公共函數 get_post_author(): string
{
return $this->post->post_author;
}

公共函數 get_post_date(): string
{
return $this->post->post_date;
}

// …
}

在實現 PostAPI 時,由於方法 get_posts 返回 PostInterface[],我們必須將對象從 WP_Post 轉換為 PostWrapper:

命名空間 OwnerMyAppForWPContractImplementations;

使用 OwnerMyAppContractsPostAPIInterface;
使用 WP_Post;

class PostAPI 實現 PostAPIInterface
{
public function get_posts(array $args = null): PostInterface[]|int[]
{
// 這個變數將包含 WP_Post[] 或 int[]
$wpPosts = get_posts($args);

// 將 WP_Post[] 轉換為 PostWrapper[]
return array_map(
function (WP_Post|int $post) {
if ($post instanceof WP_Post) {
return new PostWrapper($post);
}
return $post
},
$wpPosts
);
}
}

使用依賴注入

依賴注入是一種設計模式,可以讓您以鬆散耦合的方式將所有應用程序部分粘合在一起。通過依賴注入,應用程序通過它們的合約訪問服務,合約實現通過配置被「注入」到應用程序中。

只需更改配置,我們就可以輕鬆地從一個合約提供者切換到另一個。我們可以選擇幾個依賴注入庫。我們建議選擇符合 PHP 標準建議(通常稱為「PSR」)的庫,以便在需要時可以輕鬆地用另一個庫替換該庫。關於依賴注入,庫必須滿足 PSR-11,它提供了「容器介面」的規範。其中,以下庫符合 PSR-11:

  • Symfony 的依賴注入
  • PHP-DI
  • 奧拉迪
  • 容器(依賴注入)
  • Yii 依賴注入

通過服務容器訪問服務

依賴注入庫將提供一個「服務容器」,它將契約解析為其相應的實現類。應用程序必須依賴服務容器來訪問所有功能。例如,雖然我們通常會直接調用 WordPress 函數:

$posts = get_posts();

……有了服務容器,首先要獲取滿足PostAPIInterface的服務,並通過它執行功能:

使用 OwnerMyAppContractsPostAPIInterface;

// 獲取服務容器,由我們使用的庫指定
$serviceContainer = ContainerBuilderFactory::getInstance();

// 獲取的服務屬於 OwnerMyAppForWPContractImplementationsPostAPI
$postAPI = $serviceContainer->get(PostAPIInterface::class);

// 現在我們可以調用 WordPress 功能
$posts = $postAPI->get_posts();

使用 Symfony 的 DependencyInjection

Symfony 的 DependencyInjection 組件是目前最流行的依賴注入庫。它允許您通過 PHP、YAML 或 XML 代碼配置服務容器。例如,要定義該合約 PostAPIInterface 通過類 PostAPI 在 YAML 中配置得到滿足,如下所示:

服務:
OwnerMyAppContractsPostAPIInterface:
類:OwnerMyAppForWPContractImplementationsPostAPI

Symfony 的 DependencyInjection 還允許將來自一個服務的實例自動注入(或「自動裝配」)到依賴它的任何其他服務中。此外,還可以輕鬆定義類是其自身服務的實現。例如,考慮以下 YAML 配置:

服務:
_defaults:
public:true
自動裝配:true

GraphQLAPIGraphQLAPIRegistriesUserAuthorizationSchemeRegistryInterface:
類:’GraphQLAPIGraphQLAPIRegistriesUserAuthorizationSchemeRegistry’

GraphQLAPIGraphQLAPISecurityUserAuthorizationInterface:
類:’GraphQLAPIGraphQLAPISecurityUserAuthorization’

GraphQLAPIGraphQLAPISecurityUserAuthorizationSchemes:
資源:’../src/Security/UserAuthorizationSchemes/*’

此配置定義了以下內容:

  • 通過類 UserAuthorizationSchemeRegistry 滿足合同 UserAuthorizationSchemeRegistryInterface
  • 通過類 UserAuthorization 滿足合同 UserAuthorizationInterface
  • UserAuthorizationSchemes/文件夾下的所有類都是自己的實現
  • 服務必須自動相互注入(autowire:true)

讓我們看看自動裝配是如何工作的。UserAuthorization 類依賴於具有合約 UserAuthorizationSchemeRegistryInterface 的服務:

類 UserAuthorization 實現 UserAuthorizationInterface
{
public function __construct(
protected UserAuthorizationSchemeRegistryInterface $userAuthorizationSchemeRegistry
) {
}

// …
}

感謝 autowire: true,DependencyInjection 組件將自動讓服務 UserAuthorization 接收其所需的依賴項,它是 UserAuthorizationSchemeRegistry 的一個實例。

何時抽象

抽象代碼會消耗大量的時間和精力,所以我們應該只在它的收益大於成本的情況下進行它。以下是關於何時抽象代碼可能值得的建議。您可以使用本文中的代碼片段或下面建議的抽象 WordPress 插件來完成此操作。

獲得對工具的訪問

如前所述,在 WordPress 上運行 PHP-Scoper 很困難。通過將 WordPress 代碼解耦到不同的包中,直接確定 WordPress 插件的範圍變得可行。

減少加工時間和成本

運行 PHPUnit 測試套件在需要初始化和運行 WordPress 時比不需要時需要更長的時間。更少的時間也可以轉化為運行測試所花的錢更少——例如,GitHub Actions 根據使用它們的時間對 GitHub 託管的運行器收費。

不需要大量重構

一個現有的項目可能需要大量重構來引入所需的架構(依賴注入、將代碼拆分成包等),從而難以拉出。從頭開始創建項目時抽象代碼使其更易於管理。

為多個平台生成代碼

通過將 90% 的代碼提取到與 CMS 無關的包中,我們可以生成適用於不同 CMS 或框架的庫版本,只需替換整個代碼庫的 10%。

訂閱時事通訊

想知道我們是如何將流量增加超過 1000% 的嗎?

加入 20,000 多名其他人的行列,他們會收到我們的每周時事通訊,其中包含 WordPress 內幕技巧!

現在訂閱

遷移到不同的平台

如果我們需要將一個項目從 Drupal 遷移到 WordPress,從 WordPress 遷移到 Laravel 或任何其他組合,那麼只需重寫 10% 的代碼——這是一個顯著的節省。

最佳實踐

在設計契約來抽象我們的代碼時,我們可以對代碼庫進行一些改進。

遵守 PSR-12

在定義訪問 WordPress 方法的介面時,我們應該遵守 PSR-12。這個最近的規範旨在減少掃描來自不同作者的代碼時的認知摩擦。遵守 PSR-12 意味著重命名 WordPress 功能。

WordPress 使用snake_case 命名函數,而PSR-12 使用camelCase。因此,函數 get_posts 將變成 getPosts:

interface PostAPIInterface
{
public function getPosts(array $args = null): PostInterface[]|int[];
}

…和:

class PostAPI implements PostAPIInterface
{
public function getPosts(array $args = null): PostInterface[]|int[]
{
// 這個變數將包含 WP_Post[] 或 int[]
$wpPosts = get_posts($args);

// 其餘代碼
// …
}
}

拆分方法

界面中的方法不需要是來自 WordPress 的方法的副本。我們可以在有意義的時候轉換它們。例如,WordPress 函數 get_user_by($field, $value) 知道如何通過參數 $field 從資料庫中檢索用戶,該參數接受值「id」、「ID」、「slug」、「email」或「login」 . 這個設計有幾個問題:

  • 如果我們傳遞錯誤的字元串,它不會在編譯時失敗
  • 參數 $value 需要接受所有選項的所有不同類型,即使在傳遞「ID」時它需要一個 int,在傳遞「email」時它也只能接收一個字元串

我們可以通過將函數拆分成幾個來改善這種情況:

命名空間 OwnerMyAppContracts;

interface UserAPIInterface
{
public function getUserById(int $id): ?UserInterface;
公共函數 getUserByEmail(string $email): ?UserInterface;
公共函數 getUserBySlug(string $slug): ?UserInterface;
公共函數 getUserByLogin(string $login): ?UserInterface;
}

WordPress 的合同是這樣解決的(假設我們已經創建了 UserWrapper 和 UserInterface,如前所述):

命名空間 OwnerMyAppForWPContractImplementations;

使用 OwnerMyAppContractsUserAPIInterface;

class UserAPI 實現 UserAPIInterface
{
public function getUserById(int $id): ?UserInterface
{
return $this->getUserByProp(‘id’, $id);
}

public function getUserByEmail(string $email): ?UserInterface
{
return $this->getUserByProp(’email’, $email);
}

公共函數 getUserBySlug(string $slug): ?UserInterface
{
return $this->getUserByProp(‘slug’, $slug);
}

public function getUserByLogin(string $login): ?UserInterface
{
return $this->getUserByProp(‘login’, $login);
}

私有函數 getUserByProp(string $prop, int|string $value): ?UserInterface
{
if ($user = get_user_by($prop, $value)) {
return new UserWrapper($user); }
}
返回null;
}
}

從函數簽名中刪除實​​現細節

WordPress 中的函數可能會提供有關它們如何在自己的簽名中實現的信息。從抽象的角度評估功能時,可以刪除此信息。例如,在 WordPress 中獲取用戶的姓氏是通過調用 get_the_author_meta 來完成的,從而明確地將用戶的姓氏存儲為「meta」值(在表 wp_usermeta 上):

$userLastname = get_the_author_meta(“user_lastname”, $user_id);

您不必將此信息傳達給合同。介面只關心什麼,而不關心如何。因此,合約可以有一個方法 getUserLastname,它不提供任何關於它是如何實現的信息:

所有 Kinsta 託管計劃都包括來自我們經驗豐富的 WordPress 開發人員和工程師的 24/7 支持。與支持我們財富 500 強客戶的同一團隊聊天。看看我們的計劃!

interface UserAPIInterface
{
public function getUserLastname(UserWrapper $userWrapper): string;

}

添加更嚴格的類型

一些 WordPress 函數可以通過不同的方式接收參數,從而導致歧義。例如,函數 add_query_arg 可以接收單個鍵和值:

$url = add_query_arg(‘id’, 5, $url);

… 或一組鍵 => 值:

$url = add_query_arg([‘id’ => 5], $url);

我們的界面可以通過將這些功能分成幾個單獨的功能來定義更易於理解的意圖,每個功能都接受獨特的輸入組合:

公共函數 addQueryArg(string $key, string $value, string $url);
公共函數 addQueryArgs(array $keyValues, string $url);

消除技術債務

WordPress 函數 get_posts 不僅返回「帖子」,還返回「頁面」或任何類型為「自定義帖子」的實體,並且這些實體不可互換。帖子和頁面都是自定義帖子,但頁面不是帖子也不是頁面。因此,執行 get_posts 可以返回頁面。這種行為是概念上的差異。

為了使其正確,get_posts 應改為 get_customposts,但它從未在 WordPress 核心中重命名。這是大多數持久軟體的常見問題,被稱為「技術債務」——有問題但從未修復的代碼,因為它引入了破壞性更改。

但是,在創建合同時,我們有機會避免這種類型的技術債務。在這種情況下,我們可以創建一個新的介面 ModelAPIInterface 來處理不同類型的實體,並且我們創建了幾個方法,每個方法處理不同的類型:

interface ModelAPIInterface
{
public function getPosts(array $args): array;
公共函數 getPages(array $args): 數組;
公共函數 getCustomPosts(array $args): 數組;
}

這樣,差異就不會再發生,您將看到以下結果:

  • getPosts 只返回帖子
  • getPages 只返回頁面
  • getCustomPosts 返迴文章和頁面

抽象代碼的好處

抽象應用程序代碼的主要優點是:

  • 在僅包含業務代碼的包上運行的工具更易於設置,並且運行所需的時間(和資金)更少。
  • 我們可以使用不適用於 WordPress 的工具,例如使用 PHP-Scoper 確定插件的範圍。
  • 我們生產的包可以是自主的,可以輕鬆地用於其他應用程序。
  • 將應用程序遷移到其他平台變得更加容易。
  • 我們可以將我們的思維方式從 WordPress 思維轉變為我們的業務邏輯思維。
  • 合同描述了應用程序的意圖,使其更易於理解。
  • 該應用程序通過包進行組織,創建一個包含最低限度的精益應用程序,並根據需要逐步增強它。
  • 我們可以清理技術債務。

抽象代碼的問題

抽象應用程序代碼的缺點是:

  • 它最初涉及大量工作。
  • 代碼變得更加冗長;添加額外的代碼層以實現相同的結果。
  • 您最終可能會生成數十個包,然後必須對其進行管理和維護。
  • 您可能需要一個 monorepo 來一起管理所有包。
  • 對於簡單的應用程序(減少回報),依賴注入可能是過度的。
  • 抽象代碼永遠不會完全完成,因為在 CMS 的體系結構中通常隱含著一個普遍的偏好。

抽象 WordPress 插件選項

儘管在處理代碼之前將代碼提取到本地環境通常是最明智的,但一些 WordPress 插件可以幫助您實現抽象目標。這些是我們的首選。

1.WPide

由 WebFactory Ltd 生產的流行 WPide 插件極大地擴展了 WordPress 的默認代碼編輯器的功能。它作為一個抽象的 WordPress 插件,允許您原位查看代碼以更好地可視化需要注意的內容。

WPide 抽象 wordpress 插件

WPide 插件。

WPide 還具有搜索和替換功能,用於快速定位過時或過期的代碼並用重構的再現替換它。

最重要的是,WPide 提供了許多額外的功能,包括:

  • 語法和塊高亮
  • 自動備份
  • 文件和文件夾創建
  • 綜合文件樹瀏覽器
  • 訪問 WordPress 文件系統 API

2. 終極資料庫管理器

WPHobby 的 Ultimate WP DB Manager 插件為您提供了一種快速下載完整資料庫以進行提取和重構的方法。

Ultimate DB Manager 插件徽標的屏幕截圖,帶有以下文字:

終極資料庫管理器插件。

當然,Kinsta 用戶不需要這種類型的插件,因為 Kinsta 為所有客戶提供直接的資料庫訪問。但是,如果您的託管服務提供商沒有足夠的資料庫訪問許可權,Ultimate DB Manager 可以作為抽象的 WordPress 插件派上用場。

3.您自己的自定義抽象WordPress插件

最後,抽象的最佳選擇始終是創建您的插件。這似乎是一項艱巨的任務,但如果您直接管理 WordPress 核心文件的能力有限,這提供了一種抽象友好的解決方法。

這樣做有明顯的好處:

  • 從主題文件中抽象出你的函數
  • 通過主題更改和資料庫更新保留您的代碼

您可以通過 WordPress 的插件開發手冊了解如何創建抽象的 WordPress 插件。

 在這份詳盡的指南中了解如何手動抽象您的代碼並使用抽象的 WordPress 插件功能🚀⬇️點擊推文

概括

我們應該抽象應用程序中的代碼嗎?與所有事情一樣,沒有預定義的「正確答案」,因為它取決於逐個項目的基礎。那些需要大量時間使用 PHPUnit 或 PHPStan 進行分析的項目可能會受益最大,但實現它所需的努力可能並不總是值得的。

您已經了解了開始抽象 WordPress 代碼所需的一切知識。

您是否計劃在您的項目中實施此策略?如果是這樣,您會使用抽象的 WordPress 插件嗎?請在評價部分留下您的意見!

通過以下方式節省時間、成本並最大限度地提高站點性能:

  • 來自 WordPress 託管專家的即時幫助,24/7。
  • Cloudflare 企業集成。
  • 全球受眾覆蓋全球 28 個數據中心。
  • 使用我們內置的應用程序性能監控進行優化。

所有這些以及更多,都在一個沒有長期合同、協助遷移和 30 天退款保證的計劃中。查看我們的計劃或與銷售人員交談以找到適合您的計劃。

相關文章