---
title: "モジュール"
description: "Nibiru モジュールの構築 — MMVC の 2 番目の M。特性、プラグイン、インターフェース、設定、オブザーバー。"
---

A **モジュール**は自己完結型のドメイン単位です。各モジュールは、設定、プラグイン（サービス）、トレイト（再利用可能なメソッド）、インターフェースを所有しており、必要に応じて独自のMVCスライスも持つことができます。Nibiruは、サービスコンテナなしで*太いコントローラー*問題を回避するために、モジュールを使用しています。

<figure>
  <img src="/img/modules-anatomy.png" alt="Nibiru モジュールの植物学的解剖、ローズの重ねられた花びらのように描かれており — 外側の花びら、中間の花びら、内側の花びら、萼層、受精器。" />
  <figcaption>各層別にモジュール。</figcaption>
</figure>

## 構造
```
application/module/<name>/
├── <name>.php          # main class (implements IModule, optionally SplSubject)
├── interfaces/         # contracts for plugins / external consumers
│   └── <name>.php
├── plugins/            # stateless services usable from controllers
│   ├── <thing>.php
│   └── <other>.php
├── settings/           # auto-discovered .ini files
│   ├── <name>.ini
│   └── <name>.production.ini
└── traits/             # reusable method groups
    └── <name>.php
```
`Registry` は起動時に `application/module/` を走査し、各モジュールの `settings/*.ini` を発見します。モジュール名（大文字）に一致するセクションを解析し、`Registry::getInstance()->loadModuleConfigByName('users')` 経由で検索するためにキャッシュします。

## 最小限のモジュール
```php
<?php
namespace Nibiru\Module\Users;

use Nibiru\Module as ModuleAdapter;
use Nibiru\Interfaces\IModule;
use Nibiru\Registry;

class Users extends ModuleAdapter implements IModule, \SplSubject
{
    use Traits\Users;

    const CONFIG_MODULE_NAME = 'users';

    protected static \stdClass $usersRegistry;
    protected \SplObjectStorage $observers;

    public function __construct() {
        $this->setUsersRegistry();
        $this->observers = new \SplObjectStorage();
    }

    public function attach(\SplObserver $o): void { $this->observers->attach($o); }
    public function detach(\SplObserver $o): void { $this->observers->detach($o); }
    public function notify(): void {
        foreach ($this->observers as $o) { $o->update($this); }
    }

    protected function setUsersRegistry(): void {
        self::$usersRegistry = Registry::getInstance()
            ->loadModuleConfigByName(self::CONFIG_MODULE_NAME);
    }
}
```
`IModule` インターフェースは意図的にマーカーです — 実際の動作はすべてトレイトとプラグインにあります。

## プラグイン：ステートレスサービス

プラグインは、モジュール機能にアクセスするためにコントローラーがインスタンス化できるクラスです：
```php
<?php
// application/module/users/plugins/user.php
namespace Nibiru\Module\Users\Plugin;

use Nibiru\Module\Users\Users;
use Nibiru\Pdo;

class User extends Users
{
    public function isAuthorized(): bool {
        return isset($_SESSION['auth']['user_id']);
    }

    public function checkForStandardUser(): bool {
        return $this->isAuthorized()
            && ($_SESSION['auth']['role'] ?? '') === 'standard';
    }
}
```
コントローラー内で：
```php
$this->user = new \Nibiru\Module\Users\Plugin\User();
if (!$this->user->isAuthorized()) {
    View::forwardTo('/login');
}
```
プラグインはモジュールクラスを継承するので、レジストリ、設定、およびオブザーバーの機能にアクセスできます。

## 特性: 再利用可能なメソッド

トレイトは、モジュールクラスが必要な再利用可能なメソッド本体を保持します。一般的なパターン：フォームファクトリーです。
```php
<?php
// application/module/users/traits/users.php
namespace Nibiru\Module\Users\Traits;

use Nibiru\Form;

trait Users
{
    public function loginForm(): string {
        Form::create();
        Form::addOpenDiv(['class' => 'form-group']);
        Form::addInputTypeText([
            'class' => 'form-control', 'name' => 'login',
            'placeholder' => 'Username',
        ]);
        Form::addCloseDiv();
        Form::addOpenDiv(['class' => 'form-group']);
        Form::addInputTypePassword([
            'class' => 'form-control', 'name' => 'password',
            'placeholder' => 'Password',
        ]);
        Form::addCloseDiv();
        return Form::addForm([
            'method' => 'POST',
            'action' => '/login',
            'name'   => 'loginForm',
        ]);
    }
}
```
メインの `Users` クラスは、`use Traits\Users;` を通じて取り込みます。

## モジュール設定 (INI)

各モジュールは独自の INI ファイルを持つことができます。Registry は `settings/` 内のすべての `*.ini` を解析し、モジュール名（大文字）で命名されたセクションを探します。
```ini
; application/module/users/settings/users.ini
[USERS]
session.lifetime    = 7200
password.min.length = 12
allowed.roles[]     = "admin"
allowed.roles[]     = "editor"
allowed.roles[]     = "standard"
```
どこからでも読み返す:
```php
$cfg = \Nibiru\Registry::getInstance()->loadModuleConfigByName('users');
$cfg->session_lifetime;     // 7200
$cfg->password_min_length;  // 12
$cfg->allowed_roles;        // [admin, editor, standard]
```
環境オーバーレイ：`APPLICATION_ENV=production` の場合、`users.production.ini` が `users.ini` よりも優先されます。

