これは「a-blog cms Advent Calendar 2021」の、17日目の記事です。
前置き
管理しているサイトで、数年前に「今表示しているエントリーが関連付けられているエントリーを表示できませんか」と相談されました。言い換えると現在のエントリーを関連エントリーに登録しているエントリー、ということになります。
そのサイトでは、ブログを横断した関連エントリーを多用していました。ここではA→Bとします。
数十件くらい投稿してから、B→Aの関連付けも追加して相互リンクにしようとしたところ、a-blog cmsの仕様上、エントリーBの編集画面へ行っても「関連付けをしているのがエントリーAである」ことがわからないので、AとBの編集画面を両方表示して作業している、とのことでした。
これは非常に難しいです。a-blog cmsのタグだけでは「関連付けられているエントリー」を調べる方法がないからです。相談された方は「これができたら助かるなー」レベルのご相談だったので、まとまった作業になるとお返事して、そのときは終了となりました。
WsEntryRelatedモジュールの仕様
今年のAdvent Calendarのテーマは拡張アプリにしよう、ということで、当時の課題を実現しました。
「WsEntryRelated」アプリの仕様は、以下のとおりです。
- 現在のエントリーを「関連エントリー」に登録しているエントリーを検索し表示する
- すべてのブログを対象とする
- 日付、主カテゴリー、タイトルを公開日降順で表示する
- エントリー、カテゴリー、ブログが非公開、下書き、予約投稿になっている場合は、ログイン中かつ編集者以上のときのみ表示する
独自のSQL文を書きます。処理の流れは以下のとおりです。
-
relationship
テーブルのrelation_eid
に現在のエントリーIDが入っている行をすべて取得する -
entry
テーブルのentry_id
にそのID群が入っている行を再度取得する
SQL文を複雑にすれば一回のリクエストでできた気がしますが、今のところ試作なので二段階に分けます。
参考記事
a-blog cmsは基本的にPHPを書きませんが、拡張アプリ作成のリファレンスは一通りあります。
開発 | ドキュメント | a-blog cms developer
拡張アプリを実際に開発している、ジェノベーゼさんの記事も参考になると思います。
a-blog cmsの拡張アプリ開発時によく使う記述 | a-blog cms | MR. GRADATION
ファイル構造
拡張アプリの中身は定義とGET関連の処理だけです。WsEntryRelatedディレクトリはa-blog cmsの /extension/plugins/
以下に置きます。
WsEntryRelated
├ ServiceProvider.php
└ GET
└ WsEntryRelated.php
アプリの内容
実際のコードを公開します。細かい解説は後ほど。
ServiceProvider.php
DBテーブルを作らないので、定義ファイルには最低限の処理しか書いていません。
<?php
namespace Acms\Plugins\WsEntryRelated;
use ACMS_App;
use Acms\Services\Common\HookFactory;
use Acms\Services\Common\CorrectorFactory;
use Acms\Services\Common\ValidatorFactory;
use Acms\Services\Common\InjectTemplate;
class ServiceProvider extends ACMS_App
{
/**
* @var string
*/
public $version = '1.0.0';
/**
* @var string
*/
public $name = 'WsEntryRelated';
/**
* @var string
*/
public $author = 'webbingstudio';
/**
* @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;
}
}
GET/WsEntryRelated.php
アプリの本体です。ACMS_GETやDB関連を継承しています。ACMS_Entryを継承するともっと高度な処理ができるようになります。
<?php
namespace Acms\Plugins\WsEntryRelated\GET;
use ACMS_GET;
use Template;
use DB;
use SQL;
use SQL_Select;
use ACMS_Corrector;
class WsEntryRelated extends ACMS_GET
{
public $_scope = array(
'bid' => 'global',
'cid' => 'global',
'eid' => 'global',
);
function get()
{
// 表示件数の上限
$this->limit = 50;
// 関連エントリータイプ
$this->relation_type = 'default';
$Tpl = new Template($this->tpl, new ACMS_Corrector());
$DB = DB::singleton(dsn());
$output = array();
// 現在のエントリーを関連付けているエントリーのIDを配列で取得
$SQL = SQL::newSelect('relationship');
$SQL->addSelect('*');
$SQL->addWhereOpr('relation_eid', $this->eid);
$SQL->addWhereOpr('relation_type', $this->relation_type);
$SQL->addOrder('relation_eid', 'ASC');
$SQL->setLimit($this->limit, 0);
$q = $SQL->get(dsn());
$this->related = $DB->query($q, 'all');
$this->related_eids = array_column($this->related, 'relation_id');
// var_dump($this->related);
// 1件以上ならエントリー情報を取得
if( $this->related_eids && is_array($this->related_eids) ) {
$SQL = SQL::newSelect('entry');
$SQL->addSelect('*');
$SQL->addLeftJoin('blog', 'blog_id', 'entry_blog_id');
$SQL->addLeftJoin('category', 'entry_category_id', 'category_id');
$SQL->addWhereIn('entry_id', $this->related_eids);
$SQL->addWhereOpr('entry_status', 'trash', '<>');
$SQL->addWhereOpr('entry_indexing', 'on');
$SQL->addOrder('entry_posted_datetime', 'DESC');
$SQL->setLimit($this->limit, 0);
$q = $SQL->get(dsn());
$this->entries = $DB->query($q, 'all');
// var_dump($this->entries);
foreach( $this->entries as $row ) {
$show = false;
if(
($row['blog_status'] == '' || $row['blog_status'] == 'open')
&& ($row['category_status'] == '' || $row['category_status'] == 'open')
&& ($row['entry_status'] == '' || $row['entry_status'] == 'open')
&& (strToTime($row['entry_start_datetime']) <= REQUEST_TIME )
) {
$show = true;
} else if( sessionWithCompilation($this->bid) ) {
$show = true;
}
if( $show ) {
$output[] = array(
'eid' => $row['entry_id'],
'title' => addPrefixEntryTitle(
$row['entry_title']
, $row['entry_status']
, $row['entry_start_datetime']
, $row['entry_end_datetime']
, $row['entry_approval']
),
'date' => $row['entry_datetime'],
'cid' => $row['category_id'],
'categoryCode' => $row['category_code'],
'categoryName' => $row['category_name'],
'url' => acmsLink(array(
'eid' => $row['entry_id'],
'cid' => $row['category_id']
))
);
}
}
}
$obj = array(
'entry' => $output,
'count' => count($output),
);
return $Tpl->render($obj);
}
}
表示部分のテンプレートタグ
上のアプリを管理ページ「拡張アプリ」からインストールして、テーマの表示したい箇所に以下のテンプレートタグを書きます。
変数名はa-blog cmsのエントリー関連モジュールと同じですが、対象が存在しているかを判定する entry:veil
category:veil
オプションを有効にする方法がわからなかったので、IFブロックで代用しています。
<!-- BEGIN_MODULE WsEntryRelated -->
<!-- BEGIN_IF [{count}/gte/1] -->
<section class="module-section">
<div class="module-header clearfix">
<h2 class="module-heading">「%{ENTRY_TITLE}」を関連付けているエントリー</h2>
</div>
<ul class="headline acms-list-group clearfix"><!-- BEGIN entry:loop -->
<li class="headline-item">
<a href="{url}" class="acms-list-group-item headline-link">
<time class="headline-dat" datetime="{date}[date('c')]">{date}[date('Y年m月d日')]</time>
<!-- BEGIN_IF [{cid}/nem] --><span class="acms-label">{categoryName}</span><!-- END_IF -->
<span class="headline-title">{title}</span>
</a>
</li><!-- END entry:loop -->
</ul>
</section>
<!-- END_IF -->
<!-- END_MODULE WsEntryRelated -->
アプリの解説
関連付けられを調べる
基本的な話ですが、生のSQL文を書いてはいけません。a-blog cmsのSQLクラスを利用します。$SQL->add***
で条件を追加していき、$SQL->get(dsn())
を実行すると、連想配列で結果が返ってきます。var_dump
をコメントアウトしてあるので、結果を出力してチェックしてください。
データベース関連のクラスの解説はこちら。
データベース関係クラスの使用(2) SQL Select編 | 開発 | ドキュメント | a-blog cms developer
現在表示しているページのエントリーIDは$this->eid
で取れます。
a-blog cmsの関連エントリーは複数のグループを作れるので、グループ名(relation_type)で絞り込む必要があります。ここでは固定値として、最初からある「default」を$this->relation_type
に入れています。
// 現在のエントリーを関連付けているエントリーのIDを配列で取得
$SQL = SQL::newSelect('relationship');
$SQL->addSelect('*');
$SQL->addWhereOpr('relation_eid', $this->eid);
$SQL->addWhereOpr('relation_type', $this->relation_type);
$SQL->addOrder('relation_eid', 'ASC');
$SQL->setLimit($this->limit, 0);
$q = $SQL->get(dsn());
IDの配列からエントリーを調べる
続いて結果から再度、エントリーを返すSQL文を作成します。$SQL->addLeftJoin
でブログとカテゴリーの情報も引っ張れます。あとで非公開判定に使用します。
$SQL->addWhereIn
は配列を条件にすることができます。
「ゴミ箱に入っているエントリー」「一覧に表示しないエントリー(エントリーの編集画面でインデキシングがオフになっている)」を除外することに注意してください。
$SQL = SQL::newSelect('entry');
$SQL->addSelect('*');
$SQL->addLeftJoin('blog', 'blog_id', 'entry_blog_id');
$SQL->addLeftJoin('category', 'entry_category_id', 'category_id');
$SQL->addWhereIn('entry_id', $this->related_eids);
$SQL->addWhereOpr('entry_status', 'trash', '<>');
$SQL->addWhereOpr('entry_indexing', 'on');
$SQL->addOrder('entry_posted_datetime', 'DESC');
$SQL->setLimit($this->limit, 0);
$q = $SQL->get(dsn());
非公開判定
管理目的で使用する場合は、非公開判定を実装しなければなりません。
- 日付、主カテゴリー、タイトルを公開日降順で表示する
- エントリー、カテゴリー、ブログが非公開、下書き、予約投稿になっている場合は、ログイン中かつ編集者以上のときのみ表示する
このアプリでは一つひとつ判定していく方向でif文を書きましたが、もしかしたらACMS_Entryクラスに関数があるかもしれないです。
$show
がtrueならそのエントリーを表示します。
$show = false;
if(
($row['blog_status'] == '' || $row['blog_status'] == 'open')
&& ($row['category_status'] == '' || $row['category_status'] == 'open')
&& ($row['entry_status'] == '' || $row['entry_status'] == 'open')
&& (strToTime($row['entry_start_datetime']) <= REQUEST_TIME )
) {
$show = true;
} else if( sessionWithCompilation($this->bid) ) {
$show = true;
}
テンプレートのレンダリング
取得したエントリーからテンプレート変数用の連想配列を組み立て、$Tpl->render
を通してreturnすればアプリの処理は完了です。配列名はWordPressぽく$output
としていますが何でもいいです。
$output[] = array(
...
);
$obj = array(
'entry' => $output,
'count' => count($output),
);
return $Tpl->render($obj);
キーが変数名になりますから、ここでは以下の変数が使えることになります。a-blog cmsのビルトインモジュールと同じように、自動でHTMLタグがエスケープされ、校正オプションも使えます。
{eid},{title},{date},{cid},{categoryCode},{categoryName},{url}
ログイン中、制限されたエントリーのタイトルには【非公開】や【公開予約】が付きます。この処理はaddPrefixEntryTitle
関数でできます。
'title' => addPrefixEntryTitle(
$row['entry_title']
, $row['entry_status']
, $row['entry_start_datetime']
, $row['entry_end_datetime']
, $row['entry_approval']
),
また、パーマリンクの生成もacmsLink
関数でできます。a-blog cmsのパーマリンクはカテゴリーに依存するのでカテゴリーIDも渡します。
'url' => acmsLink(array(
'eid' => $row['entry_id'],
'cid' => $row['category_id']
))
実用にはまだまだ…
アプリの解説は以上です。
リンクを表示するだけでしたら充分使えますので、ご自由に使用・改良してください。ブログの構成によっては想定外の挙動があるかもしれません。必ず利用予定の環境で動作テストをしてください。
そんなこんなで要件の達成はできましたが、実際のサイトでがっつり使うための機能は不足しています。正式な拡張アプリにするなら、以下も実装しなければならないでしょう。
- メイン画像とメディア管理
- カスタムフィールド
- 使用する関連エントリーグループ名を管理ページで選択、もしくはモジュールIDで渡す
- date#Y などのフォーマット変数
- 関連エントリーで既に表示されていた場合は除外する
- キャッシュの考慮(これは大丈夫かも)
拡張が必要になるのはだいたいエントリーの処理関係なので、ACMS_Entryの関数の解説資料がもう少しあればいいなあ♪と無茶を言って今年のAdvent Calendarの記事を終わります。