プラグイン(拡張アプリ)

目次

名前空間

プラグイン(拡張アプリ)での拡張方法を見て行く前に、名前空間について説明します。

名前空間

ファイルのパスと命名規則はPSR4に従います。(ただし下位互換性を保つため一部したがっていない場合があります)

extension/plugins の名前空間は Acms\Plugins になります。

パス extension/plugins/SamplePlugin/ServiceProvider.php

クラス名 Acms\Plugins\SamplePlugin\ServiceProvider
記述例 <?php

namespace Acms\Plugins\SamplePlugin;

class ServiceProvider extends \ACMS_App
{
}

プラグイン(拡張アプリ)開発の基本

プラグイン(拡張アプリ)として拡張機能を作ることにより、様々な拡張を1パッケージにして、ブログ単位でインストール出来るようになります。

拡張できる機能

  • 校正オプションの拡張
  • フック処理の拡張
  • Validatorの拡張
  • 独自GETモジュールの追加
  • 独自POSTモジュールの追加
  • テンプレートの挿入(テンプレートの拡張)

最低限必要なファイル

プラグイン(拡張アプリ)を作るためには、以下のファイルが最低限必要になります。

  • extension/プラグイン(拡張アプリ)のディレクトリ/ServiceProvider.php

このファイルがあれば、最小限のプラグイン(拡張アプリ)として成立し、管理画面からインストールもできるようになります。このファイルは、アプリのバージョンや説明やインストール・アップデート時の処理や起動の初期処理を記述できるファイルになっています。詳しく見ていきましょう。

ServiceProvider.php

下のコードは、 ServiceProvider.php の 基本的なコードになります。

<?php

namespace Acms\Plugins\SamplePlugin;

use ACMS_App;

class ServiceProvider extends ACMS_App
{
    /**
     * @var string
     */
    public $version = '1.0.0';

    /**
     * @var string
     */
    public $name = 'SamplePlugin';

    /**
     * @var string
     */
    public $author = 'com.appleple';

    /**
     * @var bool
     */
    public $module = false;

    /**
     * @var bool|string
     */
    public $menu = false;

    /**
     * @var string
     */
    public $desc = 'サンプルのプラグインです。';

    /**
     * サービスの初期処理
     */
    public function init()
    {

    }

    /**
     * インストールする前の環境チェック処理
     *
     * @return bool
     */
    public function checkRequirements()
    {
        return true;
    }

    /**
     * インストールするときの処理
     * データベーステーブルの初期化など
     *
     * @return void
     */
    public function install()
    {

    }

    /**
     * アンインストールするときの処理
     * データベーステーブルの始末など
     *
     * @return void
     */
    public function uninstall()
    {

    }

    /**
     * アップデートするときの処理
     *
     * @return bool
     */
    public function update()
    {
        return true;
    }

    /**
     * 有効化するときの処理
     *
     * @return bool
     */
    public function activate()
    {
        return true;
    }

    /**
     * 無効化するときの処理
     *
     * @return bool
     */
    public function deactivate()
    {
        return true;
    }
}

プロパティ


プロパティ名 説明
$version バージョンを指定します。バージョン表記に決まりはありませんが、セマンティックバージョニングで付けるといいと思います。
$name プラグイン(拡張アプリ)の名前を指定します。ここの名前が管理画面に表示されます。特に命名規則はありません
$author 作者を指定します。
$module ここにString(英数字)を指定すると、その名前でモジュールID作成の時、選択肢として表示されます。(非推奨)
$menu ここにString(英数字)を指定すると、そのURLでプラグイン(拡張アプリ)の管理画面が作成されます。
例: 'sample_index'
URL: /bid/1/admin/app_sample_index/
パス: themes/system/admin/app/sample/index.html
$desc プラグイン(拡張アプリ)の詳細を記述します。管理画面で表示されます。

メソッド


メソッド名 説明
init インストールされた場合に動作し初期処理を記述できます。
例えば、Hook処理のバインドやテンプレートの挿入処理、autoloadの設定などに利用します。
checkRequirements インストール前のチェックに利用します。「false」を返すとインストールが出来ません。
install ルートブログのアプリ管理画面からアプリのインストール時に実行されます。データベースの初期化処理などに利用ください。
uninstall ルートブログのアプリ管理画面からアプリのアンインストール時に実行されます。データベースの削除処理などに利用ください。
update プラグイン(拡張アプリ)のアップデート時に利用します。データ構造の変更などある場合に利用します。
activate プラグイン(拡張アプリ)の有効時(ブログ単位)に動作します。インストール時にもここが実行されます。
deactivate プラグイン(拡張アプリ)を無効化時(ブログ単位)に動作します。アンインストール時にもここが実行されます。

ポイント

  • extension/plugins/ の名前空間は 「Acms\Plugins」 です。
  • \ACMS_App を継承します。

これでプラグイン(拡張アプリ)開発の基本は終了です。次から実際の拡張方法について見ていきます。

モジュールの拡張

プラグイン(拡張アプリ)でのモジュールの拡張方法を見ていきます。基本的には、通常の拡張方法と同様ですが名前空間・クラス名に気をつけましょう。

GET・POST モジュールは、extension/plugins/独自ディレクトリ/(GET|POST) ディレクトリ以下に設置します。

例: extension/plugins/SamplePlugin/GET/Sample.php

<?php

namespace Acms\Plugins\SamplePlugin\GET;

use ACMS_GET;
use Template;
use ACMS_Corrector;

/**
 * テンプレート上では、標準のGETモジュールと同様に、
 * '<!-- BEGIN_MODULE Sample --><!--END_MODULE Sample -->' で呼び出されます。
 */
class Sample extends ACMS_GET
{
    function get()
    {
        $Tpl = new Template($this->tpl, new ACMS_Corrector());
        $data = array();

        return $Tpl->render($obj);
    }
}

気をつける点としては、「GET」 は 全て大文字 なので注意してください。あとは、モジュールの開発 を参照ください。

校正オプションの拡張

プラグイン(拡張アプリ)での校正オプションの拡張の仕方を見ていきます。名前空間・クラス名に気をつけましょう。

ファイル名は任意ですが、わかりやすくする為、Corrector.php とします。

例: extension/plugins/SamplePlugin/Corrector.php

<?php

namespace Acms\Plugins\SamplePlugin;

class Corrector
{
    /**
     * sample
     * 校正オプションのサンプルメソッド
     *
     * @param  string $txt  - 校正オプションが適用されている文字列
     * @param  array  $args - 校正オプションの引数 {var}[sample('ここの値')]
     * @return string       - 校正後の文字列
     */
    public function sample($txt, $args = array())
    {
        // 例 {var}[sample('hoge','fuga')]
        // {var}の中は,'a-blogcms' とする

        $hoge = isset($args[0]) ? $args[0] : null; // 'hoge'
        $fuga = isset($args[1]) ? $args[1] : null; // 'fuga'

        return $hoge.$fuga.'+'.$txt; // 'hogefuga+a-blog cms'
    }
}

ServiceProviderの設定

これだけでは、通常の拡張方法と違い動作しません。動作させるためには、このクラスのオブジェクトをa-blog cmsに登録する必要があります。

ServiceProviderで校正オプションの実装クラスを登録する

拡張アプリの基本で作成した ServiceProviderに少し手を加えます。

<?php

namespace Acms\Plugins\SamplePlugin;

use ACMS_App;
use Acms\Services\Common\CorrectorFactory;

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

    /**
     * サービスの初期処理
     */
    public function init()
    {
        $corrector = CorrectorFactory::singleton();
        $corrector->attach('SampleCorrector', new Corrector);
    }

    /* 省略... */
}

ポイント

Acms\Services\Common\CorrectorFactory のオブジェクトを

CorrectorFactory::singleton();

で取得して 先ほど作成した Acms\Plugins\SamplePlugin\Correctorattach します。

  • 第一引数: 被らないような任意の文字列
  • 第2引数: 実装クラスのオブジェクト

これでプラグイン(拡張アプリ)をインストールすれば、Corrector.php で実装した校正オプションが利用できるようになります。

フックの拡張

拡張アプリでのフックの拡張の仕方を見ていきます。名前空間・クラス名に気をつけましょう。

ファイル名は任意ですが、わかりやすくする為、Hook.php とします。

例: extension/plugins/SamplePlugin/Hook.php

<?php

namespace Acms\Plugins\SamplePlugin;

class Hook
{
    /**
     * 例: グローバル変数の拡張
     *
     * @param array &$globalVars
     */
    public function extendsGlobalVars(&$globalVars)
    {
         $globalVars->set('SAMPLE', 'サンプルのグローバル変数です');
    }
}

