壊れにくいDrupalの作り方 モジュラーモノリス実践ガイド
新田 幸子
最近、社内ポータルの設計に取り組んでいます。Drupalというモノリスをいかに壊れにくく作るか?試行錯誤の末にたどり着いたのは、Drupalが元々持っているモジュールの仕組みをフル活用する、つまり「モジュラーモノリス」の考え方を意識的に取り入れるという結論でした。
本記事では、モジュラーモノリスの考え方を整理した上で、Drupalのモジュール設計に活かすための実践的な指針を紹介します。
モジュラーモノリスとは
ひとことで言えば、1つのアプリケーションの中に明確なモジュール境界を設ける設計です。マイクロサービスのように分散システムの複雑さを抱えることなく、モジュール間の独立性を高められます。Shopifyがマイクロサービスではなくこのアプローチを選択したことで話題になりました。(参考: Shopifyにおけるモジュラモノリスへの移行(Qiita))
主なメリットは以下のとおりです。
- ネットワーク遅延がない。 モジュール間の呼び出しはプロセス内で完結するため、マイクロサービスのようなサービス間通信のオーバーヘッドがない。
- デプロイが単純。 デプロイ対象が1つなので、サービス間のバージョン整合性を気にする必要がない。
- トランザクション管理が容易。 分散トランザクションを考えなくてよい。
- テスト・デバッグがしやすい。 単一プロセスで動くため、マイクロサービスに比べて問題の再現や追跡が簡単。
- モジュール単位で責務が明確になる。 モノリスの手軽さを維持しつつ、コードの見通しがよくなる。
Shopifyはこの構造を持っていなかったRailsに、後からモジュール境界を導入するために大きな労力を費やしました。専用のツール(Packwerk)まで自作しています。
Drupalはすでにモジュラーモノリス
Drupalエンジニアの方ならお気づきかと思いますが、Drupalのモジュール構造はまさにモジュラーモノリスそのものです。コア・contribモジュール・カスタムモジュールという階層構造があり、サービスやイベントを通じてモジュール同士が連携します。Shopifyが後から苦労して手に入れた構造を、Drupalは最初から持ってます。
でも、現場では...
せっかくモジュールシステムという強力な仕組みがあるのだから、それを活かさない手はありません。しかし実際のプロジェクトでは、こんなことが起きがちです。
- 1つのカスタムモジュールに何でもかんでも詰め込んでしまう。 「custom_utils」のような万能モジュールが生まれ、どんどん肥大化する。
- モジュール間の依存がグチャグチャになる。 どのモジュールがどのモジュールに依存しているか、誰も把握できない状態に。
- hookでどこからでも何でも触れてしまう。 便利な反面、意図しない場所から別モジュールのデータを書き換えてしまう。
仕組みがあるだけでは不十分で、意識して境界を守らないとモノリスの沼に落ちるということです。
境界を守るためにできること
実践の中で効果を感じている指針を3つ挙げます。
ドメイン境界でモジュールを分ける
申請フォームと承認ワークフローは一緒に使われる機能ですが、ドメインとしては別物です。「一緒に使うから1つにまとめる」ではなく、「責務が違うなら分ける」と考えます。(DDDでいう「境界づけられたコンテキスト(Bounded Context)」の考え方に近いです。)
❌ Bad: 1つのモジュールに全部入り
custom_workflow/ ← 何でも入りモジュール
├── 申請フォームのロジック
├── 承認ワークフローのロジック
└── 通知のロジック
⭕ Good: ドメインごとにモジュールを分割
application_form/ ← 申請フォームのロジック
|
v
approval_workflow/ ← 承認ワークフローのロジック
|
v
notification/ ← 通知のロジック
1つのモジュール内でMVCを完結させる
Controller、テンプレート、ロジックがモジュール内に揃っていれば、そのモジュールだけ見ればその機能を理解できます。技術レイヤーではなくドメインでコードをまとめる発想です。(ソフトウェア設計でいう「高凝集(High Cohesion)」の実践です。)
❌ Bad: 技術レイヤーで分割(どこに何があるかわからない)
modules/custom/
├── controllers/
│ ├── ApplicationFormController.php
│ └── ApprovalWorkflowController.php
├── services/
│ ├── ApplicationFormService.php
│ └── ApprovalWorkflowService.php
└── templates/
├── application-form.html.twig
└── approval-workflow.html.twig
⭕ Good: ドメインで分割(モジュール内にMVCが揃う)
modules/custom/
├── application_form/
│ ├── src/Controller/ApplicationFormController.php
│ ├── src/Service/ApplicationFormService.php
│ └── templates/application-form.html.twig
└── approval_workflow/
├── src/Controller/ApprovalWorkflowController.php
├── src/Service/ApprovalWorkflowService.php
└── templates/approval-workflow.html.twig
依存を一方向にする
たとえば「申請フォーム」モジュールと「承認ワークフロー」モジュールがあるとき、申請フォームから承認ワークフローを参照するのはOKだけど、逆方向の依存は作らない。方向を決めるだけで構造が劇的に整理されます。(これはRobert C. Martinが『Clean Architecture』で提唱する「非循環依存の原則(ADP)」にあたります。)
❌ Bad: 双方向に依存している
申請フォーム <--依存--> 承認ワークフロー
⭕ Good: 一方向の依存
申請フォーム --依存--> 承認ワークフロー
# application_form.info.yml
dependencies:
- approval_workflow # 申請フォーム → 承認ワークフローへの依存のみ
# approval_workflow.info.yml
dependencies: [] # 逆方向の依存はなし
境界を超えて連携するには
モジュールを分けたら、次に考えるのは「どうやって連携するか」です。境界を守りつつモジュール間で連携する方法は、大きく2つあります。
相手を知っている場合 → サービスコンテナ経由で呼ぶ
モジュールが公開するServiceをDI(依存性注入)で利用します。依存が明示的になり、.info.ymlのdependenciesとも一致します。
// application_form モジュールから approval_workflow のサービスを利用する
class ApplicationFormService {
public function __construct(
private ApprovalWorkflowService $approvalWorkflow,
) {}
public function submit(array $data): void {
// 申請データを保存した後、承認ワークフローを開始
$this->approvalWorkflow->start($data);
}
}
相手を知らなくていい場合 → イベントで疎結合にする
「承認が完了した」というイベントをdispatchし、通知モジュールがsubscribeする形です。呼び出す側が相手を知らないので、依存の方向が片方向に保てます。
// approval_workflow モジュール:イベントを発火するだけ
$event = new ApprovalCompletedEvent($entity);
$this->eventDispatcher->dispatch($event);
// notification モジュール:イベントを受け取って通知する
class ApprovalNotificationSubscriber implements EventSubscriberInterface {
public static function getSubscribedEvents() {
return [ApprovalCompletedEvent::class => 'onApprovalCompleted'];
}
public function onApprovalCompleted(ApprovalCompletedEvent $event): void {
// 承認完了の通知を送る
}
}
拡張ポイントを提供する場合 → プラグインで差し替え可能にする
Drupalのプラグインシステムも、親が子を知らなくていい仕組みとして強力です。プラグインマネージャーがインターフェースに基づいて実装を自動検出するため、呼び出す側は具体的な実装を知る必要がありません。社内ポータルでもカスタムプラグインを随所で活用しています。
// プラグインマネージャー経由で実装を取得(具体的なクラスを知らなくていい)
$plugins = $this->approvalStepManager->getDefinitions();
foreach ($plugins as $plugin_id => $definition) {
$instance = $this->approvalStepManager->createInstance($plugin_id);
$instance->execute($entity);
}
hookの使いすぎに注意
hookはDrupalの伝統的な連携手段ですが、どこからでも何でも触れてしまうため、モジュール間の境界を簡単に壊します。
ありがちなのが「Aを更新したらBも更新する」というケースです。
❌ Bad: AのpostSaveフックでBを直接更新する
// module_a.module
function module_a_entity_postsave(EntityInterface $entity) {
// AのhookからBを直接更新 → AがBの内部構造を知ってしまう
$b = Node::load($entity->get('field_related')->target_id);
$b->set('field_status', 'updated');
$b->save();
}
この書き方だと、AがBの内部構造(フィールド名やロジック)を知っている必要があり、BのフィールドやロジックAを変更したときにAのhookも壊れます。hookが増えるほど影響範囲が追えなくなります。
⭕ Good①: イベントで疎結合にする
// module_a: イベントを発火するだけ。Bの存在を知らない
$event = new EntityAUpdatedEvent($entity);
$this->eventDispatcher->dispatch($event);
// module_b: イベントを受け取って自分自身を更新する
class ModuleBSubscriber implements EventSubscriberInterface {
public static function getSubscribedEvents() {
return [EntityAUpdatedEvent::class => 'onEntityAUpdated'];
}
public function onEntityAUpdated(EntityAUpdatedEvent $event): void {
// Bの更新ロジックはBの中に閉じる
}
}
⭕ Good②: AとBの更新をまとめるサービスを作る
// 上位のサービスがAとBの更新をオーケストレーションする
class SyncService {
public function __construct(
private ModuleAService $moduleA,
private ModuleBService $moduleB,
) {}
public function updateBoth(array $data): void {
$this->moduleA->update($data);
$this->moduleB->syncWith($data);
}
}
hookに頼りすぎると依存関係が暗黙的になり、影響範囲が追えなくなります。境界を意識した設計では、サービス・イベント・プラグインを優先し、hookは最小限に留めるのがおすすめです。
⭕ Good: 連携手段を使い分ける
申請フォーム --サービス経由--> 承認ワークフロー
|--イベント発火--> 通知
|--プラグイン----> 承認ステップの実装
基本はサービス経由、モジュールが相手を知る必要がないケースはイベントやプラグイン、という使い分けがシンプルです。
まとめ
Drupalはモジュラーモノリスの仕組みを最初から備えています。しかし、仕組みがあるだけでは不十分で、意識して境界を設計しなければモノリスの沼に落ちてしまいます。
本記事で紹介した指針をまとめます。
- ドメイン境界でモジュールを分ける。 「一緒に使うから」ではなく「責務が違うなら分ける」。
- 1モジュール内でMVCを完結させる。 技術レイヤーではなくドメインでまとめる。
- 依存は一方向に保つ。 循環依存を避けるだけで構造が整理される。
- 連携にはサービス・イベント・プラグインを使い分ける。 hookに頼りすぎない。
社内ポータルの設計を通じて実感したのは、Drupalのモジュールシステムは正しく使えば十分に強力だということです。特別なツールを導入しなくても、設計の意識を変えるだけでコードの見通しは大きく改善します。
新田 幸子/ Drupalエンジニア
北海道在住のDrupalエンジニアです。バックエンドを主に担当しています。三度の飯よりAIが好きです。趣味はドライブ、猫と遊ぶことです。


