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 天退款保证的计划中。查看我们的计划或与销售人员交谈以找到适合您的计划。

相关文章