コンテンツタイプフィールドで紐付けをされたコンテンツデータから、紐付けをしているコンテンツデータを逆引きするタグの作成

Movable Type Advent Calendar 2019の2日目です。会社で別のCMSを扱っているこの頃ですが、今回は個人の趣味・研究活動、またPerl+PHPのプラグインも書けないと仕事で困ることがあり技量維持・向上のため、と取り組んでみました。それに社外の方に情報発信をしたりコミュニケーションをとったりしたいですしね。

さて、先日MTQにて「コンテンツデータ間の紐付け」という質問が上がりました。コンテンツタイプでコンテンツタイプフィールドを利用すると、他のコンテンツタイプのコンテンツデータを紐付けできます。その紐付けられた側のコンテンツデータから紐付けをしているコンテンツデータを参照したい(逆引きしたい)という内容でした。2019年8月に出た「コンテンツデータのリンクに関して」も似た内容かもしれません。

テンプレートで頑張れば確かに目的のデータは取り出せるかもしれませんが、ここはやはりMT::Object->load()職人の出番かなと感じました。私のようなUI開発者の作業なのか、サーバーサイドのエンジニアの作業ではないのか、と思われるかもしれませんが、各職域にはオーバラップしているような部分があって、UI開発者がタグを作る事ができればプロジェクトはスムーズ進み、テンプレートは分かりやすいコードに仕上がるのではないだろうか、と私は考えました。

タグを作成する

ブロックタグ作成はmovabletype/DocumentationリポジトリのWikiにある「ブロックタグ プラグインの開発について」のサンプルコードが利用できます。loadメソッドを変更して紐付けをしているコンテンツデータのIDを取り出します。

コンテンツデータの紐付けデータ格納状況を調査したところ、「mt_cf_idx」テーブルに格納されていました。cf_idx_content_field_idに紐付けをするフィールドのID、cf_idx_content_data_idが紐付けをしているコンテンツデータのID、cf_idx_value_integerが紐付けをされているコンテンツデータのIDです。つまり、フィールドのIDと紐付けをされている側のコンテンツデータIDさえ分かれば元のコンテンツデータは容易に分かるのです。これはプラグインがシンプルな内容になることも意味します。

よって、以下のようなMT::ContentFieldIndex->load()でコンテンツデータIDが取得できました。フィールドIDはテンプレート編集画面の右側で調べることができるのですが、フィールド名を使用してもプラグイン内でMT::ContentField->load()を使用して調べることができます。(1つクエリが増えるのでIDがおすすめです。)

my @contents_field_idxs = MT::ContentFieldIndex->load(
    {   content_field_id => $field_id,
        value_integer => $args->{related_id}
    },
    { fetchonly => ['content_data_id'] }
);

ちなみに、fetchonlycontent_data_idのみを取得するようにしました。クエリログを見ると、fetchonlyを付ける場合と付けない場合で取得するカラムが違うだけではなく、発行されるクエリ数も違ってくるようでした。(fetchonlyを付けない場合はなぜか1つクエリが増える。)

後はコンテンツデータのIDを基にコンテンツデータを取得してコンテキストにセットしていきます。

my $direction = ($args->{sort_order} || '') eq 'ascend' ? 'ASC' : 'DESC';
my $args = ();
$args->{sort} = [
    { column => 'authored_on', desc => $direction },
    { column => 'id',          desc => $direction },
];
my @contents = MT::ContentData->load(
    {   id => \@source_ids,
        status => MT::ContentStatus::RELEASE()
    },
    $args
);

# コンテキストにコンテンツデータをセットしてブロックタグで囲われた範囲の処理をする(要点のみ抜粋)
for my $content_data (@contents) {
    local $ctx->{__stash}{content} = $content_data;

    my $tokens = $ctx->stash('tokens');
    my $builder = $ctx->stash('builder');
    $out .= $builder->build($ctx, $tokens, $cond)
        || return $ctx->error($builder->errstr);
    $i++;
}

テンプレートタグ例

リポジトリのREADME.mdにも書いている例です。「セミナー」コンテンツタイプの「登壇講師」フィールドは、「講師」コンテンツタイプのデータを選択してリンクしているとします。この前提において「講師」コンテンツタイプの各講師データを出力する例を紹介します。タグはMTContentRelatedSourcesです。紐付けをしているセミナーコンテンツタイプのフィールドの名前もしくはIDと、紐付けをされている講師コンテンツデータのIDを渡します。

<mt:Contents content_type="講師">
  <mt:ContentsHeader><div class="seminar"></mt:ContentsHeader>
  <h3><mt:ContentField content_field="氏名"><mt:ContentFieldValue escape /></mt:ContentField></h3>
  <dl>
    <div>
      <dt>担当セミナー</dt>
      <dd>
        <mt:ContentID setvar="speaker_id" />
        <mt:ContentRelatedSources field_name="登壇講師" related_id="$speaker_id">
          <mt:If name="__first__"><ul></mt:If>
          <li><mt:ContentField content_field="セミナー名称"><mt:ContentFieldValue escape /></mt:ContentField></li>
          <mt:If name="__last__"></ul></mt:If>
        </mt:ContentRelatedSources>
      </dd>
    </div>
  </dl>
  <mt:ContentsFooter></div></mt:ContentsFooter>
</mt:Contents>

MTContentRelatedSourcesの間は「講師」コンテンツタイプのデータがセットされているので、標準で備わっているコンテンツタイプのテンプレートタグをそのまま利用してデータが出力できます。実際に書いてみるとシンプルで分かりやすく便利に感じました。また、テンプレートタグを頑張って書いて出力する場合と比べて発行されるクエリは少ないはずです。

上記テンプレートタグを表示すると以下のようにコンテンツデータの逆引きができてセミナー名称が表示されます。
コンテンツデータの逆引きが成功している画面の様子

参考までに、標準機能を用いて「セミナー」コンテンツタイプの「登壇講師」フィールド(コンテンツタイプフィールド)を表示した場合は以下のようになります。
標準機能を用いてセミナーコンテンツタイプを表示している画面の様子

ダイナミックパブリッシングへの対応

ダイナミックパブリッシングへの対応のため、PHPでもMTContentRelatedSourcesタグの実装を行いました。データの取り方は同じなのですが、利用できるメソッドなどが違うので書き方をかなり研究しました。ちょうど案件でもPHPでMTオブジェクトを操作することが必要になっていて、研究の成果はこのプラグインと案件の両方に役立ちました。(福山市か尾道市にあり、12:00に営業している豚骨スープか味噌スープのラーメン屋を抽出するような仕組みを作りました。)

プラグインについて

hideki-a/mt-plugin-cd-related-sourceリポジトリに格納してありますので自由に試してみてください。ライセンスは検討中です。