Część 8 – WordPress i programowanie obiektowe: przykład WordPressa – Implementacja: Opcje

Opublikowany: 2022-02-04

Do tej pory musieliśmy przechowywać tylko opcje zdefiniowane przez użytkownika, więc korzystaliśmy z interfejsu Settings API. Jednak nasza wtyczka musi być w stanie sama odczytać/zapisać opcje, aby „zapamiętać”, ile razy adres IP próbował się zalogować bez powodzenia, jeśli jest aktualnie zablokowany itp.

Potrzebujemy zorientowanego obiektowo sposobu przechowywania i pobierania opcji. W fazie „Projektowania” omówiliśmy to krótko, ale odrzuciliśmy niektóre szczegóły implementacji, skupiając się wyłącznie na czynnościach, które chcielibyśmy wykonać — pobieraniu , ustawianiu i usuwaniu opcji.

Posortujemy również opcje „grupowania” razem na podstawie ich sekcji, aby były uporządkowane. To zależy wyłącznie od osobistych preferencji.

Zamieńmy to w interfejs:

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

Najlepiej byłoby, gdybyśmy mogli wchodzić w interakcję z interfejsem API opcji WordPress, wykonując coś takiego:

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

W tym momencie możesz się zastanawiać, dlaczego po prostu nie używamy funkcji WordPress get_option() , zamiast zajmować się tworzeniem własnego interfejsu i klasy. Podczas gdy bezpośrednie korzystanie z funkcji WordPressa byłoby całkowicie akceptowalnym sposobem rozwijania naszej wtyczki, idąc o krok dalej i tworząc interfejs, na którym będziemy polegać, pozostajemy elastyczni.

Nasza klasa WP_Options zaimplementuje nasz interfejs Options . W ten sposób będziemy gotowi na zmianę naszych potrzeb w przyszłości. Na przykład, możemy potrzebować przechowywać nasze opcje w niestandardowej tabeli, w zewnętrznej bazie danych, w pamięci (np. Redis), którą nazywasz. Dzięki zależności od abstrakcji (np. interfejsu), zmiana czegoś w implementacji jest tak prosta, jak utworzenie nowej klasy implementującej ten sam interfejs.

WP_Opcje

Zacznijmy pisać naszą klasę WP_Options , od pobrania wszystkich opcji za pomocą funkcji WordPress get_option() w jej konstruktorze.

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

Ponieważ właściwość $options będzie używana wewnętrznie, zadeklarujemy ją jako private , więc dostęp do niej może uzyskać tylko klasa, która ją zdefiniowała, klasa WP_Options .

Teraz zaimplementujmy nasz interfejs Options za pomocą operatora implements .

 class WP_Options implements Options { // ...

Nasze IDE krzyczy na nas, abyśmy albo zadeklarowali naszą abstrakcyjną klasę, albo zaimplementowali metody get() , set() i remove() zdefiniowane w interfejsie.

Zacznijmy więc wdrażać te metody!

Uzyskiwanie opcji

Zaczniemy od metody get() , która będzie szukać określonej nazwy opcji we właściwości $options i albo zwróci jej wartość, albo false , jeśli nie istnieje.

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

Teraz nadszedł dobry moment, aby pomyśleć o opcjach domyślnych.

Opcje domyślne

Jak wspomniano wcześniej, chcielibyśmy pogrupować opcje na podstawie ich sekcji. Tak więc prawdopodobnie podzielimy opcje na kilka sekcji. Sekcja „Opcje ogólne” i kolejna dla danych, które musimy śledzić. Blokady, ponawianie prób, dzienniki blokad i łączna liczba blokad — będziemy arbitralnie nazywać ten stan.

Użyjemy stałej do przechowywania naszych domyślnych opcji. Wartości stałej nie można zmienić podczas wykonywania naszego kodu, co czyni ją idealną dla czegoś takiego jak nasze domyślne opcje. Stałe klas są przydzielane raz na klasę, a nie dla każdej instancji klasy.

UWAGA: Zgodnie z konwencją nazwa stałej jest pisana wielkimi literami.

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

W tablicy zagnieżdżonej DEFAULT_OPTIONS ustawiliśmy wartość domyślną dla wszystkich naszych opcji.

To, co chcielibyśmy teraz zrobić, to zapisać domyślne wartości opcji w bazie danych po zainicjowaniu wtyczki za pomocą funkcji 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; } }

