Drupal 8初心者講座

この記事の目次

前回は Drupal の標準クエリビルダーである Views モジュールの機能を紹介しました。CMS(コンテンツ管理システム)において、コンテンツを管理するデータベースへの問い合わせは中核的な処理であり、その実装はソフトウェアの特長が表れる部分ではないかと思います。そこで今回は Views 以外のクエリー手段として、他の CMS の例も見ながら考察してみたいと思います。

題材

前回は Views のサンプルとして、独自に定義したコンテンツタイプ「自動車」のコンテンツを 10 件用意し、それらをデータベースから取り出してテーブル形式で表示する例を示しました。

属性(フィールド) 説明
タイトル 自動車コンテンツのタイトル
説明 自動車コンテンツの内容本文
定員 乗車できる定員
車種 あらかじめ要した選択肢から選ぶ車種
写真 自動車の写真

独自のコンテンツ構造を定義する方法については、この連載の第8回で Wordpress と Movable Type(以下 MT)のカスタムフィールドを紹介しました。今回もこの2製品で、同様のコンテンツ構造を定義してクエリーを発行する方法を見てみましょう。

MT6:テンプレートで独自タグを利用する

MT は次期バージョン 7 でコンテンツタイプがサポートされますが、ここでは現時点の最新版である MT6 で上と同様のサンプルを作ってみましょう。MT6 の場合、コンテンツの型を定義する手段はありませんが、コンテンツの蓄積場所であるブログは1つのサイトにいくつでも作ることができ、用途に応じて各ブログ毎にテーマやカスタムフィールドなど固有の設定が可能です。ここでは「自動車」コンテンツ専用のブログを作成し、このブログの記事に「自動車」用のカスタムフィールドを追加することにします。

MT6 では固有の設定を持つブログを任意の数作成できる

MT6 では固有の設定を持つブログを任意の数作成できる

タイトルと本文は標準の入力欄を使用し、追加フィールドとして、写真、色、定員、車種のカスタムフィールドを定義します。前回 Drupal のサンプルではタクソノミーのターム参照として定義した「車種」は、あらかじめ選択肢を指定したドロップダウンを代用します。こうして「自動車」ブログに独自のフィールド構造を持つ記事を蓄積できるようになりました。

「自動車」ブログの記事に定義したカスタムフィールドで自動車コンテンツを作る

「自動車」ブログの記事に定義したカスタムフィールドで自動車コンテンツを作る

このブログに「自動車」コンテンツを登録し、サンプルのコンテンツを用意しました。

登録したサンプルコンテンツ

登録したサンプルコンテンツ

次に、前回 Views のサンプルで示したように、このコンテンツの一覧をテーブル形式でユーザーに提供してみましょう。MT には、Drupal のブロックシステムのような、管理 UI でコンテンツの構成やレイアウトを定義する標準機能はなく、基本的にテーマ内のテンプレートとその構成を適切に設計することでサイトを構築します。コンテンツのクエリーもテンプレートの中から行うことができます。

MT は独自のテンプレートエンジンを持っており、MTタグと呼ばれる専用のタグによってデータベースへのクエリーが抽象化されています。たとえば、MTEntries というタグを使用して記事の一覧を取得することができます。このタグには、出力件数の制限、並べ替えの順序指定、フィールド値による絞り込みなど、種々機能を提供する属性(モディファイア)が用意されています。MTタグを利用して、上記の自動車コンテンツから黄色のものを抽出してテーブル形式で一覧を出力するコード例を示します。

<table border="1">
  <tr>・・・見出し・・・</tr>
  <mt:Entries include_blogs="2" field:color="黄">
    <tr>
      <td>
        <$mt:EntryTitle$>
      </td>
      <td>
        <mt:ColorData>
      </td>
      <td>
        <$mt:EntryBody$>
      </td>
      <td>
        <mt:CapacityData>
      </td>
      <td>
        <mt:CartypeData>
      </td>
      <td>
        <mt:If tag="PhotoData">
          <mt:PhotoDataAsset>
            <img src="<$MTAssetURL$>" width="100" />
          </mt:PhotoDataAsset>
        </mt:If>
      </td>
    </tr>
  </mt:Entries>
</table>

MT タグを利用して「自動車」コンテンツのクエリーを行うテンプレートの記述例

MT には、他のテンプレートから呼び出せるモジュールテンプレートという種類のテンプレートがあり、たとえば、上記を「自動車の一覧」という名前のモジュールテンプレートとして定義しておくと、他のテンプレートから下記コードを利用して任意の位置に自動車のテーブル出力を埋め込むことができます。

<$mt:Include module="自動車の一覧"$>

こうして作成したテンプレートを基に、いわゆる「再構築」と呼ばれる処理を経て、最終的なコンテンツが生成されます。ユーザーはこの静的なファイルを閲覧する形になります。

