Часть 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( /* ... */ ); }

Хорошо, это кажется простым в использовании, и мы уже можем сказать, что нам, вероятно, потребуется создать три новых класса: Section , Field и Element .

 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, живущие в классе, и они могут иметь любой тип данных ( string , integer , 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 ); } }

Кажется, что для идентификации раздела требуется слаг-имя (используется в атрибуте 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 , который будет использовать функцию WordPress add_settings_field() .

 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 ); } }

Здесь мы также хотели бы предоставить значения по умолчанию для идентификатора, метки и описания поля. Мы можем сделать это, передав массив параметров конструктору и используя функцию WordPress wp_parse_args() для анализа этих параметров.

 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 } }

Убедитесь, что вы экранируете свой вывод — как мы делаем здесь, используя функции WordPress esc_attr() и esc_html() — чтобы предотвратить любые атаки межсайтового скриптинга. Несмотря на то, что мы визуализируем наши элементы только на страницах администрирования, все равно рекомендуется всегда экранировать любые выходные данные.

ПРИМЕЧАНИЕ. Межсайтовые сценарии (или XSS) — это тип уязвимости системы безопасности, обычно встречающийся в веб-приложениях. 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 } }

Точно так же мы можем создать класс Radio_Element Checkbox_Element даже класс Custom_Element для остальных наших элементов.

Обратите внимание, что мы строим наши классы таким образом, чтобы их можно было использовать одинаково . Вызов метода render() для любого дочернего элемента Element выведет некоторый HTML.

Это пример полиморфизма , одной из основных концепций объектно-ориентированного программирования.

Полиморфизм

«Полиморфизм» буквально означает «много форм» (от греческих слов «поли» — «много» и «морфе» — «форма»). Дочерний класс Element может иметь множество форм , поскольку он может принимать любую форму класса в своей родительской иерархии.

Мы можем использовать Number_Element , Checkbox_Element или любой другой подтип в любом месте, где ожидается объект Element , поскольку все дочерние объекты могут использоваться точно таким же образом (т.е. вызывать их метод render() ), при этом сохраняя возможность вести себя по-разному (вывод будет отличаться для каждого типа элемента).

Как вы, вероятно, заметили, полиморфизм и наследование — тесно связанные понятия.

взаимозаменяемость

Принцип замещения Лисков (или LSP) , буква «L» в слове SOLID, гласит:

«В компьютерной программе, если 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_Page , Section , Element и т. д. Эти базовые классы не содержат никакого кода, который необходимо изменить для добавления новой страницы, раздела или элемента.

Принцип открытости/закрытости (или OCP), буква «O» в слове SOLID, гласит:

«Программные сущности (классы, модули, функции и т. д.) должны быть открыты для расширения, но закрыты для модификации».

Это означает, что мы должны иметь возможность расширять такой класс, как Admin_Page , и использовать его повторно, но для этого нам не нужно его модифицировать .

Вывод

В этой статье мы прописали наши секции, поля и элементы. При их реализации мы более подробно рассмотрели, что такое полиморфизм и почему он полезен. Мы также рассмотрели пару принципов SOLID, «принцип подстановки Лисков» и «принцип открытости/закрытости».

Оставайтесь с нами в следующей части этого путешествия, где мы более подробно рассмотрим, как мы можем улучшить способ управления нашими хуками WordPress.

Нажмите здесь, чтобы прочитать часть 7 в нашей серии объектно-ориентированного программирования

Смотрите также

  • WordPress и объектно-ориентированное программирование — обзор
  • Часть 2 — WordPress и объектно-ориентированное программирование: пример из реальной жизни
  • Часть 3 — WordPress и объектно-ориентированное программирование: пример WordPress — определение области
  • Часть 4 — WordPress и объектно-ориентированное программирование: пример WordPress — дизайн
  • Часть 5 — WordPress и объектно-ориентированное программирование: пример WordPress — реализация: меню администрирования