通过挂钩驱动开发来提升插件开发
#网络开发人员 #教程 #php #wordpress

在本系列的previous article之后,非常技术性,今天我们将喘口气,沉迷于一个理论上的理论上。我们的主题是挂钩驱动的开发,为什么您也应该在插件中实现它。

在WordPress插件开发中,挂钩是一种基本和必不可少的工具。挂钩确保插件可扩展,可扩展并保持干净结构。挂钩驱动的开发是插件开发的一种方法,它不仅涉及使用WordPress和其他插件的现有钩子,而且还涉及将您自己的钩子合并到插件本身中。

在本文中,我假设对钩子及其在开发WordPress插件中的使用。

挂钩开发的优势

我们不想将挂钩驱动的开发用于娱乐 - 其应用程序也有一些令人信服的优势。

优势1:模块化和灵活性

通过始终使用插件中的钩子,您可以增强其模块化和灵活性。

  • 易于修改和扩展
    挂钩可以更轻松地适应个人或新需求。它们还促进了不更改现有代码的情况下添加额外功能。使用挂钩时,可以在实际的插件代码中定义新功能,也可以通过其他插件(例如插件)添加。这使您可以提供免费和付费版本的插件。您可以将免费版本作为完整的插件开发,然后将付费版本的其他功能集成为付费插件插件。但是,如果您想实现免费和付费版本的这种模型,请确保免费版本提供实际的附加值,并避免以不足的免费版本激怒您的用户,该版本希望随时出售付费版本!这样的插件已经太多了。

  • 更换组件
    通过挂钩驱动开发开发的插件的模块化性质允许插件的单个组件可以轻松替换而不影响代码的其余部分。

可维护性和可伸缩性

插件的可维护性和可伸缩性也受益于基于挂钩的开发。

  • 更好的大型项目结构这里我们回到了我最喜欢的话题!通过严格将钩子作为代码结构的基本要素,实现可以自然增长并保持透明的精心设计的插件变得更加容易。在哪里放置新实施的代码的问题很少(如果有的话)。
  • 更轻松地更新和调试具有明确定义的挂钩以及小类中相应的回调,更新单个组件和故障排除变得非常容易 - 都不影响整个系统。

互操作性和协作

钩子的使用促进了插件与开发人员之间的合作之间的互操作性。

  • 互操作性 作为一个清晰的接口,钩子有助于插件之间的通信。为了反映WordPress和插件中许多插件的适应性,您应该在插件的重要点上提供挂钩。当然,如果出于安全原因,这些事情不应该改变,则绝对可以将某些东西留在没有的情况下。
  • 通过共享标准更容易合作 钩子构成了插件开发的共同基础,插件开发是所有插件开发人员熟悉的。这使得赢得开发人员的协作开发变得更加容易。但是,当您定义足够的挂钩以使您的插件可以根据个人需求定制时,您的插件也将在开发人员社区中获得尊重。

挂钩开发原理

所有这些听起来很棒,但是在实践中应该是什么样的?我会在这里概述。

动作和过滤作为插件开发中的核心概念

我们将动作和过滤器定义为插件开发中的中心概念。您可能熟悉MVC模式(Model, View, Controller)。在某种程度上,可以说,在挂钩驱动的开发中,钩子(或方法作为钩子的回调的类)从MVC方法中扮演了controllers的角色。在MVC模式中,控制器是model(即商业逻辑)和view的中央控制单元。

在我们的钩驱动方法,钩子或包含钩子的类中,形成中央控制单元。

围绕挂钩设计插件架构

有效的挂钩驱动策略要求从头开始设计插件架构以集成钩子。这意味着以将其分解为较小的可重复使用的模块的方式构造代码,然后通过钩子通信。

这将插件的有效结构促进了可重复使用的模块,即功能和职责的明确分离。结果,将来可以轻松快速扩展该插件,并且可以易于维护和测试。

定义和使用自己的钩子

如果您的插件已经使用了WordPress和/或其他插件(应该)的挂钩,则它可能仍然不会自动依赖于挂钩驱动的开发。只有在足够广泛的插件中定义和使用自己的挂钩才是良好成就且成功的挂钩驱动开发的基础。

通过使用自己的钩子,您可以向外界打开插件,并允许其他开发人员在插件上构建或修改各个方面,同时确保您的代码结构良好。

挂钩开发的最佳实践

