WordPress抽象程式碼最佳實踐和相關外掛

WordPress抽象程式碼最佳實踐和相關外掛

WordPress 是一個歷史悠久的內容管理系統,但也是使用最多的一種。由於支援過時的 PHP 版本和遺留程式碼,WordPress 在實施現代編碼實踐方面仍然存在不足,WordPress 抽象就是一個例子。

例如,如果能將 WordPress 核心程式碼庫拆分成由 Composer 管理的軟體包,效果會好得多。又或者,從檔案路徑自動載入 WordPress 類。

本文將詳細介紹程式碼抽象相關的資訊,WordPress 程式碼抽象的最佳實踐及相關外掛。

WordPress 與 PHP 工具的整合問題

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

  • 在 WordPress 5.6 支援 PHP 8.0 之前,Yoast 的一份報告描述了在 WordPress 核心上執行 PHPStan 會產生數千個問題
  • 由於WordPress仍支援PHP 5.6,因此 WordPress 測試套件目前只支援 PHPUnit 到 7.5 版本,而 7.5 版本的生命週期已經結束。
  • 通過 PHP-Scoper 擴充套件 WordPress 外掛非常具有挑戰性

在我們的專案中,WordPress 程式碼只佔總程式碼的一小部分;專案還將包含與底層 CMS 無關的業務程式碼。然而,僅僅因為有一些 WordPress 程式碼,專案就可能無法與工具正常整合。

有鑑於此,將專案拆分成若干個軟體包,其中一些包含 WordPress 程式碼,另一些則只包含使用 “vanilla” PHP 的業務程式碼,而不包含 WordPress 程式碼。這樣,後面這些軟體包就不會受到上述問題的影響,而是可以與工具完美整合。

什麼是程式碼抽象?

程式碼抽象可以消除程式碼中的固定依賴關係,生成通過契約相互影響的軟體包。這些軟體包可以通過不同的堆疊新增到不同的應用程式中,從而最大限度地提高其可用性。程式碼抽象的結果是基於以下支柱的簡潔的解耦程式碼庫:

  1. 針對介面而非實現進行編碼。
  2. 建立包並通過 Composer 釋出。
  3. 通過依賴注入將所有部分粘合在一起。

針對介面而非實現進行編碼

針對介面編碼是指使用合約讓程式碼片段相互互動的做法。合約是一個簡單的 PHP 介面(或任何其他語言),它定義了哪些函式可用及其簽名,即它們接收哪些輸入及其輸出。

介面宣告瞭功能的意圖,但不解釋功能將如何實現。通過介面訪問功能,我們的應用程式可以依賴自主的程式碼片段來實現特定目標,而無需知道或關心它們是如何實現的。這樣,應用程式就不需要進行調整,就能切換到另一段程式碼來實現相同的目標,例如,從不同的提供商那裡獲取程式碼。

合約示例

下面的程式碼使用了 Symfony 的合約  CacheInterface 和 PHP 標準建議(PSR)合約  CacheItemInterface  來實現快取功能:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
use Psr\Cache\CacheItemInterface;
use Symfony\Contracts\Cache\CacheInterface;
$value = $cache->get('my_cache_key', function (CacheItemInterface $item) {
$item->expiresAfter(3600);
return 'foobar';
});
use Psr\Cache\CacheItemInterface; use Symfony\Contracts\Cache\CacheInterface; $value = $cache->get('my_cache_key', function (CacheItemInterface $item) { $item->expiresAfter(3600); return 'foobar'; });
use Psr\Cache\CacheItemInterface;
use Symfony\Contracts\Cache\CacheInterface;
$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 就有這樣的簽名:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
/**
* @param array $args
* @return WP_Post[]|int[] Array of post objects or post IDs.
*/
function get_posts( $args = null )
/** * @param array $args * @return WP_Post[]|int[] Array of post objects or post IDs. */ function get_posts( $args = null )
/**
* @param array $args
* @return WP_Post[]|int[] Array of post objects or post IDs.
*/
function get_posts( $args = null )

