Teil 8 – WordPress und objektorientierte Programmierung: Ein WordPress-Beispiel – Implementierung: Optionen

Veröffentlicht: 2022-02-04

Bisher mussten wir nur benutzerdefinierte Optionen speichern, also haben wir die Einstellungs-API verwendet. Unser Plugin muss jedoch in der Lage sein, selbst Optionen zu lesen/schreiben, um sich zu „merken“, wie oft eine IP-Adresse versucht hat, sich erfolglos anzumelden, ob sie derzeit gesperrt ist usw.

Wir brauchen eine objektorientierte Methode zum Speichern und Abrufen von Optionen. Während der „Design“-Phase haben wir dies kurz besprochen, aber einige der Implementierungsdetails abstrahiert und uns ausschließlich auf die Aktionen konzentriert, die wir gerne ausführen würden – Abrufen , Setzen und Entfernen einer Option.

Wir werden Optionen auch basierend auf ihrem Abschnitt „gruppieren“, um sie organisiert zu halten. Das basiert rein auf persönlichen Vorlieben.

Lassen Sie uns dies in eine Schnittstelle umwandeln:

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

Idealerweise könnten wir mit der WordPress-Options-API interagieren, indem wir so etwas tun:

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

An dieser Stelle fragen Sie sich vielleicht, warum wir nicht einfach die get_option() WordPress-Funktion verwenden, anstatt uns die Mühe zu machen, unsere eigene Schnittstelle und Klasse zu erstellen. Während die direkte Verwendung von WordPress-Funktionen ein durchaus akzeptabler Weg zur Entwicklung unseres Plugins wäre, bleiben wir flexibel, indem wir einen Schritt weiter gehen und eine Schnittstelle schaffen, auf die wir uns verlassen können.

Unsere WP_Options -Klasse wird unsere Options implementieren. So sind wir gerüstet, wenn sich unsere Anforderungen in Zukunft ändern. Beispielsweise müssen wir unsere Optionen möglicherweise in einer benutzerdefinierten Tabelle, in einer externen Datenbank oder im Arbeitsspeicher (z. B. Redis) speichern. Durch die Abhängigkeit von einer Abstraktion (dh einer Schnittstelle) ist das Ändern von etwas in der Implementierung so einfach wie das Erstellen einer neuen Klasse, die dieselbe Schnittstelle implementiert.

WP_Optionen

Beginnen wir mit dem Schreiben unserer Klasse WP_Options , indem wir alle Optionen mit der WordPress-Funktion get_option() in ihrem Konstruktor abrufen.

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

Da die Eigenschaft $options intern verwendet wird, deklarieren wir sie als private , sodass nur die Klasse, die sie definiert hat, die Klasse WP_Options , darauf zugreifen kann.

Lassen Sie uns nun unsere Options implementieren, indem wir den implements -Operator verwenden.

 class WP_Options implements Options { // ...

Unsere IDE schreit uns an, entweder unsere Klasse als abstrakt zu deklarieren oder die in der Schnittstelle definierten Methoden get() , set() und remove() zu implementieren.

Beginnen wir also mit der Implementierung dieser Methoden!

Option bekommen

Wir beginnen mit der get() -Methode, die nach dem angegebenen Optionsnamen in unserer $options Eigenschaft sucht und entweder ihren Wert oder false , wenn er nicht existiert.

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

Jetzt ist es an der Zeit, über Standardoptionen nachzudenken.

Standardoptionen

Wie bereits erwähnt, möchten wir die Optionen basierend auf ihrem Abschnitt gruppieren. Also werden wir die Optionen wahrscheinlich in ein paar Abschnitte aufteilen. Der Abschnitt „Allgemeine Optionen“ und ein weiterer für die Daten, die wir im Auge behalten müssen. Sperrungen, Wiederholungen, Sperrprotokolle und Gesamtzahl der Sperrungen – wir nennen diesen Zustand willkürlich.

Wir verwenden eine Konstante , um unsere Standardoptionen zu speichern. Der Wert einer Konstante kann nicht geändert werden, während unser Code ausgeführt wird, was ihn ideal für so etwas wie unsere Standardoptionen macht. Klassenkonstanten werden einmal pro Klasse zugewiesen und nicht für jede Klasseninstanz.

HINWEIS: Der Name einer Konstante wird per Konvention in Großbuchstaben geschrieben.

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

Im verschachtelten Array DEFAULT_OPTIONS haben wir einen Standardwert für alle unsere Optionen festgelegt.

Als Nächstes möchten wir die Standardoptionswerte in der Datenbank speichern, sobald das Plugin initialisiert ist, indem wir die WordPress-Funktion add_option() verwenden.

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

Schauen wir uns diesen Ausschnitt genauer an. Zuerst iterieren wir das Standardoptions-Array und rufen die Optionen mit der WordPress-Funktion 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 ); // ...