Przyjrzyjmy się bliżej temu fragmentowi. Najpierw iterujemy domyślną tablicę opcji i pobieramy opcje za pomocą funkcji 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 ); // ...

Następnie sprawdzamy, czy każda opcja już istnieje w bazie danych, a jeśli nie, przechowujemy jej domyślną opcję.

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

Na koniec zbieramy opcje wszystkich sekcji.

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

Przechowuj je we właściwości $options , abyśmy mogli później uzyskać do nich dostęp.

 $this->options = $all_options;

Tabela opcji WordPress w bazie danych będzie miała kilka wierszy, w których option_name składa się z prefiksu wtyczki połączonego z nazwą sekcji.

Przejdźmy teraz do pozostałych metod, które musimy wdrożyć.

Przechowywanie opcji

Podobnie chcielibyśmy łatwo zapisać nową opcję w bazie danych i nadpisać poprzednią wartość, na przykład:

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

Zaimplementujmy więc metodę set() , która będzie korzystać z funkcji WordPressa 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 ); }

Usuwanie opcji

Na koniec zaimplementujemy metodę remove() , która ustawi opcję na jej początkową wartość:

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

Połączyliśmy wszystko w jedną klasę. Wszystkie dane związane z opcjami (tj. nasze właściwości) i szczegóły implementacji (tj. metody, które właśnie zaimplementowaliśmy) są zawarte w klasie WP_Options .

Hermetyzacja/abstrakcja

Zawijanie wszystkiego w jedną klasę, zamykanie wnętrzności (jak w kapsułce), zasadniczo „ukrywanie” ich przed światem zewnętrznym, nazywamy enkapsulacją . Enkapsulacja to kolejna podstawowa koncepcja programowania obiektowego.

Hostuj swoją stronę internetową z Pressidium

60- DNIOWA GWARANCJA ZWROTU PIENIĘDZY

ZOBACZ NASZE PLANY

Korzystając z interfejsu Options , skupiliśmy się na tym, co robimy z naszymi opcjami, a nie na tym, jak to robimy, abstrahując ideę opcji, upraszczając rzeczy koncepcyjnie. Nazywamy to abstrakcją , kolejną podstawową koncepcją programowania obiektowego.

Enkapsulacja i abstrakcja to zupełnie inne pojęcia, ale wyraźnie, jak widać, bardzo powiązane. Ich główna różnica polega na tym, że enkapsulacja istnieje na poziomie implementacji, podczas gdy abstrakcja istnieje na poziomie projektu.

Zależności

Rozważmy następujący scenariusz:

Istnieje klasa Lockouts , odpowiedzialna za określenie, czy adres IP powinien zostać zablokowany, jaki powinien być czas trwania tej blokady, czy aktywna blokada jest nadal ważna lub wygasła itp. Ta klasa zawiera should_get_locked_out() , odpowiedzialną za określenie czy adres IP powinien zostać zablokowany. Ta metoda musiałaby odczytać maksymalną dozwoloną liczbę ponownych prób, zanim adres IP zostanie zablokowany, co jest wartością konfigurowalną, co oznacza, że ​​jest przechowywany jako opcja .

Tak więc kod, który właśnie opisaliśmy, wyglądałby podobnie do tego:

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

Zasadniczo tworzymy nową instancję WP_Options w konstruktorze, a następnie używamy tej instancji do pobrania wartości opcji allowed_retries .

To absolutnie w porządku, ale musimy pamiętać, że nasza klasa Lockouts zależy teraz od WP_Options . WP_Options nazywamy zależnością .

