Bagian 8 – WordPress dan Pemrograman Berorientasi Objek: Contoh WordPress – Implementasi: Opsi

Diterbitkan: 2022-02-04

Sejauh ini kami hanya perlu menyimpan opsi yang ditentukan pengguna, jadi kami menggunakan API Pengaturan. Namun, plugin kami harus dapat membaca/menulis opsi itu sendiri untuk "mengingat" berapa kali alamat IP mencoba masuk tidak berhasil, jika saat ini terkunci, dll.

Kami membutuhkan cara berorientasi objek untuk menyimpan dan mengambil opsi. Selama fase "Desain", kami membahas hal ini secara singkat, tetapi mengabstraksikan beberapa detail implementasi, hanya berfokus pada tindakan yang ingin kami lakukan— mendapatkan , menyetel , dan menghapus opsi.

Kami juga akan mengurutkan opsi "kelompok" berdasarkan bagiannya agar tetap teratur. Itu murni berdasarkan preferensi pribadi.

Mari kita ubah ini menjadi sebuah antarmuka:

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

Idealnya, kita dapat berinteraksi dengan WordPress Options API, dengan melakukan sesuatu seperti ini:

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

Pada titik ini, Anda mungkin bertanya-tanya mengapa kami tidak menggunakan fungsi get_option() WordPress saja, alih-alih bersusah payah membuat antarmuka dan kelas kami sendiri. Meskipun menggunakan fungsi WordPress secara langsung akan menjadi cara yang dapat diterima untuk mengembangkan plugin kami, dengan melangkah lebih jauh dan membuat antarmuka untuk diandalkan, kami tetap fleksibel.

Kelas WP_Options kami akan mengimplementasikan antarmuka Options kami. Dengan begitu, kita akan siap jika kebutuhan kita berubah di masa depan. Misalnya, kami mungkin perlu menyimpan opsi kami di tabel khusus, di database eksternal, di memori (misalnya Redis), sebut saja. Dengan bergantung pada abstraksi (yaitu antarmuka), mengubah sesuatu dalam implementasi, semudah membuat kelas baru yang mengimplementasikan antarmuka yang sama.

WP_Opsi

Mari kita mulai menulis kelas WP_Options kita, dengan mengambil semua opsi menggunakan fungsi get_option() WordPress di konstruktornya.

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

Karena properti $options akan digunakan secara internal, kami akan mendeklarasikannya sebagai private sehingga hanya dapat diakses oleh kelas yang mendefinisikannya, kelas WP_Options .

Sekarang, mari kita implementasikan antarmuka Options kita dengan menggunakan operator implements .

 class WP_Options implements Options { // ...

IDE kami meneriaki kami untuk mendeklarasikan abstrak kelas kami atau mengimplementasikan metode get() , set() , dan remove() , yang didefinisikan dalam antarmuka.

Jadi, mari kita mulai menerapkan metode ini!

Mendapatkan pilihan

Kita akan mulai dengan metode get() , yang akan mencari nama opsi yang ditentukan di properti $options kita, dan mengembalikan nilainya atau false jika tidak ada.

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

Sekarang saat yang tepat untuk memikirkan opsi default.

Opsi default

Seperti disebutkan sebelumnya, kami ingin mengelompokkan opsi, berdasarkan bagiannya. Jadi, kami mungkin akan membagi opsi menjadi beberapa bagian. Bagian "Opsi Umum" dan satu lagi untuk data yang perlu kami lacak. Penguncian, percobaan ulang, log penguncian, dan jumlah total penguncian—kami akan menyebut status ini secara sewenang-wenang.

Kami akan menggunakan konstanta untuk menyimpan opsi default kami. Nilai konstanta tidak dapat diubah saat kode kita dijalankan, yang membuatnya ideal untuk sesuatu seperti opsi default kita. Konstanta kelas dialokasikan sekali per kelas, dan bukan untuk setiap instance kelas.

CATATAN: Nama konstanta ditulis dalam huruf besar semua berdasarkan konvensi.

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

Dalam array bersarang DEFAULT_OPTIONS , kami telah menetapkan nilai default untuk semua opsi kami.

Apa yang ingin kita lakukan selanjutnya, adalah menyimpan nilai opsi default dalam database setelah plugin diinisialisasi, dengan menggunakan fungsi 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; } }

