2026年10月9日
2026年10月9日
WordPressプラグイン開発でService LocatorとDIコンテナを比較する実践ガイド
結論から言うと、WordPress のプラグインでは「クラスは依存をコンストラクタで受け取る(DI)」を基本にし、組み立ては起動時の1か所にまとめるのが扱いやすい形です。Service Locator はその「組み立ての場所」に閉じ込めて使えば便利ですが、各クラスの中から呼び出すようになるとテストと保守が難しくなります。この記事では、2つのパターンの違いを同じ例で比べ、小規模なら手書きの組み立て、中規模以上なら DI コンテナ、という選び方と、WordPress 特有の注意点(フック、Composer の衝突)を説明します。
2つのパターンは「依存をどう手に入れるか」の違い
あるクラスが別のクラスやサービスを必要とすることを「依存」と呼びます。たとえば、注文を通知するクラスがメール送信とログ記録を必要とする場合、メール送信とログ記録が依存です。
- Service Locator:クラスが自分で「メール送信をください」と中央の登録所に取りに行く方式
- 依存性の注入(DI):クラスは何も取りに行かず、外から渡されたものを使う方式。渡す作業を自動化する道具が DI コンテナ
どちらも「使う側が具体的なクラス名を知らなくて済む」という目的は同じです。違いは、依存がクラスの外から見えるかどうかにあります。
Service Locator の書き方と問題点
まず Service Locator の典型的な形です。プラグイン全体で1つの登録所を持ち、名前でサービスを取り出します。
<?php
namespace MyPlugin;
final class Locator {
private static array $factories = array();
private static array $instances = array();
public static function set( string $id, callable $factory ): void {
self::$factories[ $id ] = $factory;
}
public static function get( string $id ) {
if ( ! isset( self::$instances[ $id ] ) ) {
if ( ! isset( self::$factories[ $id ] ) ) {
throw new \RuntimeException( "Service not found: {$id}" );
}
self::$instances[ $id ] = ( self::$factories[ $id ] )();
}
return self::$instances[ $id ];
}
}
final class OrderNotifier {
public function notify( int $order_id ): void {
$mailer = Locator::get( 'mailer' ); // クラスの中で取りに行く
$logger = Locator::get( 'logger' );
$mailer->send( $order_id );
$logger->info( "notified {$order_id}" );
}
}
手軽に書けて、どこからでも使えるのが長所です。WordPress のコアや多くのプラグインがグローバル関数やシングルトンを多用しているため、発想としても馴染みやすい形です。
一方で、次のような問題が出てきます。
- 依存が隠れる:
OrderNotifierのコンストラクタを見ても、メール送信とログ記録が必要なことが分かりません。中身を読まないと依存関係を把握できません - 失敗が実行時まで分からない:登録し忘れたサービスは、そのコードが実際に動いた瞬間に例外になります。特定の条件でしか通らない処理だと、本番で初めて気づくことがあります
- テストで差し替えにくい:テストのたびに静的な登録所の中身を入れ替え、終わったら元に戻す必要があります。戻し忘れると別のテストに影響します
DI(コンストラクタ注入)の書き方
同じクラスを、依存をコンストラクタで受け取る形に書き換えます。
<?php
namespace MyPlugin;
interface Mailer {
public function send( int $order_id ): void;
}
interface Logger {
public function info( string $message ): void;
}
final class OrderNotifier {
public function __construct(
private Mailer $mailer,
private Logger $logger
) {}
public function notify( int $order_id ): void {
$this->mailer->send( $order_id );
$this->logger->info( "notified {$order_id}" );
}
}
コンストラクタを見るだけで「この通知クラスはメール送信とログ記録に依存する」と分かります。テストでは、偽物の Mailer を作って渡すだけで済み、静的な状態を掃除する必要もありません。なお、この例のコンストラクタ引数での private 宣言(プロパティの昇格)は PHP 8.0 以降の書き方です。PHP 7.4 も対象にするなら、プロパティを別に宣言して代入してください。
では、誰がこれらを組み立てて渡すのか。小さなプラグインなら、起動時に手で組み立てれば十分です。
<?php
// my-plugin.php(メインファイル)
add_action( 'plugins_loaded', function () {
$logger = new MyPlugin\ErrorLogLogger();
$mailer = new MyPlugin\WpMailMailer();
$notifier = new MyPlugin\OrderNotifier( $mailer, $logger );
add_action( 'my_plugin_order_completed', array( $notifier, 'notify' ) );
} );
new を書くのはこの起動処理の中だけ、と決めておくのがポイントです。組み立てが一か所に集まるので、依存の全体像もここを読めば分かります。
DI コンテナを使う場合
クラスが数十個になり、手での組み立てが長くなってきたら DI コンテナの出番です。PHP では PHP-DI や League Container などが広く使われており、多くは PSR-11(Psr\Container\ContainerInterface の get() と has())に対応しています。PHP-DI は型宣言を読み取って依存を自動で解決する「オートワイヤリング」を備えており、インターフェースに対してどの実装を使うかだけを定義すれば済みます。
<?php
// Composer で php-di/php-di を導入している前提
use DI\ContainerBuilder;
use function DI\autowire;
add_action( 'plugins_loaded', function () {
$builder = new ContainerBuilder();
$builder->addDefinitions( array(
MyPlugin\Mailer::class => autowire( MyPlugin\WpMailMailer::class ),
MyPlugin\Logger::class => autowire( MyPlugin\ErrorLogLogger::class ),
) );
$container = $builder->build();
$notifier = $container->get( MyPlugin\OrderNotifier::class );
add_action( 'my_plugin_order_completed', array( $notifier, 'notify' ) );
} );
関数名や定義の書き方はライブラリとバージョンで異なるため、使う版の公式ドキュメントで確認してください(2026年10月現在)。
コンテナを Service Locator にしない
コンテナを導入しても、各クラスの中で $container->get() を呼べば、それは Service Locator と同じ問題を抱えます。コンテナに触れてよいのは起動処理(コンポジションルートと呼ばれる組み立ての場所)だけ、というルールを決めておくと、DI の利点が保たれます。
ただし WordPress では、フックのコールバックやショートコード、REST API のコールバックなど、WordPress が直接呼び出す入口がたくさんあります。これらの入口では、起動処理で組み立てたオブジェクトのメソッドを登録する形にすると、入口の中でコンテナを探しに行かずに済みます。
比較:どちらを選ぶか
| 観点 | Service Locator | DI(手書き・コンテナ) |
|---|---|---|
| 依存の見えやすさ | クラスの中を読まないと分からない | コンストラクタで分かる |
| 書き始めの手軽さ | 手軽 | 組み立て処理を書く手間がある |
| テストでの差し替え | 静的状態の掃除が必要 | 偽物を渡すだけ |
| 登録漏れの発見 | 実行時に該当箇所が動いたとき | 起動時の組み立てで見つかりやすい |
目安としては次のように考えるとよいでしょう。
- 数クラス程度の小さなプラグイン:手書きの組み立てで十分。コンテナは不要です
- クラスが増えた中〜大規模なプラグイン:DI コンテナで組み立てを自動化します
- 既存のコードが Service Locator だらけ:一度に書き換えず、新しく作るクラスからコンストラクタ注入にし、Locator の呼び出しは起動処理の側へ少しずつ寄せていきます
WordPress で特に気をつけること
Composer のライブラリ衝突
DI コンテナを Composer で同梱すると、別のプラグインが同じライブラリの違うバージョンを同梱していた場合に、先に読み込まれた方のクラスが使われてしまいます。動作がおかしくなったり、致命的エラーになったりする原因になります。配布するプラグインでは、PHP-Scoper や Strauss のような道具で、同梱ライブラリの名前空間に独自の接頭辞を付ける対策が一般的です。社内サイト専用で同梱ライブラリを管理できているなら、そこまで必要ない場合もあります。
読み込みのタイミング
組み立ては plugins_loaded など、必要な関数が使える時点で行います。ユーザー情報や翻訳を使うサービスを早すぎる時点で作ると、正しく動かないことがあります。重いサービスは、コンテナのファクトリー(呼ばれたときに初めて作る仕組み)にしておくと、使わないリクエストで無駄に作らずに済みます。
変更前の準備
既存プラグインの構造を変えるときは、バックアップを取り、ステージング環境で主要な画面とフックの動作を確認してから本番に反映してください。リファクタリングの前に、現状の動作を確かめるテストを用意しておくと、書き換えの影響を追いやすくなります。
まとめ
- Service Locator はクラスが依存を取りに行く方式、DI は外から渡す方式。違いは依存が外から見えるかどうか
- クラスはコンストラクタで依存を受け取り、組み立ては
plugins_loadedなどの起動処理1か所にまとめる - 小規模なら手書き、中規模以上なら PHP-DI などの PSR-11 対応コンテナを使う。コンテナに触れるのは起動処理だけにする
- 配布するプラグインで Composer のライブラリを同梱するなら、名前空間の接頭辞付けで衝突を避ける