Jeśli nasze potrzeby zmienią się w przyszłości, na przykład, będziemy potrzebować odczytu/zapisu opcji w zewnętrznej bazie danych, musielibyśmy zastąpić klasę WP_Options klasą DB_Options . Nie wydaje się to takie złe, jeśli musimy pobierać opcje tylko w jednej klasie. Jednak może to być trochę trudne, gdy istnieje wiele klas z wieloma zależnościami. Wszelkie zmiany w jednej zależności prawdopodobnie będą rozchodzić się po całej bazie kodu, zmuszając nas do zmodyfikowania klasy, jeśli zmieni się jedna z jej zależności.

Możemy wyeliminować ten problem, przepisując nasz kod zgodnie z zasadą odwrócenia zależności .

Oddzielenie

Zasada odwrócenia zależności (DIP), „D” w SOLID, stwierdza:

  • Moduły wysokopoziomowe nie powinny importować niczego z modułów niskopoziomowych. Oba powinny zależeć od abstrakcji.
  • Abstrakcje nie powinny zależeć od szczegółów. Szczegóły (realizacje konkretne) powinny zależeć od abstrakcji.

W naszym przypadku klasa Lockouts jest „modułem wysokiego poziomu” i zależy od „modułu niskiego poziomu”, klasy WP_Options .

Zmienimy to za pomocą Dependency Injection , co jest łatwiejsze niż mogłoby się wydawać. Nasza klasa Lockouts otrzyma obiekty, od których zależy, zamiast je tworzyć.

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

Wprowadzamy więc zależność:

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

Właśnie uczyniliśmy naszą klasę Lockouts łatwiejszą w utrzymaniu, ponieważ jest teraz luźno połączona z zależnością WP_Options . Dodatkowo będziemy mogli mockować zależności, ułatwiając testowanie naszego kodu. Zastąpienie WP_Options obiektem naśladującym jego zachowanie pozwoli nam przetestować nasz kod bez faktycznego wykonywania jakichkolwiek zapytań w bazie danych.

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

Mimo że przekazaliśmy kontrolę nad zależnościami Lockouts innej klasie (w przeciwieństwie do Lockouts kontrolujących same zależności), Lockouts nadal oczekuje obiektu WP_Options . Oznacza to, że nadal zależy od konkretnej klasy WP_Options , a nie od abstrakcji. Jak wcześniej wspomniano, oba moduły powinny być zależne od abstrakcji .

Naprawmy to!

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

I po prostu zmieniając typ argumentu $options z klasy WP_Options na interfejs Options , nasza klasa Lockouts zależy od abstrakcji i możemy przekazać obiekt DB_Options lub instancję dowolnej klasy, która implementuje ten sam interfejs, do swojego konstruktora.

Pojedyncza odpowiedzialność

Warto zauważyć, że użyliśmy metody o nazwie should_get_locked_out() , aby sprawdzić, czy adres IP powinien zostać zablokowany, czy nie.

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

Z łatwością moglibyśmy napisać taki jeden linijek:

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

Jednak przeniesienie tego kawałka logiki do własnej małej metody ma wiele zalet.

  • Jeśli warunek określający, czy adres IP powinien zostać zablokowany, kiedykolwiek się zmieni, będziemy musieli tylko zmodyfikować tę metodę (zamiast szukać wszystkich wystąpień naszej instrukcji if)
  • Pisanie testów jednostkowych staje się łatwiejsze, gdy każda „jednostka” jest mniejsza
  • Znacznie poprawia czytelność naszego kodu

Czytając to:

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

wydaje nam się o wiele łatwiejsze niż czytanie tego:

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

Zrobiliśmy to dla prawie każdej metody naszej wtyczki. Wydobywanie metod z dłuższych, aż nie ma już nic do wydobycia. To samo dotyczy klas, każda klasa i metoda powinna mieć jedną odpowiedzialność.

Zasada pojedynczej odpowiedzialności (SRP) , „S” w SOLID, stanowi:

„Każdy moduł, klasa lub funkcja w programie komputerowym powinna odpowiadać za pojedynczą część funkcjonalności tego programu i powinna zawierać tę część”.

Albo, jak mówi Robert C. Martin („Wujek Bob”):

