PowerCMS Xの編集表示「選択項目」を扱うTips

公開

PowerCMS Xで編集表示を「選択項目」にし、ラジオボタンやチェックボックスで情報を選択して保存するフィールドに関する話題です。

ラベルと保存値

使用頻度が低く記憶の彼方に飛んでいたのですが、PowerCMS X 2.57で「選択項目カラムのオプションで、値とラベルをデリミタ「=」で繋いだ形式のデータ指定をサポート」とあります。例として「セミナー分類」編集表示のオプションを以下のように設定すると編集画面にはCMSやUXが表示され、保存値は数値となります。

1=CMS,2=UX,3=アクセシビリティ,4=HTML/CSS,5=JavaScript

ラジオボタン、つまり単一選択の時はカラムタイプ「数値」で保存できます。チェックボックス、つまり複数選択の時はカンマ区切りで複数の数値になるのでカラムタイプ「テキスト(50)」等で保存できます。

この時、ラベルを英語にしておくと多言語対応がしやすくなります。(翻訳を登録し、ラベルを出力する際にtranslateモディファイアを付ける)このオプションフィールドではテンプレートタグも利用できるのですが本稿では扱いません。

選択項目を列挙する

例えばセミナー分類毎のリンクを作る、セミナーを絞り込むフォームを作る時、mt:columnpropertyを利用することでモデルで定義した選択項目を呼び出すことができます。セミナーモデルのカテゴリカラムの選択項目オプションを呼び出し、内容を連想配列化するコードを示します。連想配列ではなくラベルだけの配列にするとオプションの順序が入れ替わった時に不都合が起こると想定されます。

<mt:var name="request._custom_filter_categories" setvar="category_param" />
<mt:columnproperty model="seminar" name="category" property="options" setvar="tmp_option_categories" />
<mt:var name="tmp_option_categories" split="," setvar="tmp_option_categories" /><mt:ignore>オプションを配列にする</mt:ignore>
<mt:setvar name="option_categories" value="" />

<mt:ignore>配列をループし、それぞれキーとラベル(バリュー)に分割して配列に格納する</mt:ignore>
<mt:loop name="tmp_option_categories">
  <mt:var name="__value__" split="=" setvar="tmp_category" />
  <mt:setvar name="option_categories" key="$tmp_category[0]" value="$tmp_category[1]" />
</mt:loop>

長いし何度も使いそうなので独自テンプレートタグ化して「Develop Utilityプラグイン」に格納しようかとも考えました。後述しますが、上記のようにして連想配列化したオプションのデータはセミナー分類カラムのデータを表示する時にも使えます。

連想配列のデータは以下のテンプレートで表示できます。

<mt:ignore>オプション項目を表示する</mt:ignore>
<mt:loop name="option_categories">
  <input type="radio" name="_custom_filter_categories" id="category_<mt:var name="__key__" />" value="<mt:var name="__key__" escape />"<mt:if name="category_param" eq="$__key__"> checked</mt:if>>
  <label for="category_<mt:var name="__key__" />"><mt:if name="page_is_english"><mt:var name="__value__" escape /><mt:else><mt:var name="__value__" translate escape /></mt:if></label>
</mt:loop>

オブジェクトの保存値に対するラベルを表示する

セミナー分類カラムのデータを表示する時は先程作成した連想配列を利用して表示します。保存された数値をキーにすると目的のラベルが表示できます。ラベルを英語で登録した場合はここでtranslateモディファイアを利用して翻訳表示します。単一選択でも複数選択でも同じテンプレートで出力できます。

<mt:seminarcategory split="," setvar="selected_categories" />
<mt:loop name="selected_categories"><mt:var name="__value__" setvar="tmp_selected_key" /><span class="c-mediaCard__category"><mt:var name="option_categories" key="$tmp_selected_key" translate escape /></span></mt:loop>

保存値を検索する

単一選択の場合は数値タイプのカラムに値が一つ入っているので、単純に目的の値に一致するものを抽出すればよいです。複数選択の場合は1,2,10のように値が格納されているため、1を検索したつもりでも検索結果には10が混ざってきます。そのためMySQLのFIND_IN_SET関数を利用してみました。たしか「知っておくと便利なMySQL関数 "FIND_IN_SET" | バシャログ。」を読んで知りました。

/**
 * カンマ区切りで保存しているデータの検索条件を追加する
 * @param string $model 検索対象モデル
 * @param string $column 検索対象カラム
 * @param array  $cond_array 検索値の配列
 * @param string $extra
 * @param array  $extra_values
 * @return void
 */
private function set_find_in_set( $model, $column, $cond_array, &$extra, &$extra_values ) {
   $extra_stack = [];
   foreach ( $cond_array as $cond ) {
       if ( ! empty( $cond ) ) {
           array_push( $extra_stack, "FIND_IN_SET(?, {$model}_{$column})" );
           array_push( $extra_values, $cond );
       }
   }

   if ( count( $extra_stack ) ) {
       $extra .= ' AND (' . implode( ' OR ', $extra_stack ) . ')';
   }
}

/**
 * pre_listingコールバックでの処理(セミナー)
 */
public function pre_listing_seminar( $cb, $app, &$terms, &$args, &$extra, &$extra_values ) {
   if ( $app->id !== 'Bootstrapper' ) {
       return;
   }

   $selected_categories = $app->param( '_custom_filter_categories' );
   if ( ! empty( $selected_categories ) ) {
       if ( ! is_array( $selected_categories ) ) {
           $selected_categories = explode( ',', $selected_categories );
       }
       $this->set_find_in_set( 'seminar', 'category', $selected_categories, $extra, $extra_values );
   }
}

この方式の弱点は検索時に「フルテーブルスキャン」になることです。複数選択の場合はリレーションにした方が良いのかな。(パフォーマンス測定まではできておらず。)