自動車コンテンツを抽出したテーブル出力の例

自動車コンテンツを抽出したテーブル出力の例

このように、MT のテンプレートを利用したクエリーでは、再構築後の静的なファイルとしてクエリー結果が提供されるため、コンテンツの更新と結果反映との間にタイムラグが生じる可能性があり、即時性が求められる用途には向いていません。こうした用途では、Data-API と呼ばれる REST API を利用する方法があり、動的なクエリ結果を JSON 形式で取得できます。この場合は、JavaScript 等を利用して、クライアント側で結果を加工してページに出力する形になります。

WordPress:API を利用したデータアクセス

次に WordPress で同じことをやってみましょう。第8回で紹介したように、Wordpress では投稿タイプとして独自のコンテンツの型を定義することができます。基本的には API を使用しますが、Custom Post Type UI というプラグインを使用すると Drupal のように管理画面から投稿タイプを定義できるようになります。また、Advanced Custom Field というプラグインを使用することで、種々のカスタムフィールドを追加することも可能になります。ここでは、これらのプラグインを利用して「自動車」という名前(スラッグは car)の投稿タイプを定義します。

投稿タイプの定義

投稿タイプの定義

さらに「自動車の属性」というフィールドグループを作成し、色、定員、写真の各カスタムフィールドを追加して、このグループが投稿タイプ「自動車」に適用されるように設定しておきます。

フィールドグループの設定

フィールドグループの設定

こうして、投稿タイプ「自動車」独自のフィールド構造を持つコンテンツを投稿できるようになりました。

投稿タイプ「自動車」独自のフィールド構造を持つコンテンツを投稿

この投稿タイプを使用して、MT の例と同様「自動車」投稿サンプルを用意しました。このコンテンツにクエリーを発行して、テーブル形式の一覧表として表示してみます。

WordPress では、コンテンツを取得するための API が用意されており、内部的なデータベース クエリーを抽象化しています。また、Advanced Custom Field プラグインは、このプラグインで定義したカスタムフィールドにアクセスするための API を公開しています。これらを組み合わせることで、独自に定義した投稿タイプの投稿やカスタムフィールドにアクセスすることが可能です。

WordPress のテンプレートは PHP で記述されており、テンプレートの中から API を直接呼び出して結果を埋め込むこともできてしまいますが、ここではショートコードと呼ばれるタグを利用して、テーブル出力を任意のコンテンツに埋め込めるようにしてみます。ショートコードとは、[タグ名 属性="値",・・・] の形式で記述するタグで、対応する PHP コードの実行結果をタグ位置に展開する仕組みで、add_shortcode という Wordpres の API を使って定義することができます。これを利用して、投稿データの取得とテーブル出力を行うショートコード carstag を定義します。

function carstag_func( $atts ) {
  $args = array(
    'numberposts' => 3,  //表示(取得)する記事の数
    'post_type' => 'car'   //投稿タイプの指定
  );
  $posts = get_posts( $args );
  $ret = "<table border='1'>";
  if( $posts ) {
    foreach( $posts as $post ) {
      $ret .= '<tr>';
      $ret .= '<td>' . $post->post_title . '</td>';
      $ret .= '<td>' . $post->post_content . '</td>';
      $ret .= '<td>' . get_field('色', $post->ID) . '</td>';
      $ret .= '<td>' . get_field('定員', $post->ID) . '</td>';
      $p = get_field('写真', $post->ID);
      $ret .= '<td><img width="100" src="’ . $p['url'] .'"></td>';
      $ret .= '</tr>';
    }
  }
  $ret .= '</table>';
  wp_reset_query();
  return $ret;
}
add_shortcode('carstag', 'carstag_func');

簡単に利用するため、ここではテーマの functions.php に記述しておきます。

固定ページ等を作成し、下記のようなコンテンツを保存し、

コンテンツを保存

ページを表示すると、期待通り自動車投稿タイプの一覧が出力されました。

自動車投稿タイプの一覧

ショートコードで自動車コンテンツの一覧表を出力した例

このように、Wordpress ではカスタム投稿タイプやカスタムフィールドを含め、API を通じたコンテンツの取得が可能です。上の例ではテーマ内でショートコードを定義しましたが、プラグイン化してテーマから切り離したり、ウィジェットエリアに配置できる部品化することも可能でしょう。

また、プラグインということでは、コンテンツのクエリをサポートする様々なプラグインが存在します。たとえば、Query Wrangler というプラグインは、Drupal の Views とほとんど同じ機能を Wordpress 上で実現します。

Query Wrangler

Query Wrangler プラグイン:Views そっくりの管理画面

有償のものを含めると、コンテンツのクエリーに利用できるプラグラインが他にも数多く存在します。例を示します。