ServiceProviderの設定

これだけでは、通常の拡張方法と違い動作しません。動作させるためには、このクラスのオブジェクトをa-blog cmsに登録する必要があります。

ServiceProviderでHook実装クラスを登録する

拡張アプリの基本で作成した ServiceProviderに少し手を加えます。

<?php

namespace Acms\Plugins\SamplePlugin;

use ACMS_App;
use Acms\Services\Common\HookFactory;

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

    /**
     * サービスの初期処理
     */
    public function init()
    {
        $hook = HookFactory::singleton();
        $hook->attach('SampleHook', new Hook);
    }

    /* 省略... */
}

ポイント

Acms\Services\Common\HookFactory のオブジェクトを

HookFactory::singleton();

で取得して 先ほど作成した Acms\Plugins\SamplePlugin\Hookattach します。

  • 第一引数: 被らないような任意の文字列
  • 第2引数: 実装クラスのオブジェクト

これで拡張アプリをインストールすれば、Hook.php で実装したHookが利用できるようになります。

Validatorの拡張

拡張アプリでのValidatorの拡張の仕方を見ていきます。名前空間・クラス名に気をつけましょう。

ファイル名は任意ですが、わかりやすくする為、Validator.php とします。

例: extension/plugins/SamplePlugin/Validator.php

<?php

namespace Acms\Plugins\SamplePlugin;

class Validator
{
    /**
     * sample
     * バリデーターのサンプルメソッド
     *
     * @param  string $val - その変数の値
     * @param  string $arg - <input type="hidden" name="var:v#sample" value="ここの値">
     * @return boolean     - 入力が正しい場合は "true" そうでない場合は "false" を返す
     */
    function sample($val, $arg)
    {
        /**
         * 例:
         * <input type="text" name="var" value="{var}">
         * <input type="hidden" name="field[]" value="var">
         * <input type="hidden" name="var:v#sample" value="cms">
         *
         * <!-- BEGIN var:validator#sample -->
         *   <p class="acms-admin-text-error">cmsという文字列を含めてください。</p>
         * <!-- END var:validator#sample -->
         *
         * {var}の中は,'a-blogcms' とする
         */

        // name="var:v#sample" value="cms" で指定した
        // 文字列が含まれていなかったらエラーを出す
        return (strpos($val, $arg) !== false);
    }
}

ServiceProviderの設定

これだけでは、通常の拡張方法と違い動作しません。動作させるためには、このクラスのオブジェクトをa-blog cmsに登録する必要があります。

ServiceProviderでValidator実装クラスを登録する

拡張アプリの基本で作成した ServiceProviderに少し手を加えます。

<?php

namespace Acms\Plugins\SamplePlugin;

use ACMS_App;
use Acms\Services\Common\ValidatorFactory;

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

    /**
     * サービスの初期処理
     */
    public function init()
    {
        $validator = ValidatorFactory::singleton();
        $validator->attach('SampleValidator', new Validator);
    }

    /* 省略... */
}

ポイント

Acms\Services\Common\ValidatorFactory のオブジェクトを

ValidatorFactory::singleton();

で取得して 先ほど作成した Acms\Plugins\SamplePlugin\Validatorattach します。

  • 第一引数: 被らないような任意の文字列
  • 第2引数: 実装クラスのオブジェクト

これで拡張アプリをインストールすれば、Validator.php で実装したValidatorが利用できるようになります。

テンプレートの挿入


拡張アプリで様々な拡張をする場合、プログラムだけではなく管理ページを拡張したい場合があると思います。例えば、モジュールID化したいため、モジュール選択肢のテンプレートを拡張したい、拡張アプリ用の管理画面ページ(設定ページ)を作りたいなどです。


対応バージョン: Ver. 3.2 .24 で、差し込みエントリーに callable(HTML を返す関数)を渡せるようになりました。


仕組み

差し込みポイントは、システムテーマのテンプレートに置かれた Admin_InjectTemplate モジュールです。id 属性が差し込みポイントの種別になります。

<!-- BEGIN_MODULE Admin_InjectTemplate id="admin-main" --><!-- END_MODULE Admin_InjectTemplate -->

プラグインは InjectTemplate::add() で「この $type のときはこの内容を出す」と登録します。管理画面の描画時に、登録された内容がこの位置へ差し込まれます。

API

Acms\Services\Common\InjectTemplate はシングルトンです。singleton() で取得して使います。

add(string $type, string|callable $entry): void

差し込みポイント $type に内容を登録します。同じ $type に複数回登録でき、登録順に出力されます。

$entry は次のどちらかを渡せます。

種類

渡すもの

処理

文字列パス

標準テンプレートの相対パス(PLUGIN_DIR 起点)

<!--#include file="..." --> として展開される

callable

HTML 文字列を返す関数

戻り値の HTML をそのまま差し込む(空文字なら出力しない)

use Acms\Services\Common\InjectTemplate;
use Acms\Services\Facades\Twig;

$inject = InjectTemplate::singleton();

// (1) 標準テンプレートのパスを渡す(従来の方法)
$inject->add('admin-module-select', PLUGIN_DIR . 'MyPlugin/template/module-select.html');

// (2) callable を渡して Twig の描画結果を差し込む
$inject->add('admin-main', static function (): string {
    return Twig::renderTemplate('@myplugin/admin/main.twig');
});

get(string $type): array

$type に登録されたエントリーの配列を返します。通常はフレームワーク側が描画時に呼ぶため、拡張側で直接使う場面は多くありません。

利用できる差し込みポイント

標準で用意されている主な $type です。配置場所はシステムテーマのテンプレートです。

$type

差し込まれる場所

admin-main

管理画面メイン領域(プラグイン専用画面の本体)

admin-topicpath

パンくずリスト

admin-action

アクションボックス

admin-module-select

モジュール選択画面

admin-module-config-{モジュール名}

各モジュールの設定画面

admin-form / admin-form-option-extend

フォーム編集画面

admin-entry-editor-top

エントリー編集画面の上部

admin-entry-field / admin-entry-field-foot

エントリー編集画面のフィールド領域

admin-category-field

カテゴリー編集画面のフィールド領域

admin-blog-field

ブログ編集画面のフィールド領域

admin-user-field

ユーザー編集画面のフィールド領域

admin-module-config-{モジュール名}{モジュール名} には、対象モジュールの名前が入ります(例: admin-module-config-Sample)。

差し込みポイントは利用テーマ側のテンプレートにも置けます。独自の差し込みポイントを増やしたい場合は、テンプレートに Admin_InjectTemplate モジュールを id 付きで配置し、同じ id を $type として add() してください。

callable と文字列パスの使い分け

  • Twig テンプレートで画面を組むなら callable を渡し、その中で Twig::renderTemplate() を呼びます。

  • 既存の 標準テンプレートを差し込むなら文字列パスを渡します。従来どおりの書き方です。

  • callable が空文字を返した場合、その分は出力されません。条件によって出し分けたいときに利用できます。

注意点

  • callable は管理画面の描画時に実行されます。重い処理を避け、表示に必要な HTML を返すことに徹してください。

  • 文字列パスと callable は同じ $type に混在できます。登録順に連結されます。

関連ドキュメント

Twig でプラグイン(拡張アプリ)の管理画面を実装する


プラグイン(拡張アプリ)の管理画面を Twig テンプレートで組み立てられます。本ドキュメントでは、仕組みの全体像から、最小構成の画面を表示する手順、設定値の保存・バリデーションを伴う設定画面の作り方までを通して解説します。

これまで管理画面への差し込みは 標準テンプレートエンジンが中心でしたが、Twig で部分的に描画して差し込めるようになりました。テンプレートの継承(extends)やマクロといった Twig の表現力を、管理画面の拡張でもそのまま使えます。


対応バージョン: Ver. 3.2.4 以上で利用できます。


2 つの Twig 経路を区別する

a-blog cms には Twig を使う経路が 2 つあり、本ドキュメントで扱うのは後者です。両者は独立していて、片方を使うのにもう片方の設定は要りません。

経路

有効化の方法

主な用途

公開側テーマ全体の Twig 化

コンフィグ tpl_twig を「有効」にする

公開ページのテンプレートを .twig で書く

プラグイン(拡張アプリ)からの部分描画

フック・InjectTemplate から呼ぶ

管理画面に独自の UI / 設定画面を差し込む

tpl_twig を有効にしていなくても、プラグイン側の経路は使えます。