Dann prüfen wir, ob jede Option bereits in der Datenbank vorhanden ist, und wenn nicht, speichern wir ihre Standardoption.

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

Schließlich sammeln wir die Optionen aller Abschnitte.

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

Und speichern Sie sie in der Eigenschaft $options , damit wir später darauf zugreifen können.

 $this->options = $all_options;

Die WordPress-Optionstabelle in der Datenbank wird ein paar Zeilen haben, wobei der option_name aus dem Präfix des Plugins besteht, das mit dem Abschnittsnamen verkettet ist.

Kommen wir nun zu den restlichen Methoden, die wir implementieren müssen.

Speichern einer Option

Ebenso möchten wir einfach eine neue Option in der Datenbank speichern und alle vorherigen Werte wie folgt überschreiben:

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

Implementieren wir also die Methode set() , die die WordPress-Funktion update_option() verwendet.

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

Entfernen einer Option

Zuletzt implementieren wir die Methode remove() , die die Option auf ihren Anfangswert setzt:

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

Wir haben alles in einer Klasse gebündelt. Alle optionenbezogenen Daten (dh unsere Eigenschaften) und die Implementierungsdetails (dh die Methoden, die wir gerade implementiert haben) sind in der Klasse WP_Options gekapselt .

Kapselung/Abstraktion

Alles in eine einzige Klasse zu packen, die Interna (wie in einer Kapsel) einzuschließen, sie im Wesentlichen vor der Außenwelt zu „verstecken“, nennen wir Kapselung . Kapselung ist ein weiteres Kernkonzept der objektorientierten Programmierung.

Hosten Sie Ihre Website mit Pressidium

60- TÄGIGE GELD-ZURÜCK-GARANTIE

SEHEN SIE UNSERE PLÄNE

Bei der Verwendung der Options haben wir uns darauf konzentriert, was wir mit unseren Optionen machen, anstatt darauf, wie wir es tun, indem wir die Idee von Optionen abstrahiert und die Dinge konzeptionell vereinfacht haben. Das nennen wir Abstraktion , ein weiteres Kernkonzept der objektorientierten Programmierung.

Kapselung und Abstraktion sind völlig unterschiedliche Konzepte, aber, wie Sie sehen können, eng miteinander verwandt. Ihr Hauptunterschied besteht darin, dass die Kapselung auf der Implementierungsebene existiert, während die Abstraktion auf der Designebene existiert.

Abhängigkeiten

Betrachten wir das folgende Szenario:

Es gibt eine Lockouts -Klasse, die bestimmt, ob eine IP-Adresse gesperrt werden soll, wie lange diese Sperrung dauern soll, ob eine aktive Sperrung noch gültig oder abgelaufen ist usw. Diese Klasse enthält eine should_get_locked_out() -Methode, die für die Bestimmung verantwortlich ist ob eine IP-Adresse gesperrt werden soll. Diese Methode müsste die maximale Anzahl zulässiger Wiederholungen lesen, bevor eine IP-Adresse gesperrt wird, was ein konfigurierbarer Wert ist, dh als Option gespeichert wird.

Der gerade beschriebene Code würde also etwa so aussehen:

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

Im Grunde erstellen wir eine neue Instanz von WP_Options im Konstruktor und verwenden diese Instanz dann, um den Wert der Option allowed_retries “ abzurufen.

Das ist absolut in Ordnung, aber wir müssen bedenken, dass unsere Lockouts -Klasse jetzt von WP_Options abhängt. Wir nennen WP_Options eine Abhängigkeit .

