Parte 8 – WordPress e programmazione orientata agli oggetti: un esempio di WordPress – Implementazione: Opzioni

Pubblicato: 2022-02-04

Finora avevamo solo bisogno di memorizzare le opzioni definite dall'utente, quindi abbiamo utilizzato l'API delle impostazioni. Tuttavia, il nostro plug-in deve essere in grado di leggere/scrivere le opzioni per "ricordare" quante volte un indirizzo IP ha tentato di accedere senza successo, se è attualmente bloccato, ecc.

Abbiamo bisogno di un modo orientato agli oggetti per memorizzare e recuperare le opzioni. Durante la fase di "Progettazione", ne abbiamo discusso brevemente, ma abbiamo astratto alcuni dettagli di implementazione, concentrandoci esclusivamente sulle azioni che vorremmo essere in grado di eseguire: ottenere , impostare e rimuovere un'opzione.

Cercheremo anche di "raggruppare" le opzioni in base alla loro sezione per mantenerle organizzate. Questo è puramente basato sulle preferenze personali.

Trasformiamo questo in un'interfaccia:

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

Idealmente, saremmo in grado di interagire con l'API delle opzioni di WordPress, facendo qualcosa del genere:

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

A questo punto, ti starai chiedendo perché non usiamo semplicemente la funzione get_option() di WordPress, invece di occuparci di creare la nostra interfaccia e classe. Sebbene l'utilizzo diretto delle funzioni di WordPress sarebbe un modo perfettamente accettabile per sviluppare il nostro plug-in, facendo un ulteriore passo avanti e creando un'interfaccia su cui fare affidamento, rimaniamo flessibili.

La nostra classe WP_Options implementerà la nostra interfaccia Options . In questo modo, saremo pronti se le nostre esigenze cambieranno in futuro. Ad esempio, potremmo aver bisogno di memorizzare le nostre opzioni in una tabella personalizzata, in un database esterno, in memoria (es. Redis), tu lo chiami. Dipendendo da un'astrazione (cioè un'interfaccia), cambiare qualcosa nell'implementazione è semplice come creare una nuova classe che implementa la stessa interfaccia.

WP_Options

Iniziamo a scrivere la nostra classe WP_Options , recuperando tutte le opzioni usando la get_option() di WordPress nel suo costruttore.

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

Poiché la proprietà $options verrà utilizzata internamente, la dichiareremo private in modo che possa accedervi solo dalla classe che l'ha definita, la classe WP_Options .

Ora implementiamo la nostra interfaccia Options utilizzando l'operatore implements .

 class WP_Options implements Options { // ...

Il nostro IDE ci sta urlando di dichiarare la nostra classe abstract o di implementare i metodi get() , set() e remove() , definiti nell'interfaccia.

Quindi, iniziamo a implementare questi metodi!

Ottenere un'opzione

Inizieremo con il metodo get() , che cercherà il nome dell'opzione specificato nella nostra proprietà $options e restituirà il suo valore o false se non esiste.

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

Ora è un buon momento per pensare alle opzioni predefinite.

Opzioni predefinite

Come accennato in precedenza, vorremmo raggruppare le opzioni in base alla loro sezione. Quindi, probabilmente divideremo le opzioni in un paio di sezioni. La sezione "Opzioni generali" e un'altra per i dati di cui dobbiamo tenere traccia. Blocchi, tentativi, log di blocco e numero totale di blocchi: chiameremo arbitrariamente questo stato.

Useremo una costante per memorizzare le nostre opzioni predefinite. Il valore di una costante non può essere modificato mentre il nostro codice è in esecuzione, il che lo rende ideale per qualcosa come le nostre opzioni predefinite. Le costanti di classe vengono allocate una volta per classe e non per ogni istanza di classe.

NOTA: il nome di una costante è tutto maiuscolo per convenzione.

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

Nell'array nidificato DEFAULT_OPTIONS , abbiamo impostato un valore predefinito per tutte le nostre opzioni.

Quello che vorremmo fare dopo, è memorizzare i valori delle opzioni predefinite nel database una volta che il plugin è stato inizializzato, usando la add_option() di 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; } }

Diamo un'occhiata più da vicino a questo frammento. Innanzitutto, ripetiamo l'array di opzioni predefinito e recuperiamo le opzioni utilizzando la 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 ); // ...

Quindi, controlliamo se ogni opzione esiste già nel database e, in caso contrario, memorizziamo la sua opzione predefinita.

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

Infine, raccogliamo le opzioni di tutte le sezioni.

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

E salvali nella proprietà $options così potremo accedervi in ​​seguito.

 $this->options = $all_options;

La tabella delle opzioni di WordPress nel database avrà un paio di righe, dove option_name è costituito dal prefisso del plugin concatenato al nome della sezione.

Passiamo ora al resto dei metodi che dobbiamo implementare.

Memorizzazione di un'opzione

Allo stesso modo, vorremmo memorizzare facilmente una nuova opzione nel database e sovrascrivere qualsiasi valore precedente, in questo modo:

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

Quindi, implementiamo il metodo set() , che utilizzerà la update_option() di 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 ); }

