Partea 8 – WordPress și programare orientată pe obiecte: Un exemplu WordPress – Implementare: Opțiuni

Publicat: 2022-02-04

Până acum aveam nevoie doar să stocăm opțiunile definite de utilizator, așa că am folosit API-ul Setări. Cu toate acestea, pluginul nostru trebuie să poată citi/scrie opțiunile în sine pentru a „aminte” de câte ori o adresă IP a încercat să se autentifice fără succes, dacă este blocată în prezent etc.

Avem nevoie de o modalitate orientată pe obiecte de a stoca și de a prelua opțiunile. În timpul fazei de „Proiectare”, am discutat pe scurt acest lucru, dar am renunțat la unele dintre detaliile implementării, concentrându-ne doar pe acțiunile pe care ne-am dori să le putem efectua - obținerea , setarea și eliminarea unei opțiuni.

Vom sorta, de asemenea, opțiuni de „grup” în funcție de secțiunea lor, pentru a le menține organizate. Asta se bazează exclusiv pe preferințele personale.

Să transformăm asta într-o interfață:

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

În mod ideal, am putea interacționa cu API-ul WordPress Options, făcând ceva de genul acesta:

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

În acest moment, s-ar putea să vă întrebați de ce nu folosim doar funcția get_option() WordPress, în loc să ne punem probleme în a ne crea propria interfață și clasă. În timp ce folosirea directă a funcțiilor WordPress ar fi o modalitate perfect acceptabilă de a dezvolta pluginul nostru, făcând un pas mai departe și creând o interfață de care depindem, rămânem flexibili.

Clasa noastră WP_Options va implementa interfața Options . În acest fel, vom fi pregătiți dacă nevoile noastre se schimbă în viitor. De exemplu, ar putea fi nevoie să ne stocăm opțiunile într-un tabel personalizat, într-o bază de date externă, în memorie (de exemplu, Redis), numele dumneavoastră. Depinzând de o abstracție (adică interfață), schimbarea ceva în implementare este la fel de simplă ca și crearea unei clase noi care implementează aceeași interfață.

WP_Opțiuni

Să începem să scriem clasa noastră WP_Options , prin preluarea tuturor opțiunilor folosind funcția WordPress get_option() din constructorul său.

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

Deoarece proprietatea $options va fi folosită intern, o vom declara private , astfel încât să poată fi accesată doar de clasa care a definit-o, clasa WP_Options .

Acum, să implementăm interfața Options folosind operatorul de implements .

 class WP_Options implements Options { // ...

IDE-ul nostru strigă la noi fie să ne declarăm abstractul clasei, fie să implementăm metodele get() , set() și remove() , definite în interfață.

Deci, să începem să implementăm aceste metode!

Obținerea unei opțiuni

Vom începe cu metoda get() , care va căuta numele opțiunii specificate în proprietatea noastră $options și va returna valoarea sau false dacă nu există.

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

Acum este un moment bun să ne gândim la opțiunile implicite.

Opțiuni implicite

După cum am menționat anterior, am dori să grupăm opțiunile împreună, în funcție de secțiunea lor. Deci, probabil vom împărți opțiunile în câteva secțiuni. Secțiunea „Opțiuni generale” și încă una pentru datele pe care trebuie să le urmărim. Blocări, reîncercări, jurnalele de blocare și numărul total de blocări - vom numi arbitrar această stare.

Vom folosi o constantă pentru a stoca opțiunile noastre implicite. Valoarea unei constante nu poate fi modificată în timp ce codul nostru se execută, ceea ce o face ideală pentru ceva precum opțiunile noastre implicite. Constantele de clasă sunt alocate o dată pe clasă și nu pentru fiecare instanță de clasă.

NOTĂ: Numele unei constante este scris cu majuscule prin convenție.

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

În matricea imbricată DEFAULT_OPTIONS , am setat o valoare implicită pentru toate opțiunile noastre.

Ceea ce am dori să facem în continuare, este să stocăm valorile implicite ale opțiunilor în baza de date odată ce pluginul este inițializat, folosind funcția add_option() 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; } }

Să aruncăm o privire mai atentă la acest fragment. În primul rând, repetăm ​​matricea de opțiuni implicite și recuperăm opțiunile folosind funcția 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 ); // ...

Apoi, verificăm dacă fiecare opțiune există deja în baza de date, iar dacă nu, stocăm opțiunea implicită.

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

În cele din urmă, colectăm opțiunile tuturor secțiunilor.

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

Și stocați-le în proprietatea $options , astfel încât să le putem accesa mai târziu.

 $this->options = $all_options;

Tabelul de opțiuni WordPress din baza de date va avea câteva rânduri, unde option_name constă din prefixul pluginului concatenat cu numele secțiunii.

Să trecem acum la restul metodelor pe care trebuie să le implementăm.

Stocarea unei opțiuni

În mod similar, am dori să stocăm cu ușurință o nouă opțiune în baza de date și să suprascriem orice valoare anterioară, astfel:

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

Deci, să implementăm metoda set() , care va folosi funcția 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 ); }