我們可以通過 Owner\MyApp\Contracts\PostsAPIInterface 合同來訪問它,而不是直接呼叫這個方法:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
namespace Owner\MyApp\Contracts;
interface PostAPIInterface
{
public function get_posts(array $args = null): PostInterface[]|int[];
}
namespace Owner\MyApp\Contracts; interface PostAPIInterface { public function get_posts(array $args = null): PostInterface[]|int[]; }
namespace Owner\MyApp\Contracts;
interface PostAPIInterface
{
public function get_posts(array $args = null): PostInterface[]|int[];
}

請注意,WordPress 函式 get_posts 可以返回類 WP_Post 的物件,這是 WordPress 特有的。在抽象程式碼時,我們需要刪除這種固定的依賴關係。合約中的 get_posts 方法會返回 PostInterface 型別的物件,這樣就可以不明確地引用 WP_Post 類。PostInterface 類需要提供對 WP_Post 所有方法和屬性的訪問許可權:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
namespace Owner\MyApp\Contracts;
interface PostInterface
{
public function get_ID(): int;
public function get_post_author(): string;
public function get_post_date(): string;
// ...
}
namespace Owner\MyApp\Contracts; interface PostInterface { public function get_ID(): int; public function get_post_author(): string; public function get_post_date(): string; // ... }
namespace Owner\MyApp\Contracts;
interface PostInterface
{
public function get_ID(): int;
public function get_post_author(): string;
public function get_post_date(): string;
// ...
}

執行這一策略可以改變我們對 WordPress 在堆疊中位置的理解。我們不再把 WordPress 視為應用程式本身(我們在其上安裝主題和外掛),而是將其視為應用程式中的另一個依賴項,與其他元件一樣可以替換。(儘管我們在實踐中不會替換 WordPress,但從概念上講,它是可以替換的)。

建立和分發軟體包

Composer 是 PHP 的軟體包管理器。它允許 PHP 應用程式從資源庫中獲取軟體包(即程式碼片段),並將其作為依賴項安裝。為了將應用程式與 WordPress 解耦,我們必須將其程式碼分成兩種不同型別的包:包含 WordPress 程式碼的包和包含業務邏輯(即不包含 WordPress 程式碼)的包。

最後,我們將所有軟體包作為依賴項新增到應用程式中,並通過 Composer 進行安裝。由於工具將應用於業務程式碼包,因此這些包必須包含應用程式的大部分程式碼;百分比越高越好。讓它們管理整個程式碼的 90% 左右是個不錯的目標。

將 WordPress 程式碼提取到程式碼包中

按照前面的例子,PostAPIInterfacePostInterface 合同將被新增到包含業務程式碼的包中,而另一個包將包含這些合同的 WordPress 實現。為了滿足 PostInterface 的要求,我們建立了一個 PostWrapper 類,它將從 WP_Post 物件中獲取所有屬性:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
namespace Owner\MyAppForWP\ContractImplementations;
use Owner\MyApp\Contracts\PostInterface;
use WP_Post;
class PostWrapper implements PostInterface
{
private WP_Post $post;
public function __construct(WP_Post $post)
{
$this->post = $post;
}
public function get_ID(): int
{
return $this->post->ID;
}
public function get_post_author(): string
{
return $this->post->post_author;
}
public function get_post_date(): string
{
return $this->post->post_date;
}
// ...
}
namespace Owner\MyAppForWP\ContractImplementations; use Owner\MyApp\Contracts\PostInterface; use WP_Post; class PostWrapper implements PostInterface { private WP_Post $post; public function __construct(WP_Post $post) { $this->post = $post; } public function get_ID(): int { return $this->post->ID; } public function get_post_author(): string { return $this->post->post_author; } public function get_post_date(): string { return $this->post->post_date; } // ... }
namespace Owner\MyAppForWP\ContractImplementations;
use Owner\MyApp\Contracts\PostInterface;
use WP_Post;
class PostWrapper implements PostInterface
{
private WP_Post $post;
public function __construct(WP_Post $post)
{
$this->post = $post;
}
public function get_ID(): int
{
return $this->post->ID;
}
public function get_post_author(): string
{
return $this->post->post_author;
}
public function get_post_date(): string
{
return $this->post->post_date;
}
// ...
}

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
namespace Owner\MyAppForWP\ContractImplementations;
use Owner\MyApp\Contracts\PostAPIInterface;
use WP_Post;
class PostAPI implements PostAPIInterface
{
public function get_posts(array $args = null): PostInterface[]|int[]
{
// This var will contain WP_Post[] or int[]
$wpPosts = \get_posts($args);
// Convert WP_Post[] to PostWrapper[]
return array_map(
function (WP_Post|int $post) {
if ($post instanceof WP_Post) {
return new PostWrapper($post);
}
return $post
},
$wpPosts
);
}
}
namespace Owner\MyAppForWP\ContractImplementations; use Owner\MyApp\Contracts\PostAPIInterface; use WP_Post; class PostAPI implements PostAPIInterface { public function get_posts(array $args = null): PostInterface[]|int[] { // This var will contain WP_Post[] or int[] $wpPosts = \get_posts($args); // Convert WP_Post[] to PostWrapper[] return array_map( function (WP_Post|int $post) { if ($post instanceof WP_Post) { return new PostWrapper($post); } return $post }, $wpPosts ); } }
namespace Owner\MyAppForWP\ContractImplementations;
use Owner\MyApp\Contracts\PostAPIInterface;
use WP_Post;
class PostAPI implements PostAPIInterface
{
public function get_posts(array $args = null): PostInterface[]|int[]
{
// This var will contain WP_Post[] or int[]
$wpPosts = \get_posts($args);
// Convert WP_Post[] to 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 標準:

通過服務容器訪問服務

依賴注入庫將提供一個 “服務容器”,它將合同解析為相應的實現類。應用程式必須依賴服務容器來訪問所有功能。例如,我們通常會直接呼叫 WordPress 函式:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
$posts = get_posts();
$posts = get_posts();
$posts = get_posts();

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
use Owner\MyApp\Contracts\PostAPIInterface;
// Obtain the service container, as specified by the library we use
$serviceContainer = ContainerBuilderFactory::getInstance();
// The obtained service will be of class Owner\MyAppForWP\ContractImplementations\PostAPI
$postAPI = $serviceContainer->get(PostAPIInterface::class);
// Now we can invoke the WordPress functionality
$posts = $postAPI->get_posts();
use Owner\MyApp\Contracts\PostAPIInterface; // Obtain the service container, as specified by the library we use $serviceContainer = ContainerBuilderFactory::getInstance(); // The obtained service will be of class Owner\MyAppForWP\ContractImplementations\PostAPI $postAPI = $serviceContainer->get(PostAPIInterface::class); // Now we can invoke the WordPress functionality $posts = $postAPI->get_posts();
use Owner\MyApp\Contracts\PostAPIInterface;
// Obtain the service container, as specified by the library we use
$serviceContainer = ContainerBuilderFactory::getInstance();
// The obtained service will be of class Owner\MyAppForWP\ContractImplementations\PostAPI
$postAPI = $serviceContainer->get(PostAPIInterface::class);
// Now we can invoke the WordPress functionality
$posts = $postAPI->get_posts();

使用 Symfony 的依賴注入

Symfony 的 DependencyInjection 元件是目前最流行的依賴注入庫。它允許你通過 PHP、YAML 或 XML 程式碼來配置服務容器。例如,要通過類 PostAPI 來定義 PostAPIInterface 合同,可以這樣在 YAML 中配置:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
services:
Owner\MyApp\Contracts\PostAPIInterface:
class: \Owner\MyAppForWP\ContractImplementations\PostAPI
services: Owner\MyApp\Contracts\PostAPIInterface: class: \Owner\MyAppForWP\ContractImplementations\PostAPI
services:
Owner\MyApp\Contracts\PostAPIInterface:
class: \Owner\MyAppForWP\ContractImplementations\PostAPI

Symfony 的依賴注入(DependencyInjection)還允許將一個服務的例項自動注入(或 “autowired”)到依賴於它的任何其他服務中。此外,它還可以輕鬆定義一個類是其自身服務的實現。例如,請看下面的 YAML 配置

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
services:
_defaults:
public: true
autowire: true
GraphQLAPI\GraphQLAPI\Registries\UserAuthorizationSchemeRegistryInterface:
class: '\GraphQLAPI\GraphQLAPI\Registries\UserAuthorizationSchemeRegistry'
GraphQLAPI\GraphQLAPI\Security\UserAuthorizationInterface:
class: '\GraphQLAPI\GraphQLAPI\Security\UserAuthorization'
GraphQLAPI\GraphQLAPI\Security\UserAuthorizationSchemes\:
resource: '../src/Security/UserAuthorizationSchemes/*'
services: _defaults: public: true autowire: true GraphQLAPI\GraphQLAPI\Registries\UserAuthorizationSchemeRegistryInterface: class: '\GraphQLAPI\GraphQLAPI\Registries\UserAuthorizationSchemeRegistry' GraphQLAPI\GraphQLAPI\Security\UserAuthorizationInterface: class: '\GraphQLAPI\GraphQLAPI\Security\UserAuthorization' GraphQLAPI\GraphQLAPI\Security\UserAuthorizationSchemes\: resource: '../src/Security/UserAuthorizationSchemes/*'
services:
_defaults:
public: true
autowire: true
GraphQLAPI\GraphQLAPI\Registries\UserAuthorizationSchemeRegistryInterface:
class: '\GraphQLAPI\GraphQLAPI\Registries\UserAuthorizationSchemeRegistry'
GraphQLAPI\GraphQLAPI\Security\UserAuthorizationInterface:
class: '\GraphQLAPI\GraphQLAPI\Security\UserAuthorization'
GraphQLAPI\GraphQLAPI\Security\UserAuthorizationSchemes\:
resource: '../src/Security/UserAuthorizationSchemes/*'

該配置定義了以下內容:

  • 通過類 UserAuthorizationSchemeRegistry 滿足合約 UserAuthorizationSchemeRegistryInterface
  • 通過類 UserAuthorization 滿足合約 UserAuthorizationInterface
  • UserAuthorizationSchemes/ 資料夾下的所有類都是其自身的實現
  • 服務之間必須自動注入( autowire: true

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
class UserAuthorization implements UserAuthorizationInterface
{
public function __construct(
protected UserAuthorizationSchemeRegistryInterface $userAuthorizationSchemeRegistry
) {
}
// ...
}
class UserAuthorization implements UserAuthorizationInterface { public function __construct( protected UserAuthorizationSchemeRegistryInterface $userAuthorizationSchemeRegistry ) { } // ... }
class UserAuthorization implements UserAuthorizationInterface
{
public function __construct(
protected UserAuthorizationSchemeRegistryInterface $userAuthorizationSchemeRegistry
) {
}
// ...
}

由於使用了 autowire: true,DependencyInjection 元件將自動讓 UserAuthorization 服務接收其所需的依賴關係,即 UserAuthorizationSchemeRegistry 的例項。

何時抽象

抽象程式碼會耗費大量的時間和精力,因此我們只有在利大於弊的情況下才進行抽象。以下是值得抽象程式碼的建議。您可以使用本文中的程式碼片段或下面建議的抽象 WordPress 外掛來實現這一點。

獲取工具

如前所述,在 WordPress 上執行 PHP-Scoper 是很困難的。通過將 WordPress 程式碼解耦為不同的軟體包,就可以直接使用 WordPress 外掛

減少工具開發時間和成本

執行 PHPUnit 測試套件時,需要初始化和執行 WordPress 的時間比不需要初始化和執行 WordPress 的時間長。時間越少,執行測試所需的費用也就越少 – 例如,GitHub Actions 會根據使用時間對 GitHub 託管的執行程式收費。

不需要大量重構

現有專案可能需要進行大量重構才能引入所需的架構(依賴注入、將程式碼拆分成包等),因此很難抽離。在從頭開始建立專案時對程式碼進行抽象,則更易於管理。

為多個平臺生成程式碼

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

遷移到不同的平臺

如果我們需要將一個專案從 Drupal 遷移到 WordPress、從 WordPress 遷移到 Laravel 或其他任何組合,那麼只需重寫 10%的程式碼,這將大大節省成本。

最佳實踐

在設計合同以抽象我們的程式碼時,我們可以對程式碼庫進行一些改進。

遵守 PSR-12

在定義訪問 WordPress 方法的介面時,我們應遵守 PSR-12。這一最新規範旨在減少掃描不同作者程式碼時的認知摩擦。遵守 PSR-12 意味著重新命名 WordPress 函式。

WordPress 使用 snake_case 寫為函式命名,而 PSR-12 則使用 camelCase。因此,函式 get_posts 將變為 getPosts

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
interface PostAPIInterface
{
public function getPosts(array $args = null): PostInterface[]|int[];
}
interface PostAPIInterface { public function getPosts(array $args = null): PostInterface[]|int[]; }
interface PostAPIInterface
{
public function getPosts(array $args = null): PostInterface[]|int[];
}

……還有:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
class PostAPI implements PostAPIInterface
{
public function getPosts(array $args = null): PostInterface[]|int[]
{
// This var will contain WP_Post[] or int[]
$wpPosts = \get_posts($args);
// Rest of the code
// ...
}
}
class PostAPI implements PostAPIInterface { public function getPosts(array $args = null): PostInterface[]|int[] { // This var will contain WP_Post[] or int[] $wpPosts = \get_posts($args); // Rest of the code // ... } }
class PostAPI implements PostAPIInterface
{
public function getPosts(array $args = null): PostInterface[]|int[]
{
// This var will contain WP_Post[] or int[]
$wpPosts = \get_posts($args);
// Rest of the code
// ...
}
}

拆分方法

介面中的方法不必完全照搬 WordPress 中的方法。只要合理,我們就可以對它們進行轉換。例如,WordPress 函式 get_user_by($field,$value) 知道如何通過引數 $field 從資料庫中檢索使用者,該引數可接受的值有  "id""ID""slug""email" 或 "login"。這種設計存在一些問題:

  • 如果我們傳遞了一個錯誤的字串,它不會在編譯時失敗
  • 引數 $value 需要接受所有選項的所有不同型別,即使在傳遞 "ID" 時,它期望的是一個 int,但在傳遞 "email" 時,它只能接收一個 string

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
namespace Owner\MyApp\Contracts;
interface UserAPIInterface
{
public function getUserById(int $id): ?UserInterface;
public function getUserByEmail(string $email): ?UserInterface;
public function getUserBySlug(string $slug): ?UserInterface;
public function getUserByLogin(string $login): ?UserInterface;
}
namespace Owner\MyApp\Contracts; interface UserAPIInterface { public function getUserById(int $id): ?UserInterface; public function getUserByEmail(string $email): ?UserInterface; public function getUserBySlug(string $slug): ?UserInterface; public function getUserByLogin(string $login): ?UserInterface; }
namespace Owner\MyApp\Contracts;
interface UserAPIInterface
{
public function getUserById(int $id): ?UserInterface;
public function getUserByEmail(string $email): ?UserInterface;
public function getUserBySlug(string $slug): ?UserInterface;
public function getUserByLogin(string $login): ?UserInterface;
}

對於 WordPress 來說,合同是這樣解決的(假設我們已經建立了 UserWrapperUserInterface,如前所述):

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
namespace Owner\MyAppForWP\ContractImplementations;
use Owner\MyApp\Contracts\UserAPIInterface;
class UserAPI implements UserAPIInterface
{
public function getUserById(int $id): ?UserInterface
{
return $this->getUserByProp('id', $id);
}
public function getUserByEmail(string $email): ?UserInterface
{
return $this->getUserByProp('email', $email);
}
public function getUserBySlug(string $slug): ?UserInterface
{
return $this->getUserByProp('slug', $slug);
}
public function getUserByLogin(string $login): ?UserInterface
{
return $this->getUserByProp('login', $login);
}
private function getUserByProp(string $prop, int|string $value): ?UserInterface
{
if ($user = \get_user_by($prop, $value)) {
return new UserWrapper($user);
}
return null;
}
}
namespace Owner\MyAppForWP\ContractImplementations; use Owner\MyApp\Contracts\UserAPIInterface; class UserAPI implements UserAPIInterface { public function getUserById(int $id): ?UserInterface { return $this->getUserByProp('id', $id); } public function getUserByEmail(string $email): ?UserInterface { return $this->getUserByProp('email', $email); } public function getUserBySlug(string $slug): ?UserInterface { return $this->getUserByProp('slug', $slug); } public function getUserByLogin(string $login): ?UserInterface { return $this->getUserByProp('login', $login); } private function getUserByProp(string $prop, int|string $value): ?UserInterface { if ($user = \get_user_by($prop, $value)) { return new UserWrapper($user); } return null; } }
namespace Owner\MyAppForWP\ContractImplementations;
use Owner\MyApp\Contracts\UserAPIInterface;
class UserAPI implements UserAPIInterface
{
public function getUserById(int $id): ?UserInterface
{
return $this->getUserByProp('id', $id);
}
public function getUserByEmail(string $email): ?UserInterface
{
return $this->getUserByProp('email', $email);
}
public function getUserBySlug(string $slug): ?UserInterface
{
return $this->getUserByProp('slug', $slug);
}
public function getUserByLogin(string $login): ?UserInterface
{
return $this->getUserByProp('login', $login);
}
private function getUserByProp(string $prop, int|string $value): ?UserInterface
{
if ($user = \get_user_by($prop, $value)) {
return new UserWrapper($user);
}
return null;
}
}

刪除函式簽名中的實現細節

WordPress 中的函式可能會在自己的簽名中提供如何實現的資訊。在從抽象的角度評估函式時,可以刪除這些資訊。例如,WordPress 中獲取使用者姓氏的方法是呼叫 get_the_author_meta,明確說明使用者姓氏是作為 “meta” 值儲存的(在 wp_usermeta 表中):

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
$userLastname = get_the_author_meta("user_lastname", $user_id);
$userLastname = get_the_author_meta("user_lastname", $user_id);
$userLastname = get_the_author_meta("user_lastname", $user_id);

您不必將這些資訊傳達給合同。介面只關心 “是什麼”,而不關心 “怎麼做”。因此,合約中可以有一個 getUserLastname 方法,但不提供任何關於如何實現的資訊:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
interface UserAPIInterface
{
public function getUserLastname(UserWrapper $userWrapper): string;
...
}
interface UserAPIInterface { public function getUserLastname(UserWrapper $userWrapper): string; ... }
interface UserAPIInterface
{
public function getUserLastname(UserWrapper $userWrapper): string;
...
}

新增更嚴格的型別

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
$url = add_query_arg('id', 5, $url);
$url = add_query_arg('id', 5, $url);
$url = add_query_arg('id', 5, $url);

… 或 key => value 的陣列:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
$url = add_query_arg(['id' => 5], $url);
$url = add_query_arg(['id' => 5], $url);
$url = add_query_arg(['id' => 5], $url);

我們的介面可以將這些函式拆分成幾個獨立的函式,每個函式接受一個獨特的輸入組合,從而定義一個更易於理解的意圖:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
public function addQueryArg(string $key, string $value, string $url);
public function addQueryArgs(array $keyValues, string $url);
public function addQueryArg(string $key, string $value, string $url); public function addQueryArgs(array $keyValues, string $url);
public function addQueryArg(string $key, string $value, string $url);
public function addQueryArgs(array $keyValues, string $url);

消除技術債務

WordPress 函式 get_posts 不僅返回 “帖子”,也返回 “頁面 “或任何 “自定義帖子” 型別的實體,而這些實體是不能互換的。帖子和頁面都是自定義帖子,但頁面既不是帖子,也不是頁面。因此,執行 get_posts 可以返回頁面。這種行為是概念上的差異。

為使其正確,get_posts 應被稱為 get_customposts,但 WordPress 核心從未對其進行重新命名。這是大多數長期軟體的常見問題,被稱為 “技術債務”-程式碼存在問題,但由於引入了破壞性更改而從未得到修復。

不過,在建立合同時,我們有機會避免這種技術債務。在這種情況下,我們可以建立一個新的介面 ModelAPIInterface,它可以處理不同型別的實體,然後我們再建立幾個方法,每個方法處理不同型別的實體:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
interface ModelAPIInterface
{
public function getPosts(array $args): array;
public function getPages(array $args): array;
public function getCustomPosts(array $args): array;
}
interface ModelAPIInterface { public function getPosts(array $args): array; public function getPages(array $args): array; public function getCustomPosts(array $args): array; }
interface ModelAPIInterface
{
public function getPosts(array $args): array;
public function getPages(array $args): array;
public function getCustomPosts(array $args): array;
}

這樣,就不會再出現差異,您將看到這些結果:

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

抽象程式碼的好處

抽象應用程式程式碼的主要優點有:

  • 在僅包含業務程式碼的軟體包上執行的工具更易於設定,執行所需的時間(和資金)也更少。
  • 我們可以使用與 WordPress 不相容的工具,例如使用 PHP-Scoper 對外掛進行掃描。
  • 我們製作的軟體包可以很容易地自主用於其他應用程式。
  • 將應用程式遷移到其他平臺變得更加容易。
  • 我們可以將思維方式從 WordPress 轉變為業務邏輯思維。
  • 合約描述了應用程式的意圖,使其更易於理解。
  • 應用程式通過軟體包進行組織,建立一個包含最基本內容的精簡應用程式,並根據需要逐步增強。
  • 我們可以清除技術債務。

抽象程式碼的問題

抽象應用程式程式碼的缺點是:

  • 一開始需要做大量工作。
  • 程式碼變得更加冗長;為了實現同樣的結果,需要增加額外的程式碼層。
  • 最終可能會產生幾十個軟體包,而這些軟體包又必須進行管理和維護。
  • 您可能需要一個 monorepo 來統一管理所有軟體包。
  • 依賴注入對於簡單的應用程式來說可能是矯枉過正(收益遞減)。
  • 抽象程式碼永遠無法完全實現,因為 CMS 的架構中通常隱含著一種普遍的偏好。

抽象 WordPress 外掛選項

雖然通常最明智的做法是先將程式碼提取到本地環境,然後再對其進行處理,但一些 WordPress 外掛可以幫助你實現抽象目標。這些是我們的首選。

1. WPide

由 WebFactory Ltd 製作的 WPide 外掛很受歡迎,它極大地擴充套件了 WordPress 預設程式碼編輯器的功能。作為一款抽象的 WordPress 外掛,它允許您在原處檢視程式碼,以便更直觀地瞭解需要注意的地方。

 

WPide 外掛

WPide 外掛

WPide 還具有搜尋和替換功能,可快速查詢過時或過期的程式碼,並用重構後的程式碼進行替換。

除此之外,WPide 還提供了大量額外功能,包括:

  • 語法和程式碼塊高亮
  • 自動備份
  • 建立檔案和資料夾
  • 全面的檔案樹瀏覽器
  • 訪問 WordPress 檔案系統 API

2. Ultimate DB Manager

來自 WPHobby 的 Ultimate WP DB Manager 外掛為您提供了一種快速方法,可完整下載您的資料庫,以便提取和重構。

Ultimate DB Manager 外掛

Ultimate DB Manager 外掛

3. 自己定製的抽象 WordPress 外掛

最後,抽象的最佳選擇永遠是建立自己的外掛。這看似是一項大工程,但如果你直接管理 WordPress 核心檔案的能力有限,這就提供了一種便於抽象的變通辦法。

這樣做的好處顯而易見:

  • 從主題檔案中抽取功能
  • 在主題更改和資料庫更新時保留程式碼

您可以通過 WordPress 的《外掛開發者手冊》瞭解如何建立抽象 WordPress 外掛。

小結

我們是否應該對應用程式中的程式碼進行抽象?就像任何事情一樣,沒有預定義的 “正確答案”,因為這取決於每個專案的具體情況。那些需要花費大量時間使用 PHPUnit 或 PHPStan 進行分析的專案可以從中獲益最多,但付出的努力並不總是值得的。

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

你打算在你的專案中實施這一策略嗎?如果是,您會使用抽象 WordPress 外掛嗎?請在評論區告訴我們!

評論留言