Rimozione di un'opzione

Infine, implementeremo il metodo remove() , che imposterà l'opzione al suo valore iniziale:

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

Abbiamo raggruppato tutto insieme in un'unica classe. Tutti i dati relativi alle opzioni (cioè le nostre proprietà) ei dettagli di implementazione (cioè i metodi che abbiamo appena implementato) sono incapsulati nella classe WP_Options .

Incapsulamento/astrazione

Avvolgere tutto in un'unica classe, racchiudere gli interni (come in una capsula), essenzialmente "nasconderli" dal mondo esterno, è ciò che chiamiamo incapsulamento . L'incapsulamento è un altro concetto fondamentale della programmazione orientata agli oggetti.

Ospita il tuo sito web con Pressidium

GARANZIA DI RIMBORSO DI 60 GIORNI

GUARDA I NOSTRI PIANI

Utilizzando l'interfaccia Options , ci siamo concentrati su ciò che facciamo con le nostre opzioni invece che su come lo facciamo, astraendo l'idea delle opzioni, semplificando le cose concettualmente. Questo è ciò che chiamiamo astrazione , un altro concetto fondamentale della programmazione orientata agli oggetti.

Incapsulamento e astrazione sono concetti completamente diversi , ma chiaramente, come puoi vedere, altamente correlati. La loro principale differenza è che l'incapsulamento esiste a livello di implementazione, mentre l'astrazione esiste a livello di progettazione.

Dipendenze

Consideriamo il seguente scenario:

C'è una classe Lockouts , responsabile di determinare se un indirizzo IP deve essere bloccato, quale dovrebbe essere la durata di quel blocco, se un blocco attivo è ancora valido o è scaduto ecc. Quella classe contiene un metodo should_get_locked_out() , responsabile della determinazione se un indirizzo IP deve essere bloccato. Questo metodo dovrebbe leggere il numero massimo di tentativi consentiti prima che un indirizzo IP venga bloccato, che è un valore configurabile, il che significa che è archiviato come opzione .

Quindi, il codice che abbiamo appena descritto sarebbe simile a questo:

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

Fondamentalmente, stiamo creando una nuova istanza di WP_Options nel costruttore, quindi utilizziamo quell'istanza per recuperare il valore dell'opzione allowed_retries .

Va assolutamente bene, ma dobbiamo tenere a mente che la nostra classe Lockouts ora dipende da WP_Options . Chiamiamo WP_Options una dipendenza .

Se le nostre esigenze cambiano in futuro, ad esempio, dobbiamo leggere/scrivere opzioni su un database esterno, dovremmo sostituire WP_Options con una classe DB_Options . Non sembra così male, se dobbiamo recuperare le opzioni in una sola classe. Tuttavia, può diventare un po' complicato quando ci sono molte classi con più dipendenze. Qualsiasi modifica a una singola dipendenza probabilmente si diffonderà nella codebase, costringendoci a modificare una classe se una delle sue dipendenze cambia.

Possiamo eliminare questo problema riscrivendo il nostro codice per seguire il principio di inversione delle dipendenze .

Disaccoppiamento

Il principio di inversione delle dipendenze (DIP), la "D" in SOLID, afferma:

  • I moduli di alto livello non devono importare nulla dai moduli di basso livello. Entrambi dovrebbero dipendere dalle astrazioni.
  • Le astrazioni non dovrebbero dipendere dai dettagli. I dettagli (implementazioni concrete) dovrebbero dipendere dalle astrazioni.

Nel nostro caso, la classe Lockouts è il “modulo di alto livello” e dipende da un “modulo di basso livello”, la classe WP_Options .

Lo cambieremo, usando Dependency Injection , che è più facile di quanto possa sembrare. La nostra classe Lockouts riceverà gli oggetti da cui dipende, invece di crearli.

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

Quindi, iniettiamo una dipendenza:

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

Abbiamo appena reso la nostra classe Lockouts più facile da mantenere poiché ora è liberamente accoppiata con la sua dipendenza WP_Options . Inoltre, saremo in grado di deridere le dipendenze, rendendo il nostro codice più facile da testare. La sostituzione di WP_Options con un oggetto che ne imita il comportamento ci consentirà di testare il nostro codice senza eseguire effettivamente alcuna query su un database.

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

Anche se abbiamo assegnato il controllo delle dipendenze di Lockouts a un'altra classe (al contrario di Lockouts che controlla le dipendenze stesse), Lockouts si aspetta comunque un oggetto WP_Options . Ciò significa che dipende ancora dalla classe WP_Options concreta, anziché da un'astrazione. Come accennato in precedenza, entrambi i moduli dovrebbero dipendere dalle astrazioni .

Risolviamolo!

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

E semplicemente cambiando il tipo dell'argomento $options dalla classe WP_Options all'interfaccia Options , la nostra classe Lockouts dipende da un'astrazione e siamo liberi di passare un oggetto DB_Options , o un'istanza di qualsiasi classe che implementa la stessa interfaccia, al suo costruttore.

Unica responsabilità