对于您的挂钩驱动方法,要真正成功并一方面促进插件的结构,并有意义地将插件打开外部,您应该考虑一些最佳实践。只有这样才能有效且可持续的实施。

命名约定

为钩子选择独特而有意义的名称对于代码的可读性和可理解性至关重要。以下是一些有效命名约定的建议:

  • 使用前缀 因此,插件的钩子可以直接识别,因此它们应始终如一地使用自己的前缀。这也使得通过WordPress或其他插件定义相同的挂钩名称的可能性较小。如果前缀足够短,则可以是插件的名称,也可以使用插件名称的缩写。例如,hooks of WooCommerce都以woocommerce_开头。
  • 使用描述性名称 为您的钩子选择描述性名称。您的钩子的用途应该尽可能清楚。应该很明显的是,像myplugin_hook_1这样的名称几乎没有用,而myplugin_menu_items建议它可能是菜单条目的过滤器。
  • 一致的符号 保持钩子名称的一致符号。您如何写钩子不是很相关 - 重要的是不是混合不同的符号。 WordPress以及许多插件使用下划线(myplugin_menu_items),而其他插件也使用 Camelcase mypluginMenuItems)。对于具有许多挂钩的特别广泛的插件,使用斜线(myplugin/navigation/menuItems)可能会导致更加清晰,并且在插件世界中有一些例子,例如著名的插件Elementor

文档

不仅需要其他插件的开发人员,还需要有关插件挂钩的良好文档。您自己(尤其是您的团队,如果有的话)也需要此文档。一段时间后,您将很高兴能够查找某个钩子的工作以及使用的位置。

有两种方法:

  • 内联评论 对于您和其他开发人员而言,当有挂钩名称和基础代码不直接明显的功能和依赖项时,依靠内联评论尤为重要。 PHP评论的一般规则在这里适用:使用尽可能少的内联评论,但尽可能多地使用。
  • 外部文档 特别是对于广泛的插件,创建外部文档可能是有利的。您可以在网站或插件网站上创建此此此功能,也可以使用已为此目的设计的服务,例如GitBookRead the Docs。还有一个有趣的项目自动记录您的钩子:Pronamic WordPress Documentor。它会根据您的挂钩声明自动以各种格式生成文档。

测试钩子

由于挂钩通常是与WordPress其他插件或部分部分的接口,因此必须确保其稳定性和可靠性。例如,这需要对PHP单元进行仔细的测试,我们将在以后的文章中介绍。重要的是要考虑以下几点:

  • 测试覆盖范围 确保所有钩子或相应的回调都被测试涵盖。
  • 对外部插件的测试依赖性 在插件开发过程中,应尽可能避免对其他插件的依赖。但是,如果您的插件仍然需要另一个插件,则必须清楚地传达依赖项,并且您的插件应检查在激活过程中使用适当的方法在激活过程中是否已安装和激活所需的插件。必须测试所有依赖项,并且当更新外部插件时,必须确保通信仍然可以顺利进行。
  • 测试与其他插件的不兼容 在WordPress安装中测试您的插件,该插件已安装和激活流行插件。您的钩子问题是否出现在这里,例如名称碰撞?所有这些都必须进行测试以确保运行平稳。

重命名挂钩时向后兼容

您可能需要或想重命名钩子。也许是因为您只是阅读了这篇文章,并且一直在考虑您的命名惯例。

只要您的插件仍在开发中,您只需要确保相应地更改挂钩的所有出现。

但是,如果您的插件已经在网站上使用,情况会有所不同。在这种情况下,其他插件(或主题)可能正在使用钩子。如果您简单地将挂钩重命名,并且在网站上更新了插件的新版本,那么其他插件使用的钩子将不再可用。这可能会影响使用您插件的用户网站。

要避免这种冲突,您需要以适当的方式响应钩子重命名。

让我们想象,您注意到从开发的早期阶段,您的插件中的挂钩仍然使用了带有不太好的名称myplugin_hook_01的钩子。在创建外部文档时,您会被它烦恼,并决定挂钩应该具有新名称myplugin_menu_items

如果其他开发人员已经在其插件中使用myplugin_hook_01,则有一种完美的方法来实现向后兼容性(并且已经对名称感到沮丧)!

在此示例中,让我们假设带有旧挂钩名称的调用看起来像这样:

$menuItems = apply_filters('myplugin_hook_01', $menuItems, $menuId, $menuObject);

在第一步,您重命名钩子:

