サービスコンテナ


a-blog cms は横断的な機能(認証、キャッシュ、ロギング、メール、Markdown 変換など)を「サービス」として DI コンテナ(Acms\Services\Container)に登録し、Facades/ 配下の静的アクセスポイント(例: Config::get()、Media::upload())経由で利用する構成になっています。ここではコンテナの基本 API と、Ver. 3.2.30 で追加されたサービス差し替え API(override / extend)を合わせて紹介します。

サービスを取得する

基本的には Facades/ 配下の各ファサード(Config、Media、Auth など)を経由して使います。コンテナに登録されたエイリアス名が分かっていれば、Application ファサード経由で直接取得することもできます。

use Acms\Services\Facades\Application as App;

// エイリアス名を指定して直接取得する
$converter = App::make('markdown');
$html = $converter->convert('# Hello');

// 登録済みかどうかを調べる
if (App::exists('markdown')) {
    // ...
}

サービスを登録する — bind() と singleton()

bind() は呼び出すたびに新しいインスタンスを生成し、singleton() は初回だけ生成して以降はキャッシュを返します。第 2 引数にはクラス名またはファクトリクロージャを渡せます。

// 呼び出すたびに新しいインスタンス
App::bind('my.service', MyService::class);

// 初回だけ生成してキャッシュする
App::singleton('my.other_service', function () {
    return new MyOtherService(App::make('config'));
});

プラグイン(拡張アプリ)の ServiceProvider は ACMS_App を継承し、register() は呼ばれず init() のみが呼ばれます。新しいサービスをコンテナに登録したい場合も、init() 内で bind() / singleton() を呼んでください。

生成直後に処理を差し込む — bootstrap()

bootstrap() で登録したコールバックは、対象サービスが make() で解決されるたびに、生成済みのインスタンスを引数に呼び出されます。共通の初期化処理をコンテナ側に寄せたいときに使います。

App::bootstrap('my.service', function ($instance) {
    $instance->setLogger(App::make('logger'));
});

既存サービスを差し替える — override() と extend()

対応バージョン: Ver. 3.2.30 以上で利用できます。すでに登録済みのサービスをプラグイン側から差し替え・デコレーションするための正式な API です。bind() / singleton() を直接呼んで上書きするのではなく、こちらを使うことで owner・priority による競合検出と、ファサード側のキャッシュ自動無効化の恩恵を受けられます。まるごと別実装に差し替えたいときは override()、既存実装は残したまま前後に処理を差し込みたいときは extend() を使います。

override() — サービスを丸ごと差し替える

Markdown 変換サービス(markdown エイリアス)を league/commonmark ベースの独自実装に丸ごと差し替える例です。

App::override('markdown', MyCommonMarkConverter::class, [
    'owner' => 'Acms\Plugins\MyPlugin',
]);

override は登録済みのエイリアスにのみ使えます。未登録のエイリアスに対して呼ぶとタイポとみなしエラーになります。owner は必須で、同じエイリアスに対して同一 priority の override が複数登録されると競合として扱われます(デバッグモードでは例外、本番では警告ログを残して先に登録した側が勝ちます)。

extend() — 前後処理を差し込む

既存の実装は残したまま、Markdown 変換の前後にログを差し込む例です。デコレーターはラップ対象($inner)を必ず保持し、担当外の処理は inner に委譲します。inner を無視すると他のプラグインのデコレーションを潰してしまいます。

namespace Acms\Plugins\MyPlugin;

use Acms\Services\Markdown\Contracts\Converter;
use AcmsLogger;

class LoggingMarkdownConverter implements Converter
{
    public function __construct(
        private readonly Converter $inner // ラップ対象。担当外の処理は必ずここへ委譲する
    ) {
    }

    public function convert(string $markdown): string
    {
        $start = microtime(true);
        $html = $this->inner->convert($markdown);
        AcmsLogger::info('markdown convert', [
            'bytes' => strlen($markdown),
            'time' => microtime(true) - $start,
        ]);
        return $html;
    }
}

登録するタイミング

bind() / singleton() / override() / extend() いずれも、プラグインから登録する正しいタイミングは ServiceProvider::init() 内です。コアの主要ファサードはこの時点で既に解決・キャッシュ済みですが、再登録と同時にファサード側のキャッシュが自動で破棄されるため、init() で登録すれば確実に反映されます。手動でのキャッシュクリアは不要です。

namespace Acms\Plugins\SamplePlugin;

use ACMS_App;
use Acms\Services\Facades\Application as App;

class ServiceProvider extends ACMS_App
{
    /* 省略... */

    public function init()
    {
        // (1) デコレーション: Markdown 変換の前後にログを差し込む
        App::extend('markdown', function ($inner) {
            return new LoggingMarkdownConverter($inner);
        }, ['owner' => 'Acms\Plugins\SamplePlugin', 'priority' => 10]);

        // (2) 丸ごと差し替え: 変換器自体を league/commonmark ベースに置換
        App::override('markdown', MyCommonMarkConverter::class, [
            'owner' => 'Acms\Plugins\SamplePlugin',
        ]);
    }
}

参照実装: markdown サービス

override() / extend() 自体は markdown 専用ではなく、コンテナに登録済みの任意のエイリアスに使える汎用 API です。ただし、markdown サービス(Acms\Services\Markdown\Contracts\Converter)は、Common::parseMarkdown() 経由・Twig の {% markdown %} タグ経由・ユニットモデル経由という全ての呼び出し経路をこのサービス 1 つに統一した上で新設されており、override / extend の内容がどの経路からでも確実に反映されることが検証済みの参照実装です。他のコアサービスに override / extend を使う場合は、対象が Facade 経由でのみ利用されているか(内部で直接インスタンスを保持・生成している箇所が残っていないか)を事前に確認してください。

ポイント

  • bind() は毎回新規生成、singleton() は初回のみ生成してキャッシュします。用途に応じて使い分けてください。

  • プラグインの ServiceProvider は init() のみが呼ばれます(register() は呼ばれません)。サービスの登録・差し替えはすべて init() 内で行ってください。

  • override の owner は必須です。差し替え元の特定に使われ、競合時のエラーメッセージにも含まれます。priority が同じ override 同士は競合として扱われるため、優先順位を明示したいときは priority を指定してください。

  • extend() は積み重ね可能です。適用順は priority 降順に内側(元実装に近い側)から、同 priority は登録順(先に登録した方が内側)になります。

  • override / extend の登録は既存 bind() / singleton() で再登録すると破棄されます。逆に override / extend を呼んでも既存の bind() / singleton() 自体の挙動は変わりません。