Vale la pena notare che abbiamo utilizzato un metodo chiamato should_get_locked_out() per verificare se l'indirizzo IP deve essere bloccato o meno.

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

Potremmo facilmente scrivere un one-liner come questo:

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

Tuttavia, spostare quel pezzo di logica nel suo piccolo metodo ha molti vantaggi.

  • Se la condizione per determinare se un indirizzo IP deve essere bloccato cambia mai, dovremo solo modificare questo metodo (invece di cercare tutte le occorrenze della nostra istruzione if)
  • La scrittura di unit test diventa più facile quando ogni "unità" è più piccola
  • Migliora molto la leggibilità del nostro codice

Leggendo questo:

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

ci sembra molto più facile che leggere che:

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

Lo abbiamo fatto praticamente per tutti i metodi del nostro plugin. Estrarre metodi da quelli più lunghi finché non c'è nient'altro da estrarre. Lo stesso vale per le classi, ogni classe e metodo dovrebbe avere un'unica responsabilità.

Il Principio di Responsabilità Unica (SRP) , la “S” in SOLID, afferma:

"Ogni modulo, classe o funzione in un programma per computer dovrebbe avere la responsabilità di una singola parte della funzionalità di quel programma e dovrebbe incapsulare quella parte."

Oppure, come dice Robert C. Martin ("Zio Bob"):

"Una classe dovrebbe avere una, e una sola, ragione per cambiare."

Rivisitazione del file principale del plugin

Al momento, il nostro file di plugin principale contiene solo questo:

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

Ancora una volta, avvolgeremo tutto in una classe Plugin, questa volta solo per evitare collisioni di nomi.

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

Istanziamo questa classe Plugin alla fine del file, che eseguirà il codice nel suo costruttore.

 new Plugin();

Nel costruttore ci collegheremo all'azione plugins_loaded, che si attiva una volta che i plugin attivati ​​sono stati caricati.

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

Chiameremo anche un metodo require_files() per caricare tutti i nostri file 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'; // ... }

Infine, inizializzeremo il nostro plugin creando alcuni oggetti nel nostro metodo init() .

NOTA: il frammento di codice seguente contiene solo una piccola parte del file principale del plug-in. Puoi leggere il file effettivo nel repository GitHub del plugin.

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

Organizzare i file

Mantenere i file organizzati è fondamentale, soprattutto quando si lavora su plug-in di grandi dimensioni con molto codice. La struttura delle cartelle dovrebbe raggruppare file simili, aiutando te e i tuoi compagni di squadra a rimanere organizzati.

Abbiamo già definito uno spazio dei nomi ( Pressidium\Limit_Login_Attempts ), contenente diversi sottospazi dei nomi per Pages , Sections , Fields , Elements , ecc. Seguendo quella gerarchia per organizzare le nostre directory e file, siamo finiti con una struttura simile a questa:

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

Ogni file contiene una singola classe. I file prendono il nome dalle classi che contengono e le directory e le sottodirectory prendono il nome dai (sotto)spazi dei nomi.

Esistono più modelli di architettura e schemi di denominazione che puoi utilizzare. Sta a te sceglierne uno che abbia senso per te e si adatti alle esigenze del tuo progetto. Quando si tratta di strutturare il proprio progetto, l'importante è essere coerenti .

Conclusione

Congratulazioni! Hai completato la nostra serie di articoli su WordPress e la programmazione orientata agli oggetti.

Spero che tu abbia imparato alcune cose e sia entusiasta di iniziare ad applicare ciò che hai imparato sui tuoi progetti!

Ecco un breve riepilogo di ciò che abbiamo trattato in questa serie:

  • Raccolta dei requisiti: abbiamo deciso cosa dovrebbe fare il plugin.
  • Design: abbiamo pensato a come sarà strutturato il plugin, alle relazioni tra le nostre potenziali classi e a una panoramica di alto livello delle nostre astrazioni.
  • Implementazione: abbiamo scritto il codice effettivo di alcune parti chiave del plugin. Nel farlo, ti abbiamo presentato diversi concetti e principi.

Tuttavia, abbiamo a malapena scalfito la superficie di ciò che OOP è e ha da offrire. Diventare bravi in ​​una nuova abilità richiede pratica, quindi vai avanti e inizia a creare i tuoi plugin WordPress orientati agli oggetti. Buona codifica!

Guarda anche

  • WordPress e programmazione orientata agli oggetti: una panoramica
  • Parte 2 – WordPress e programmazione orientata agli oggetti: un esempio del mondo reale
  • Parte 3 – WordPress e programmazione orientata agli oggetti: Α Esempio di WordPress – Definizione dell'ambito
  • Parte 4 – WordPress e programmazione orientata agli oggetti: un esempio di WordPress – Design
  • Parte 5 – WordPress e programmazione orientata agli oggetti: un esempio di WordPress – Implementazione: il menu di amministrazione
  • Parte 6 – WordPress e programmazione orientata agli oggetti: un esempio di WordPress – Implementazione: registrazione delle sezioni
  • Parte 7 – WordPress e programmazione orientata agli oggetti: un esempio di WordPress – Implementazione: gestione degli hook di WordPress