Часть 8 — WordPress и объектно-ориентированное программирование: пример WordPress — реализация: параметры

Опубликовано: 2022-02-04

До сих пор нам нужно было хранить только пользовательские параметры, поэтому мы использовали 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 ); }

В идеале мы могли бы взаимодействовать с API параметров WordPress, выполнив что-то вроде этого:

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

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

Наш класс WP_Options будет реализовывать наш интерфейс Options . Таким образом, мы будем готовы, если наши потребности изменятся в будущем. Например, нам может понадобиться сохранить наши параметры в пользовательской таблице, во внешней базе данных, в памяти (например, Redis), как вы это называете. В зависимости от абстракции (т.е. интерфейса) изменить что-либо в реализации так же просто, как создать новый класс, реализующий тот же интерфейс.

WP_Options

Давайте начнем писать наш класс WP_Options , извлекая все параметры с помощью функции WordPress get_option() в его конструкторе.

 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 с помощью оператора tools.

 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 мы установили значения по умолчанию для всех наших параметров.

Далее мы хотели бы сохранить значения параметров по умолчанию в базе данных после инициализации плагина с помощью функции WordPress add_option() .

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

Давайте внимательно посмотрим на этот фрагмент. Во-первых, мы перебираем массив параметров по умолчанию и извлекаем параметры с помощью функции WordPress get_option() .

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

 /** * 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-адрес будет заблокирован, что является настраиваемым значением, то есть оно сохраняется как опция .

Итак, код, который мы только что описали, будет выглядеть примерно так:

 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), буква «D» в слове SOLID, гласит:

  • Модули высокого уровня не должны ничего импортировать из модулей низкого уровня. Оба должны зависеть от абстракций.
  • Абстракции не должны зависеть от деталей. Детали (конкретные реализации) должны зависеть от абстракций.

В нашем случае класс 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) , буква S в слове SOLID, гласит:

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

Или, как говорит Роберт С. Мартин («Дядя Боб»):

«У класса должна быть одна и только одна причина для изменения».

Пересмотр основного файла плагина

На данный момент наш основной файл плагина содержит только это:

 /** * 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; }

И снова мы обернем все в класс Plugin, на этот раз просто для того, чтобы избежать коллизий имен.

 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 ), содержащее несколько подпространств имен для Pages , Sections , Fields , Elements и т. д. Следуя этой иерархии для организации наших каталогов и файлов, мы получили структуру, подобную этой:

 . ├── 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 и объектно-ориентированном программировании.

Надеюсь, вы узнали кое-что и рады начать применять полученные знания в своих собственных проектах!

Вот краткий обзор того, что мы рассмотрели в этой серии:

  • Сбор требований: Мы определились с тем, что должен делать плагин.
  • Дизайн: мы подумали о том, как будет структурирован плагин, о взаимоотношениях между нашими потенциальными классами и о высокоуровневом обзоре наших абстракций.
  • Реализация: мы написали фактический код некоторых ключевых частей плагина. При этом мы познакомили вас с несколькими концепциями и принципами.

Тем не менее, мы едва коснулись того, что такое ООП и что может предложить. Чтобы овладеть новым навыком, нужна практика, так что начните создавать свои собственные объектно-ориентированные плагины WordPress. Удачного кодирования!

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

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