全体像

プラグインの ServiceProvider::init() で各種登録を行い、管理画面のレンダリング時に差し込みポイントへ HTML を流し込みます。

ServiceProvider::init()
  ├─ Twig::addTemplatePath()         … 自前テンプレートディレクトリを名前空間付きで登録
  ├─ InjectTemplate::add($type, …)   … 差し込みポイントに「文字列パス」または「callable」を登録
  └─ (Filter / Function / Extension は extendsTwig フックで登録)

       │  管理画面リクエスト
       ▼
Admin_InjectTemplate モジュール(差し込みポイント)
  └─ 登録された callable を実行
        └─ Twig::renderTemplate('@namespace/...') の結果 HTML を差し込む

差し込みポイントは、システムテーマ側のテンプレートに置かれた Admin_InjectTemplate モジュールです。プラグインはそこに対して「この $type のときはこの HTML を出す」と登録します。

実装は次の API を組み合わせます。詳細は各リファレンスを参照してください。

同梱の SamplePluginextension/plugins/SamplePlugin/)が、本機能の動作するサンプルです。合わせて参照してください。

チュートリアル: 最小構成の管理画面

まずは、プラグインの管理メニューを開いたときに Twig で書いた画面を表示するところまでを作ります。

1. テンプレートを用意する

プラグイン内にテンプレートディレクトリを作り、Twig ファイルを置きます。

extension/plugins/MyPlugin/
├─ ServiceProvider.php
└─ template/
   └─ admin/
      └─ main.twig

差し込みポイントには、管理画面シェル(ヘッダー・メニューなど)の内側に HTML が挿入されます。そのため main.twig にはページ全体ではなく、表示したい中身の断片だけを書きます。

<header>
  <h1 class="acms-admin-admin-title">MyPlugin 設定</h1>
</header>

<p>はじめての Twig 管理画面です。</p>

2. テンプレートディレクトリを登録する

ServiceProvider::init() で、テンプレートディレクトリを名前空間付きで登録します。Twig::addTemplatePath() に渡すのはファイルシステムのパスなので PLUGIN_LIB_DIR を使います。

use Acms\Services\Facades\Twig;

Twig::addTemplatePath(PLUGIN_LIB_DIR . 'MyPlugin/template', 'myplugin');

これで、テンプレートを @myplugin/admin/main.twig のように名前空間付きで参照できます。

3. 差し込みポイントに登録する

管理画面のメイン領域には admin-main という差し込みポイントがあります。ここに「HTML を返す callable」を登録すると、その戻り値が画面に差し込まれます。callable の中で Twig::renderTemplate() を呼んで、Twig の描画結果を返します。

use Acms\Services\Common\InjectTemplate;
use Acms\Services\Facades\Twig;

$inject = InjectTemplate::singleton();

if (defined('ADMIN') && ADMIN === 'app_' . $this->menu) {
    $inject->add('admin-main', static function (): string {
        return Twig::renderTemplate('@myplugin/admin/main.twig');
    });
}

ADMIN === 'app_' . $this->menu で「自分のプラグイン画面を開いているときだけ」差し込むようにしています。$this->menuServiceProvider$menu プロパティで指定したメニュー識別子です。

4. 完成形(ServiceProvider::init)

ここまでをまとめると、init() は次のようになります。

use Acms\Services\Common\InjectTemplate;
use Acms\Services\Facades\Twig;

public function init()
{
    // 1. テンプレートディレクトリの登録(init で一度だけでよい)
    Twig::addTemplatePath(PLUGIN_LIB_DIR . 'MyPlugin/template', 'myplugin');

    // 2. 自分の管理メニュー画面のときだけ、メイン領域に差し込む
    if (defined('ADMIN') && ADMIN === 'app_' . $this->menu) {
        $inject = InjectTemplate::singleton();
        $inject->add('admin-main', static function (): string {
            return Twig::renderTemplate('@myplugin/admin/main.twig');
        });
    }
}

5. 動作確認

プラグインを有効化し、管理画面のプラグインメニューから対象の画面を開きます。main.twig の内容が表示されれば成功です。

設定画面を Twig で書く

acms_config() と a-blog cms 標準のフォームを組み合わせると、コンフィグの保存・バリデーションを伴う設定画面を Twig だけで書くことができます。

値を参照するだけの基本 API は 組み込み Twig 関数 acms_config() を参照してください。ここでは、フォーム送信・保存・バリデーションの「編集レイヤー」を扱います。

保存の仕組み

設定画面のフォームは、標準の ACMS_POST_Config に送信します。

  • 送信ボタンの nameACMS_POST_Config にする

  • <input name="config[]" value="保存するキー"> で保存対象のキーを宣言する

  • バリデーションは hidden input で宣言する(後述)

ACMS_POST_Config が POST 値を保存し、バリデーション結果を保持したまま画面へ戻します。Twig 側は acms_config() から保存通知やエラーを取得して表示します。

<form action="" method="post" class="acms-admin-form">
  <button type="submit" name="ACMS_POST_Config" class="acms-admin-btn-admin acms-admin-btn-admin-primary">
    保存
  </button>

  <input type="text" name="site_title" value="{{ config.get('site_title') }}">
  <input type="hidden" name="config[]" value="site_title">
</form>

config[] で宣言したキーだけが保存対象になります。

バリデーションの宣言

バリデーションは a-blog cms 標準フォームと同じ作法で、hidden input として宣言します。ACMS_POST_Config がこれを解釈し、自動的に検証します。

<input type="hidden" name="キー:validator#メソッド" value="引数">
  • value 属性がバリデータへの引数になります(例: maxlength の文字数)。

  • :validator# は短縮形の :v# でも書けます。

<!-- 必須・最大 50 文字 -->
<input type="hidden" name="site_title:validator#required" value="true">
<input type="hidden" name="site_title:validator#maxlength" value="50">

使える主なバリデータ

メソッド

引数

内容

required

必須

email

メールアドレス形式

maxlength

文字数

最大文字数

digits

数字のみ

regex

正規表現

パターンに一致

in

候補(カンマ区切り等)

許可値のいずれか

min / max

数値

最小値 / 最大値

equalTo

比較対象

値が一致

詳しくは フォームオプション をご覧ください。

結果を取得するメソッド

acms_config() のアクセサから、保存通知とバリデーション結果を取得できます。

メソッド

戻り値

説明

saved()

bool

設定が保存されたかどうか

notice()

string

保存通知の文字列(saved / reset など)

posted()

bool

直近のリクエストがコンフィグ保存のPOSTりくえすとだったかどうか

valid()

bool

フォーム全体がバリデーションを通過したか

errors()

array

失敗したフィールドの一覧(フィールド名 → 失敗メソッド名のリスト

invalid(key, method = null, index = null)

bool

指定フィールド(とメソッド)が失敗したか

引数のないメソッドはプロパティ記法でも書けます(config.saved / config.valid / config.errors)。

保存通知とエラーサマリー

{% if config.saved %}
  <div role="alert" class="acms-admin-alert acms-admin-alert-info">保存しました</div>
{% endif %}

{% if config.posted and not config.valid %}
  <div role="alert" class="acms-admin-alert acms-admin-alert-danger">
    入力内容に誤りがあります
    {% if config.errors %}
      <ul>
        {% for field, methods in config.errors %}
          <li>{{ field }}: {{ methods|join(', ') }}</li>
        {% endfor %}
      </ul>
    {% endif %}
  </div>
{% endif %}

フィールドごとのエラー表示

invalid() で、特定フィールドの特定バリデータが失敗したかを判定できます。標準フォームと同じ data-validator-label のパターンに合わせると、既存の管理画面の見た目に揃います。

<input type="text" name="site_title" value="{{ config.get('site_title') }}">
<input type="hidden" name="config[]" value="site_title">
<input type="hidden" name="site_title:validator#required" value="true" id="validator-site_title-required">

<div role="alert" aria-live="assertive">
  <div data-validator-label="validator-site_title-required"
       class="validator-result-{{ config.invalid('site_title', 'required') ? '0' : '1' }}">
    <p class="error-text">サイトタイトルを入力してください</p>
  </div>
</div>

バリデーションエラーで再描画されたときも、acms_config() は POST 値を重ね合わせるため config.get() がユーザーの入力値を返します。入力をやり直させずに済みます。

完成例

テキスト(必須・最大文字数)、メール(必須・形式)、セレクト、チェックボックスを含む設定画面の例です。同梱の SamplePluginextension/plugins/SamplePlugin/template/admin/main.twig)が動作するサンプルです。

