WordPress抽象代码最佳实践和相关插件

2024年 4月 19日 40.0k 0

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 代码。这样,后面这些软件包就不会受到上述问题的影响,而是可以与工具完美集成。

什么是代码抽象?

代码抽象可以消除代码中的固定依赖关系,生成通过契约相互影响的软件包。这些软件包可以通过不同的堆栈添加到不同的应用程序中,从而最大限度地提高其可用性。代码抽象的结果是基于以下支柱的简洁的解耦代码库:

  • 针对接口而非实现进行编码。
  • 创建包并通过 Composer 发布。
  • 通过依赖注入将所有部分粘合在一起。
  • 针对接口而非实现进行编码

    针对接口编码是指使用合约让代码片段相互交互的做法。合约是一个简单的 PHP 接口(或任何其他语言),它定义了哪些函数可用及其签名,即它们接收哪些输入及其输出。

    接口声明了功能的意图,但不解释功能将如何实现。通过接口访问功能,我们的应用程序可以依赖自主的代码片段来实现特定目标,而无需知道或关心它们是如何实现的。这样,应用程序就不需要进行调整,就能切换到另一段代码来实现相同的目标,例如,从不同的提供商那里获取代码。

    合约示例

    下面的代码使用了 Symfony 的合约  CacheInterface 和 PHP 标准建议(PSR)合约  CacheItemInterface  来实现缓存功能:

    use PsrCacheCacheItemInterface;
    use 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[] Array of post objects or post IDs.
    */
    function get_posts( $args = null )
    

    我们可以通过 OwnerMyAppContractsPostsAPIInterface 合同来访问它,而不是直接调用这个方法:

    namespace OwnerMyAppContracts;
    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 所有方法和属性的访问权限:

    namespace OwnerMyAppContracts;
    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 对象中获取所有属性:

    namespace OwnerMyAppForWPContractImplementations;
    use OwnerMyAppContractsPostInterface;
    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 对象:

    namespace OwnerMyAppForWPContractImplementations;
    use OwnerMyAppContractsPostAPIInterface;
    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 标准:

    • Symfony’s DependencyInjection
    • PHP-DI
    • Aura.Di
    • Container (Dependency Injection)
    • Yii Dependency Injection

    通过服务容器访问服务

    依赖注入库将提供一个 “服务容器”,它将合同解析为相应的实现类。应用程序必须依赖服务容器来访问所有功能。例如,我们通常会直接调用 WordPress 函数:

    $posts = get_posts();
    

    ……有了服务容器,我们必须首先获取满足 PostAPIInterface 的服务,并通过它执行功能:

    use OwnerMyAppContractsPostAPIInterface;
    // Obtain the service container, as specified by the library we use
    $serviceContainer = ContainerBuilderFactory::getInstance();
    // The obtained service will be of class OwnerMyAppForWPContractImplementationsPostAPI
    $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 中配置:

    services:
    OwnerMyAppContractsPostAPIInterface:
    class: OwnerMyAppForWPContractImplementationsPostAPI
    

    Symfony 的依赖注入(DependencyInjection)还允许将一个服务的实例自动注入(或 “autowired”)到依赖于它的任何其他服务中。此外,它还可以轻松定义一个类是其自身服务的实现。例如,请看下面的 YAML 配置:

    services:
    _defaults:
    public: true
    autowire: true
    GraphQLAPIGraphQLAPIRegistriesUserAuthorizationSchemeRegistryInterface:
    class: 'GraphQLAPIGraphQLAPIRegistriesUserAuthorizationSchemeRegistry'
    GraphQLAPIGraphQLAPISecurityUserAuthorizationInterface:
    class: 'GraphQLAPIGraphQLAPISecurityUserAuthorization'
    GraphQLAPIGraphQLAPISecurityUserAuthorizationSchemes:
    resource: '../src/Security/UserAuthorizationSchemes/*'
    

    该配置定义了以下内容:

    • 通过类 UserAuthorizationSchemeRegistry 满足合约 UserAuthorizationSchemeRegistryInterface
    • 通过类 UserAuthorization 满足合约 UserAuthorizationInterface
    • UserAuthorizationSchemes/ 文件夹下的所有类都是其自身的实现
    • 服务之间必须自动注入( autowire: true

    让我们看看自动连接是如何工作的。UserAuthorization 类依赖于带有 UserAuthorizationSchemeRegistryInterface 合约的服务:

    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

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

    ……还有:

    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

    我们可以通过将函数拆分成几个函数来改善这种情况:

    namespace OwnerMyAppContracts;
    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,如前所述):

    namespace OwnerMyAppForWPContractImplementations;
    use OwnerMyAppContractsUserAPIInterface;
    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 表中):

    $userLastname = get_the_author_meta("user_lastname", $user_id);
    

    您不必将这些信息传达给合同。接口只关心 “是什么”,而不关心 “怎么做”。因此,合约中可以有一个 getUserLastname 方法,但不提供任何关于如何实现的信息:

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

    添加更严格的类型

    WordPress 的某些函数可以以不同的方式接收参数,从而导致歧义。例如,函数 add_query_arg 可以接收单个键和值:

    $url = add_query_arg('id', 5, $url);
    

    … 或 key => value 的数组:

    $url = add_query_arg(['id' => 5], $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,它可以处理不同类型的实体,然后我们再创建几个方法,每个方法处理不同类型的实体:

    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 插件吗?请在评论区告诉我们!

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论