„Klasa powinna mieć jeden i tylko jeden powód do zmiany”.

Ponowne odwiedzanie głównego pliku wtyczki

W tej chwili nasz główny plik wtyczki zawiera tylko to:

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

Ponownie zawijamy wszystko w klasę Plugin, tym razem tylko po to, aby uniknąć kolizji nazw.

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

Utworzymy instancję tej klasy Plugin na końcu pliku, który wykona kod w swoim konstruktorze.

 new Plugin();

W konstruktorze podepniemy się do akcji plugins_loaded, która uruchamia się po załadowaniu aktywowanych wtyczek.

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

Wywołamy również metodę require_files() w celu załadowania wszystkich naszych plików 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'; // ... }

Na koniec zainicjujemy naszą wtyczkę, tworząc kilka obiektów w naszej metodzie init() .

UWAGA: Poniższy fragment zawiera tylko niewielką część głównego pliku wtyczki. Możesz przeczytać rzeczywisty plik w repozytorium GitHub wtyczki.

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

Organizowanie plików

Utrzymanie porządku w plikach ma kluczowe znaczenie, szczególnie podczas pracy z dużymi wtyczkami z dużą ilością kodu. Twoja struktura folderów powinna grupować podobne pliki, pomagając Tobie i Twoim kolegom z zespołu zachować porządek.

Zdefiniowaliśmy już przestrzeń nazw ( Pressidium\Limit_Login_Attempts ), zawierającą kilka podprzestrzeni nazw dla Pages , Sections , Fields , Elements , itp. Podążając za tą hierarchią w celu uporządkowania naszych katalogów i plików, otrzymaliśmy strukturę podobną do tej:

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

Każdy plik zawiera jedną klasę. Pliki są nazwane według klas, które zawierają, a katalogi i podkatalogi są nazwane według (pod)przestrzeni nazw.

Istnieje wiele wzorców architektury i schematów nazewnictwa, których możesz użyć. Od Ciebie zależy, czy wybierzesz taki, który ma dla Ciebie sens i odpowiada potrzebom Twojego projektu. Jeśli chodzi o strukturę projektu, ważne jest, aby zachować spójność .

Wniosek

Gratulacje! Zakończyłeś naszą serię artykułów o WordPressie i programowaniu obiektowym.

Mam nadzieję, że nauczyłeś się kilku rzeczy i jesteś podekscytowany możliwością zastosowania tego, czego się nauczyłeś we własnych projektach!

Oto krótkie podsumowanie tego, co omówiliśmy w tej serii:

  • Zbieranie wymagań: Zdecydowaliśmy, co powinna zrobić wtyczka.
  • Projekt: Zastanawialiśmy się nad strukturą wtyczki, relacjami między naszymi potencjalnymi klasami i ogólnym przeglądem naszych abstrakcji.
  • Implementacja: Napisaliśmy rzeczywisty kod niektórych kluczowych części wtyczki. Czyniąc to, przedstawiliśmy Ci kilka pojęć i zasad.

Jednak ledwo zarysowaliśmy powierzchnię tego, co jest i ma do zaoferowania OOP. Osiągnięcie dobrych umiejętności w nowej umiejętności wymaga praktyki, więc śmiało zacznij tworzyć własne wtyczki do WordPressa zorientowane obiektowo. Udanego kodowania!

Zobacz też

  • WordPress i programowanie obiektowe – przegląd
  • Część 2 – WordPress i programowanie obiektowe: przykład ze świata rzeczywistego
  • Część 3 – WordPress i programowanie obiektowe: Α Przykład WordPress – definiowanie zakresu
  • Część 4 – WordPress i programowanie obiektowe: przykład WordPressa – projektowanie
  • Część 5 – WordPress i programowanie obiektowe: przykład WordPressa – Implementacja: menu administracyjne
  • Część 6 – WordPress i programowanie obiektowe: przykład WordPressa – Implementacja: rejestracja sekcji
  • Część 7 – WordPress i programowanie obiektowe: przykład WordPressa – Implementacja: Zarządzanie hookami WordPressa