いずれも、Wordpress 本体とは別の拡張プラグインであり、その品質や安定性、サポートの継続性はまちまちである点に注意が必要でしょう。

Views の特長に見る Drupal の思想

以上、前回 Views のサンプルで紹介した例と同様の処理を MT や Wordpress で実現してみました。得られる結果は同じでも、それを実現する方法や、問題解決への考え方には、かなり違いがあるように感じられたのではないでしょうか。これらの実装例をふまえ、改めて Drupal のアプローチに見られる特徴を挙げてみましょう。

テーマやテンプレートに依存しない

Views で作成したクエリーは、構成設定としてデータベース上に保存され、テーマやテンプレートとは関係のない形で定義されます。テーマを変更しても Views のクエリーが影響を受けることはありません。これは、クエリーの定義に相当するタグの記述や API の呼び出しが、テーマ上のファイルに実装されていた上の例とは対照的です。

コアの共通基盤に基づく

Drupal では、コンテンツタイプの定義も、フィールドの定義も、その構造に基づく Views のクエリーも、すべて Drupal のコアが提供する共通基盤の上で実現されています。こうした統合フレームワークとしての一貫性は、Drupal が長い時間をかけて本質的な機能を選別・集約してコアに取り込んできた成果と言えるでしょう。

不要なコーディングを避ける

コーディングは自由度が高い反面、その分リスクも大きくなります。特にメンテナンスとセキュリティのリスクには注意が必要です。PHP のコードが自由に書けてしまう Wordpress のテンプレートは管理困難ないわゆる魔改造の温床になりやすく、また独自に書いたコードには、問題発覚まで放置される脆弱性のリスクが付きまといます。たとえば、上で示した Wordpress のショートコードにも XSS の脆弱性があります。コードを書くという手法を排除することで、少なくともこうしたリスクを回避できるわけです。 過去何度か指摘したように、ブログとして発達してきた CMS にとって、ここで示したサンプルのように独自に構造を定義したデータの扱いが必ずしも得意でないのは無理もないことで、それは製品の優劣と別の問題です。ただ、汎用的なデータ管理基盤として CMS を利用する場合、こうした統一フレームワークとしての一貫性や成熟度が重要であることは間違いありません。私たちが Drupal にこだわる理由はそこにあります。

Drupal の API を利用したデータアクセス

コアの共通基盤を利用し、コーディングをできるだけ排除するのが望ましいとしても、プロジェクトによっては、独自の開発が必要になる場合も出てきます。もちろん、Drupal でもデータベース API を利用してデータにアクセスすることができます。こうした開発の詳細はこの連載の範囲を超えるので、ここでは簡単なサンプルコードを示しておきます。

Drupal のデータベース API によるクエリーは、大きく静的と動的の2つに分かれます。

静的クエリーは、実行時にパラメータを指定することのない、固定的な SQL を実行するもので、決まった条件でデータを取得する SELECT 文の発行で主に使用されます。例として、「自動車」コンテンツのサンプルから、タイトル、本文、定員の各値を全件取得してみましょう。Drupal ではデータベースのテーブルをフィールドごとに分ける設計になっているため、複数列を取得するにはテーブルの結合が必要です。たとえば、次のように SQL を書くことができます。

SELECT title, body_value, field_capacity_value
FROM node_field_data d, node__body b, node__field_capacity c
WHERE d.nid=b.entity_id AND d.nid=c.entity_id 

この SQL を Drupal 8 のデータベース API で静的クエリとして発行し、結果を連想配列として取得して、HTMLのテーブル形式で表示するコード例を示します。詳細な説明は省きますが、冒頭部分で SQL 文を静的な文字列として作成し、これを基に query() や fetchAll() といった API を呼び出して結果を取得していることがわかるかと思います。

class DemoController {
    public function content() {
    //データベース接続の取得
    $con = \Drupal::database();
    
    //静的クエリーの発行
    $sql = 'SELECT title, body_value, field_capacity_value ' .
        'FROM node_field_data d, node__body b, node__field_capacity c ' .
'WHERE d.nid=b.entity_id AND d.nid=c.entity_id';
    $query = $con->query($sql);
    $result = $query->fetchAll();

    //取得したレコードの連想配列を要素とする配列を作成
    foreach ($result as $record) {
          $element = array('#markup'=>$record->body_value);
$rows[] = array($record->title,
\Drupal::service('renderer')->render($element),
$record->field_capacity_value);
    }

    //HTMLのテーブル表示用の配列を作成して返却
    $output['table'] = [
'#type' => 'table',
'#header' => array(t('Title'), t('Body'), t('Capacity')),
'#rows' => $rows,
    ];
    return $output;
    }
}

ページ出力用の簡単なモジュールを作成して上記コードを適用すると、次のような表示が得られます。

