【ITニュース解説】Solving PHP's Module Coupling Problem: A Journey Into Modular Architecture
2025年09月23日に「Dev.to」が公開したITニュース「Solving PHP's Module Coupling Problem: A Journey Into Modular Architecture」について初心者にもわかりやすく解説しています。
ITニュース概要
PHPアプリ開発で、機能間の繋がりが不明瞭な問題を解決するため、筆者は「PowerModules」を開発した。これは、各機能が利用・提供するものをコードで明示的に定義することで、システムの構造を見やすくし、保守性やチーム開発、将来のマイクロサービス化を容易にするフレームワークだ。
ITニュース解説
PHPアプリケーションを開発する際、コードを機能ごとに分割し、「モジュール」として扱うことが一般的である。例えば、ユーザー管理に関する機能は「ユーザーモジュール」、注文処理に関する機能は「注文モジュール」といった具合だ。しかし、これらモジュール間の「依存関係」、つまりあるモジュールが別のモジュールの機能を使っているという関係が、PHPの多くのアプリケーションでは曖昧になりがちだった。この曖昧さが、長年にわたり多くの開発者を悩ませてきた問題である。
具体的に考えてみよう。例えば、注文処理を行うOrderServiceというクラスが、ユーザー情報を扱うUserService、決済を管理するPaymentService、在庫を管理するInventoryService、メール送信を行うEmailServiceといった複数のサービスに依存しているとする。これらの依存関係は、通常「依存性注入(DI)コンテナ」という仕組みを通じて間接的に設定されるため、コード上ではOrderServiceのコンストラクタに他のサービスが注入されているように見えるだけだ。
この「見えない依存関係」は、いくつかの深刻な問題を引き起こす。まず、UserServiceの内容を変更(リファクタリング)しようとする際、それがOrderServiceにどのように影響するかをコードを見ただけでは判断できない。思わぬところでエラーが発生する可能性がある。次に、OrderServiceの機能だけをテストしたい場合、依存する四つのサービスも同時に動作させるか、それぞれを「モック」(本物の代わりにテスト用のダミー)に置き換える必要があり、テストコードが複雑になる。さらに、もし将来的に決済機能を独立したマイクロサービスとして切り出したいと考えた場合、どのモジュールがPaymentServiceに依存しているかを把握するのが困難で、切り出し作業が非常にリスクを伴うものとなる。新しい開発者がプロジェクトに参加した際も、どの部分が何に依存しているのかが把握しづらく、コードの全体像を理解するのに時間がかかってしまう。
このような問題意識から、筆者は他のプログラミング言語やフレームワークの仕組みに目を向けた。例えば、Webフロントエンド開発で使われるAngularというフレームワークでは、@NgModuleという仕組みを通じて、モジュールが他のどのモジュールを「インポート」し、自身のどの機能を外部に「エクスポート」するのかを明示的に宣言する。また、JavaのOSGiというモジュールシステムでも、各バンドル(モジュール)がインポートするパッケージとエクスポートするパッケージを明確に記述する。これらの事例から得られた共通の教訓は、「成功しているモジュールシステムは、依存関係をコード上で明示的にする」という点だった。
この教訓をPHPの世界にもたらすべく、筆者は「PowerModules」というフレームワークを開発した。その核心となるアイデアは三つある。
一つ目は、「各モジュールが独自の依存性注入コンテナを持つ」という点だ。従来の多くのフレームワークでは、アプリケーション全体で一つの大きなDIコンテナが使われていた。しかし、PowerModulesでは、OrderModuleならOrderModule専用の、UserModuleならUserModule専用のDIコンテナを持つ。これにより、各モジュールが内部で利用するサービスは、そのモジュールのコンテナ内で完結し、他のモジュールからは直接アクセスできないようになる。これは、モジュール内部のカプセル化を強力に促進する。
二つ目は、「明示的なエクスポート契約」だ。もしあるモジュールが、自身のコンテナ内で定義したサービスを他のモジュールに提供したい場合、そのサービスを明示的に「エクスポート」として宣言する必要がある。例えば、UserModuleが提供するUserServiceだけを外部に公開し、内部で使われるPasswordHasherのようなサービスは公開しない、といった具合だ。これにより、モジュールの外部インターフェースが明確になる。
三つ目は、「明示的なインポート契約」だ。他のモジュールのサービスを利用したい場合、利用側のモジュールは、どのモジュールからどのサービスを「インポート」するのかを明示的に宣言しなければならない。例えば、OrderModuleがUserModuleのUserServiceとPaymentModuleのPaymentServiceを使う場合、それぞれを具体的に指定する。このインポートとエクスポートの仕組みにより、OrderServiceが依存するUserServiceやPaymentServiceがどこから提供されるのかが、コード上で見てすぐにわかるようになる。従来の隠れた依存関係は解消され、アーキテクチャ全体が透明になるのだ。
さらにPowerModulesは、「PowerModuleSetup」という独自のパターンを導入した。これは、ルーティングやイベント処理、ロギングといった、アプリケーション全体に関わる共通機能(クロスカッティング機能)を、各モジュールの内部実装に触れることなく、かつモジュールの分離性を保ちながら追加するための仕組みだ。例えば、各モジュールが定義したルート情報を一箇所に集めてルーティングシステムに登録するような処理を、このPowerModuleSetupを使って実現できる。これにより、各モジュールは自身の専門機能に集中しつつ、共通のインフラ機能の恩恵を受けられる。
このフレームワークを開発する過程で、筆者はいくつかの重要な学びを得た。最も複雑だったのは「依存関係の解決」だ。モジュール間のインポート/エクスポート関係から依存グラフを構築し、循環依存(AがBに依存し、BがAに依存するような状態)を検出・回避し、モジュールが正しい順序で読み込まれるように「トポロジカルソート」というアルゴリズムを実装する必要があった。また、「二段階ローディング」が不可欠であることも判明した。これは、まず全てのモジュールのエクスポート可能なサービスを収集し、その後で初めて各モジュールのインポートを解決し、PowerModuleSetupを適用するという二段階のプロセスを踏むことで、全ての依存関係が正しく解決されるようにする仕組みだ。コンテナの階層構造も重要で、各モジュールが分離されたコンテナを持つ一方で、エクスポートされたサービスはルートコンテナからアクセスできるように「エイリアス」(別名)として登録される。これら全ての学びは、「明示的な設計が、隠れた設計よりもはるかに優れている」という結論を裏付けるものだった。
PowerModulesのようなアプローチは、実際の開発現場に大きなメリットをもたらす。例えば、大規模なチームで開発を行う場合、チームごとに担当モジュールを分けることで、各チームはモジュール間の明確な「契約」(インポート/エクスポート)に基づいて独立して作業を進められるようになる。テストの面でも、あるモジュールだけをテストしたいときに、そのモジュールが依存する他のモジュールをモック(偽物)に置き換えることが容易になり、テストが簡単になる。さらに、将来的にアプリケーションの一部をマイクロサービスとして独立させたいと考えた場合、モジュール間のインポート/エクスポート契約がそのままマイクロサービス間のAPI契約へと自然に発展させられるため、移行がスムーズに進む可能性が高まる。
PowerModulesのコアコードは約1,600行と比較的コンパクトで、PHPの最新バージョン(8.4+)と厳格な型付けに対応し、高い品質基準で開発されている。主要な構成要素としては、モジュールの基本的なインターフェースであるPowerModule、カスタマイズ可能なDIコンテナであるConfigurableContainer、アプリ構築のフローを管理するModularAppBuilder、そして拡張システムであるPowerModuleSetup、依存関係を宣言するImportItemなどが挙げられる。
既存のPHPフレームワークと比較すると、SymfonyのBundlesはサービス定義をYAMLファイルに記述するため、依存関係がコード上からは見えにくい傾向がある。PowerModulesでは、インポート宣言によって依存関係がコード内で明確になる。Laravelのサービスプロバイダも、デフォルトではグローバルなDIコンテナを共有するが、PowerModulesは各モジュールに分離されたコンテナを提供することで、より厳格なカプセル化を実現している。
このフレームワークの開発を通じて得られた最大の教訓は、依存性注入やモジュールシステム、アーキテクチャパターンについて、既存のツールを使うだけでなく、自ら手を動かして構築することで、より深く理解できたということだ。明示的な依存関係、強制されるモジュール境界、カプセル化を損なわずにクロスカッティングな機能を追加する仕組み、そしてモノリスからマイクロサービスへの進化をサポートするアーキテクチャの重要性。これらは、複雑なPHPアプリケーション開発において直面する現実的な問題に対する有効な解決策となりうると筆者は考えている。