第 6 部分 – WordPress 和麵向對象編程:一個 WordPress 示例 – 實現:註冊部分

已發表: 2022-02-04

歡迎回到我們的面向對象編程系列。

正如我們在本系列的設計部分中所解釋的,管理頁面由多個部分組成。 每個部分都包含一個或多個字段,並且每個字段都包含一個或多個元素

這在代碼中看起來如何?

 public function register_sections() { $my_section = $this->register_section( /* ... */ ); $my_field = $my_section->add_field( /* ... */ ); $my_element = $my_field->add_element( /* ... */ ); }

好吧,這看起來很容易使用,我們已經可以看出我們可能需要創建三個新類: SectionFieldElement

 class Section {}
 class Field {}
 class Element {}

讓我們花點時間問問自己,到目前為止我們對這些課程了解多少。

  • $my_section->add_field()Section類應該能夠添加(和存儲)一個新的Field對象
  • $my_field->add_element()Field類應該能夠添加(和存儲)一個新的Element對象。

我們首先將我們的 Field 對象存儲在一個數組中,就像我們通常做的那樣:

 class Section { /** * @var Field[] Section field objects. */ protected $fields = array();

這個$fields變量是一個類成員,它就是我們所說的屬性。 屬性是 PHP 變量,存在於一個類中,它們可以是任何數據類型( stringinteger 、對object等)。

我們還將編寫add_field()方法來創建和添加新字段。

 public function add_field() { $field = new Field( /* ... */ ); $this->fields[] = $field; return $field; }

此方法創建一個新的Field對象,將其添加到 fields 屬性並返回該新創建的對象。 很簡單。

讓我們對Field類也重複相同的過程。

 class Field { /** * @var Element[] Field elements. */ private $elements = array(); /** * Create a new element object. * * @return Element */ private function create_element() { return new Element( /* ... */ ); } /** * Add a new element object to this field. */ public function add_element() { $element = $this->create_element(); $this->elements[] = $element; } }

那是一個開始! 下一步是什麼?

部分類

創建新部分時,我們需要調用 add_settings_section()。 再一次,構造方法是執行初始化的好方法。 讓我們在類中添加它:

 class Section { // ... public function __construct() { add_settings_section( $this->id, $this->title, array( $this, 'print_description' ), $this->page ); } }

似乎 Section 需要一個 slug-name 來標識它(用於標籤的 id 屬性)。 它還可以有標題、描述和屬於特定頁面。

 class Section { /** * @var Field[] Section field objects. */ protected $fields = array(); /** * @var string Section title. */ public $title; /** * @var string Section id. */ public $id; /** * @var string Slug-name of the settings page this section belongs to. */ public $page; /** * @var string Section description. */ public $description;

我們可以通過執行以下操作來設置部分的標題:

 $section = new Section(); $section->title = __( 'Hello world', 'prsdm-limit-login-attempts' );

嗯,這不太對。 儘管上面的代碼是完全有效的,但它實際上並沒有做我們期望它做的事情。

構造函數方法在創建新的 Section 對象時執行。 所以add_settings_section()將在我們有機會設置標題之前被調用。 因此,該部分將沒有標題。

使用 Pressidium 託管您的網站

60 天退款保證

查看我們的計劃

標題需要在我們的對像初始化期間可用,因此我們需要在構造函數中執行此操作。

 class Section { /** * @var string Section title. */ private $title; public function __construct( $title ) { $this->title = $title; // ... } // ..

注意$this->title指的是 title 類屬性,其中$title指的是構造函數的參數。

在這裡,我們還利用了可見性。 由於我們的$title屬性只能被定義它的類訪問,我們可以將其聲明為private 。 因此,我們阻止它在類外被訪問。

哦,我們還必須添加一個print_description()方法,該方法將打印該部分的描述。

 /** * Print the section description. */ public function print_description() { echo esc_html( $this->description ); }

總而言之,我們的 Section 類看起來像這樣。

 class Section { /** * @var Field[] Section field objects. */ protected $fields = array(); /** * @var string Section title. */ private $title; /** * @var string Section id. */ private $id; /** * @var string Slug-name of the settings page this section belongs to. */ private $page; /** * @var string Section description. */ private $description; /** * Section constructor. * * @param string $id Section id. * @param string $title Section title. * @param string $page Slug-name of the settings page. * @param string $description Section description. */ public function __construct( $id, $title, $page, $description ) { $this->id = $id; $this->title = $title; $this->page = $page; $this->description = $description; add_settings_section( $this->id, $this->title, array( $this, 'print_description' ), $this->page ); } /** * Print the section description. */ public function print_description() { echo esc_html( $this->description ); } /** * Create and add a new field object to this section. */ public function add_field() { $field = new Field( /* ... */ ); $this->fields[] = $field; return $field; } }

字段類

以與Section類似的方式,我們現在可以繼續並構建Field類,它將利用add_settings_field() WordPress 函數。

 class Field { /** * @var Element[] Field elements. */ private $elements = array(); /** * @var string ID of the section this field belongs to. */ private $section_id; /** * @var string Field description. */ private $description; /** * Field constructor. * * @param string $id Field ID. * @param string $label Field label. * @param string $page Slug-name of the settings page. * @param string $section_id ID of the section this field belongs to. * @param string $description Field description. */ public function __construct( $id, $label, $page, $section_id, $description ) { $this->section_id = $section_id; $this->description = $description; add_settings_field( $id, $label, array( $this, 'render' ), $page, $this->section_id ); } }

在這裡,我們還想為字段的 ID、標籤和描述提供默認值。 我們可以通過將選項數組傳遞給構造函數並使用 wp_parse_args() WordPress 函數來解析這些選項來做到這一點。

 class Field { /** * @var int Number of fields instantiated. */ private static $number_of_fields = 0; // ... /** * Field constructor. * * @param string $section_id ID of the section this field belongs to. * @param string $page Slug-name of the settings page. * @param array $options Options. */ public function __construct( $section_id, $page, $options = array() ) { self::$number_of_fields++; $options = wp_parse_args( $options, array( 'label' => sprintf( __( 'Field #%s', 'prsdm-limit-login-attempts' ), self::$number_of_fields 'id' => 'field_' . self::$number_of_fields, 'description' => '' ) ); $this->section_id = $section_id; $this->description = $options['description']; add_settings_field( $options['id'], $options['label'], array( $this, 'render' ), $page, $this->section_id ); } }

wp_parse_args() 函數將允許我們將用戶定義的值( $options數組)與默認值合併。

 array( 'label' => sprintf( __( 'Field #%s', 'prsdm-limit-login-attempts' ), self::$number_of_fields 'id' => 'field_' . self::$number_of_fields, 'description' => '' )

我們還必須為每個字段設置唯一的標籤。 我們可以通過將標籤設置為前綴 ( 'field_' ) 後跟一個數字來處理這個問題,每次創建新的 Field 對象時該數字都會增加。 我們將把這個數字存儲在$number_of_fields靜態屬性中。

 /** * @var int Number of fields instantiated. */ private static $number_of_fields = 0;

可以直接訪問靜態屬性,而無需先創建類的實例。

 'id' => 'field_' . self::$number_of_fields

self關鍵字用於引用當前類,並且在範圍解析運算符:: :(通常稱為“雙冒號”)的幫助下,我們可以訪問我們的靜態屬性。

這樣,在構造函數中,我們總是訪問相同$number_of_fields屬性,每次創建對象時都會增加它的值,從而為每個字段附加一個唯一的標籤。

展望未來, render()方法在打印描述(如果存在)之後,會遍歷所有元素並渲染它們中的每一個。

 public function render() { if ( ! empty( $this->description ) ) { printf( '<p class="description">%s</p>', esc_html( $this->description ) ); } foreach ( $this->elements as $key => $element ) { $element->render(); } }

把它們放在一起……

 class Field { /** * @var int Number of fields instantiated. */ private static $number_of_fields = 0; /** * @var Element[] Field elements. */ private $elements = array(); /** * @var string ID of the section this field belongs to. */ private $section_id; /** * @var string Field description. */ private $description; /** * Field constructor. * * @param string $section_id ID of the section this field belongs to. * @param string $page Slug-name of the settings page. * @param array $options Options. */ public function __construct( $section_id, $page, $options = array() ) { self::$number_of_fields++; $options = wp_parse_args( $options, array( 'label' => sprintf( /* translators: %s is the unique s/n of the field. */ __( 'Field #%s', 'prsdm-limit-login-attempts' ), self::$number_of_fields 'id' => 'field_' . self::$number_of_fields, 'description' => '' ) ); $this->section_id = $section_id; $this->description = $options['description']; add_settings_field( $options['id'], $options['label'], array( $this, 'render' ), $page, $this->section_id ); } /** * Create a new element object. * * @return Element */ private function create_element() { return new Element( /* ... */ ); } /** * Add a new element object to this field. */ public function add_element() { $element = $this->create_element(); $this->elements[] = $element; } /** * Render the field. */ public function render() { if ( ! empty( $this->description ) ) { printf( '<p class="description">%s</p>', esc_html( $this->description ) ); } foreach ( $this->elements as $key => $element ) { $element->render(); } } }

元素類

展望未來,我們將以類似的方式構建Element類!

我們將開始編寫這樣的類:

 class Element { /** * @var int Number of elements instantiated. */ private static $number_of_elements = 0; /** * @var string Element label. */ private $label; /** * @var string Element name. */ private $name; /** * @var mixed Element value. */ private $value; /** * Element constructor. * * @param string $section_id Section ID. * @param array $options Options. */ public function __construct( $section_id, $options = array() ) { self::$number_of_elements++; $options = wp_parse_args( $options, array( 'label' => sprintf( /* translators: %s is the unique s/n of the element. */ __( 'Element #%s', 'prsdm-limit-login-attempts' ), self::$number_of_elements ), 'name' => 'element_' . self::$number_of_elements ) ); $this->label = $options['label']; $this->name = $options['name']; $this->value = ''; } /** * Render the element. */ public function render() { ?> <fieldset> <label> <input type="number" name="<?php echo esc_attr( $this->name ); ?>" value="<?php echo esc_attr( $this->value ); ?>" /> <?php echo esc_html(); ?> </label> </fieldset> <?php } }

確保您正在轉義您的輸出——就像我們在這裡所做的那樣,使用 esc_attr() 和 esc_html() WordPress 函數——以防止任何跨站點腳本攻擊。 即使我們只在管理頁面中呈現我們的元素,始終轉義任何輸出數據仍然是一個好主意。

注意:跨站點腳本(或 XSS)是一種通常在 Web 應用程序中發現的安全漏洞。 XSS 使攻擊者能夠將客戶端代碼注入其他用戶查看的網頁中。 攻擊者可能會利用跨站腳本漏洞繞過同源策略等訪問控制。

當我們收集插件的需求時,我們注意到有多種元素類型——複選框、單選按鈕、數字字段等。當我們提出我們的設計時,我們決定構建一個旨在擴展的Element類。 所以,我們知道我們最終會為每個元素類型創建一個子類。

輸出應根據元素類型而有所不同,因此我們將render()轉換為抽象方法。 當然,這意味著類本身也應該是抽象的。

 abstract class Element { /** * @var int Number of elements instantiated. */ private static $number_of_elements = 0; /** * @var string Element label. */ protected $label; /** * @var string Element name. */ protected $name; /** * @var mixed Element value. */ protected $value; /** * Element constructor. * * @param string $section_id Section ID. * @param array $options Options. */ public function __construct( $section_id, $options = array() ) { self::$number_of_elements++; $options = wp_parse_args( $options, array( 'label' => sprintf( /* translators: %s is the unique s/n of the element. */ __( 'Element #%s', 'prsdm-limit-login-attempts' ), self::$number_of_elements ), 'name' => 'element_' . self::$number_of_elements ) ); $this->label = $options['label']; $this->name = $options['name']; $this->value = ''; } /** * Render the element. */ abstract public function render(); }

例如,一個Number_Element類看起來像這樣:

 class Number_Element extends Element { /** * Render the element. */ public function render() { ?> <fieldset> <label> <input type="number" name="<?php echo esc_attr( $this->name ); ?>" value="<?php echo esc_attr( $this->value ); ?>" /> <?php echo esc_html(); ?> </label> </fieldset> <?php } }

同樣,我們可以為其餘元素構建一個Checkbox_Element 、一個Radio_Element甚至一個Custom_Element類。

請注意,我們正在構建我們的類,因此它們都可以以相同的方式使用。 在 Element 的任何子節點上調用render()方法都會輸出一些 HTML。

這是多態性的一個例子,它是面向對象編程的核心概念之一。

多態性

“多態性”的字面意思是“多種形式”(來自希臘語“poly”的意思是“許多”,“morphe”的意思是“形式”)。 Element 子類可以有多種形式,因為它可以採用其父層次結構中的任何形式的類。

我們可以在任何需要Element對象的地方使用Number_ElementCheckbox_Element或任何其他子類型,因為所有子對像都可以以完全相同的方式使用(即調用它們的render()方法),同時仍然能夠表現不同(每種元素類型的輸出會有所不同)。

您可能會說,多態性和繼承是密切相關的概念。

可替代性

Liskov 替換原則(或 LSP) ,即 SOLID 中的“L”,指出:

“在計算機程序中,如果 S 是 T 的子類型,則類型 T 的對象可以替換為類型 S 的對象(即,類型 T 的對象可以替換為子類型 S 的任何對象)而不改變任何程序的理想屬性。”

用外行的話來說,您應該能夠使用任何子類代替其父類,而不會出現任何意外行為。

工廠

讓我們回到我們的Field類,我們目前有一個create_element()方法創建一個新的Element

 /** * Create a new element object. * * @return Element */ private function create_element() { return new Element( /* ... */ ); } /** * Add a new element object to this field. */ public function add_element() { $element = $this->create_element(); $this->elements[] = $element; }

返回新對象的方法通常稱為簡單工廠(不要與“工廠方法”混淆,後者是一種設計模式)。

知道任何子類型都可以用來代替Element父類,我們將繼續修改這個工廠,以便它能夠創建任何子類的對象。

 /** * Create a new element object. * * @throws Exception If there are no classes for the given element type. * @throws Exception If the given element type is not an `Element`. * * @param string $element_type * @param array $options * * @return Element */ private function create_element( $element_type, $options ) { $element_type = __NAMESPACE__ . '\\Elements\\' . $element_type; if ( ! class_exists( $element_type ) ) { throw new Exception( 'No class exists for the specified type' ); } $element = new $element_type( $this->section_id, $options ); if ( ! ( $element instanceof Element ) ) { throw new Exception( 'The specified type is invalid' ); } return $element; } /** * Add a new element object to this field. * * @param string $element_type * @param array $options */ public function add_element( $element_type, $options ) { try { $element = $this->create_element( $element_type, $options ); $this->elements[] = $element; } catch ( Exception $e ) { // Handle the exception } }

我們首先在元素類型前加上當前名稱:

 $element_type = __NAMESPACE__ . '\\Elements\\' . $element_type;

__NAMESPACE__魔術常量包含當前命名空間名稱。

然後,我們確保有一個指定元素類型的類:

 if ( ! class_exists( $element_type ) ) { throw new Exception( 'No class exists for the specified type' ); }

接下來,我們創建一個新對象:

 $element = new $element_type( $this->section_id, $options );

最後,我們確保新創建的對象確實是 Element 的一個實例:

 if ( ! ( $element instanceof Element ) ) { return; }

擴展

值得指出的是,我們已經構建了可擴展的插件。 添加不同類型的頁面、部分、元素就像創建一個擴展Admin_PageSectionElement等的新類一樣簡單。這些基類不包含任何需要更改以添加新頁面、部分或元素的代碼。

開放/封閉原則(或 OCP),即 SOLID 中的“O”,指出:

“軟件實體(類、模塊、函數等)應該對擴展開放,對修改關閉。”

這意味著我們應該能夠擴展Admin_Page這樣的類並重用它,但我們不必修改它來做到這一點。

結論

在本文中,我們註冊了我們的部分、字段和元素。 在實現這些時,我們仔細研究了多態性是什麼以及它為什麼有用。 我們還瀏覽了一些 SOLID 原則,“Liskov 替換原則”和“開放/封閉原則”。

在此旅程的下一部分中,我們將繼續關注我們,在那裡我們將仔細研究如何改進我們管理 WordPress 鉤子的方式。

單擊此處閱讀面向對象編程系列的第 7 部分

也可以看看

  • WordPress 和麵向對象的編程——概述
  • 第 2 部分 – WordPress 和麵向對象編程:一個真實世界的示例
  • 第 3 部分 – WordPress 和麵向對象編程:A WordPress 示例 – 定義範圍
  • 第 4 部分 – WordPress 和麵向對象編程:一個 WordPress 示例 – 設計
  • 第 5 部分 – WordPress 和麵向對象編程:WordPress 示例 – 實施:管理菜單