レイアウトのmetaタグやサイドバーの内容をコンテンツテンプレートから上書き
こんにちは、ナカエです。
さてさて、当記事の目的はbaserCMSにおいて保守しやすく、かつ柔軟に各コンテンツの出力を調整できるテーマを作ろうというものです。
試した環境
baserCMS3.0.5.1(baserCMS3系なら問題ないはずです)
一般的なbaserCMSのテーマ(またはビュー)の構成
baserCMSのテーマの一般的な構成は下図の通りです。 (現在のデフォルトテーマであるm-singleは特殊なシングルページデザインですが置いておくとして)
3種類のテンプレートファイルが利用されています。
例としてブログの単一記事を表示するページ
http://example.com/news/archives/1
を見てみましょう。
コンテンツテンプレート
app/webroot/theme/{テーマ名}/Blog/default/single.php
baserCMSが出力する各ページ(正確にはアクション)の個別の内容を記述するファイルです。 レイアウトテンプレート中の
<?php $this->BcBaser->content() ?>
と記述した部分にコンテンツテンプレートの内容がはめ込まれます。
※単にテンプレートなどと表記されることもありますが、この記事では下記のレイアウトテンプレート・エレメントテンプレート、およびこれらの総称としてのテンプレートという名前の区別を明確にするためコンテンツテンプレートと表記します。
レイアウトテンプレート
app/webroot/theme/{テーマ名}/Layouts/default.php
すべてのページに共通で表示したい内容、つまりWebページの枠組みがこのファイルに記述されます。
エレメントテンプレート
app/webroot/theme/{テーマ名}/Elements/header.php
コンテンツテンプレートやレイアウトテンプレートで利用されるWebページの部品となるテンプレートです。
<?php $this->BcBaser->element('header') ?>
の形でレイアウトテンプレートやコンテンツテンプレートから呼び出されます。エレメントテンプレートはテーマを作る上で必ずしも必要ではありませんが、ファイルを分けておくことによって再利用可能な部品として便利に扱えます。
コンテンツ部分以外をいじりたい
上図のレイアウトにおけるコンテンツ部分は各ページのコンテンツテンプレートに対応しているので好き放題変更することが可能ですよね。しかし、コンテンツテンプレート上からレイアウトのコンテンツ部分以外の場所(サイドバーやヘッダーやフッターやheadタグの中身など)をいじる、という趣旨の説明記事はほとんど見たことないように思います。 果たしてそんなことが可能なんでしょうか?
作成するテーマへの要望の例
抽象的に書くとひじょーーーーーにわかりづらので、具体的な条件を出して考えていきます。
- ブログページだけ他のページと違うサイドバーを表示したい
- ブログ記事にOpen Graph Protocol用のmetaタグを設定したい(トップページとはmetaタグの内容を変えたい)
ブログのサイドバーにブログの最新投稿やカレンダーを表示し、ほかのページのサイドバーには共通のウィジェットを表示したいということはよくありますよね。OGPもWebサイトと各種SNSの連携において欠かせない要素となっています。
単純な解決策(あまりお勧めしません)
1.コンテンツやページによって利用するレイアウトテンプレートを変更する
1つ目の解決策です。baserCMSにはレイアウトテンプレートの切り替え機能が備わっています。
- app/webroot/theme/{テーマ名}/Layouts/default.php
- app/webroot/theme/{テーマ名}/Layouts/blog.php
とサイドバーの内容だけ変えた2つのレイアウトテンプレートを用意し、ブログのページには後者を用いることを考えます。
baserCMSで構築するWebサイトにおいて、サイドバーの種類が2つ程度ならこれで事足りるかもしれません。しかしより細やかに表示内容を調整しようとすると問題が出てくる可能性があります。
-
app/webroot/theme/{テーマ名}/Blog/archives.php ブログアーカイブ表示用のコンテンツテンプレート →ブログの最新記事5件をサイドバーに表示したい
-
app/webroot/theme/{テーマ名}/Blog/single.php ブログの単一記事表示用のコンテンツテンプレート →ブログの最新記事に加えて、ブログ記事の関連投稿をサイドバーに表示したい!
のようにサイドバーの表示をさらに場合分けしたい場合はどうしましょうか? 3つ目のレイアウトテンプレートを作成しますか? さらにサイドバーの種類が増えたらどうしますか?
……この解決策では似たようなレイアウトテンプレートが増えてしまいます。最悪の場合、全ページのレイアウトのHTMLをまとめて変更したいときに、レイアウトテンプレートの数だけ同じ修正を繰り返す必要があるかもしれません。どんどん保守しにくいテーマになっていきます。
( ゚Д゚) < ダメダコリャ
- 各ビューに共通のひな形として使えないなら何のためのレイアウトテンプレートかわからないよ!
2.レイアウトテンプレート内でif節を利用してコンテンツ/ページごとに表示を分ける
もう1つはレイアウトテンプレートは増やさず、ifを用いてサイドバーを切り替える作戦です。 こちらもサイドバーの表示の場合分けが増えていくとレイアウトテンプレートが混沌としてきます。
( ゚Д゚) < イマイチ
- ifの分岐が増えて見辛くなる可能性がある
- レイアウトテンプレートがコンテンツテンプレートに依存してしまう≒コンテンツテンプレートを増やした時にレイアウトテンプレートも修正しなければならないかもしれない
- 1つのビュー特有の設定がレイアウトテンプレートとコンテンツテンプレート2ファイルに分散してしまう
この2つの解決策の問題点として共通するのは、出力内容の指定まですべてレイアウトテンプレートに押し込めてしまう点です。コンテンツテンプレートが増えれば増えるほど、レイアウトテンプレートがかさばることになります。
どうにか各コンテンツテンプレートのサイドバーやmetaタグの内容をコンテンツテンプレート自身の中で指定する方法はないものでしょうか? レイアウトテンプレートの該当部分を各コンテンツテンプレートの中で上書きできれば、より構造がわかりやすく編集しやすいテーマが作れるはずです。
さてここまでが前置きです。長くなりましたね。
baserCMSに用意されているレイアウトテンプレートの上書き手段の一例
実はこの問題を解決するためのヒントは既存のほとんどのテーマの中にもあります。 多くのテーマで、出力位置の指定はレイアウトテンプレートでなされているはずなのに、各コンテンツテンプレートから出力内容を上書きできている部分がありますね。 metaタグのdescriptionやkeywordsがその一例です。
コンテンツテンプレートでそのページ独自の設定を記述すれば
<?php $this->BcBaser->setDescription('meta descriptionに設定したい文字列') ?>
<?php $this->BcBaser->setKeywords('meta keywordsに設定したい文字列') ?>
レイアウトテンプレートでその設定が出力されるます。
<?php $this->BcBaser->description() ?>
//出力 <meta name="description" content="meta descriptionに設定したい文字列" />
<?php $this->BcBaser->keywords() ?>
//出力 <meta name="keywords" content="meta keywordsに設定したい文字列" />
コンテンツテンプレートにおけるmetaタグのdescription、keywordsの内容はコンテンツテンプレートの内部で指定されており、 レイアウトテンプレートを複数用意することもなく、コンテンツテンプレートごとに違う内容を出力することができます。 実はこれをさらに抽象化した応用範囲の広いメソッドがBcBaserHelperクラスに存在します。
テンプレートで利用する変数を設定するBcBaserHelper::set()
各テンプレートで共通して利用する変数をテンプレート内で設定することが可能です。
コンテンツテンプレートで
<?php $this->BcBaser->set('variableName', '変数に設定する値') ?>
と記述すると変数$variableNameをレイアウトテンプレートでも使用できます。
<?php echo $variableName // 出力: 変数に設定する値?>
実は上記の$this->BcBaser->setKeywords()と$this->BcBaser->setDescription()も内部的には同じ仕組みを利用しています。ただし単なるラッパーメソッドではなく、出力の際に変数$descriptionや$keywordsの値が空ならばサイト基本設定が格納されている変数$siteConfigに含まれる値をデフォルト値として出力するという気の利いた動作になっています。
例:OGPを設定する
例として出していたOGPの設定の上書きはこの変数の設定を利用することで解決できます。
レイアウトテンプレート
<meta property="og:type" content="<?php echo $ogType ?>" />
<meta property="og:title" content="<?php echo $ogTitle ?>" />
<meta property="og:description" content="<?php echo $ogDescription ?>" />
<meta property="og:url" content="<?php echo $ogUrl ?>" />
<meta property="og:image" content="<?php echo $ogImage ?>" />
<meta property="og:site_name" content="サイト名" />
コンテンツテンプレート
app/webroot/theme/{テーマ名}/Blog/default/single.php
<?php $this->BcBaser->set('ogType', 'article') ?>
<?php $this->BcBaser->set('ogTitle', $this->Blog->getPostTitle($post, false)) ?>
<?php $this->BcBaser->set('ogDescription', $this->Blog->getPostContent($post, false, false, 150)) ?>
<?php //以下それぞれの変数を設定 ?>
実用的にはレイアウトテンプレートで各変数がセットされているかどうかをチェックして、空の場合はデフォルト値を出力するようにしないと「未定義の変数を使うなこの野郎!(意訳」とPHPのNoticeが出ますので注意が必要です。
デフォルト値を指定したいときは後述のView::get()ないし、ビューブロックのほうが便利かと思います。
注意
$script_for_layoutや$title_for_layout、$descriptionなどCakePHPやbaserCMSが内部的に利用している変数と変数名が被らないよう配慮する必要があります。
<?php var_dump($this->getVars()) ?>
とテンプレートファイルに記述すればビュー現在の文脈において設定されている変数の一覧(一部除く)を出力できますので参考にしてください。
おまけ:CakePHPのViewクラスにより変数を設定・取得する
実はBcBaserHelper::set()はView::set()のラッパーです。 baserCMSにおいては、(おそらく学習コストを最小化するために)BcBaserHelperを通じて主要な出力を記述できるようにメソッドが集約されていますが、個人的にはCakePHPのクラスを利用したほうがすっきり書ける部分もあるかなと思っています。
Viewクラスのメソッドを用いた変数の設定は
<?php $this->set('variableName') ?>
となります。各テンプレートファイルの中では$thisはViewクラスのインスタンスを示すということに注意してください。また、レイアウトテンプレートでの変数の取得を変数の直接的な記述に依らず
<?php echo $this->get('variableName', '変数が未定義の場合のデフォルト値') ?>
とすることもできます。デフォルト値を設定できるのでそのまま変数を利用するよりフォールバックが楽になります。
ビューブロック
本日のメインディッシュがこちらになります。
通常、レイアウトテンプレートの中にコンテンツテンプレートの記述内容を書いたまま反映できるのは <?php $this->BcBaser->content() ?> と記述したコンテンツ部分だけですが、CakePHP2.1で追加されたビューブロックという仕組みを利用することで、レイアウトテンプレートの任意の位置にコンテンツテンプレートから上書き・追記ができる領域を設置することができます。
参考: ビュー ― CakePHP Cookbook #ビューブロック
使い方
View::fetch()メソッドによりブロックの名前と設置する位置を指定し、View::start()とView::end()でブロックの中に出力したい内容を囲みます。
レイアウトテンプレート
<?php echo $this->fetch('sidebar') ?>
コンテンツテンプレート
//sidebarブロックを作成
<?php $this->start('sidebar') ?>
ここに記述した内容が
レイアウトテンプレートの<?php echo $this->fetch('sidebar') ?>の部分に出力される
・・・
<?php $this->end() ?>
デフォルトの出力を設定する
ブロックの中身の作成はレイアウトテンプレート内でも可能です。View::startIfEmpty()メソッドを利用すると、コンテンツテンプレートで指定したビューブロックが空もしくは定義されていない場合のみブロックを開始できます。 先ほどのサイドバーを例にしてみると
レイアウトテンプレート
<?php $this->startIfEmpty('sidebar') ?>
コンテンツテンプレートで'sidebar'ブロックが作成されていない場合のデフォルトの出力をここで設定
場所はView::fetch()より前ならどこでも問題ないが傍に書いたほうがわかりやすい。
<?php $this->BcBaser->element('sidebar-default') ?>
<?php $this->end() ?>
<?php echo $this->fetch('sidebar') //こちらが実際の出力箇所 ?>
コンテンツテンプレート
app/webroot/{テーマ名}/Blog/defautl.single.php
<?php $this->start('sidebar') ?>
ビューテンプレートで上書きする内容
<?php $this->BcBaser->element('sidebar-blog') ?>
<?php $this->BcBaser->element('blog_related_posts') ?>
<?php $this->end() ?>
となります。ほかのページのコンテンツテンプレートにおいてsidbarブロックが定義されていない場合は、レイアウトテンプレートで指定した内容が表示されます。ビューの処理においては、レイアウトテンプレートより先にコンテンツテンプレートの内容が読み込まれている、という点に注意してください。
デフォルトの出力を設定する方法その2
View::fetch()の第二引数にデフォルト値を設定することができます。また、View::start()とView::end()で囲んでビューブロックを作成する方法以外にも、View::assign()というメソッドがあります。 ブロックの出力内容の記述が複数行にまたがらない場合、例えば単なる文字列やエレメントテンプレートの呼び出し1回で済む場合などはこれらを利用するとすっきりかけます。 先ほどのmetaタグのOGPの例をビューブロックを用いて書き直してみます。
レイアウトテンプレート
<meta property="og:type" content="<?php echo $this->fetch('og:type', 'website') ?>">
<meta property="og:title" content="<?php echo $this->fetch('og:title', $this->BcBaser->getTitle()) ?>">
<meta property="og:description" content="<?php $this->fetch('og:description', $this->BcBaser->getDescription()) ?>">
<meta property="og:url" content="<?php echo $this->fetch('og:url', $this->BcBaser->getUri($this->BcBaser->getHere())) ?>">
<meta property="og:image" content="<?php echo $this->fetch('og:image', $this->BcBaser->getUri('/images/sns.png')) ?>">
コンテンツテンプレート
app/webroot/theme/{テーマ名}/Blog/default/single.php
//上書きする部分だけを指定
<?php
$this->assign('og:type', 'article');
$this->assign('og:title', $this->Blog->getPostTitle($post, false));
$this->assign('og:description', $this->Blog->getPostContent($post, false, false, 150));
?>
非常にわかりやすく書けていると思いますがいかがでしょう?
注意
'content'という名前のビューブロックはCakePHPがレイアウトテンプレート内にコンテンツテンプレートを出力するために内部的に利用していますので、使わないようにしましょう。
まとめ
コンテンツテンプレートからレイアウトテンプレートのコンテンツ部分以外の出力に介入する2つの方法を紹介しました。
- コンテンツテンプレートでテンプレート用の変数を設定してレイアウトテンプレートで利用する
- レイアウトテンプレートにビューブロックを設置してコンテンツテンプレートから内容を上書きする
コンテンツテンプレートが自身の出力内容の面倒を自身で見る、というところがポイントです。
デフォルト値を設定したりや既存の内容への追記したりする場合は特にView::get()やビューブロックを利用すると便利です。 ビューブロックやbaserCMSのテーマで利用できるマニュアルにないメソッドについてより詳しい内容を知りたければCakePHPのCookbookをご覧ください。