a-blog cmsの拡張アプリ開発時によく使う記述

この記事は、a-blog cms Advent Calendar 2021 の3日目の記事です。

拡張アプリとは

a-blog cmsではほとんどのことを標準機能で実装できますが、「拡張アプリ」を使用することで、WordPressやMovable Typeでいう「プラグイン」と同様に、サイトに標準機能にない独自の機能を追加することができます。「拡張アプリ」には無償で配布されているものと、ビジネスパートナー向けに販売されているものがあります。

無償の拡張アプリ

無償の拡張アプリは外部サービスと連携するものが中心に配布されています。
https://developer.a-blogcms.jp/app/

  • Slack連携(フォームの送信内容をSlackに通知)
  • ChatWork連携(フォームの送信内容をChatWorkに通知)
  • Zoho CRM連携(フォームの送信内容をZoho CRMに記録)
  • Google Spreadsheets連携(フォームの送信内容をSpreadsheetsに記録)
  • Google Calendar(フォームの送信内容をGoogle Calendarに追加)
  • reCAPTCHA(フォームにボットからの送信防止機能の追加)
  • Base連携(ECサービスのBASEの商品一覧、商品詳細を表示)

ビジネスパートナー向けの拡張アプリ

ビジネスパートナー向けには多言語サイトやポータルサイトに役に立つ拡張アプリが販売されています。ビジネスパートナー制度についてはこちらをご覧ください。
https://developer.a-blogcms.jp/partner/detail.html

  • 多言語サイト管理
  • エントリーの閲覧履歴表示
  • エントリーのアクセスランキング
  • エントリーのお気に入り登録機能
  • フォームからエントリーに投稿する機能

拡張アプリの利用方法

拡張アプリは下記の手順で利用することができます。

  1. config.server.php を変更して HOOK_ENABLE を1にする
  2. /extension/plugins/ にプラグインをアップロード
  3. 管理画面の「拡張アプリ」よりインストール
  4. 拡張アプリごとに各種設定を行う

拡張アプリを開発する

a-blog cmsでは独自のモジュールや、校正オプション、フック処理を追加することができますが、拡張アプリではそれらを1つのパッケージにまとめて利用することができます。

1つのパッケージにまとめることで、必要な機能をそれぞれのファイルの中に分散することなく、1つのフォルダにまとめて管理することができます。また、インストールしたらデータベースを作成、アンインストールしたらデータベースを削除するといったデータの保全性を保つこともできます。

拡張アプリの基本的な考え方

拡張アプリは大別すると下記のいずれかになると思います。a-blog cmsのフォームや記事に絡めて処理の追加を行います。また、単独で利用するシステムの場合でも、a-blog cms以外の領域に独自のシステムを開発することもできますが、a-blog cmsのテンプレートやページングなどの機能を利用できるのがメリットになります。

  • フォームの送信をしたときに何かをする(記事の保存をしたときも含む)
  • 標準機能以外の管理画面を追加する
  • 標準機能の管理画面に項目を追加する

詳しくは公式ドキュメントや、先人たちのブログをご覧ください。

2021年に開発した拡張アプリ

今年は4本拡張アプリを開発しました(リンクを張っていないものは時間ができたときに公開予定です)。ドキュメントは踏み込んだ内容は書かれていないので、他の拡張アプリのソースコードを見て、使える関数などを探しました。この記事では拡張アプリの開発に使った関数を紹介したいと思います。

拡張アプリでよく使用する記述

特定のカスタムフィールドを取得する

loadBlogField()loadCategoryField()loadEntryField()loadUserField() 関数でブログ、カテゴリー、エントリー、ユーザのカスタムフィールドの情報を取得することができます。カスタムフィールドの値を取り出すには、値が単一の場合は $Field->get() 、チェックボックスなど複数の場合は $Field->getArray() を使用します。

例えばエントリーの entry_pickup というカスタムフィールドを取得する場合は、このようなかたちになります。

$EntryField = loadEntryField(EID);
$entry_pickup = $EntryField->get('entry_pickup');