Eliminarea unei opțiuni

În cele din urmă, vom implementa metoda remove() , care va seta opțiunea la valoarea sa inițială:

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

Am adunat totul într-o singură clasă. Toate datele legate de opțiuni (adică proprietățile noastre) și detaliile de implementare (adică metodele pe care tocmai le-am implementat) sunt încapsulate în clasa WP_Options .

Încapsulare/Abstracție

Înfășurarea totul într-o singură clasă, înglobarea elementelor interne (ca într-o capsulă), în esență „ascunderea” lor de lumea exterioară, este ceea ce numim încapsulare . Încapsularea este un alt concept de bază al programarii orientate pe obiecte.

Găzduiește-ți site-ul web cu Pressidium

GARANTIE 60 DE ZILE BANI RAPIS

VEZI PLANUL NOSTRU

Folosind interfața Options , ne-am concentrat pe ceea ce facem cu opțiunile noastre, în loc de modul în care o facem, abstragând ideea de opțiuni, simplificând lucrurile conceptual. Acesta este ceea ce numim abstractizare , un alt concept de bază al programarii orientate pe obiecte.

Încapsularea și abstracția sunt concepte complet diferite , dar în mod clar, după cum puteți vedea, strâns legate. Principala lor diferență este că încapsularea există la nivelul de implementare, în timp ce abstractizarea există la nivelul de proiectare.

Dependente

Să luăm în considerare următorul scenariu:

Există o clasă Lockouts , responsabilă pentru a determina dacă o adresă IP ar trebui să fie blocată, care ar trebui să fie durata acelei blocări, dacă o blocare activă este încă valabilă sau a expirat etc. Acea clasă conține o metodă should_get_locked_out() , responsabilă pentru determinarea dacă o adresă IP ar trebui să fie blocată. Această metodă ar trebui să citească numărul maxim de încercări permise înainte ca o adresă IP să fie blocată, ceea ce este o valoare configurabilă, ceea ce înseamnă că este stocată ca opțiune .

Deci, codul pe care tocmai l-am descris ar arăta similar cu acesta:

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

Practic, creăm o nouă instanță a WP_Options în constructor și apoi folosim acea instanță pentru a prelua valoarea opțiunii allowed_retries .

Este absolut în regulă, dar trebuie să ținem cont de faptul că clasa noastră Lockouts depinde acum de WP_Options . Numim WP_Options o dependență .

Dacă nevoile noastre se schimbă în viitor, de exemplu, trebuie să citim/scriem opțiuni pe o bază de date externă, ar trebui să înlocuim WP_Options cu o clasă DB_Options . Asta nu pare atât de rău, dacă trebuie să recuperăm opțiuni dintr-o singură clasă. Cu toate acestea, poate deveni puțin complicat atunci când există multe clase cu dependențe multiple. Orice modificare a unei singure dependențe se va răspândi probabil în baza de cod, forțându-ne să modificăm o clasă dacă una dintre dependențele sale se schimbă.

Putem elimina această problemă prin rescrierea codului nostru pentru a urma principiul inversării dependenței .

Decuplare

Principiul inversării dependenței (DIP), „D” în SOLID, afirmă:

  • Modulele de nivel înalt nu ar trebui să importe nimic din modulele de nivel scăzut. Ambele ar trebui să depindă de abstracții.
  • Abstracțiile nu ar trebui să depindă de detalii. Detaliile (implementări concrete) ar trebui să depindă de abstracții.

În cazul nostru, clasa Lockouts este „modulul de nivel înalt” și depinde de un „modul de nivel scăzut”, clasa WP_Options .

Vom schimba asta, folosind Dependency Injection , care este mai ușor decât pare. Clasa noastră Lockouts va primi obiectele de care depinde, în loc să le creeze.

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

Deci, injectăm o dependență:

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

Tocmai am făcut clasa noastră Lockouts mai ușor de întreținut, deoarece acum este cuplată cu dependența sa WP_Options . În plus, vom putea să batem joc de dependențe, făcând codul nostru mai ușor de testat. Înlocuirea WP_Options cu un obiect care imită comportamentul acestuia ne va permite să ne testăm codul fără a executa efectiv interogări într-o bază de date.

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

Chiar dacă am dat controlul dependențelor Lockouts unei alte clase (spre deosebire de Lockouts care controlează dependențele în sine), Lockouts încă așteaptă un obiect WP_Options . Adică încă depinde de clasa WP_Options concretă, în loc de o abstractizare. După cum sa menționat anterior, ambele module ar trebui să depindă de abstracții .

Să reparăm asta!

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

Și prin simpla schimbare a tipului argumentului $options din clasa WP_Options în interfața Options , clasa noastră Lockouts depinde de o abstractizare și suntem liberi să transmitem un obiect DB_Options sau o instanță a oricărei clase care implementează aceeași interfață, către constructorul său.

Responsabilitate unică

Este de remarcat faptul că am folosit o metodă numită should_get_locked_out() pentru a verifica dacă adresa IP ar trebui să fie blocată sau nu.

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

Am putea scrie cu ușurință un text de o linie ca acesta:

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