DemoControllerの出力例

次に、動的クエリーを実行してみましょう。動的クエリーとは、実行時にクエリーの内容を動的に組み立てることができるクエリーのことです。固定的な SQL 文を文字列として渡すのではなく、種々の API 呼び出しの組み合わせでクエリーを実行時に作成することができます。例として、上と同じテーブルから定員の降順に並べ替えた先頭5件を動的クエリーで取得するコードを示します。

class DemoController {
  public function content() {
    //データベース接続の取得
    $con = \Drupal::database();
    
    //動的クエリーの発行
    $query = $con->select('node_field_data', 'd');
    $query->fields('d', ['title']);
    $query->join('node__body', 'b', 'd.nid = b.entity_id');
    $query->fields('b', ['body_value']);
    $query->join('node__field_capacity', 'c', 'd.nid = c.entity_id');
    $query->fields('c', ['field_capacity_value']);
    $query->range(0, 5);
$query->orderBy('c.field_capacity_value', 'DESC');
    $result = $query->execute();

    //以下、静的クエリーの例と同じ
    ・・・

静的クエリーとの大きな違いは、select() メソッドで取得したクエリーオブジェクトに対して、fields()、join()、range()、orderBy() といったメソッドを呼び出すことでクエリーを組み立てている点です。各メソッドの引数は文字列や文字列の配列として与えることができるので、実行時にさまざまなクエリーを作成することが可能です。クエリーを作成したら最後に execute() メソッドでクエリーを発行します。実行結果を示します。

DemoControllerの出力例 (動的クエリー)

ちなみに Wordpress で同じ処理を実装するとどうなるでしょう。先の例では get_posts() という API を使用しましたが、この関数は内部的に WP_Query というクラスを利用しています。ここでは WP_Query クラスを利用して同じクエリーを発行する例を示します。

function demo_func($atts) {
  global $post; //グローバル変数をインポート
 
  $args = array(
      'posts_per_page' => 5,            //表示(取得)する記事の数
      'post_type' => 'car',             //投稿タイプの指定
      'orderby' => 'meta_value_num',    //カスタムフィールドの値で、
      'order' => 'DESC',            //降順に並べ替える
      'meta_query' => array(
              array(
                      'key' => '定員'        //並べ替えの基準フィールド
              )
      )
  );
  $the_query = new WP_Query($args);
 
  if ($the_query->have_posts()) {
    $ret = "<table border='1'>";
    $ret .= "<tr><th>タイトル</th><th>本文</th><th>定員</th></tr>";
    while ($the_query->have_posts()) {
          $the_query->the_post();    //カーソルを1つ進める
          $ret .= "<tr>";
          $ret .= "<td>" . $post->post_title . "</td>";
          $ret .= "<td>" . $post->post_content . "</td>";
          $ret .= "<td>" . get_field('定員', $post->ID) . "</td>";
          $ret .= "</tr>";
    }
    $ret .= "</table>";
  } else {
  }
  wp_reset_postdata();
  return $ret;
}
add_shortcode('demo', 'demo_func');

WP_Query の場合、コンストラクタに渡す配列(上記コードでは $args)でクエリーを組み立てます。API 仕様で定められた各キーの値としてクエリーの各種条件を定義します。WP_Query インスタンスを生成したら、あとは各種メソッドを呼び出して条件に一致するレコードを連想配列として取得できます。実行すると先の Drupal の例と同じ結果が得られます。

WP_Queryの例

ところで、このサンプルでは the_post() メソッドを利用してレコードの参照位置を進め、現在の参照先がセットされるグローバル変数 $post を利用してレコードの値を取得しています。こうしたグローバル変数を利用するスタイルは Wordpress の API のひとつの特徴といえるでしょう。

関数間の疎結合を阻害するグローバル変数の多用は一般に推奨されません。こうした API は廃止するのが望ましいようにも思えますが、それを行うと CMS が過去のリリースとの互換性を失うことになります。グローバル変数を利用する API は、過去リリースとの互換性を重視する Wordpress のスタンスを反映した結果なのかもしれません。リリースのたびに劇的な変化を遂げながら発展してきた Drupal と対照的です。

Drupal 8 は Symfony2 をベースとし、クラスや継承といったオブジェクト指向のメカニズムや、コンテナによる依存性注入など現代的な手法を利用した API が提供されています。英語であれば、リファレンスやサンプルコードなどの技術情報も豊富に入手できます。

まとめ

今回は他の CMS 製品で独自タイプのコンテンツをクエリーする例を見ながら、Drupal の Views が提供するクエリー基盤の特長を考えてみました。Drupal がコンテンツを管理するための統合的なフレームワークとして機能しているということを改めて感じていただけたでしょうか。

次回は、Drupal が提供する共通基盤のひとつである多言語機能を紹介します。