## 観察者パターン

`SplSubject` を実装するモジュールは、イベントをアタッチされたオブザーバーにブロードキャストすることができます。デモンストレーションでは、`analytics` モジュールは各ページビュー時にアタッチされたトラッカーに通知します。
```php
// in a controller
$analytics = new \Nibiru\Module\Analytics\Analytics();
$analytics->attach(new \Nibiru\Module\Analytics\Plugin\Matomo());
$analytics->attach(new \Nibiru\Module\Analytics\Plugin\Plausible());

$analytics->trackPageView();   // internally calls notify()
```
各観察者の `update($subject)` は、分析インスタンスを受け取り、関心のあるイベントデータを取得します。

## 実際の生産モジュール

展示アプリケーションから：

- **`auth`** (TPMS) — セッション管理、QRコードログイン、ロールベースのアクセス。`SplSubject` を実装してログイン/ログアウトイベントをログ記録や監査モジュールに拡散させます。
- **`cms`** (prod.maschinen-stockert.de) — コントローラパス + 言語でキー付けされたコンテンツストア。非開発者がサイトのコピーを更新できるようにします。
- **`graph_mail`** (TPMS) — トランザクションメール用の Microsoft Graph API ラッパー。
- **`pdfgenerator`** (prod.maschinen-stockert.de) — DBドライブテンプレートから機械カタログを生成します。
- **`machineryscout`** — インデックス管理トレイトを使用した Elasticsearch インデックス管理。
- **`assetmanager`** — 中央の CSS/JS パイプライン、言語ごとにテーマをスワップするために使用されます。
- **`analytics`** — オブザーバドライブトラッカー拡散（Matomo 等）。

## モジュール登録: フレームワークがモジュールを見つける方法

ディスク上のモジュールフォルダは必要ですが、十分ではありません。フレームワークもそれを**読み込むように**知らなければなりません — これは、あなたの `[AUTOLOADER]` 設定で3つの位置配列で行われます：
```ini
; application/settings/config/settings.development.ini

[AUTOLOADER]
iface.pos[]        = "users"      ; load application/module/users/interfaces/
iface.pos[]        = "billing"    ; load application/module/billing/interfaces/
trait.pos[]        = "users"      ; load application/module/users/traits/
trait.pos[]        = "billing"
class.pos[]        = "users"      ; load application/module/users/users.php
class.pos[]        = "billing"
class.plugin.pos[] = ""           ; reserved
```
名前は**小文字のフォルダ名**であり、`application/module/`の下に表示される完全に同じです。フレームワークの`Auto::loader()`（[Dispatcher](/en/core/dispatcher/)から呼び出されます）は、各モジュールを順番に読み込みます。

### プラグインの名前空間規約

プラグインクラスは、**複数形の**名前空間 `Plugins` の下にあります：
```php
// application/module/billing/plugins/invoice.php
namespace Nibiru\Module\Billing\Plugins;     // ← plural
class Invoice extends \Nibiru\Module\Billing\Billing { /* ... */ }
```
名前空間が一致しないと、オートローダーのミスが発生します。CLI スキャフォールド (`./nibiru -m billing`) は正しい名前空間を生成してくれます。

### 設定の発見

モジュールの INI ファイルを **登録しないでください** — [Registry](/en/core/registry/) は、`[AUTOLOADER]` がモジュールクラスを読み込んだ後、`application/module/<name>/settings/*.ini` を走査して自動的に発見します。各 INI の `[<MODULE>]`（大文字）セクションは次のようになります：
```php
$cfg = \Nibiru\Registry::getInstance()->loadModuleConfigByName('billing');
$cfg->invoice_prefix;            // [BILLING] invoice.prefix → property
```
レジストリは、`APPLICATION_ENV` が一致する場合に `<module>.<env>.ini`（例：`billing.production.ini`）を優先します。

### マイグレーション

モジュールにデータベーステーブルがある場合、既存の範囲を超えて番号を付けたSQLファイルを`application/settings/config/database/`に配置してください。AIモジュールで使用されている規約です。
```
200-ai_rag_collection.sql
201-ai_rag_chunk.sql
202-ai_conversation.sql
203-ai_message.sql
```
`./nibiru -mi local`で実行します。フレームワークは、`[GENERATOR] database = true`が設定されている場合、`application/model/<table>.php`にモデルを自動生成し、`\Nibiru\Pdo::fetchAll(…)`で使用できるようにします。

## モジュールの生成
```bash
./nibiru -m billing
```
スキャフォールド:
```
application/module/billing/
├── billing.php
├── interfaces/billing.php
├── plugins/
├── settings/billing.ini
└── traits/
```
Graylogのログフックをスキャフォールドに事前に組み込む場合は、`-g` スイッチ（`./nibiru -m billing -g`）を追加してください。

## モジュールを作成しない場合

すべてのコントローラーをモジュールに昇格させないでください。閾値は次の通りです： *少なくとも1つのトレイト、1つのプラグイン、1つのINIキーを持っていますか？* はいの場合、独自のモジュールフォルダを獲得します。そうでない場合は、共有トレイトファイルまたは小さな`application/lib/`ヘルパーが十分です。