例えばエントリーの entry_area というチェックボックスのカスタムフィールドを取得する場合は、このようなかたちになります。

$EntryField = loadEntryField(EID);
$entry_area = $EntryField->getArray('entry_area');

a-blog cmsのカスタマイズが捗る、Hook.php の設定 で知らないうちに使ったことがある人は多いと思います。

管理画面のある拡張アプリで使用する記述

データベースを作成する

プラグインのインストール時にデータベースを作成します。プラグインのアンインストール時はデータベースを削除します。DBのスキーマを用意し、ServiceProvider.php に記述を追加します。

/plugins/*****/schcema/db-index.yaml

%{PREFIX}appname:
  - PRIMARY KEY (appname_fieldname)

/plugins/*****/schcema/db-schema.yaml

---
%{PREFIX}appname:
  appname_id:
    Field: appname_id
    Type: int(11)
    'Null': NO
    Key: UNI
    Default:
    Extra: 'AUTO_INCREMENT'
  appname_fieldname:
    Field: appname_fieldname
    Type: varchar(255)
    'Null': 
    Key: 
    Default:
    Extra:

/plugins/*****/ServiceProvider.php

    protected $installTable = array(
        'appname',
    );

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

        //------------
        //テーブル削除
        dbDropTables($this->installTable);

        //---------------------
        // テーブルデータ読み込み
        $yamlTable = preg_replace('/%{PREFIX}/', DB_PREFIX,
            Storage::get(dirname(__FILE__) . '/schema/db-schema.yaml'));
        $tablesData = Config::yamlParse($yamlTable);
        if (!is_array($tablesData)) {
            $tablesData = array();
        }
        if (!empty($tablesData[0])) {
            unset($tablesData[0]);
        }
        $tableList = array_merge(array_diff(array_keys($tablesData), array('')));

        $yamlIndex = preg_replace('/%{PREFIX}/', DB_PREFIX,
            Storage::get(dirname(__FILE__) . '/schema/db-index.yaml'));
        $indexData = Config::yamlParse($yamlIndex);
        if (!is_array($indexData)) {
            $indexData = array();
        }
        if (!empty($indexData[0])) {
            unset($indexData[0]);
        }

        //---------------
        // テーブル作成
        foreach ($tableList as $tb) {
            $index = isset($indexData[$tb]) ? $indexData[$tb] : null;
            dbCreateTables($tb, $tablesData[$tb], $index);
        }
    }

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

シーケンスを利用する

a-blog cmsではエントリーのIDなどの連番となる情報はシーケンステーブルで管理されています。シーケンスを利用する場合は ServiceProvider.php に記述を追加します。acms_sequence_plugin という拡張アプリ用のテーブルにシーケンスが記録されます。

/plugins/*****/ServiceProvider.php

    protected $sequence_key = array(
        'sequence_appname_id',
    );

    /**
     * インストールするときの処理
     * データベーステーブルの初期化など
     *
     * @return void
     */
    public function install()
    {
        //---------------
        // 初期データ生成
        $DB = DB::singleton(dsn());
        foreach ( $this->sequence_key as $key ) {
            $SQL = SQL::newInsert('sequence_plugin');
            $SQL->addInsert('sequence_plugin_key', $key);
            $SQL->addInsert('sequence_plugin_value', 1);
            $DB->query($SQL->get(dsn()), 'exec');
        }
    }

    /**
     * アンインストールするときの処理
     * データベーステーブルの始末など
     *
     * @return void
     */
    public function uninstall()
    {
        $DB = DB::singleton(dsn());
        foreach ( $this->sequence_key as $key ) {
            $SQL    = SQL::newDelete('sequence_plugin');
            $SQL->addWhereOpr('sequence_plugin_key', $key);
            $DB->query($SQL->get(dsn()), 'exec');
        }
    }

シーケンスを加算するには nextval() 関数を利用します。

$appname_id = $DB->query(SQL::nextval('appname_id', dsn(), true), 'seq');

ページング付きでDBの一覧を出力する

データベースから情報を取得し、ページング付きで出力するGETモジュールは下記のような記述です。