{% set config = acms_config(blogId: BID) %}

{% if config.saved %}
  <div role="alert" class="acms-admin-alert acms-admin-alert-info">設定を保存しました</div>
{% endif %}

{% if config.posted and not config.valid %}
  <div role="alert" class="acms-admin-alert acms-admin-alert-danger">入力内容に誤りがあります</div>
{% endif %}

<form action="" method="post" class="acms-admin-form">
  <button type="submit" name="ACMS_POST_Config" class="acms-admin-btn-admin acms-admin-btn-admin-primary">
    保存
  </button>

  {# テキスト:必須・最大 50 文字 #}
  <label for="sample_config_text">サンプルテキスト</label>
  <input id="sample_config_text" type="text" name="sample_config_text" value="{{ config.get('sample_config_text') }}">
  <input type="hidden" name="config[]" value="sample_config_text">
  <input type="hidden" name="sample_config_text:validator#required" value="true" id="validator-sample_config_text-required">
  <input type="hidden" name="sample_config_text:validator#maxlength" value="50" id="validator-sample_config_text-maxlength">
  <div role="alert" aria-live="assertive">
    <div data-validator-label="validator-sample_config_text-required"
         class="validator-result-{{ config.invalid('sample_config_text', 'required') ? '0' : '1' }}">
      <p class="error-text">サンプルテキストを入力してください</p>
    </div>
    <div data-validator-label="validator-sample_config_text-maxlength"
         class="validator-result-{{ config.invalid('sample_config_text', 'maxlength') ? '0' : '1' }}">
      <p class="error-text">50 文字以内で入力してください</p>
    </div>
  </div>

  {# メール:必須・メール形式 #}
  <label for="sample_config_email">メールアドレス</label>
  <input id="sample_config_email" type="text" name="sample_config_email" value="{{ config.get('sample_config_email') }}">
  <input type="hidden" name="config[]" value="sample_config_email">
  <input type="hidden" name="sample_config_email:validator#required" value="true" id="validator-sample_config_email-required">
  <input type="hidden" name="sample_config_email:validator#email" value="true" id="validator-sample_config_email-email">

  {# セレクト #}
  <label for="sample_config_status">公開ステータス</label>
  <select id="sample_config_status" name="sample_config_status">
    <option value="open" {{ config.selected('sample_config_status', 'open') }}>公開</option>
    <option value="close" {{ config.selected('sample_config_status', 'close') }}>非公開</option>
  </select>
  <input type="hidden" name="config[]" value="sample_config_status">

  {# チェックボックス:未チェック時に off を送るため hidden を前置 #}
  <label>メールマガジン</label>
  <input type="hidden" name="sample_config_mail" value="off">
  <input type="checkbox" name="sample_config_mail" value="on" {{ config.checked('sample_config_mail', 'on') }} id="sample_config_mail">
  <input type="hidden" name="config[]" value="sample_config_mail">
</form>

つまずきやすいポイント

  • テンプレートが見つからない: @myplugin/... の名前空間が addTemplatePath() の第 2 引数と一致しているか、ファイルシステムのパス(PLUGIN_LIB_DIR)を渡しているかを確認してください。

  • 画面に何も出ない: ADMIN の比較条件を確認してください。$menu を設定していない、または条件が現在の画面と一致していない可能性があります。

  • ページ全体を書いてしまう: 差し込みポイントには管理画面テンプレートの内側に断片が挿入されます。<html> や共通メニューは書かず、中身だけを出力してください。

  • @system を自分のテンプレートに使ってしまう: @system はシステムテーマ参照用の予約名前空間です。自前のテンプレートには別の名前空間(例では myplugin)を使ってください。

  • 保存されない: 保存したいキーを <input name="config[]" value="キー"> で宣言しているか確認してください。宣言のないキーは保存されません。

  • チェックボックスの OFF が保存されない: チェックボックスは未チェック時に値が送信されません。OFF を保存したい場合は、同名の hidden(value="off")をチェックボックスの前に置きます。

Twig 拡張 API(テンプレート・Filter・Function・Extension の登録)


プラグイン(拡張アプリ)やサイト拡張から、Twig を拡張するための API をまとめたリファレンスです。Twig ファサード(Acms\Services\Facades\Twig)と extendsTwig フックを通じて、テンプレートディレクトリの登録・部分描画・独自フィルターや関数の追加を行います。

全体像と位置づけは プラグイン(拡張アプリ)で Twig 管理画面を作る を参照してください。


対応バージョン: Ver. 3.2.24 以上で利用できます。


登録経路は 2 つに分かれる

拡張は「登録するもの」によって書く場所が変わります。これは内部の都合(後述)によるもので、最初に押さえておくと迷いません。

登録するもの

書く場所

API

テンプレートディレクトリ

ServiceProvider::init()(一度だけ)

Twig::addTemplatePath()

Filter / Function / Extension

extendsTwig フック

Twig::registerFilter() ほか

テンプレートディレクトリの登録

Twig::addTemplatePath(string $dir, ?string $namespace = null): void

自前のテンプレートディレクトリを Twig のローダーに登録します。$namespace を指定すると、@namespace/... の形でテンプレートを参照できます。省略するとメイン名前空間に登録されます。

use Acms\Services\Facades\Twig;

// init() の中で一度だけ呼べばよい
Twig::addTemplatePath(PLUGIN_LIB_DIR . 'MyPlugin/template', 'myplugin');
{# 登録後はこう参照できる #}
{{ include('@myplugin/parts/header.twig') }}
  • $dir にはファイルシステムのパスを渡します。プラグインでは PLUGIN_LIB_DIR を使います。

  • ローダーの状態は Twig サービス内に保持され、Twig 環境が作り直されても引き継がれます。そのため初期化時に一度呼べば十分です。

  • @system はシステムテーマ参照用に予約された名前空間です。プラグインは独自の名前空間を切ってください。

Twig テンプレートを解決する

Twig::renderTemplate(string $name, array $context = []): string

指定したテンプレートを描画して、結果の HTML 文字列を返します。$context で変数を渡せます。差し込みポイントに登録する callable の中などで使います。

use Acms\Services\Facades\Twig;

$html = Twig::renderTemplate('@myplugin/admin/main.twig', [
    'title' => 'MyPlugin 設定',
    'items' => $items,
]);
<h1>{{ title }}</h1>
<ul>
  {% for item in items %}
    <li>{{ item }}</li>
  {% endfor %}
</ul>

描画結果をどこに出すかは InjectTemplate を参照してください。

Filter / Function / Extension の登録

独自のフィルター・関数・拡張クラスは、extendsTwig フックの中で登録します。フックは Twig 環境が組み立てられるたびに呼ばれます。

extendsTwig フック

extension/acms/Hook.php やプラグイン(拡張アプリ)のフックに extendsTwig メソッドを実装します。引数には Twig サービスが渡されます。

/**
 * Twig Environment 構築時に呼ばれる拡張ポイント
 */
public function extendsTwig(\Acms\Services\Template\Twig $twig): void
{
    $twig->registerFilter('site_format', fn(string $s) => '⚡' . $s);
    $twig->registerFunction('site_now', fn() => date('Y-m-d H:i:s'));
    $twig->registerExtension(new \Acms\Custom\Twig\SiteExtension());
}
{{ 'hello'|site_format }}   {# ⚡hello #}
{{ site_now() }}            {# 2026-06-17 10:00:00 #}

各メソッド

メソッド

説明

registerFilter(string $name, callable $callable, array $options = []): void

フィルターを登録する

registerFunction(string $name, callable $callable, array $options = []): void

関数を登録する

registerExtension(\Twig\Extension\AbstractExtension $extension): void

Twig Extension クラスを登録する

$options には Twig 標準の TwigFilter / TwigFunction のオプション(is_safe など)を渡せます。

同名のフィルター・関数を複数回登録した場合は、最後の登録が採用されます。同じ型の Extension も最後の登録にまとまります。

なぜ登録場所が分かれるのか

テンプレートディレクトリの登録と、Filter / Function / Extension の登録で書く場所が違うのには理由があります。

  • テンプレートディレクトリは Twig の「ローダー」に追加します。ローダーの状態はサービス側に保持され、環境が作り直されても保たれるため、初期化で一度登録すれば以後ずっと有効です。

  • Filter / Function / Extension は Twig の「環境(Environment)」インスタンスに結び付きます。環境は状況によって作り直されることがあり、そのたびに登録し直す必要があります。extendsTwig フックは環境の構築時に毎回呼ばれるので、ここに書いておけば作り直されても確実に再登録されます。

init()registerFilter() を呼んでも、内部で保留して環境構築時に反映する作りにはなっていますが、意図が明確になる extendsTwig フックに書くことを推奨します。

プラグイン(拡張アプリ)のテスト


自作プラグイン(拡張アプリ)に PHPUnit のテストを書き、変更しても壊れていないことを自動で確かめられるようにするための入門ガイドです。

PHPUnit とはPHPのコードをテストするための公式のテストフレームワークです。

https://phpunit.de

このガイドで何ができるようになるか

プラグインは a-blog cms 本体のクラスや DB に触れるため、これまでは「本体を丸ごと用意しないとテストできない」のが実情でした。テスト基盤 ablogcms/testing-framework を使うと、プラグイン単体のリポジトリのまま、本体コアに触れる処理(Services / Validator / Hook や DB を伴う処理)をテストできます。本体のソースや難読化イメージを手元に持っていなくても、composer require --dev して phpunit を叩くだけでテストが回ります。

用意するものは大きく 3 つです。用途に応じて必要なものだけ使います。

提供物

入手先

役割

ablogcms/testing-framework

Packagistcomposer require --dev

PHPUnit の基底クラス・Seeder・DB 作成コマンド・PHPStan 設定を提供

appleple/acms(開発用 Docker イメージ)

Docker Hub

本体同梱の実行環境。docker compose up だけでテストが回る環境が手に入る

ablogcms/plugin-skeleton

Packagistcomposer create-project

テスト・PHPStan・CI が設定済みのプラグインひな型

appleple/acms は開発・CI 用途のイメージです。 本番運用には使わないでください。

前提

  • Docker(本体を含む環境の中でテストを実行するため)

  • テストを書くプラグインが名前空間規約 Acms\Plugins\{Name}\ に沿っていること

テストは a-blog cms 本体を含む環境の中で実行します。ローカルに本体一式が入った Docker コンテナがある場合はそれを使えますし、無ければ次章のスケルトン(同梱イメージ appleple/acms を参照)が最短です。

最短ルート: スケルトンから始める

ゼロから 1 つプラグインを作るなら、テスト・PHPStan・CI が最初から設定されたひな型を使うのが早いです。

composer create-project ablogcms/plugin-skeleton my-plugin
cd my-plugin
docker compose up -d
docker compose exec acms bash -lc "composer install && vendor/bin/acms-create-database && vendor/bin/phpunit"
# → サンプルテストが緑になれば環境は完成

この時点で tests/Unittests/Integration にサンプルが入っています。あとは src/ に自分のコードを、tests/ に対応するテストを足していくだけです。書き方は テストの書き方 を参照してください。

既存のプラグインに導入する

すでにあるプラグインへ後から組み込む場合の手順です。追加するファイルは 2 つ(composer.json への追記と phpunit.xml.dist)だけで、プラグインごとに tests/bootstrap.php を書く必要はありません

1. composer.json に追記する

{
  "require-dev": {
    "ablogcms/testing-framework": "3.2.*"
  },
  "autoload":     { "psr-4": { "Acms\\Plugins\\Foo\\": "src/" } },
  "autoload-dev": { "psr-4": { "Acms\\Plugins\\Foo\\Tests\\": "tests/" } }
}

バージョンは、テストしたい a-blog cms のマイナーバージョンに合わせます(3.2 系なら 3.2.*、3.3 系なら 3.3.*)。^3.2 は 3.3 系まで拾ってしまうため使いません(理由は CI と複数バージョン検証 を参照)。

2. phpunit.xml.dist を置く

起動処理はテスト基盤に同梱の共有エントリにまとまっているので、bootstrap でそれを直接指します。

<?xml version="1.0"?>
<phpunit
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  bootstrap="vendor/ablogcms/testing-framework/bootstrap.php"
  colors="true"
  cacheDirectory=".phpunit.cache"
>
  <testsuites>
    <testsuite name="Unit">
      <directory>tests/Unit</directory>
    </testsuite>
    <testsuite name="Integration">
      <directory>tests/Integration</directory>
    </testsuite>
  </testsuites>
  <php>
    <!-- a-blog cms 本体の場所。環境(Docker コンテナ内のパス等)に読み替える -->
    <env name="ACMS_ROOT" value="/var/www/html"/>
  </php>
</phpunit>

ACMS_ROOT には本体のルートを渡します。同梱イメージ appleple/acms を使う場合は /var/www/html、本体ソースのモノレポ/作業ツリーを使う場合はそのルート(ablogcms/ を含むディレクトリ)を指定します。どちらのレイアウトでも自動判定されます。

phpunit.xml<env> には ACMS_ROOT だけを書きます。 DB の接続情報はここに書きません。書いてしまうと後述の .env.testing 側の値が無視されるためです(環境変数は「先に設定済みの値を上書きしない」ため)。テスト用 DB の用意は次の手順を参照してください。

追加の初期化(フィクスチャ読み込みなど)が必要なときだけ、自前の tests/bootstrap.php で共有エントリを require してから足します(置き換えではなく上乗せ)。

<?php
// tests/bootstrap.php(カスタムが必要なときだけ)
require_once __DIR__ . '/../vendor/ablogcms/testing-framework/bootstrap.php';
// ここに独自の初期化を足す

3. テスト用データベースを用意する(統合テストを書く場合)

統合テスト(DatabaseTestCase)は実際の DB に接続します。単体テストしか書かないなら、この手順は不要です。

用意するものは 3 つです。

  • MySQL(互換)サーバを起動する。同梱イメージ appleple/acms を docker compose で使う場合は、db サービスがこれにあたります。

  • テスト専用のデータベースを 1 つ用意する(本番とは必ず分けます)。docker compose なら db サービスの MYSQL_DATABASE、手動なら CREATE DATABASE で作ります。vendor/bin/acms-create-databaseその中にテーブルを作るだけで、データベース自体は作りません。

  • 接続情報を渡す。方法は 2 通りで、どちらも ACMS_DB_* を使います。

方法 A: 本体の .env.testingACMS_ROOT 直下に置くファイル)

# <ACMS_ROOT>/.env.testing
ACMS_DB_HOST=db
ACMS_DB_PORT=3306
ACMS_DB_NAME=db_acms_test
ACMS_DB_USER=root
ACMS_DB_PASS=root

方法 B: 環境変数(docker compose の environment や CI の env)

同じ ACMS_DB_* を環境変数で渡します。Docker で開発しているなら compose の environment に書くのが手軽です。

未指定時の既定は 127.0.0.1 / 3306 / db_acms_test / root / root です。ACMS_DB_NAME に指定した名前のデータベースが存在している必要があります。

各統合テストはトランザクション内で実行され、終了時に自動でロールバックされます。テスト用 DB にデータが残り続けることはありませんが、事故防止のため本番とは別の DB を使ってください。

4. 最初のテストを緑にする

本体を含む環境(Docker コンテナ)の中で実行します。

composer install                 # 依存のインストール(初回)
vendor/bin/acms-create-database  # テスト用 DB のテーブル作成(統合テストの前に一度だけ)
vendor/bin/phpunit               # テスト実行

acms-create-database はテスト用 DB のテーブルを無人で作成するコマンドです。統合テストを回す前に一度だけ実行します(単体テストしか書かないなら不要)。

うまくいかないときは

最初の緑にたどり着けないときに多い原因です。

症状

主な原因と対処

ACMS_ROOT が未設定または不正です

phpunit.xml<env name="ACMS_ROOT"> が本体のパスを指しているか確認します。同梱イメージ内なら /var/www/html

本体のオートローダが見つかりません

ACMS_ROOT が本体ルート(ablogcms/ を含む階層、または配布物なら php/ を含む階層)を指しているか確認します。

DB 接続でエラー/意図しない既定 DB につながる

ACMS_DB_*.env.testing か環境変数で渡しているか確認します。phpunit.xml<env> に書くと無視されます。

Unknown database / テーブルが無いと言われる

ACMS_DB_NAME のデータベースを作成し、vendor/bin/acms-create-database を実行したか確認します。

autoload.php が見つかりません

先に composer install を実行します。

ディレクトリ構成の目安

my-plugin/
├── composer.json               ← require-dev / autoload を追記
├── phpunit.xml.dist            ← bootstrap は共有エントリを直接指す
├── src/                        ← プラグイン本体(テスト対象)
│   ├── ServiceProvider.php
│   ├── GET/
│   ├── POST/
│   └── Services/
└── tests/
    ├── Unit/                   ← DB 不要(TestCase)
    └── Integration/            ← DB あり(DatabaseTestCase)

関連ドキュメント

プラグイン(拡張アプリ)のテストの書き方


テスト基盤 ablogcms/testing-framework を使って、単体テスト・統合テストを書く具体的な手順です。導入がまだの場合は先に プラグイン(拡張アプリ)のテスト を読んでください。

テストの種類と使い分け

テストは大きく 2 種類です。速くて壊れにくい単体テストを優先し、DB や本体の状態がどうしても必要なときだけ統合テストに上げるのが基本です。

種類

基底クラス

DB

速さ

主な対象

単体テスト

TestCase

使わない

速い

純粋なロジック(Services / Validator / 校正オプション / Hook の計算部分)

統合テスト

DatabaseTestCase

使う(自動ロールバック)

やや遅い

DB への保存・取得、ServiceProvider の登録、複数レコードにまたがる振る舞い

同じ確信が得られるなら、依存が少なく速いテストを選びます。まず「単体で書けないか」を考え、書けないときだけ統合テストにするのがコツです。

何をテストするか

a-blog cms のハンドラ(ACMS_GET_* / ACMS_POST_*)は CSRF・セッション・グローバル変数に依存するため、そのままでは直接テストしにくい構造です。ロジックを Services / Validator に切り出し、そこをテストするのが基本方針です。ハンドラ本体は薄く保ち、切り出したクラス経由で間接的にカバーします。

対象

テストする

使う基底クラス

Services/ の純粋なロジック

✅ 単体

TestCase

Validator の入出力

✅ 単体

TestCase

Corrector(校正オプション)の入出力

✅ 単体

TestCase

Hook の各メソッドのロジック

✅ 単体

TestCase

DB を伴う Services / データ取得

✅ 統合

DatabaseTestCase

ServiceProvider の登録

✅ 統合

DatabaseTestCase

GET/ POST/ ハンドラ本体

ロジックを Services に切り出して間接的に

単体テスト(TestCase

DB に触れない純粋なロジックは Acms\TestingFramework\TestCase を継承します。速く、外部依存が無いので、まずここに寄せられないかを考えます。

<?php

declare(strict_types=1);

namespace Acms\Plugins\Foo\Tests\Unit\Services;

use Acms\Plugins\Foo\Services\Foo\Helper;
use Acms\TestingFramework\TestCase;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\TestDox;

final class HelperTest extends TestCase
{
    #[Test]
    #[TestDox('先頭にアットマークが無ければ付与する')]
    public function 先頭にアットマークを付与する(): void
    {
        // Arrange
        $helper = new Helper();

        // Act
        $result = $helper->formatHandle('appleple');

        // Assert
        $this->assertSame('@appleple', $result);
    }
}

テストメソッドは PHPUnit の Attribute(#[Test] / #[TestDox])で書きます。test プレフィックスは不要です。#[TestDox] にはテストレポートに出したい説明を日本語で書けます。中身は Arrange(準備)→ Act(実行)→ Assert(検証)の AAA パターンで整えると読みやすくなります。

複数の入力をまとめて検証する(データプロバイダ)

同じロジックを入力違いで何度も確かめたいときは、PHPUnit の データプロバイダ を使うとテストが 1 つで済みます。

use PHPUnit\Framework\Attributes\DataProvider;

#[Test]
#[DataProvider('provideHandles')]
#[TestDox('$input を正規化すると $expected になる')]
public function ハンドルを正規化する(string $input, string $expected): void
{
    $this->assertSame($expected, (new Helper())->formatHandle($input));
}

public static function provideHandles(): array
{
    return [
        'アットマーク無し' => ['appleple', '@appleple'],
        'アットマーク付き' => ['@appleple', '@appleple'],
        '前後の空白'       => ['  appleple ', '@appleple'],
    ];
}

モックとテストダブル

外部サービスや重い依存に触れるコードは、その依存を テストダブル(モック)に差し替えると単体テストにできます。前提は「依存をコンストラクタ引数などで外から渡せる形にしておく」ことです。

テストダブル(モック)は PHPUnit の createMock メソッドを利用して作成できます。

#[Test]
public function 通知の送信をクライアントに委譲する(): void
{
    // 送信クライアントをモックに差し替える
    $client = $this->createMock(HttpClient::class);
    $client->expects($this->once())
        ->method('post')
        ->with('https://example.com/hook', ['text' => 'hello']);

    (new Notifier($client))->notify('hello');
}
  • グローバル関数や Facade(Auth:: など)に直接触れるコードはモックしにくいです。無理にモックするより、その呼び出しを薄いラッパークラス(seam)に隔離し、ラッパーを注入する形にリファクタします。隔離した本体ロジックは単体テストで固定でき、ラッパー自体は統合テストで薄く担保します。

  • モックが多すぎる・作りにくいと感じたら、たいてい設計を見直す合図です(後述の「テストを書きやすくするコツ」)。

テスト用データベースを用意する

統合テストは実際の DB に接続します。テスト専用のデータベースを 1 つ用意してください(本番とは必ず分けます)。各テストはトランザクション内で実行され、終了時に自動でロールバックされるので、テストがデータを残すことはありませんが、事故防止のため別 DB にします。単体テストしか書かないなら、この準備は不要です。

用意するものは 3 つです。

  • MySQL(互換)サーバ — 同梱イメージ appleple/acms を docker compose で使う場合は db サービスがこれにあたります。

  • テスト用データベースが 1 つ存在すること — docker compose なら db サービスの MYSQL_DATABASE、手動なら CREATE DATABASE で作ります。vendor/bin/acms-create-databaseその中にテーブルを作るだけで、データベース自体は作りません。

  • 接続情報を渡すことphpunit.xml ではなく、本体の .env.testingACMS_ROOT 直下)か環境変数で渡します。

.env.testingACMS_ROOT(本体側)に置くファイルで、テスト用の接続先をまとめます。

# <ACMS_ROOT>/.env.testing
ACMS_DB_HOST=db
ACMS_DB_PORT=3306
ACMS_DB_NAME=db_acms_test
ACMS_DB_USER=root
ACMS_DB_PASS=root

同じ ACMS_DB_* は、CI や docker compose の environment に環境変数として書いても構いません。未指定時の既定は 127.0.0.1 / 3306 / db_acms_test / root / root です。テーブルは初回に一度だけ作成します。

vendor/bin/acms-create-database

環境構築の全体像(Docker の起動から)は プラグイン(拡張アプリ)のテスト を参照してください。

統合テスト(DatabaseTestCase

DB を伴う処理は Acms\TestingFramework\DatabaseTestCase を継承します。各テストはトランザクション内で実行され、終了時に自動でロールバックされます。実 DB に接続しても、テストが実際のデータを汚すことはありません。テストごとに DB がまっさらな状態から始まるので、テスト同士が互いに影響しません。

自動ロールバックが効かない操作に注意。 ALTER TABLE / DROP TABLE などの DDL は多くの DB で暗黙にコミットされ、それまでのロールバックを打ち消します。全文検索インデックス・空間インデックスを伴う処理もトランザクション外で確定します。これらを含む処理は統合テストで直接扱わず、ロジックを純粋関数に切り出して単体でテストするか、実機での確認に回してください。

テストデータは setUpDatabase() の中で用意します。

<?php

declare(strict_types=1);

namespace Acms\Plugins\Foo\Tests\Integration;

use Acms\Plugins\Foo\Services\Foo\UserMapper;
use Acms\TestingFramework\DatabaseTestCase;
use Acms\TestingFramework\Seeder\UserSeeder;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\TestDox;

final class UserMapperTest extends DatabaseTestCase
{
    private int $blogId;

    protected function setUpDatabase(): void
    {
        $this->blogId = $this->createTestBlog(['blog_name' => 'テストブログ']);
    }

    #[Test]
    #[TestDox('メールアドレスから有効なユーザーを特定できる')]
    public function メールで有効ユーザーを特定する(): void
    {
        $userId = UserSeeder::seed($this->blogId, ['user_mail' => 'alice@example.com']);

        $result = (new UserMapper($this->blogId))->resolve('alice@example.com');

        $this->assertSame($userId, $result['userId']);
    }
}

DatabaseTestCase のヘルパー

テストデータの用意・確認によく使うメソッドが揃っています。

// エンティティ作成(必要な値だけ渡す)
$blogId     = $this->createTestBlog(['blog_name' => 'テストブログ']);
$categoryId = $this->createTestCategory($blogId, ['category_name' => 'ニュース']);
$userId     = $this->createTestUser(['user_mail' => 'test@example.com']);
$entryId    = $this->createTestEntry($blogId, $categoryId, ['entry_title' => '記事']);

// 任意テーブルへの汎用操作
$id    = $this->insertTestData('table', ['col' => 'value']);
$row   = $this->fetchTestData('table', ['id' => $id]);
$count = $this->countTestData('table', ['status' => 'open']);
$this->deleteTestData('table', ['id' => $id]);

処理後の DB 状態を検証したいときは、fetchTestData() / countTestData() で実際に保存された行を取り出して assert します(更新系のテストは「戻り値」だけでなく「DB がどう変わったか」まで確認すると副作用の見落としを防げます)。

テストデータの作成(Seeder)

Acms\TestingFramework\Seeder\* は「必要な値だけ渡し、残りは自動で埋める」ためのデータ生成クラスです。直接 INSERT するより簡潔で、テストの意図(何が重要な値か)が読み取りやすくなります。

use Acms\TestingFramework\Seeder\BlogSeeder;
use Acms\TestingFramework\Seeder\CategorySeeder;
use Acms\TestingFramework\Seeder\UserSeeder;

$blogId = BlogSeeder::seed(['blog_name' => 'テストブログ']);
$userId = UserSeeder::seed($blogId, ['user_mail' => 'alice@example.com', 'user_auth' => 'administrator']);
CategorySeeder::seed($blogId, [], 5); // 5 件まとめて作成
  • Blog / Category / User / Entry のほか、Field / Tag / Module / Media / Comment / Relationship / Usergroup / Role など主要エンティティ向けの Seeder が用意されています。

  • 渡したキーは、Seeder が既定値を持つカラムを上書きします。既定で埋めないカラムも、キーとして渡せば保存されます。

  • 存在しないカラム名を渡すとエラーになります(書き間違いにその場で気づけます)。

  • ブログ ID など引数で渡す値は、同名のキーを渡しても引数側が優先されます。

プラグインの有効化を前提にしたテスト(PluginActivator

プラグインの有効/無効に依存するロジックを検証したいときに使います。プラグイン名を渡すと、app テーブルに保存される ServiceProvider のクラス名(Acms\Plugins\{名前}\ServiceProvider)へ内部で補完します。

use Acms\TestingFramework\Helpers\PluginActivator;

PluginActivator::activate('Foo', $blogId);
$this->assertTrue(PluginActivator::isActive('Foo', $blogId));

ServiceProvider が規約と異なる場所にある場合は、FQCN を直接渡すこともできます。

統合テストで「対象プラグインが有効なときだけ動く挙動」や、別のプラグインに依存する挙動を検証したいときは、activate() で必要なプラグインを先に有効化してからテストします。有効化漏れは、テストが本番と違う前提で通ってしまう(偽陽性)原因になります。

テストの実行

本体を含む環境(Docker コンテナ)の中で実行します。

vendor/bin/phpunit                       # 全部
vendor/bin/phpunit --testsuite=Unit      # 単体だけ
vendor/bin/phpunit --testsuite=Integration  # 統合だけ
vendor/bin/phpunit --filter 先頭にアットマークを付与する  # メソッド名で絞り込み

統合テストを回す前には、一度だけ vendor/bin/acms-create-database でテスト用 DB のテーブルを作成しておきます。

失敗したテストを調べる

落ちたテストの原因を追うときは、次のフラグが役立ちます。

vendor/bin/phpunit --testdox              # 何を検証しているかを文章で一覧(#[TestDox] が出る)
vendor/bin/phpunit --filter メソッド名     # 1 メソッドだけ実行して素早く再現する
vendor/bin/phpunit --display-deprecations # 非推奨の警告も表示する
vendor/bin/phpunit --stop-on-failure      # 最初の失敗で止める

スタックトレースは、自分のコードに一番近い行から読むと原因にたどり着きやすいです。

テストを書きやすくするコツ

  • ハンドラから純粋関数・Services へ切り出す。 テストしにくいと感じたら、それはテストの問題ではなく設計の合図です。ロジックを引数と戻り値で完結するクラスに移すと、単体テストで完全に固定できます。

  • 単体で書けないか先に検討する。 DB が本当に要るときだけ統合テストにします。単体テストは速く、失敗時の原因切り分けも簡単です。

  • 1 テスト 1 検証。 1 つのテストメソッドで確かめることを 1 つに絞ると、落ちたときに何が壊れたか一目でわかります。

  • 実装ではなく振る舞いをテストする。 「内部でどのメソッドを呼んだか」ではなく「入力に対して何を返すか・何が変わるか」を検証します。実装を変えても振る舞いが同じなら、テストは緑のままであるべきです。

  • モックが多すぎるのは設計の合図。 1 つのテストで大量のモックが必要なクラスは、責務や依存を持ちすぎています。分割・注入を見直すとモックもテストも減ります。

  • テストを先に書く(TDD)。 失敗するテスト → 通す最小実装 → リファクタの順で回すと、テストしやすい設計に自然と寄っていきます。

関連ドキュメント

プラグイン(拡張アプリ)の静的解析


プラグインのコードを PHPStan で静的解析し、実行前に型の不整合やタイプミスを見つけられるようにする手順です。テスト基盤 ablogcms/testing-framework に PHPStan 設定が同梱されており、設定 ファイル1 行の読み込みで a-blog cms 本体のシンボル(関数・クラス・定数)を解決した状態で解析できます。

PHPStan とはPHPプログラムの実行前にコードの品質をチェックする「静的解析ツール」です。

https://phpstan.org/

なぜ設定が必要なのか

プラグインは本体の関数(config() / dsn() など)やクラス(SQL など)、定数(BID など)を使います。PHPStan はコードを実行せずに解析するため、これらのシンボルが「どこで宣言されているか」を教えてあげないと「未定義」と誤検知します。

配布パッケージの一部(ACMS/function.php など)は難読化されていて静的には読めません。テスト基盤はその分を スタブ(シンボルの宣言だけを持つファイル)で補い、読めるコアは実ソースを直接読ませることで、どんな環境でも同じ設定で解析できるようにしています。開発者側は、同梱設定を includes で読み込むだけで済みます。

設定

phpstan/phpstanrequire-dev に追加し、プラグイン直下に phpstan.neon(または phpstan.neon.dist)を置きます。

{
  "require-dev": {
    "ablogcms/testing-framework": "3.2.*",
    "phpstan/phpstan": "^2.1"
  }
}
includes:
  - vendor/ablogcms/testing-framework/phpstan/extension.neon
parameters:
  level: max
  # 本体のサポート範囲(PHP 8.1〜8.5)で解析し、新旧バージョン差の取りこぼしを防ぐ
  phpVersion:
    min: 80100
    max: 80500
  paths:
    - src
    - tests          # テストも型チェックする(基盤の基底クラスは include で解決される)
  scanDirectories:
    - /var/www/html/php   # a-blog cms 本体の読めるコア(ACMS_ROOT 配下の php ディレクトリ)
  • includes … テスト基盤同梱の extension.neon を読み込みます。ここが本体シンボル解決の入口です。

  • scanDirectories … 難読化されていない本体コア(Services/SQL/ など)を実ソースから読ませます。パスは環境に合わせて読み替えてください(同梱イメージ appleple/acms なら /var/www/html/php)。

  • phpVersion … 本番のサポート範囲全体(8.1〜8.5)を対象にします。実行時の PHP に依存せず、バージョン差に由来する取りこぼしを防げます。

  • bootstrapFiles は不要です。 プラグイン実行時にのみ確定する定数(PLUGIN_DIR / PLUGIN_LIB_DIR など)は同梱 extension が供給します。

新規プラグインは最初から level: max で始めるのがおすすめです。あとから上げるより、積み上がる差分が小さくて済みます。

実行

本体を含む環境(Docker コンテナ)の中で実行します。

vendor/bin/phpstan analyse

誤検知が出たら

本体のレガシー領域に由来する誤検知が出ることがあります。基本方針は baseline(一括で握りつぶすファイル)を作らず、該当行に理由コメント付きで局所的に抑制することです。baseline は「いつの間にか本物のエラーまで隠れる」ため、増やしすぎないほうが安全です。

// @phpstan-ignore-next-line (理由: 本体レガシー API の戻り値型が緩いため)
$value = legacy_api();

識別子付きで抑制できる場合は、対象を絞って書くとより安全です。

$value = legacy_api(); // @phpstan-ignore return.type (理由: …)

あわせて行う品質チェック(phpcs)

PHPStan は「型・ロジックの誤り」を見ますが、コーディング規約(PSR-12)の統一は範囲外です。自分のプラグインでも squizlabs/php_codesnifferrequire-dev に入れて phpcs を回しておくと、テスト・静的解析・規約チェックの品質ゲートが揃います。phpcs も PHPStan と同じく実行時の PHP に依存しないので、CI では PHP マトリクスで回さず 1 回だけ実行すれば十分です(CI と複数バージョン検証)。

補足

  • 同梱スタブは、本体の公開 API・定数のスナップショットです。本体側で大きめの API 追加があった場合は、その追加を含むバージョンの ablogcms/testing-framework に更新すると、新しい API も解決できるようになります。

関連ドキュメント

プラグイン(拡張アプリ)の CI と複数バージョン検証


プラグインのテストを CI(継続的インテグレーション)で自動実行し、さらに複数の PHP・a-blog cms バージョンにまたがって検証する方法です。テストコードの書き方は プラグイン(拡張アプリ)のテストの書き方 を参照してください。

CI(継続的インテグレーション)とは

CI(継続的インテグレーション)とは、プログラムのテストやビルド等といった作業を自動化する仕組みのことを言います。

CI を実現するための主なサービスとして、Github ActionsBitbucket PipelinesCircle CI などが挙げられます。

全体像

CI では、a-blog cms 本体を同梱した開発用 Docker イメージ appleple/acms:<バージョン>-php<PHP> の中でテストを実行します。本体は Docker イメージから来るので、プラグインリポジトリに本体ソースを入れる必要はありません

複数バージョンで検証したいときに切り替えるのは、次の 2 つだけです。どちらも 1 つのバージョン文字列から決まります。

  • Docker イメージのタグ(例 3.2-php8.4)… PHP と a-blog cms バージョンを選ぶ

  • ablogcms/testing-framework の composer 制約(例 3.2.*)… テスト対象のマイナーバージョンに合わせる

appleple/acms は開発・CI 用途のイメージです。本番運用には使わないでください。利用可能なタグは Docker Hub の Tags タブが最新です。

composer 制約の注意(重要)

テスト基盤のバージョンは a-blog cms のマイナーバージョンに固定します。^ は使いません。

書き方

解決範囲

可否

"3.2.*"

>=3.2.0 <3.3.0

✅ 3.2 ラインに固定

"~3.2.0"

>=3.2.0 <3.3.0

✅ 同上

"^3.2"

>=3.2.0 <4.0.0

❌ 3.3 系まで拾い、CMS 3.2 と framework 3.3 が組む事故になる

CI では「CMS マイナー文字列 + .*」で組みます(例 ablogcms/testing-framework:3.2.*)。イメージタグのマイナーと framework の制約を必ず一致させてください。

バージョン対応表

テスト基盤・CMS・PHP の対応は次のとおりです。1 つの CMS マイナーに 1 本のテスト基盤ラインが対応します。

a-blog cms

テスト基盤(composer 制約)

Docker イメージ

PHP

3.2 系

ablogcms/testing-framework:3.2.*

appleple/acms:3.2-php<PHP>

8.1 / 8.2 / 8.3 / 8.4 / 8.5

3.3 系

ablogcms/testing-framework:3.3.*

appleple/acms:3.3-php<PHP>

リリース時に確定

CI マトリクスは、この表の 1 行(CMS マイナー)× PHP を展開したものです。イメージタグのマイナーと testing-framework の制約は必ず一致させます(不一致は「CMS 3.2 に framework 3.3」のような組み合わせ事故になります)。利用可能なタグの最新一覧は Docker Hub の Tags タブが一次情報です。

ローカル / compose の基本形

プラグインを本体イメージの extension/plugins/ 配下に bind mount して起動します。

# docker-compose.yml
services:
  acms:
    image: appleple/acms:3.2-php8.4
    ports: ["8080:80"]
    environment:
      ACMS_DB_HOST: db
      ACMS_DB_NAME: acms
      ACMS_DB_USER: root
      ACMS_DB_PASS: root
    volumes:
      - ./src:/var/www/html/extension/plugins/my-plugin
    depends_on:
      db:
        condition: service_healthy

  db:
    image: mysql:8.0
    environment:
      MYSQL_DATABASE: acms
      MYSQL_ROOT_PASSWORD: root
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-proot"]
      interval: 5s
      timeout: 5s
      retries: 12

GitHub Actions

ネイティブの matrix 機能があるので最も簡潔です。PHP を軸に回し、a-blog cms マイナーバージョンが増えたら acms に足すだけです。

jobs:
  phpunit:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        acms: ["3.2"]                              # 3.3 が出たら "3.3" を足す
        php:  ["8.1", "8.2", "8.3", "8.4", "8.5"]
    env:
      ACMS_IMAGE_TAG: "${{ matrix.acms }}-php${{ matrix.php }}"   # compose の image タグへ反映
    steps:
      - uses: actions/checkout@v4
      - run: docker compose up -d --wait
      - run: |
          docker compose exec -T acms bash -lc '
            cd /var/www/html/extension/plugins/my-plugin
            composer require --dev "ablogcms/testing-framework:${{ matrix.acms }}.*" --no-update
            composer update
            vendor/bin/acms-create-database
            vendor/bin/phpunit'

複数の DB エンジン(MySQL と互換 DB など)でも検証したい場合は、matrix に DB 軸を足し、db サービスのイメージを切り替えます。まずは PHP 軸だけで始め、必要になってから広げれば十分です。

Bitbucket Pipelines

ネイティブ matrix 機能が無いので、YAML アンカー + parallel で PHP を並べます。image: のタグ(<バージョン>-php<PHP>)で PHP と a-blog cms を選びます。

definitions:
  steps:
    - step: &phpunit-32                       # a-blog cms 3.2 ライン(framework も 3.2.*)
        script:
          - composer require --dev "ablogcms/testing-framework:3.2.*" --no-update
          - composer update
          - vendor/bin/acms-create-database
          - vendor/bin/phpunit
pipelines:
  default:
    # 3.3 が出たら &phpunit-33(framework:3.3.*)を足し、3.3-php* の parallel を追加するだけ
    - parallel:
        - step: { <<: *phpunit-32, name: php8.1, image: appleple/acms:3.2-php8.1 }
        - step: { <<: *phpunit-32, name: php8.2, image: appleple/acms:3.2-php8.2 }
        - step: { <<: *phpunit-32, name: php8.3, image: appleple/acms:3.2-php8.3 }
        - step: { <<: *phpunit-32, name: php8.4, image: appleple/acms:3.2-php8.4 }
        - step: { <<: *phpunit-32, name: php8.5, image: appleple/acms:3.2-php8.5 }

Bitbucket Pipelines は 1 パイプライン合計 100 ステップの上限があります。マイナー × PHP を広げるときは総ステップ数に注意してください。

静的解析(phpcs / phpstan)は 1 回だけ

phpcs / phpstan は実行時の PHP に依存せず、対応範囲全体を対象にします。PHP ごとに回さず、1 回だけ実行すれば十分です(PHPStan の設定phpVersion の範囲を指定済みのため)。PHPUnit だけを PHP マトリクスで回し、静的解析は独立した 1 ジョブに分けるのがおすすめです。

コードカバレッジ

カバレッジ計測には Xdebug か PCOV が必要で、計測はテスト実行を大きく遅くします。CI では既定でカバレッジを取らず--coverage-* を付けず)、必要なときだけ計測するのがおすすめです。ローカルで見たいときは次のように生成します。

vendor/bin/phpunit --coverage-html coverage/

同梱イメージ appleple/acms には計測ドライバ(Xdebug / PCOV)が入っています。CI で常時計測したい場合は、PHP マトリクスの 1 つだけで計測して結果をアップロードする形にすると、全レグを遅くせずに済みます。

依存の下限も検証する(任意)

composer.json に書いた依存の下限バージョンでも動くことを確かめると、「新しめの環境でしか動かない」退行を早期に捕まえられます。CI の 1 レグで composer update --prefer-lowest を使うと確認できます。

応用: 案件で複数プラグインをまとめてテストする

1 つの案件で複数プラグインを作る場合、プラグインごとに環境を用意する必要はありません。案件リポジトリに 1 つのテスト環境(composer.json / phpunit.xml.dist / phpstan.neon)を置き、各プラグインの src/extension/plugins/{Name} へ シンボリックリンクすることで、全プラグインをまとめて回せます。詳しい構成はテスト基盤(ablogcms/testing-framework)の README を参照してください。

関連ドキュメント