Wenn sich unsere Anforderungen in Zukunft ändern, müssen wir beispielsweise Optionen in einer externen Datenbank lesen/schreiben, müssen wir die WP_Options -Klasse durch eine DB_Options -Klasse ersetzen. Das scheint nicht so schlimm zu sein, wenn wir Optionen in nur einer Klasse abrufen müssen. Es kann jedoch etwas schwierig werden, wenn es viele Klassen mit mehreren Abhängigkeiten gibt. Alle Änderungen an einer einzelnen Abhängigkeit werden sich wahrscheinlich über die Codebasis ausbreiten und uns zwingen, eine Klasse zu ändern, wenn sich eine ihrer Abhängigkeiten ändert.

Wir können dieses Problem beseitigen, indem wir unseren Code so umschreiben, dass er dem Prinzip der Abhängigkeitsinversion folgt.

Entkopplung

Das Dependency Inversion Principle (DIP), das „D“ in SOLID, besagt:

  • High-Level-Module sollten nichts von Low-Level-Modulen importieren. Beide sollten von Abstraktionen abhängen.
  • Abstraktionen sollten nicht von Details abhängen. Details (konkrete Implementierungen) sollten von Abstraktionen abhängen.

In unserem Fall ist die Lockouts -Klasse das „High-Level-Modul“ und hängt von einem „Low-Level-Modul“ ab, der WP_Options -Klasse.

Wir werden das ändern, indem wir Dependency Injection verwenden, was einfacher ist, als es sich anhört. Unsere Lockouts -Klasse erhält die Objekte, von denen sie abhängt, anstatt sie zu erstellen.

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

Also injizieren wir eine Abhängigkeit:

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

Wir haben gerade unsere Lockouts -Klasse einfacher zu warten gemacht, da sie jetzt lose mit ihrer WP_Options Abhängigkeit gekoppelt ist. Darüber hinaus können wir die Abhängigkeiten simulieren, wodurch unser Code einfacher zu testen ist. Das Ersetzen von WP_Options durch ein Objekt, das sein Verhalten nachahmt, ermöglicht es uns, unseren Code zu testen, ohne tatsächlich Abfragen in einer Datenbank auszuführen.

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

Obwohl wir die Kontrolle über die Abhängigkeiten von Lockouts an eine andere Klasse übergeben haben (im Gegensatz zu Lockouts , die die Abhängigkeiten selbst kontrollieren), erwartet Lockouts immer noch ein WP_Options Objekt. Das bedeutet, dass es immer noch von der konkreten WP_Options -Klasse abhängt, anstatt von einer Abstraktion. Wie bereits erwähnt, sollten beide Module von abstractions abhängen .

Lassen Sie uns das beheben!

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

Und indem Sie einfach den Typ des $options Arguments von der WP_Options -Klasse in die Options -Schnittstelle ändern, hängt unsere Lockouts -Klasse von einer Abstraktion ab und wir können ein DB_Options Objekt oder eine Instanz einer beliebigen Klasse übergeben, die dieselbe Schnittstelle implementiert. zu seinem Erbauer.

Einzelverantwortung

Es ist erwähnenswert, dass wir eine Methode namens should_get_locked_out() verwendet haben, um zu prüfen, ob die IP-Adresse gesperrt werden soll oder nicht.

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

Wir könnten leicht einen Einzeiler wie diesen schreiben:

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

Dieses Stück Logik in eine eigene kleine Methode zu verschieben, hat jedoch viele Vorteile.

  • Wenn sich die Bedingung für die Bestimmung, ob eine IP-Adresse gesperrt werden soll, jemals ändert, müssen wir nur diese Methode ändern (anstatt nach allen Vorkommen unserer if-Anweisung zu suchen).
  • Das Schreiben von Unit-Tests wird einfacher, wenn jede „Einheit“ kleiner ist
  • Verbessert die Lesbarkeit unseres Codes erheblich

Das Lesen:

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

scheint uns viel einfacher, als das zu lesen:

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

Wir haben dies für so ziemlich jede Methode unseres Plugins getan. Extrahieren von Methoden aus längeren, bis nichts mehr zu extrahieren ist. Dasselbe gilt für Klassen, jede Klasse und Methode sollte eine einzige Verantwortung haben.

Das Single-Responsibility-Prinzip (SRP) , das „S“ in SOLID, besagt:

„Jedes Modul, jede Klasse oder Funktion in einem Computerprogramm sollte für einen einzelnen Teil der Funktionalität dieses Programms verantwortlich sein und diesen Teil kapseln.“

Oder wie Robert C. Martin („Onkel Bob“) sagt:

„Eine Klasse sollte einen und nur einen Grund haben, sich zu ändern.“