$menuItems = apply_filters('myplugin_menu_items', $menuItems, $menuId, $menuObject);

在第二步中,您可以在插件中创建一个名为 deprecatecatedHooks.php 的文件。在此文件中,您使用previous article
的php属性方法注册 new(!)挂钩的回调

<?php

namespace MK\MyPlugin\Main;

use MK\Attributes\Filter;

class DeprecatedHooks
{
    #[Filter('myplugin_menu_items', 0, 3)]
    public function menuItems(array $menuItems, int $menuId, object $menuObject): array
    {
    }
}

现在我们可以简单地解开旧钩,我们完成了:

#[Filter('myplugin_menu_items', 0, 3)]
public function menuItems(array $menuItems, int $menuId, object $menuObject): array
{
    return apply_filters('myplugin_hook_01', $menuItems, $menuId, $menuObject);
}

但是,这会默默地启动旧钩子,而其他开发人员只会意识到他们阅读您的变更词的重命名。

这就是为什么我们拥有函数apply_filters_deprecated()do_action_deprecated

在我们的示例中,我们专注于过滤器,但同样适用于动作。让我们看一下apply_filters_deprecated()

apply_filters_deprecated(
    string $hook_name, 
    array $args, 
    string $version, 
    string $replacement = '', 
    string $message = '' 
)

太好了!我们可以将旧的挂钩名称传递到函数,$args阵列中的所有参数,在$version中弃用钩子的版本以及$replacement中的新挂钩名称。此外,我们甚至可以提供有关Kude15重命名的消息。

在我们的情况下,看起来像这样:

#[Filter('myplugin_menu_items', 0, 3)]
public function menuItems(array $menuItems, int $menuId, object $menuObject): array
{
    return apply_filters_deprecated(
        'myplugin_hook_01', // Old hook name
        [$menuItems, $menuId, $menuObject], // Arguments as an array
        '1.1.0', // Version of our plugin where the renaming took place
        'myplugin_menu_items', // New hook name
        'Changed the hook name.' // Message
    );
}

现在,旧的钩子仍然可以正常工作,好像什么都没有改变。但是,如果 wp-config.php中的WP_DEBUG常数文件设置为true

Deprecated: Hook myplugin_hook_01 is deprecated since version 1.1.0! Use myplugin_menu_items instead. Changed the hook name.

相同的概念适用于使用do_action_deprecated()函数的动作。

通过遵循这种方法,我们实现了重命名钩子时重要的一切:

  • 钩子已更名。
  • 旧的钩子继续为向后兼容起作用。
  • 使用旧挂钩时,将警告其他开发人员或我们的开发团队。

成功的挂钩驱动插件的示例

已经有许多出色的插件利用了挂钩驱动的开发。插件完全基于挂钩的程度可能会有所不同。以下插件可以用作很好的例子,尽管它们在建筑决策的各个方面可能不是合适的例子。

WooCommerce

WooCommerce是由WordPress的创建者Matt Mullenweg创立的公司Automattic开发的WordPress的电子商务插件。 WooCommerce:

  • 提供了各种各样的钩子,可以广泛扩展和自定义WooCommerce。
  • 利用插件内定义的钩子广泛用于其自身功能。
  • 是通过独立的附加插件扩展的一个很好的例子,WooCommerce将其作为模块化付费插件分发。

高级自定义字段

著名的插件Advanced Custom Fields通常受到开发人员的喜爱,尤其是在Gutenberg时代,是挂钩驱动开发的另一个很好的例子。最初由Elliot Condon开发,由Delicious Brains收购,现在由WP Engine驱动。

即使在今天,该插件仍然保持强大和必不可少,可以作为挂钩驱动开发的一个很好的例子。 ACF提供了许多用于开发大型扩展的钩子。

这些只是成功挂钩驱动插件的几个示例。尽管实施水平可能有所不同,但它们证明了挂钩驱动开发的好处和可能性。

代码示例

虽然我们无法使用本文中的挂钩驱动的开发来开发完整的插件,但我可以提供一个小例子来说明概念。请注意,由于空间限制,可以简化示例。

让我们假设您已经开发了一个插件,该插件需要将菜单项添加到带有slug main-menu的特定菜单中。您已经在使用Hook "wp_nav_menu_{$menu->slug}_items"来实现此目标(documentation):