/plugins/*****/GET/AppNameList.php

<?php

namespace Acms\Plugins\*****\GET;

use ACMS_GET;
use Template;
use DB;
use SQL;
use SQL_Select;
use ACMS_Corrector;

class AppNameList extends ACMS_GET
{
    public $limit = 20;

    function get()
    { 
        $Tpl = new Template($this->tpl, new ACMS_Corrector());

        /**
         * 記事一覧を取得
         */
        $DB = DB::singleton(dsn());
        $SQL = SQL::newSelect('appname');
        $SQL->addSelect('*');
        $SQL->addOrder('appname_fieldname', 'DESC');

        // 条件を追加
        if ( KEYWORD ) {
            $SQL->addWhereOpr('appname_fieldname', '%'.KEYWORD.'%', 'LIKE');
        }

        // ページャを取得
        $Pager = new SQL_Select($SQL);
        $Pager->setSelect('DISTINCT(appname_id)', 'appname_amount', null, 'COUNT');
        $pageAmount = intval($DB->query($Pager->get(dsn()), 'one'));
        $pager = $this->buildPager(PAGE, $this->limit, $pageAmount, 3, ' class="cur"', $Tpl, array(), array('admin' => ADMIN));

        // クエリを実行
        $SQL->setLimit($this->limit, (PAGE-1)*$this->limit);
        $list = $DB->query($SQL->get(dsn()), 'all');

        /**
         * テンプレートに格納
         */
        $obj = array(
            'list'        => $list,
            'pager'       => $pager,
            'itemsAmount' => $pager['itemsAmount'],
            'itemsFrom'   => $pager['itemsFrom'],
            'itemsTo'     => $pager['itemsTo'],
        );

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

/plugins/*****/template/index.html

  <!-- BEGIN_MODULE AppNameList -->
  <div class="acms-admin-table-scroll-xs acms-admin-table-scroll-sm acms-admin-table-scroll-md">
    <table class="acms-admin-table-admin acms-admin-table-hover">
      <thead class="acms-admin-table-heading">
        <tr>
          <th>ID</th>
          <th>項目</th>
        </tr>
      </thead>
      <!-- BEGIN index:loop -->
      <tr>
        <td>{appname_id}</td>
        <td>{appname_fieldname}</td>
      </tr>
      <!-- END index:loop -->
    </table>
  </div>

  <div class="acms-admin-grid">
        @section("entry-index-pager")
        <div class="acms-admin-col-md-6 acms-admin-col-sm-12">
            <!-- BEGIN pager:veil -->
            <div class="acms-admin-pager-container">
                <ul class="acms-admin-pager"><!-- BEGIN backLink -->
                    <li class="prev"><a href="{url}">&laquo;&nbsp;<!--T-->前へ<!--/T--></a></li><!-- END backLink --><!-- BEGIN page:loop -->
                    <li{pageCurAttr}[raw]><span><!-- BEGIN link#front --><a href="{url}"><!-- END link#front -->{page}<!-- BEGIN link#rear --></a><!-- END link#rear --></span></li><!-- END page:loop --><!-- BEGIN lastPage:veil -->
                    <li class="last"><a href="{lastPageUrl}">...{lastPage}</a></li><!-- END lastPage:veil --><!-- BEGIN forwardLink -->
                    <li class="next"><a href="{url}"><!--T-->次へ<!--/T-->&nbsp;&raquo;</a></li><!-- END forwardLink -->
                </ul>
            </div>
            <!-- END pager:veil -->
        </div>
        <div class="acms-admin-col-md-6 acms-admin-col-sm-12">
            <div class="acms-admin-itemsAmount-container">
                <p>{itemsFrom}<!--T-->件<!--/T--> - {itemsTo}<!--T-->件<!--/T--> / <!--T-->全<!--/T-->{itemsAmount}<!--T-->件<!--/T--></p>
            </div>
        </div>
        @endsection
    </div><!-- /.acms-grid -->

  <!-- END_MODULE AppNameList -->

コンフィグを利用する

ブログ、カテゴリー、エントリー、ユーザ等で独自の情報を保存する場合はカスタムフィールドを利用しますが、拡張アプリでAPIキーや管理画面の設定など独自の情報を保存する場合はコンフィグに保存することができます。(コンフィグに保存した情報は拡張アプリをアンインストールしても削除されないので注意が必要です)

<!-- BEGIN_MODULE Admin_Config -->
<form action="" method="post" class="acms-admin-form">
<input type="text" name="appname_fieldname" value="{appname_fieldname}" class="acms-admin-form-width-full" autocomplete="off">
<input type="hidden" name="config[]" value="appname_fieldname">
<input type="submit" name="ACMS_POST_Config" value="<!--T-->保存<!--/T-->" class="acms-admin-btn-admin acms-admin-btn-admin-primary acms-admin-btn-admin-save"/>
</form>
<!-- END_MODULE Admin_Config -->

呼び出すときは loadBlogConfig() を使用します。

$Config = Config::loadBlogConfig(BID);
$field_value = $Config->get('appname_fieldname');

フォームの拡張アプリで使用する記述

動作するPOSTモジュールIDを限定する

フォームに関する処理はフック処理のPOSTモジュール処理前(beforePostFire)で処理します。下記はフォームの送信時(ACMS_POST_Form_Submit)のみ動作する記述です。

/plugins/*****/Hook.php

<?php

namespace Acms\Plugins\*****;

class Hook
{
    /**
     * POSTモジュール処理前
     * $thisModuleのプロパティを参照・操作するなど
     *
     * @param \ACMS_POST $thisModule
     */
    public function beforePostFire($thisModule)
    {
        $moduleName = get_class($thisModule);

        if ($moduleName !== 'ACMS_POST_Form_Submit') {
            return;
        }
        if (!$thisModule->Post->isValidAll()) {
            return;
        }
        /* do something */
    }
}

フォームの送信内容を取得する

フォームの送信内容はPOSTモジュールに保存されていますので、get()getArray() で取り出すことができます。

$field_value = $thisModule->Post->get('form_fieldname');

フォームの送信内容をテンプレートで取得する

フォームの送信内容をテンプレートの記述を使って取り出すことができます。

$tpl = '<!-- BEGIN_MODULE Form --><!-- BEGIN step#result -->{sei} {mei}<!-- END step#result --><!-- END_MODULE Form -->';
$body = build(setGlobalVars($tpl), Field_Validation::singleton('post'));

フォームの管理画面に設定項目を追加して取得する

フォームの拡張アプリではAPIキーなどをフォームIDのメール設定に保存します。

<input type="text" name="formappname_fieldname" value="{formappname_fieldname}" class="acms-admin-form-width-full" autocomplete="off">
<input type="hidden" name="mail[]" value="formappname_fieldname">

呼び出すときはデータベースから情報を取得します。実際には他の拡張アプリを参考にするともう少しコンパクトになります。

$code = $thisModule->Post->get('id');

$DB = DB::singleton(dsn());
$SQL = SQL::newSelect('form');
$SQL->addWhereOpr('form_blog_id', BID);
$SQL->addWhereOpr('form_code', $code);
$row = $DB->query($SQL->get(dsn()), 'row');
if (!$row) {
  return false;
}
$Form = new Field();
$Form->set('code', $row['form_code']);
$Form->set('name', $row['form_name']);
$Form->set('scope', $row['form_scope']);
$Form->set('log', $row['form_log']);
$Form->overload(unserialize($row['form_data']), true);

$FormConfig = $Form->getChild('mail');
$field_value = $FormConfig->get('formappname_fieldname');

以上、a-blog cmsの拡張アプリでよく使う記述をご紹介しました。拡張アプリの開発を検討したものの、行き詰まってしまった方のヒントになると幸いです。拡張アプリはとっつきにくいイメージですが、慣れてしまえば開発も容易なので、素敵な拡張アプリが誕生することを期待します!


関連記事

この記事のハッシュタグに関連する記事が見つかりませんでした。