Erneutes Aufrufen der Haupt-Plugin-Datei

Im Moment enthält unsere Haupt-Plugin-Datei nur Folgendes:

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

Wiederum packen wir alles in eine Plugin-Klasse, diesmal nur, um Namenskollisionen zu vermeiden.

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

Wir werden diese Plugin -Klasse am Ende der Datei instanziieren, die den Code in ihrem Konstruktor ausführen wird.

 new Plugin();

Im Konstruktor hängen wir uns in die Aktion plugins_loaded ein, die ausgelöst wird, sobald aktivierte Plugins geladen wurden.

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

Wir rufen auch eine require_files() Methode auf, um alle unsere PHP-Dateien zu laden.

 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'; // ... }

Schließlich initialisieren wir unser Plugin, indem wir einige Objekte in unserer init() -Methode erstellen.

HINWEIS: Das folgende Snippet enthält nur einen kleinen Teil der Haupt-Plugin-Datei. Sie können die eigentliche Datei im GitHub-Repository des Plugins lesen.

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

Organisieren der Dateien

Die Organisation Ihrer Dateien ist von entscheidender Bedeutung, insbesondere wenn Sie an großen Plugins mit viel Code arbeiten. Ihre Ordnerstruktur sollte ähnliche Dateien gruppieren, damit Sie und Ihre Teamkollegen organisiert bleiben.

Wir haben bereits einen Namensraum ( Pressidium\Limit_Login_Attempts ) definiert, der mehrere untergeordnete Namensräume für Pages , Sections , Fields , Elements usw. enthält. Wenn wir dieser Hierarchie folgen, um unsere Verzeichnisse und Dateien zu organisieren, haben wir am Ende eine ähnliche Struktur wie diese:

 . ├── 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

Jede Datei enthält eine einzelne Klasse. Dateien werden nach den enthaltenen Klassen benannt, Verzeichnisse und Unterverzeichnisse nach den (Unter-)Namensräumen.

Es gibt mehrere Architekturmuster und Benennungsschemata, die Sie verwenden können. Es liegt an Ihnen, eine auszuwählen, die für Sie sinnvoll ist und den Anforderungen Ihres Projekts entspricht. Bei der Strukturierung Ihres Projekts kommt es auf Konsistenz an .

Fazit

Herzliche Glückwünsche! Sie haben unsere Artikelserie über WordPress und objektorientierte Programmierung abgeschlossen.

Hoffentlich haben Sie ein paar Dinge gelernt und freuen sich darauf, das Gelernte in Ihren eigenen Projekten anzuwenden!

Hier ist eine kurze Zusammenfassung dessen, was wir in dieser Serie behandelt haben:

  • Anforderungserfassung: Wir haben entschieden, was das Plugin tun soll.
  • Design: Wir haben darüber nachgedacht, wie das Plugin strukturiert sein wird, die Beziehungen zwischen unseren potenziellen Klassen und einen allgemeinen Überblick über unsere Abstraktionen.
  • Implementierung: Wir haben den eigentlichen Code einiger wichtiger Teile des Plugins geschrieben. Dabei haben wir Ihnen mehrere Konzepte und Prinzipien vorgestellt.

Wir haben jedoch kaum an der Oberfläche dessen gekratzt, was OOP ist und zu bieten hat. Um in einer neuen Fähigkeit gut zu werden, braucht es Übung, also fangen Sie an und bauen Sie Ihre eigenen objektorientierten WordPress-Plugins. Viel Spaß beim Codieren!

Siehe auch

  • WordPress und objektorientierte Programmierung – Ein Überblick
  • Teil 2 – WordPress und objektorientierte Programmierung: Ein Beispiel aus der Praxis
  • Teil 3 – WordPress und objektorientierte Programmierung: Α WordPress-Beispiel – Definition des Geltungsbereichs
  • Teil 4 – WordPress und objektorientierte Programmierung: Ein WordPress-Beispiel – Design
  • Teil 5 – WordPress und objektorientierte Programmierung: Ein WordPress-Beispiel – Implementierung: Das Verwaltungsmenü
  • Teil 6 – WordPress und objektorientierte Programmierung: Ein WordPress-Beispiel – Implementierung: Registrieren der Abschnitte
  • Teil 7 – WordPress und objektorientierte Programmierung: Ein WordPress-Beispiel – Implementierung: Verwalten von WordPress-Hooks