XOOPS の依存性注入
依存性注入(DI)は、コンポーネントが内部で作成する代わりに外部ソースから依存関係を受け取ることを可能にする設計パターンです。XOOPS 4.0はPSR-11互換DIコンテナサポートを導入しています。
なぜ依存性注入を使うのか?
Section titled “なぜ依存性注入を使うのか?”DI なし(密結合)
Section titled “DI なし(密結合)”class ArticleService{ private ArticleRepository $repository; private EventDispatcher $dispatcher;
public function __construct() { // ハード依存関係 - テストや変更が困難 $this->repository = new ArticleRepository(new XoopsDatabase()); $this->dispatcher = new EventDispatcher(); }}DI あり(疎結合)
Section titled “DI あり(疎結合)”class ArticleService{ public function __construct( private readonly ArticleRepositoryInterface $repository, private readonly EventDispatcherInterface $dispatcher ) {}}PSR-11コンテナ
Section titled “PSR-11コンテナ”基本的な使用方法
Section titled “基本的な使用方法”use Psr\Container\ContainerInterface;
// コンテナを取得$container = \Xmf\Module\Helper::getHelper('mymodule')->getContainer();
// サービスを取得$articleService = $container->get(ArticleService::class);
// サービスが存在するかチェックif ($container->has(ArticleService::class)) { // サービスを使用}コンテナ設定
Section titled “コンテナ設定”use Psr\Container\ContainerInterface;
return [ // シンプルなクラスインスタンス化 ArticleRepository::class => ArticleRepository::class,
// インターフェース実装バインディング ArticleRepositoryInterface::class => ArticleRepository::class,
// ファクトリ関数 ArticleService::class => function (ContainerInterface $c): ArticleService { return new ArticleService( $c->get(ArticleRepositoryInterface::class), $c->get(EventDispatcherInterface::class) ); },
// 共有インスタンス(シングルトン) 'database' => function (): XoopsDatabase { return XoopsDatabaseFactory::getDatabaseConnection(); },];サービス登録
Section titled “サービス登録”オートワイアリング
Section titled “オートワイアリング”// コンテナは型ヒントが利用可能な場合、自動的に依存関係を解決します
class ArticleController{ public function __construct( private readonly ArticleService $service, private readonly ViewRenderer $renderer ) {}}
// コンテナはその依存関係と共にArticleControllerを作成します$controller = $container->get(ArticleController::class);return [ ArticleService::class => [ 'class' => ArticleService::class, 'arguments' => [ ArticleRepositoryInterface::class, EventDispatcherInterface::class, ], 'shared' => true, // シングルトン ],
'article.handler' => [ 'factory' => [ArticleHandlerFactory::class, 'create'], 'arguments' => ['@database'], // 他のサービスを参照 ],];コンストラクタ注入
Section titled “コンストラクタ注入”推奨アプローチ
Section titled “推奨アプローチ”final class ArticleService{ public function __construct( private readonly ArticleRepositoryInterface $repository, private readonly EventDispatcherInterface $dispatcher, private readonly LoggerInterface $logger ) {}
public function create(CreateArticleDTO $dto): Article { $this->logger->info('Creating article', ['title' => $dto->title]);
$article = Article::create($dto); $this->repository->save($article); $this->dispatcher->dispatch(new ArticleCreatedEvent($article));
return $article; }}メソッド注入
Section titled “メソッド注入”オプション依存関係用
Section titled “オプション依存関係用”class ArticleController{ public function __construct( private readonly ArticleService $service ) {}
public function show(int $id, ?CacheInterface $cache = null): Response { $cacheKey = "article_{$id}";
if ($cache && $cached = $cache->get($cacheKey)) { return $this->render($cached); }
$article = $this->service->findById($id);
$cache?->set($cacheKey, $article, 3600);
return $this->render($article); }}インターフェース バインディング
Section titled “インターフェース バインディング”インターフェースを定義
Section titled “インターフェースを定義”interface ArticleRepositoryInterface{ public function findById(int $id): ?Article; public function save(Article $article): void; public function delete(Article $article): void;}実装をバインド
Section titled “実装をバインド”return [ ArticleRepositoryInterface::class => XoopsArticleRepository::class,
// またはファクトリで ArticleRepositoryInterface::class => function (ContainerInterface $c) { return new XoopsArticleRepository( $c->get('database') ); },];DIでテスト
Section titled “DIでテスト”簡単なモッキング
Section titled “簡単なモッキング”class ArticleServiceTest extends TestCase{ public function testCreateArticle(): void { // モックを作成 $repository = $this->createMock(ArticleRepositoryInterface::class); $dispatcher = $this->createMock(EventDispatcherInterface::class); $logger = $this->createMock(LoggerInterface::class);
// モックを注入 $service = new ArticleService($repository, $dispatcher, $logger);
// 期待を設定 $repository->expects($this->once())->method('save'); $dispatcher->expects($this->once())->method('dispatch');
// テスト $dto = new CreateArticleDTO('Title', 'Content'); $article = $service->create($dto);
$this->assertInstanceOf(Article::class, $article); }}XOOPS レガシー統合
Section titled “XOOPS レガシー統合”古いコードと新しいコードをブリッジ
Section titled “古いコードと新しいコードをブリッジ”// レガシーコードのコンテナからサービスを取得function mymodule_get_articles(int $limit): array{ $container = \Xmf\Module\Helper::getHelper('mymodule')->getContainer(); $service = $container->get(ArticleService::class);
return $service->findRecent($limit);}レガシーハンドラーをラップ
Section titled “レガシーハンドラーをラップ”return [ 'article.handler' => function () { return xoops_getModuleHandler('article', 'mymodule'); },
ArticleRepositoryInterface::class => function (ContainerInterface $c) { return new LegacyArticleRepository( $c->get('article.handler') ); },];ベストプラクティス
Section titled “ベストプラクティス”- インターフェースを注入 - 実装ではなく抽象化に依存
- コンストラクタ注入 - セッター注入よりコンストラクタを優先
- 単一責任 - 各クラスは少数の依存関係を持つべき
- コンテナ認識を避ける - サービスはコンテナについて知らないべき
- 設定でコード - ワイアリングに設定ファイルを使用
関連ドキュメント
Section titled “関連ドキュメント”- ../07-XOOPS-4.0/Implementation-Guides/PSR-11-Dependency-Injection-Guide - PSR-11実装
- ../03-Module-Development/Patterns/Service-Layer - サービスパターン
- ../03-Module-Development/Best-Practices/Testing - DIでテスト
- ../07-XOOPS-4.0/XOOPS-4.0-Architecture - アーキテクチャ概要