パート8– WordPressとオブジェクト指向プログラミング:WordPressの例–実装:オプション
公開: 2022-02-04これまでは、ユーザー定義のオプションのみを保存する必要があったため、SettingsAPIを利用しました。 ただし、プラグインは、IPアドレスが現在ロックアウトされている場合など、IPアドレスがログインに失敗した回数を「記憶」するために、オプション自体を読み取り/書き込みできる必要があります。
オプションを保存および取得するためのオブジェクト指向の方法が必要です。 「設計」フェーズでは、これについて簡単に説明しましたが、オプションの取得、設定、削除など、実行できるようにしたいアクションのみに焦点を当てて、実装の詳細の一部を抽象化しました。
また、セクションに基づいて「グループ化」オプションを並べ替えて、整理します。 それは純粋に個人的な好みに基づいています。
これをインターフェースに変えましょう:
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 ); }
理想的には、次のような操作を行うことで、WordPressオプションAPIを操作できるようになります。
$options = new WP_Options(); $options->get( 'retries' );
この時点で、独自のインターフェイスとクラスを作成する手間をかける代わりに、なぜget_option()
WordPress関数を使用しないのか疑問に思われるかもしれません。 WordPress関数を直接使用することは、プラグインを開発するための完全に受け入れられる方法ですが、さらに一歩進んで、依存するインターフェイスを作成することにより、柔軟性を維持します。
WP_Options
クラスは、 Options
インターフェイスを実装します。 そうすれば、将来ニーズが変化した場合に備えて準備が整います。 たとえば、オプションをカスタムテーブル、外部データベース、メモリ(Redisなど)に保存する必要がある場合があります。 抽象化(つまりインターフェース)に依存することにより、実装で何かを変更することは、同じインターフェースを実装する新しいクラスを作成するのと同じくらい簡単です。
WP_Options
コンストラクターでget_option()
WordPress関数を使用してすべてのオプションを取得することにより、 WP_Options
クラスの作成を開始しましょう。
class WP_Options { /** * @var array Stored options. */ private $options; /** * WP_Options constructor. */ public function __construct() { $this->options = get_option( Plugin::PREFIX ); } }
$options
プロパティは内部で使用されるため、 private
として宣言し、それを定義したクラスであるWP_Options
クラスからのみアクセスできるようにします。
それでは、 implements
演算子を使用してOptions
インターフェースを実装しましょう。
class WP_Options implements Options { // ...
私たちのIDEは、クラスの抽象化を宣言するか、インターフェイスで定義されたget()
、 set()
、およびremove()
メソッドを実装するように叫んでいます。
それでは、これらのメソッドの実装を始めましょう!
オプションの取得
get()
メソッドから始めます。このメソッドは、 $options
プロパティで指定されたオプション名を検索し、その値を返すか、存在しない場合はfalse
を返します。
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 ]; } }
今度は、デフォルトのオプションについて考える良い機会です。
デフォルトオプション
前述のように、セクションに基づいてオプションをグループ化します。 したがって、おそらくオプションをいくつかのセクションに分割します。 「一般オプション」セクションと、追跡する必要のあるデータ用のセクション。 ロックアウト、再試行、ロックアウトログ、およびロックアウトの総数-この状態を任意に呼び出します。
デフォルトオプションを格納するために定数を使用します。 コードの実行中は定数の値を変更できないため、デフォルトのオプションのようなものに最適です。 クラス定数は、クラスインスタンスごとではなく、クラスごとに1回割り当てられます。
注:慣例により、定数の名前はすべて大文字になっています。
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 ) );
DEFAULT_OPTIONS
ネストされた配列では、すべてのオプションにデフォルト値を設定しています。
次に実行したいのは、プラグインが初期化されたら、 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; } }
このスニペットを詳しく見てみましょう。 まず、デフォルトのオプション配列を繰り返し、 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 ); // ...
次に、各オプションがデータベースにすでに存在するかどうかを確認し、存在しない場合は、デフォルトのオプションを保存します。
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 );
そして、それらを$options
プロパティに格納して、後でアクセスできるようにします。
$this->options = $all_options;
データベースのWordPressオプションテーブルには2行あります。ここで、 option_name
は、セクション名に連結されたプラグインのプレフィックスで構成されます。
次に、実装する必要のある残りのメソッドに移りましょう。
オプションの保存
同様に、新しいオプションをデータベースに簡単に保存し、次のように以前の値を上書きしたいと思います。
$options = new Options(); $options->set( 'retries', 4 );
それでは、 update_option()
WordPress関数を使用するset()
メソッドを実装しましょう。
/** * 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 ); }
オプションの削除
最後に、 remove()
メソッドを実装します。このメソッドは、オプションを初期値に設定します。
/** * 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 ); }
すべてを1つのクラスにまとめました。 すべてのオプション関連データ(つまり、プロパティ)と実装の詳細(つまり、実装したばかりのメソッド)は、 WP_Options
クラスにカプセル化されます。
カプセル化/抽象化
すべてを1つのクラスにまとめ、内部を(カプセルのように)囲み、本質的に外界から「隠す」ことを、カプセル化と呼びます。 カプセル化は、オブジェクト指向プログラミングのもう1つのコアコンセプトです。
Options
インターフェイスを使用して、オプションの考え方ではなく、オプションを使用して行うことに焦点を当て、オプションの概念を抽象化し、概念的に物事を単純化しました。 これは、オブジェクト指向プログラミングのもう1つのコアコンセプトである抽象化と呼ばれるものです。
カプセル化と抽象化は完全に異なる概念ですが、ご覧のとおり、明らかに関連性があります。 それらの主な違いは、カプセル化は実装レベルに存在し、抽象化は設計レベルに存在することです。
依存関係
次のシナリオを考えてみましょう。
Lockouts
クラスがあり、IPアドレスをロックアウトするかどうか、そのロックアウトの期間はどのくらいにするか、アクティブなロックアウトがまだ有効であるか期限切れであるかなどを判断します。このクラスには、 should_get_locked_out()
メソッドが含まれています。 IPアドレスをロックアウトする必要があるかどうか。 このメソッドは、IPアドレスがロックアウトされる前に、許可された再試行の最大数を読み取る必要があります。これは構成可能な値であり、オプションとして保存されることを意味します。
したがって、今説明したコードは次のようになります。
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; } // ... }
基本的に、コンストラクターでWP_Options
の新しいインスタンスを作成し、そのインスタンスを使用してallowed_retries
オプションの値を取得します。
これはまったく問題ありませんが、 Lockouts
クラスがWP_Options
に依存するようになったことを覚えておく必要があります。 WP_Optionsを依存関係と呼びます。
将来、ニーズが変化した場合、たとえば、外部データベースのオプションの読み取り/書き込みが必要な場合は、 WP_Options
をDB_Options
クラスに置き換える必要があります。 1つのクラスでのみオプションを取得する必要がある場合、それはそれほど悪くはないようです。 ただし、複数の依存関係を持つクラスが多数ある場合は、少し注意が必要になる場合があります。 単一の依存関係への変更はコードベース全体に波及する可能性が高く、依存関係の1つが変更された場合はクラスを変更する必要があります。
依存性逆転の原則に従うようにコードを書き直すことで、この問題を解消できます。
デカップリング
依存性逆転の原則(DIP)、SOLIDの「D」は次のように述べています。
- 高レベルのモジュールは、低レベルのモジュールから何もインポートしないでください。 どちらも抽象化に依存する必要があります。
- 抽象化は詳細に依存するべきではありません。 詳細(具体的な実装)は、抽象化に依存する必要があります。
この場合、 Lockouts
クラスは「高レベルモジュール」であり、「低レベルモジュール」であるWP_Options
クラスに依存します。
依存性注入を使用して、これを変更します。これは、思ったよりも簡単です。 Lockouts
クラスは、オブジェクトを作成するのではなく、依存するオブジェクトを受け取ります。
class Lockouts { // ... /** * Lockouts constructor. * * @param WP_Options $options */ public function __construct( WP_Options $options ) { $this->options = $options; } // ... }
したがって、依存関係を挿入します。
$options = new WP_Options(); $lockouts = new Lockouts( $options );
Lockouts
クラスは、 WP_Options
依存関係と緩く結合されているため、保守が容易になりました。 さらに、依存関係をモックできるようになり、コードのテストが容易になります。 WP_Options
をその動作を模倣するオブジェクトに置き換えると、データベースで実際にクエリを実行せずにコードをテストできます。
/** * Lockouts constructor. * * @param WP_Options $options */ public function __construct( WP_Options $options ) { $this->options = $options; }
Lockouts
の依存関係の制御を別のクラスに与えたとしても( Lockouts
が依存関係自体を制御するのではなく)、 Lockouts
はWP_Options
オブジェクトを期待します。 つまり、抽象化ではなく、具体的なWP_Options
クラスに依存しているということです。 前述のように、両方のモジュールは抽象化に依存する必要があります。
それを直そう!
/** * Lockouts constructor. * * @param Options $options */ public function __construct( Options $options ) { $this->options = $options; }
また、 $options
引数のタイプをWP_Options
クラスからOptions
インターフェイスに変更するだけで、 Lockouts
クラスは抽象化に依存し、 DB_Options
オブジェクト、または同じインターフェイスを実装する任意のクラスのインスタンスを自由に渡すことができます。そのコンストラクタに。
単一責任
should_get_locked_out()
というメソッドを使用して、IPアドレスをロックアウトする必要があるかどうかを確認したことは注目に値します。
/** * 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; }
次のようなワンライナーを簡単に書くことができます。
if ( $this->get_number_of_retries() % $this->options->get( 'allowed_retries' ) === 0 ) {
ただし、そのロジックを独自の小さなメソッドに移動することには、多くの利点があります。
- IPアドレスをロックアウトする必要があるかどうかを判断する条件が変更された場合は、このメソッドを変更するだけで済みます(ifステートメントのすべての出現箇所を検索するのではなく)
- 各「ユニット」が小さいほど、ユニットテストの記述が容易になります。
- コードの可読性を大幅に向上させます
これを読んで:
if ( $this->should_get_locked_out() ) { // ...
それを読むよりもずっと簡単に思えます:
if ( $this->get_number_of_retries() % $this->options->get( 'allowed_retries' ) === 0 ) { // ...
プラグインのほぼすべてのメソッドに対してこれを実行しました。 他に抽出するものがなくなるまで、長いメソッドからメソッドを抽出します。 同じことがクラスにも当てはまります。各クラスとメソッドは単一の責任を持つ必要があります。
SOLIDの「S」である単一責任原則(SRP)は、次のように述べています。
「コンピュータプログラムのすべてのモジュール、クラス、または関数は、そのプログラムの機能の1つの部分に対して責任を負い、その部分をカプセル化する必要があります。」
または、ロバートC.マーチン(「ボブおじさん」)が言うように:
「クラスには、変更する理由が1つだけある必要があります。」
メインプラグインファイルの再検討
現時点では、メインのプラグインファイルには次のものしか含まれていません。
/** * 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; }
繰り返しになりますが、今回は名前の衝突を避けるために、すべてをPluginクラスでラップします。
namespace Pressidium\Limit_Login_Attempts; if ( ! defined( 'ABSPATH' ) ) { exit; } class Plugin { /** * Plugin constructor. */ public function __construct() { // ... } }
このPlugin
クラスをファイルの最後でインスタンス化し、コンストラクターでコードを実行します。
new Plugin();
コンストラクターでは、plugins_loadedアクションにフックします。このアクションは、アクティブ化されたプラグインがロードされると起動します。
public function __construct() { add_action( 'plugins_loaded', array( $this, 'init' ) ); } public function init() { // Initialization }
また、 require_files()
メソッドを呼び出して、すべての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'; // ... }
最後に、 init()
メソッドでいくつかのオブジェクトを作成して、プラグインを初期化します。
注:次のスニペットには、メインプラグインファイルのごく一部しか含まれていません。 プラグインのGitHubリポジトリで実際のファイルを読み取ることができます。
public function init() { $options = new Options(); $hooks_manager = new Hooks_Manager(); $settings_page = new Settings_Page( $options ); $hooks_manager->register( $settings_page ); // ... }
ファイルの整理
特に大量のコードを含む大規模なプラグインで作業する場合は、ファイルを整理しておくことが重要です。 フォルダ構造は、類似したファイルをグループ化して、あなたとあなたのチームメートが整理された状態を維持できるようにする必要があります。
Pages
、 Sections
、 Fields
、 Elements
などのいくつかのサブ名前空間を含む名前空間( Pressidium\Limit_Login_Attempts
)をすでに定義しました。その階層に従ってディレクトリとファイルを整理すると、次のような構造になります。
. ├── 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
各ファイルには単一のクラスが含まれています。 ファイルには、ファイルに含まれるクラスにちなんで名前が付けられ、ディレクトリとサブディレクトリには、(サブ)名前空間にちなんで名前が付けられます。
使用できるアーキテクチャパターンと命名スキームは複数あります。 自分にとって意味があり、プロジェクトのニーズに合ったものを選ぶのはあなた次第です。 プロジェクトの構造化に関して重要なことは、一貫性を保つことです。
結論
おめでとう! WordPressとオブジェクト指向プログラミングに関する一連の記事を完了しました。
うまくいけば、あなたはいくつかのことを学び、あなたが学んだことをあなた自身のプロジェクトに適用し始めることに興奮しています!
このシリーズで取り上げた内容の簡単な要約を次に示します。
- 要件の収集:プラグインが何をすべきかを決定しました。
- 設計:プラグインがどのように構造化されるか、潜在的なクラス間の関係、および抽象化の概要について考えました。
- 実装:プラグインのいくつかの重要な部分の実際のコードを記述しました。 その間、いくつかの概念と原則を紹介しました。
ただし、OOPとは何か、そして提供しなければならないものの表面をかろうじてかじっただけです。 新しいスキルを習得するには練習が必要です。先に進んで、独自のオブジェクト指向WordPressプラグインの作成を開始してください。 ハッピーコーディング!
関連項目
- WordPressとオブジェクト指向プログラミング–概要
- パート2– WordPressとオブジェクト指向プログラミング:実際の例
- パート3– WordPressとオブジェクト指向プログラミング:ΑWordPressの例–スコープの定義
- パート4– WordPressとオブジェクト指向プログラミング:WordPressの例–デザイン
- パート5– WordPressとオブジェクト指向プログラミング:WordPressの例–実装:管理メニュー
- パート6– WordPressとオブジェクト指向プログラミング:WordPressの例–実装:セクションの登録
- パート7– WordPressとオブジェクト指向プログラミング:WordPressの例–実装:WordPressフックの管理