Mari kita lihat lebih dekat cuplikan ini. Pertama, kami mengulangi array opsi default dan mengambil opsi menggunakan fungsi 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 ); // ...

Kemudian, kami memeriksa apakah setiap opsi sudah ada di database, dan jika tidak, kami menyimpan opsi default-nya.

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

Akhirnya, kami mengumpulkan opsi dari semua bagian.

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

Dan simpan di properti $options sehingga kami dapat mengaksesnya nanti.

 $this->options = $all_options;

Tabel opsi WordPress di database akan memiliki beberapa baris, di mana option_name terdiri dari awalan plugin yang digabungkan dengan nama bagian.

Mari kita beralih sekarang ke metode lain yang perlu kita terapkan.

Menyimpan opsi

Demikian pula, kami ingin dengan mudah menyimpan opsi baru di database, dan menimpa nilai sebelumnya, seperti ini:

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

Jadi, mari kita implementasikan metode set() , yang akan menggunakan fungsi update_option() 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 ); }

Menghapus opsi

Terakhir, kita akan mengimplementasikan metode remove() , yang akan menyetel opsi ke nilai awalnya:

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

Kami telah menggabungkan semuanya dalam satu kelas. Semua data terkait opsi (yaitu properti kami) dan detail implementasi (yaitu metode yang baru saja kami implementasikan) dienkapsulasi dalam kelas WP_Options .

Enkapsulasi/Abstraksi

Membungkus semuanya dalam satu kelas, melampirkan internal (seolah-olah dalam kapsul), pada dasarnya "menyembunyikan" mereka dari dunia luar, adalah apa yang kita sebut enkapsulasi . Enkapsulasi adalah konsep inti lain dari pemrograman berorientasi objek.

Host situs web Anda dengan Pressidium

GARANSI UANG KEMBALI 60 HARI

LIHAT RENCANA KAMI

Menggunakan antarmuka Options , kami berfokus pada apa yang kami lakukan dengan opsi kami alih-alih bagaimana kami melakukannya, mengabstraksi gagasan opsi, menyederhanakan berbagai hal secara konseptual. Inilah yang kami sebut abstraksi , konsep inti lain dari pemrograman berorientasi objek.

Enkapsulasi dan abstraksi adalah konsep yang sama sekali berbeda , tetapi jelas, seperti yang Anda lihat, sangat terkait. Perbedaan utama mereka adalah bahwa enkapsulasi ada di tingkat implementasi, sedangkan abstraksi ada di tingkat desain.

Ketergantungan

Mari kita pertimbangkan skenario berikut:

Ada kelas Lockouts , yang bertanggung jawab untuk menentukan apakah alamat IP harus dikunci, berapa lama durasi penguncian itu, apakah penguncian aktif masih valid atau telah kedaluwarsa, dll. Kelas itu berisi metode should_get_locked_out() , yang bertanggung jawab untuk menentukan apakah alamat IP harus dikunci. Metode itu perlu membaca jumlah maksimum percobaan ulang yang diizinkan sebelum alamat IP dikunci, yang merupakan nilai yang dapat dikonfigurasi, artinya disimpan sebagai opsi .

Jadi, kode yang baru saja kami jelaskan akan terlihat seperti ini:

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

Pada dasarnya, kami membuat instance baru WP_Options di konstruktor, dan kemudian menggunakan instance itu untuk mengambil nilai opsi allowed_retries .

Itu baik-baik saja, tetapi kita harus ingat bahwa kelas Lockouts kita sekarang bergantung pada WP_Options . Kami menyebut WP_Options sebagai ketergantungan .

