第 8 部分 – WordPress 和面向对象编程:WordPress 示例 – 实现:选项

已发表: 2022-02-04

到目前为止,我们只需要存储用户定义的选项,因此我们使用了 Settings API。 但是,我们的插件必须能够自行读取/写入选项,以“记住”IP 地址尝试登录失败的次数、当前是否被锁定等。

我们需要一种面向对象的方式来存储和检索选项。 在“设计”阶段,我们简要讨论了这一点,但抽象出一些实现细节,只关注我们希望能够执行的操作——获取设置删除选项。

我们还将根据它们的部分将选项“分组”在一起,以使它们井井有条。 这纯粹是基于个人喜好。

让我们把它变成一个界面:

 interface Options { /** * Return the option value based on the given option name. * * @param string $name Option name. * @return mixed */ public function get( $name ); /** * Store the given value to an option with the given name. * * @param string $name Option name. * @param mixed $value Option value. * @param string $section_id Section ID. * @return bool Whether the option was added. */ public function set( $name, $value, $section_id ); /** * Remove the option with the given name. * * @param string $name Option name. * @param string $section_id Section ID. */ public function remove( $name, $section_id ); }

理想情况下,我们可以通过以下方式与 WordPress Options API 进行交互:

 $options = new WP_Options(); $options->get( 'retries' );

此时,您可能想知道为什么我们不只使用get_option() WordPress 函数,而是陷入创建自己的接口和类的麻烦。 虽然直接使用 WordPress 功能将是开发我们的插件的一种完全可接受的方式,但通过更进一步并创建一个依赖的接口,我们可以保持灵活性。

我们的WP_Options类将实现我们的Options接口。 这样,如果我们的需求在未来发生变化,我们就会做好准备。 例如,我们可能需要将我们的选项存储在自定义表中、外部数据库中、内存中(例如 Redis)中,您可以命名它。 通过依赖一个抽象(即接口),在实现中改变一些东西,就像创建一个实现相同接口的新类一样简单。

WP_Options

让我们开始编写我们的WP_Options类,通过在其构造函数中使用get_option() WordPress 函数检索所有选项。

 class WP_Options { /** * @var array Stored options. */ private $options; /** * WP_Options constructor. */ public function __construct() { $this->options = get_option( Plugin::PREFIX ); } }

由于$options属性将在内部使用,我们将其声明为private ,因此它只能由定义它的类WP_Options类访问。