class FrontendMenu
{
    #[Filter('wp_nav_menu_main-menu_items', 10, 2)]
    public function addMenuItems(string $items, object $args): string
    {
        $newItems = [
            'dashboard' => [
                'url' => esc_url(get_permalink(get_option('myplugin_dashboard_page_id'))),
                'title' => 'Dashboard',
            ],
            'faq' => [
                'url' => esc_url(get_permalink(get_option('myplugin_faq_page_id'))),
                'title' => 'FAQ',
            ],
        ];

        $newItems = array_map(function($menuItem) {
            return sprintf(
                '<li><a href="%s">%s</a></li>',
                $menuItem['url'],
                $menuItem['title']
            );
        }, $newItems);

        $items .= implode("\n", $newItems);

        return $items;
    }
}

在此代码中,我们将addMenuItems()方法注册为wp_nav_menu_main-menu_items钩的回调。在方法内部,我们定义了一个包含新菜单项的数组$newItems。我们使用array_map()将菜单项数组转换为HTML列表项字符串。最后,我们将新项目加入到$items字符串并返回。

现在,让我们迈向挂钩开发的一步,并将新菜单项暴露于外部,以便我们和其他开发人员可以操纵这些项目。我们介绍了一个名为myplugin_menu_items的新过滤器:

#[Filter('wp_nav_menu_main-menu_items', 10, 2)]
public function addMenuItems(string $items, object $args): string
{
    $newItems = apply_filters('myplugin_menu_items', [], 'main-menu', $args);

    $newItems = array_map(function($menuItem) {
        return sprintf(
            '<li><a href="%s">%s</a></li>',
            $menuItem['url'],
            $menuItem['title'],
        );
    }, $newItems);

    $items .= implode("\n", $newItems);

    return $items;
}

我们将空数组作为默认值传递给我们的新过滤器,以及菜单slug和原始过滤器的$args对象,以防我们或其他开发人员需要它。在原始类中,我们可以填充菜单:

class FrontendMenu
{
    #[Filter('wp_nav_menu_main-menu_items', 10, 2)]
    public function addMenuItems(string $items, object $args): string
    {
        $newItems = apply_filters('myplugin_menu_items', [], 'main-menu', $args);

        $newItems = array_map(function($menuItem) {
            return sprintf(
                '<li><a href="%s">%s</a></li>',
                $menuItem['url'],
                $menuItem['title'],
            );
        }, $newItems);

        $items .= implode("\n", $newItems);

        return $items;
    }

    #[Filter('myplugin_menu_items', 10, 3)]
    public function generateMenuItems(array $items, string $menuSlug, array $args): array
    {
        if ('main-menu' !== $menuSlug) {
            return $items;
        }

        $newItems = [
            'dashboard' => [
                'url' => esc_url(get_permalink(get_option('myplugin_dashoboard_page_id'))),
                'title' => 'Dashboard',
            ],
            'faq' => [
                'url' => esc_url(get_permalink(get_option('myplugin_faw_page_id'))),
                'title' => 'FAQ',
            ],
        ];

        return array_merge($items, $newItems);
    }
}

因此,首先,我们使用带有空数组的apply_filters应用过滤器以在generateMenuItems()方法中填充它。

我们用这种方法取得了什么成就?

  • 灵活性:
    • 现在,我们可以使用myplugin_menu_items钩中的任何位置添加新菜单项。例如,我们可以根据是否登录用户有条件地显示登录或注销链接。
    • 通过将wp_nav_menu_main-menu_items钩中的菜单项添加到添加菜单项,我们可以使用使用9优先级的myplugin_menu_items挂钩在仪表板链接之前插入新菜单项。
  • 可扩展性:
    • 我们允许其他插件修改新菜单项列表。其他插件可以将菜单项添加或删除到我们插件的菜单中。
    • 我们自己的代码或附加插件也可以通过使用myplugin_menu_items挂钩来影响菜单。
  • 可检验性:
    • 包含新菜单项的数组现在可以是测试的主题。

这只是一个小的,也许有些人为的例子,但它证明了钩驱动的开发的方法和好处。在其余文章中,该概念将变得更加清晰。

结论和前景

在本文中,我们了解了什么是挂钩驱动的开发,它给WordPress插件开发带来的好处以及所涉及的最佳实践。我们还看到了一个小型代码示例,该示例证明了如何在现实世界中实现这种方法。

在下一篇文章中,我们将了解有关在WordPress插件中使用依赖项注入的信息。我们将探讨为什么可能需要依赖注入,尤其是在通过PHP属性使用钩子注册时。