Jika kebutuhan kita berubah di masa mendatang, misalnya, kita perlu membaca/menulis opsi pada database eksternal, kita perlu mengganti WP_Options dengan kelas DB_Options . Tampaknya tidak terlalu buruk, jika kita perlu mengambil opsi hanya dalam satu kelas. Namun, ini mungkin menjadi sedikit rumit ketika ada banyak kelas dengan banyak dependensi. Setiap perubahan pada dependensi tunggal kemungkinan akan beriak di seluruh basis kode, memaksa kami untuk memodifikasi kelas jika salah satu dependensinya berubah.

Kami dapat menghilangkan masalah ini dengan menulis ulang kode kami untuk mengikuti Prinsip Pembalikan Ketergantungan .

memisahkan

Prinsip Pembalikan Ketergantungan (DIP), "D" dalam SOLID, menyatakan:

  • Modul tingkat tinggi tidak boleh mengimpor apa pun dari modul tingkat rendah. Keduanya harus bergantung pada abstraksi.
  • Abstraksi tidak boleh bergantung pada detail. Detail (implementasi konkret) harus bergantung pada abstraksi.

Dalam kasus kami, kelas Lockouts adalah "modul tingkat tinggi" dan itu tergantung pada "modul tingkat rendah", kelas WP_Options .

Kami akan mengubahnya, menggunakan Dependency Injection , yang lebih mudah daripada kedengarannya. Kelas Lockouts kami akan menerima objek yang bergantung padanya, alih-alih membuatnya.

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

Jadi, kami menyuntikkan ketergantungan:

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

Kami baru saja membuat kelas Lockouts kami lebih mudah dirawat karena sekarang digabungkan secara longgar dengan ketergantungan WP_Options -nya. Selain itu, kita akan dapat meniru dependensi, membuat kode kita lebih mudah untuk diuji. Mengganti WP_Options dengan objek yang meniru perilakunya akan memungkinkan kita menguji kode kita tanpa benar-benar mengeksekusi kueri apa pun di database.

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

Meskipun kami telah memberikan kontrol dependensi Lockouts ' ke kelas lain (sebagai lawan dari Lockouts yang mengontrol dependensi itu sendiri), Lockouts masih mengharapkan objek WP_Options . Artinya, itu masih tergantung pada kelas WP_Options konkret, bukan abstraksi. Seperti yang disebutkan sebelumnya, kedua modul harus bergantung pada abstractions .

Mari kita perbaiki itu!

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

Dan hanya dengan mengubah tipe argumen $options dari kelas WP_Options ke antarmuka Options , kelas Lockouts kita bergantung pada abstraksi dan kita bebas untuk melewatkan objek DB_Options , atau turunan dari kelas mana pun yang mengimplementasikan antarmuka yang sama, kepada konstruktornya.

Tanggung Jawab Tunggal

Perlu dicatat bahwa kami menggunakan metode yang disebut should_get_locked_out() untuk memeriksa apakah alamat IP harus dikunci atau tidak.

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

Kita dapat dengan mudah menulis satu baris seperti ini:

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

Namun, memindahkan bagian logika itu ke dalam metode kecilnya sendiri, memiliki banyak manfaat.

  • Jika kondisi untuk menentukan apakah alamat IP harus dikunci selalu berubah, kita hanya perlu memodifikasi metode ini (alih-alih mencari semua kemunculan pernyataan if kita)
  • Menulis tes unit menjadi lebih mudah ketika setiap "unit" lebih kecil
  • Meningkatkan keterbacaan kode kami banyak

Membaca ini:

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

bagi kami tampaknya jauh lebih mudah daripada membaca itu:

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

Kami telah melakukan ini untuk hampir semua metode plugin kami. Mengekstrak metode dari yang lebih panjang sampai tidak ada lagi yang bisa diekstraksi. Hal yang sama berlaku untuk kelas, setiap kelas dan metode harus memiliki satu tanggung jawab.

Prinsip Tanggung Jawab Tunggal (SRP) , "S" dalam SOLID, menyatakan:

“Setiap modul, kelas, atau fungsi dalam program komputer harus memiliki tanggung jawab atas satu bagian dari fungsi program itu, dan itu harus merangkum bagian itu.”

Atau, seperti yang dikatakan Robert C. Martin (“Paman Bob”):