现在,让我们使用implements操作符来实现我们的Options接口。

 class WP_Options implements Options { // ...

我们的 IDE 要求我们要么声明我们的抽象类,要么实现接口中定义的get()set()remove()方法。

那么,让我们开始实现这些方法吧!

获得选择权

我们将从get()方法开始,该方法将在$options属性中查找指定的选项名称,如果不存在则返回其值或false

 class WP_Options implements Options { private $options; public function __construct() { $this->options = get_option( Plugin::PREFIX ); } /** * Return the option value based on the given option name. * * @return mixed */ public function get( $option_name ) { if ( ! isset( $this->options[ $option_name ] ) ) { return false; } return $this->options[ $option_name ]; } }

现在是考虑默认选项的好时机。

默认选项

如前所述,我们希望根据它们的部分将选项组合在一起。 因此,我们可能会将选项分成几个部分。 “常规选项”部分和另一个用于我们需要跟踪的数据。 锁定、重试、锁定日志和锁定总数——我们将任意称为这种状态。

我们将使用一个常量来存储我们的默认选项。 当我们的代码正在执行时,常量的值不能改变,这使得它非常适合我们的默认选项之类的东西。 类常量为每个类分配一次,而不是为每个类实例分配一次。

注意:常量的名称按照约定全部大写。

 const DEFAULT_OPTIONS = array( 'general_options' => array( 'allowed_retries' => 4, 'normal_lockout_time' => 1200, // 20 minutes 'max_lockouts' => 4, 'long_lockout_time' => 86400, // 24 hours 'hours_until_retries_reset' => 43200, // 12 hours 'site_connection' => 'direct', 'handle_cookie_login' => 'yes', 'notify_on_lockout_log_ip' => true, 'notify_on_lockout_email_to_admin' => false, 'notify_after_lockouts' => 4 ), 'state' => array( 'lockouts' => array(), 'retries' => array(), 'lockout_logs' => array(), 'total_lockouts' => 0 ) );

DEFAULT_OPTIONS嵌套数组中,我们为所有选项设置了默认值。

我们接下来要做的是,在插件初始化后,使用add_option() WordPress 函数将默认选项值存储在数据库中。

 class WP_Options { public function __construct() { $all_options = array(); foreach ( self::DEFAULT_OPTIONS as $section_id => $section_default_options ) { $db_option_name = Plugin::PREFIX . '_' . $section_id; $section_options = get_option( $db_option_name ); if ( $section_options === false ) { add_option( $db_option_name, $section_default_options ); $section_options = $section_default_options; } $all_options = array_merge( $all_options, $section_options ); } $this->options = $all_options; } }

让我们仔细看看这个片段。 首先,我们迭代默认选项数组并使用get_option() WordPress 函数检索选项。

 foreach ( self::default_options as $section_id => $section_default_options ) { $db_option_name = Plugin::PREFIX . '_' . $section_id; $section_options = get_option( $db_option_name ); // ...

然后,我们检查每个选项是否已经存在于数据库中,如果不存在,我们存储其默认选项。

 if ( $section_options === false ) { add_option( $db_option_name, $section_default_options ); $section_options = $section_default_options; }

最后,我们收集所有部分的选项。

 $all_options = array_merge( $all_options, $section_options );

并将它们存储在$options属性中,以便我们以后可以访问它们。

 $this->options = $all_options;

数据库中的 WordPress 选项表将有几行,其中option_name由连接到部分名称的插件前缀组成。

现在让我们继续讨论我们需要实现的其余方法。

存储选项

同样,我们想轻松地在数据库中存储一个新选项,并覆盖任何以前的值,如下所示:

 $options = new Options(); $options->set( 'retries', 4 );

因此,让我们实现set()方法,该方法将使用update_option() WordPress 函数。

 /** * Store the given value to an option with the given name. * * @param string $name Option name. * @param mixed $value Option value. * @param string $section_id Section id. Defaults to 'state'. * @return bool Whether the option was added. */ public function set( $name, $value, $section_ ) { $db_option_name = Plugin::PREFIX . '_' . $section_id; $stored_option = get_option( $db_option_name ); $stored_option[ $name ] = $value; return update_option( $db_option_name, $stored_option ); }

删除选项

最后,我们将实现remove()方法,它将选项设置为其初始值:

 /** * Remove the option with the given name. * * @param string $name Option name. * @param string $section_id Section id. Defaults to 'state'. * @return bool Whether the option was removed. */ public function remove( $name, $section_ ) { $initial_value = array(); if ( isset( self::DEFAULT_OPTIONS[ $section_id ][ $name ] ) ) { $initial_value = self::DEFAULT_OPTIONS[ $section_id ][ $name ]; } return $this->set( $name, $initial_value, $section_id ); }

我们将所有内容捆绑在一个类中。 所有与选项相关的数据(即我们的属性)和实现细节(即我们刚刚实现的方法)都封装WP_Options类中。

封装/抽象

将所有内容包装在一个类中,将内部封装起来(就像在一个胶囊中一样),本质上是“隐藏”它们对外部世界,这就是我们所说的封装。 封装是面向对象编程的另一个核心概念。

使用 Pressidium 托管您的网站

60 天退款保证

查看我们的计划

使用Options界面,我们专注于我们如何处理我们的选项而不是我们如何做,抽象选项的概念,从概念上简化事情。 这就是我们所说的抽象,是面向对象编程的另一个核心概念。

封装和抽象是完全不同的概念,但很明显,正如您所看到的,它们是高度相关的。 它们的主要区别在于封装存在于实现层面,而抽象存在于设计层面。

依赖项

让我们考虑以下场景:

有一个Lockouts类,负责确定 IP 地址是否应该被锁定、锁定的持续时间、活动锁定是否仍然有效或已过期等。该类包含一个should_get_locked_out()方法,负责确定IP 地址是否应该被锁定。 该方法需要在 IP 地址被锁定之前读取允许的最大重试次数,这是一个可配置的值,这意味着它被存储为option

因此,我们刚刚描述的代码看起来类似于:

 class Lockouts { // ... /** * @var WP_Options An instance of `WP_Options`. */ private $options; /** * Lockouts constructor */ public function __construct() { $this->options = new WP_Options(); } /** * Return the number of retries. * * @return int */ private function get_number_of_retries() { // ... } /** * Check whether this IP address should get locked out. * * @return bool */ public function should_get_locked_out() { $retries = $this->get_number_of_retries(); $allowed_retries = $this->options->get( 'allowed_retries' ); return $retries % $allowed_retries === 0; } // ... }

基本上,我们在构造函数中创建一个新的WP_Options实例,然后使用该实例检索allowed_retries选项的值。

这绝对没问题,但我们必须记住,我们的Lockouts类现在依赖于WP_Options 。 我们称 WP_Options 为依赖项。

如果将来我们的需求发生变化,例如,我们需要在外部数据库上读取/写入选项,我们需要将WP_Options替换为DB_Options类。 如果我们只需要检索一个类中的选项,那似乎还不错。 但是,当有许多具有多个依赖项的类时,它可能会变得有点棘手。 对单个依赖项的任何更改都可能会波及整个代码库,如果其中一个依赖项发生更改,则迫使我们修改一个类。

我们可以通过重写代码以遵循依赖倒置原则来消除这个问题。

解耦

依赖倒置原则 (DIP),即 SOLID 中的“D”,指出:

  • 高级模块不应该从低级模块导入任何东西。 两者都应该依赖于抽象。
  • 抽象不应该依赖于细节。 细节(具体实现)应该依赖于抽象。

在我们的例子中, Lockouts类是“高级模块”,它依赖于“低级模块”,即WP_Options类。

我们将使用Dependency Injection来改变这一点,这比听起来容易。 我们的Lockouts类将接收它所依赖的对象,而不是创建它们。

 class Lockouts { // ... /** * Lockouts constructor. * * @param WP_Options $options */ public function __construct( WP_Options $options ) { $this->options = $options; } // ... }

所以,我们注入一个依赖:

 $options = new WP_Options(); $lockouts = new Lockouts( $options );

我们只是让我们的Lockouts类更易于维护,因为它现在与它的WP_Options依赖项松散耦合。 此外,我们将能够模拟依赖项,使我们的代码更容易测试。 将WP_Options替换为模仿其行为的对象将允许我们测试我们的代码,而无需在数据库上实际执行任何查询。

 /** * Lockouts constructor. * * @param WP_Options $options */ public function __construct( WP_Options $options ) { $this->options = $options; }

即使我们已经将Lockouts的依赖控制权交给了另一个类(与Lockouts控制依赖关系本身相反), Lockouts仍然需要一个WP_Options对象。 意思是,它仍然依赖于具体的WP_Options类,而不是抽象。 如前所述,两个模块都应该依赖于抽象

让我们解决这个问题!

 /** * Lockouts constructor. * * @param Options $options */ public function __construct( Options $options ) { $this->options = $options; }

通过简单地将$options参数的类型从WP_Options类更改为Options接口,我们的Lockouts类依赖于抽象,我们可以自由传递DB_Options对象或任何实现相同接口的类的实例,到它的构造函数。

单一职责

值得注意的是,我们使用了一个名为should_get_locked_out()的方法来检查 IP 地址是否应该被锁定。

 /** * Check whether this IP address should get locked out. * * @return bool */ public function should_get_locked_out() { $retries = $this->get_number_of_retries(); $allowed_retries = $this->options->get( 'allowed_retries' ); return $retries % $allowed_retries === 0; }

我们可以很容易地写一个这样的单行:

 if ( $this->get_number_of_retries() % $this->options->get( 'allowed_retries' ) === 0 ) {

但是,将那段逻辑移动到它自己的小方法中,有很多好处。

  • 如果确定 IP 地址是否应该被锁定的条件发生变化,我们只需修改此方法(而不是搜索所有出现的 if 语句)
  • 当每个“单元”更小时,编写单元测试会变得更容易
  • 大大提高了我们代码可读性

读这个:

 if ( $this->should_get_locked_out() ) { // ...

在我们看来,这比阅读要容易得多:

 if ( $this->get_number_of_retries() % $this->options->get( 'allowed_retries' ) === 0 ) { // ...

我们已经为插件的几乎所有方法都做到了这一点。 从较长的方法中提取方法,直到没有其他方法可以提取。 类也是如此,每个类和方法都应该有一个单一的职责。

单一职责原则 (SRP) ,即 SOLID 中的“S”,规定:

“计算机程序中的每个模块、类或函数都应负责该程序功能的单个部分,并且应该封装该部分。”

或者,正如 Robert C. Martin(“鲍勃叔叔”)所说:

“一个班级应该有一个,而且只有一个改变的理由。”

重新访问主插件文件

目前,我们的主要插件文件仅包含以下内容:

 /** * Plugin Name: PRSDM Limit Login Attempts * Plugin URI: https://pressidium.com * Description: Limit rate of login attempts, including by way of cookies, for each IP. * Author: Pressidium * Author URI: https://pressidium.com * Text Domain: prsdm-limit-login-attempts * License: GPL-2.0+ * Version: 1.0.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; }

再一次,我们将所有内容包装在一个插件类中,这一次只是为了避免命名冲突。

 namespace Pressidium\Limit_Login_Attempts; if ( ! defined( 'ABSPATH' ) ) { exit; } class Plugin { /** * Plugin constructor. */ public function __construct() { // ... } }

我们将在文件末尾实例化这个Plugin类,它将执行其构造函数中的代码。

 new Plugin();

在构造函数中,我们将挂钩 plugins_loaded 动作,一旦激活的插件加载完毕,该动作就会触发。

 public function __construct() { add_action( 'plugins_loaded', array( $this, 'init' ) ); } public function init() { // Initialization }

我们还将调用require_files()方法来加载我们所有的 PHP 文件。

 public function __construct() { $this->require_files(); add_action( 'plugins_loaded', array( $this, 'init' ) ); } private function require_files() { require_once __DIR__ . '/includes/Sections/Section.php'; require_once __DIR__ . '/includes/Pages/Admin_Page.php'; require_once __DIR__ . '/includes/Pages/Settings_Page.php'; // ... }

最后,我们将通过在我们的init()方法中创建一些对象来初始化我们的插件。

注意:以下片段仅包含主插件文件的一小部分。 您可以在插件的 GitHub 存储库中阅读实际文件。

 public function init() { $options = new Options(); $hooks_manager = new Hooks_Manager(); $settings_page = new Settings_Page( $options ); $hooks_manager->register( $settings_page ); // ... }

组织文件

保持文件井井有条至关重要,尤其是在处理包含大量代码的大型插件时。 您的文件夹结构应该将相似的文件组合在一起,帮助您和您的队友保持井井有条。

我们已经定义了一个命名空间( Pressidium\Limit_Login_Attempts ),其中包含PagesSectionsFieldsElements等的几个子命名空间。按照这个层次结构来组织我们的目录和文件,我们最终得到了一个类似于这样的结构:

 . ├── includes │ ├── Hooks │ │ ├── Actions.php │ │ ├── Filters.php │ │ └── Hooks_Manager.php │ ├── Pages │ │ ├── Admin_Page.php │ │ └── Settings_Page.php │ ├── Sections │ │ ├── Fields │ │ │ ├── Elements │ │ │ │ ├── Checkbox_Element.php │ │ │ │ ├── Custom_Element.php │ │ │ │ ├── Element.php │ │ │ │ ├── Number_Element.php │ │ │ │ └── Radio_Element.php │ │ │ └── Field.php │ │ └── Section.php │ └── WP_Options.php ├── prsdm-limit-login-attempts.php └── uninstall.php

每个文件包含一个类。 文件以其包含的类命名,目录和子目录以(子)命名空间命名。

您可以使用多种架构模式和命名方案。 您可以选择一个对您有意义并适合您的项目需求的产品。 在构建项目时,重要的是要保持一致

结论

恭喜! 您已经完成了我们关于 WordPress 和面向对象编程的系列文章。

希望您学到了一些东西,并且很高兴开始将您学到的知识应用到自己的项目中!

以下是我们在本系列中介绍的内容的快速回顾:

  • 需求收集:我们决定插件应该做什么。
  • 设计:我们考虑了插件的结构、潜在类之间的关系以及抽象的高级概述。
  • 实现:我们编写了插件一些关键部分的实际代码。 在此过程中,我们向您介绍了几个概念和原则。

然而,我们几乎没有触及 OOP 是什么以及必须提供什么的皮毛。 熟练掌握一项新技能需要练习,所以继续并开始构建您自己的面向对象的 WordPress 插件。 快乐编码!

也可以看看

  • WordPress 和面向对象的编程——概述
  • 第 2 部分 – WordPress 和面向对象编程:一个真实世界的示例
  • 第 3 部分 – WordPress 和面向对象编程:A WordPress 示例 – 定义范围
  • 第 4 部分 – WordPress 和面向对象编程:一个 WordPress 示例 – 设计
  • 第 5 部分 – WordPress 和面向对象编程:一个 WordPress 示例 – 实现:管理菜单
  • 第 6 部分 – WordPress 和面向对象编程:一个 WordPress 示例 – 实现:注册部分
  • 第 7 部分 – WordPress 和面向对象编程:一个 WordPress 示例 – 实施:管理 WordPress Hooks