Cu toate acestea, mutarea acelei bucăți de logică în propria sa mică metodă are o mulțime de beneficii.

  • Dacă condiția pentru a determina dacă o adresă IP ar trebui să fie blocată se schimbă vreodată, va trebui doar să modificăm această metodă (în loc să căutăm toate aparițiile declarației noastre if)
  • Scrierea testelor unitare devine mai ușoară atunci când fiecare „unitate” este mai mică
  • Îmbunătățește foarte mult lizibilitatea codului nostru

Citind asta:

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

ni se pare mult mai ușor decât să citim asta:

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

Am făcut acest lucru pentru aproape toate metodele pluginului nostru. Extragerea metodelor din cele mai lungi până nu mai există nimic de extras. Același lucru este valabil și pentru clase, fiecare clasă și metodă ar trebui să aibă o singură responsabilitate.

Principiul responsabilității unice (SRP) , „S” în SOLID, afirmă:

„Fiecare modul, clasă sau funcție dintr-un program de calculator ar trebui să aibă responsabilitatea pentru o singură parte a funcționalității acelui program și ar trebui să încapsuleze acea parte.”

Sau, după cum spune Robert C. Martin („Unchiul Bob”):

„O clasă ar trebui să aibă un singur motiv pentru a se schimba.”

Revizuirea fișierului plugin principal

În acest moment, fișierul nostru principal de plugin conține doar asta:

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

Încă o dată, vom include totul într-o clasă Plugin, de data aceasta doar pentru a evita coliziunile de denumire.

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

Vom instanția această clasă Plugin la sfârșitul fișierului, care va executa codul în constructorul său.

 new Plugin();

În constructor ne vom conecta la acțiunea plugins_loaded, care se declanșează odată ce pluginurile activate s-au încărcat.

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

Vom apela, de asemenea, o metodă require_files() pentru a încărca toate fișierele noastre 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'; // ... }

În cele din urmă, vom inițializa pluginul prin crearea unor obiecte în metoda noastră init() .

NOTĂ: Următorul fragment conține doar o mică parte din fișierul plugin principal. Puteți citi fișierul real în depozitul GitHub al pluginului.

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

Organizarea dosarelor

Este vital să vă păstrați fișierele organizate, mai ales când lucrați la pluginuri mari cu mult cod. Structura dvs. de foldere ar trebui să grupeze fișiere similare împreună, ajutându-vă pe dvs. și colegii dvs. de echipă să rămâneți organizați.

Am definit deja un spațiu de nume ( Pressidium\Limit_Login_Attempts ), care conține mai multe sub-spații de nume pentru Pages , Sections , Fields , Elements etc. Urmând acea ierarhie pentru a ne organiza directoarele și fișierele, am ajuns să avem o structură similară cu aceasta:

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

Fiecare fișier conține o singură clasă. Fișierele sunt denumite după clasele pe care le conțin, iar directoarele și subdirectoarele sunt denumite după (sub)spațiile de nume.

Există mai multe modele de arhitectură și scheme de denumire pe care le puteți utiliza. Depinde de tine să alegi unul care să aibă sens pentru tine și să se potrivească nevoilor proiectului tău. Când vine vorba de structurarea proiectului, important este să fii consecvent .

Concluzie

Felicitări! Ați finalizat seria noastră de articole despre WordPress și programarea orientată pe obiecte.

Sper că ați învățat câteva lucruri și sunteți încântați să începeți să aplicați ceea ce ați învățat în propriile proiecte!

Iată o scurtă recapitulare a ceea ce am tratat în această serie:

  • Colectarea cerințelor: Am decis ce ar trebui să facă pluginul.
  • Design: Ne-am gândit la modul în care va fi structurat pluginul, la relațiile dintre potențialele noastre clase și la o privire de ansamblu la nivel înalt asupra abstracțiilor noastre.
  • Implementare: Am scris codul real al unor părți cheie ale pluginului. În timp ce făceam asta, v-am prezentat mai multe concepte și principii.

Cu toate acestea, abia am zgâriat suprafața a ceea ce este și are de oferit OOP. A deveni bun la o nouă abilitate necesită practică, așa că continuă și începe să-ți construiești propriile pluginuri WordPress orientate pe obiecte. Codare fericită!

Vezi si

  • WordPress și programarea orientată pe obiecte – O prezentare generală
  • Partea 2 – WordPress și programarea orientată pe obiecte: un exemplu din lumea reală
  • Partea 3 – WordPress și programare orientată pe obiecte: Un exemplu WordPress – Definirea domeniului de aplicare
  • Partea 4 – WordPress și programare orientată pe obiecte: Un exemplu WordPress – Design
  • Partea 5 – WordPress și programare orientată pe obiecte: Un exemplu WordPress – Implementare: Meniul de administrare
  • Partea 6 – WordPress și programare orientată pe obiecte: un exemplu WordPress – Implementare: înregistrarea secțiunilor
  • Partea 7 – WordPress și programare orientată pe obiecte: un exemplu WordPress – Implementare: gestionarea cârligelor WordPress