"Sebuah kelas harus memiliki satu, dan hanya satu, alasan untuk berubah."

Meninjau kembali file plugin utama

Saat ini, file plugin utama kami hanya berisi ini:

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

Sekali lagi, kita akan membungkus semuanya dalam kelas Plugin, kali ini hanya untuk menghindari tabrakan penamaan.

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

Kami akan membuat instance kelas Plugin ini di akhir file, yang akan mengeksekusi kode di konstruktornya.

 new Plugin();

Di konstruktor, kita akan menghubungkan ke tindakan plugins_loaded, yang diaktifkan setelah plugin yang diaktifkan telah dimuat.

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

Kami juga akan memanggil metode require_files() untuk memuat semua file PHP kami.

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

Terakhir, kita akan menginisialisasi plugin kita dengan membuat beberapa objek dalam metode init() kita.

CATATAN: Cuplikan berikut hanya berisi sebagian kecil dari file plugin utama. Anda dapat membaca file sebenarnya di repositori GitHub plugin.

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

Mengatur file

Menjaga agar file Anda tetap teratur sangat penting, terutama saat mengerjakan plugin besar dengan banyak kode. Struktur folder Anda harus mengelompokkan file yang serupa, membantu Anda dan rekan tim Anda tetap teratur.

Kami telah mendefinisikan namespace ( Pressidium\Limit_Login_Attempts ), yang berisi beberapa sub-namespaces untuk Pages , Sections , Fields , Elements , dll. Mengikuti hierarki itu untuk mengatur direktori dan file kami, kami berakhir dengan struktur yang mirip dengan ini:

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

Setiap file berisi satu kelas. File diberi nama sesuai dengan kelas yang dikandungnya, dan direktori serta subdirektori dinamai menurut ruang nama (sub-).

Ada beberapa pola arsitektur dan skema penamaan yang dapat Anda gunakan. Terserah Anda untuk memilih satu yang masuk akal bagi Anda dan sesuai dengan kebutuhan proyek Anda. Ketika datang untuk menyusun proyek Anda, yang penting adalah konsisten .

Kesimpulan

Selamat! Anda telah menyelesaikan seri artikel kami tentang WordPress dan pemrograman berorientasi objek.

Semoga Anda mempelajari beberapa hal dan bersemangat untuk mulai menerapkan apa yang Anda pelajari pada proyek Anda sendiri!

Berikut rekap singkat dari apa yang kami bahas dalam seri ini:

  • Pengumpulan persyaratan: Kami memutuskan apa yang harus dilakukan plugin.
  • Desain: Kami memikirkan tentang bagaimana plugin akan terstruktur, hubungan antara kelas potensial kami, dan gambaran umum tingkat tinggi dari abstraksi kami.
  • Implementasi: Kami menulis kode aktual dari beberapa bagian penting dari plugin. Saat melakukan itu, kami memperkenalkan Anda pada beberapa konsep dan prinsip.

Namun, kami hampir tidak mengetahui apa itu OOP dan apa yang ditawarkan. Menjadi mahir dalam keterampilan baru membutuhkan latihan, jadi lanjutkan dan mulailah membangun plugin WordPress berorientasi objek Anda sendiri. Selamat mengkode!

Lihat juga

  • WordPress dan Pemrograman Berorientasi Objek – Gambaran Umum
  • Bagian 2 – WordPress dan Pemrograman Berorientasi Objek: Contoh Dunia Nyata
  • Bagian 3 – WordPress dan Pemrograman Berorientasi Objek: Contoh WordPress – Mendefinisikan Ruang Lingkup
  • Bagian 4 – WordPress dan Pemrograman Berorientasi Objek: Contoh WordPress – Desain
  • Bagian 5 – WordPress dan Pemrograman Berorientasi Objek: Contoh WordPress – Implementasi: Menu Administrasi
  • Bagian 6 – WordPress dan Pemrograman Berorientasi Objek: Contoh WordPress – Implementasi: Mendaftarkan Bagian
  • Bagian 7 – WordPress dan Pemrograman Berorientasi Objek: Contoh WordPress – Implementasi: Mengelola Hooks WordPress