第 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