add to hatena hatena.comment (30) add to del.icio.us (0) add to livedoor.clip (0) add to Yahoo!Bookmark (1) Total: 31

Symfony Templatingを使ってみませんか?

Symfony Componentsというのをご存知でしょうか?その名前のとおり、あのPHPのフレームワークで有名なsymfonyに関係するコンポーネントなのですが、symfonyの次期バージョンであるSymfony2で利用されるコアとなる各コンポーネントがライブラリとして公開されています。

近頃、その1つでテンプレート機能部分である Symfony Templatingが公開されましたので、さっそく使ってみました。軽量で柔軟性が高いのでsymfonyを使った事がある人も無い人も簡単に導入する事が可能だと感じました。

そこで、symfonyを使った事がある人も無い人も、「テンプレートエンジンといえばSmartyを使っているけど、PHPってテンプレートエンジンみたいなものだしPHPでテンプレートを書きたい。。」という方まで、ドキュメントとソースを読んで実際に使ってみた例を紹介したいと思います。

Symfony Templatingの特徴

まずはSymfony Templatingの特徴についてまとめておきます。
詳しい内容は公式サイトのドキュメントを読んでみてください。

標準はPHPでテンプレートを書く

PHPでテンプレートといえば、Smartyが一番先に思い浮かぶものですが、どう違うのでしょうか?
Symfony TemplatingはWebアプリケーションでテンプレートを作りやすくするための機能のみ提供しており、Smartyのような特別な構文が存在するわけではありません。
つまり、素のPHPでテンプレートを書くことが標準の設定になっています。
もちろん、Smartyとの連携も簡単でした。それは後ほど詳しく書きます。

  • テンプレート内で変数$nameを出力
PHP:
  1. <?php echo $name ?>

テンプレートの継承ができる

複数のページが利用する共通のレイアウト画面がある場合は、extendを使ってテンプレートの継承が可能になっています。
異なる部分のみコーディングすればよいのです。
しかも、多段継承ができます。あまり多用すると混乱しますが、場合によっては威力を発揮します。
これは、symfonyでいうlayoutをより便利にしたイメージです。

  • layout.phpテンプレートを継承したテンプレートを作成する
PHP:
  1. <?php $this->extend('layout') ?>

継承先で継承元の内容を書き換えることができる

symfonyを使っていた方はslotの機能といえば分かると思います。
継承元のテンプレートでスロットを定義しておくと、継承先から内容を書き換える事ができます。

  • 継承先のテンプレートでnaviスロットを定義する
PHP:
  1. <?php $this->extend('layout') ?>
  2. <?php $this->start('navi') ?>
  3. <p>ここに書いた内容が継承元のテンプレートで表示されます</p>
  4. <?php $this->stop() ?>

  • 継承元のlayout.phpテンプレート
PHP:
  1. <?php if (!$this->output('navi')) ?>
  2. <p>標準で表示される内容です。</p>
  3. <?php endif ?>

outputメソッドで定義されている内容を出力します。
もし定義されていない場合はfalseが戻されるので
if構文を利用して定義されていない場合に表示する内容を定義しています。

ヘルパー

symfonyのヘルパーと同じです。
Smartyでいうプラグインのようなものです。
標準で用意されているのは、画像やリンクなどのパスをハードコーディングしなくて済むようにするassetsヘルパー、
Javascript、styleシートの読み込みタグを簡単に記述できるjavascript,stylesheetヘルパーです。
symfonyのプラグインと異なるのは複数のプラグインを別名をつけることで複数読み込むことができることです。
このメリットについてはのちほど実装するときに説明します。

同じ名前のテンプレートを複数用意できる

なんだそれ?と思われるかもしれませんが、symfonyを使った事がある場合は、同じ名前の設定ファイルを複数設置でき、設置する場所によって読み込まれる優先順位が異なることは知っていると思います。
それのテンプレートバージョンが行えるのです。

具体的にはテンプレートのパスを複数指定できるので、指定した順番にテンプレートを探します。

PHP:
  1. $loader = new sfTemplateLoaderFilesystem(array(
  2.                    '/path/to/my/template/%name%.php',
  3.                    '/path/to/default/template/%name%.php',
  4.                 ));

上記の場合は、my/templateディレクトリを探しますが、無い場合は、default/templateディレクトリを探します。
たとえば、標準のエラーページ用テンプレートをdefault/template/404error.phpと用意しておき、
カスタマイズしたい場合はmy/template/404error.phpに用意すればこのテンプレートが優先的に読み込まれます。
(記憶が確かなら)Smartyには無い機能ですね。

超シンプルなCMSをつくってみる

ドキュメントしかない状況でどこまで正しい使い方になっているかがわかりませんが、Symfony Templatingを利用して簡単なCMSを作ってみたいと思います。
やりたいことは

  • テンプレートの共通化(レイアウト)
  • ぱんくずを表示、制御する
  • ヘッダー、フッターを共通化する
  • ヘッダーに「○○さん、こんにちは!」と表示する

とします。

ディレクトリ構成

CODE:
  1. |-- htdocs
  2. |   `-- html.php
  3. |-- lib
  4. |-- templates_php
  5. |   |-- html
  6. |   |   `-- index.php
  7. |   |-- layout
  8. |   |   `-- basic.php
  9. |   `-- parts
  10. |       |-- footer.php
  11. |       `-- header.php
  12. `-- vendor
  13.     `-- symfony-templating

Webサーバーのドキュメントルートはhtdocsになり、ここに今回のCMSのコントローラーとなるhtml.phpを配置します。
libは自前のライブラリ置き場ですが、標準の機能だけで実装するのでとりあえず何も置きません。
templates_phpはテンプレートとなるphpファイルの置き場です。
その中で、htmlディレクトリに各ページの本体、layoutにはレイアウトで利用するHTML(php)、partsにはヘッダー、フッター用のHTML(php)を配置します。
vendorには今回利用するSymfony Templatingを置きました。

TOPページ(index)を表示してみる

コントローラー(html.php)

なにわともあれ、一番シンプルにTOPページを表示させようと思います。
まずはコントローラー(html.php)を用意しましょう。

  • htdocs/html.php
PHP:
  1. <?php
  2. $base = dirname(dirname(__FILE__));
  3. // symfony templating
  4. require_once $base . '/vendor/symfony-templating/lib/sfTemplateAutoloader.php';
  5. sfTemplateAutoloader::register();
  6. $loader = new sfTemplateLoaderFilesystem(array(
  7.                                                $base. '/templates_php/%name%.php',
  8.                                                ));
  9. $engine = new sfTemplateEngine(
  10.                                $loader,
  11.                                array(
  12.                                      'php' => new sfTemplateRendererPhp(),
  13.                                      )
  14.                                );
  15. // set charset for escape
  16. $engine->setCharset('UTF-8');
  17. echo $engine->render('php:html/index', array('name' => "名無し"));
  18. ?>

詳しい説明はしませんが、loaderを用意し、loaderとrendererからengineを作成し、engineから表示させています。
標準で書かなくてもよいところもあるのですが、あとでSmartyと連動させる場合にわかりやすくするためにあえて書いています。

ポイントは、$engine->renderでテンプレートから出力を行いますが、renderの第1引数には利用するrendererの名前とテンプレートのパスをコロン(:)で区切って指定し、第2引数でテンプレートで利用する変数を渡します。
第1引数でrendererの名前を省略することができ、その場合はphpが指定されていると見なされます。

テンプレート(index.php)

指定しているテンプレートはhtml/indexなので、templates_php/html/index.phpが呼び出されます。

  • templates_php/html/index.php
PHP:
  1. <b><?php echo $name ?>さん、こんにちは</b>

アサインした変数は普通にechoで出力することができます。ただし、エスケープ処理はされていないので
エスケープ処理させる場合は明示的にescapeメソッドを利用し

PHP:
  1. <b><?php echo $this->escape($name) ?>さん、こんにちは</b>

のようにする必要があります。

* 実行結果

継承を利用する(layout/basic.php)

継承先で継承元となるテンプレートをextendメソッドで指定すると、
継承元のテンプレートでは$this->output('content')部分が継承先の内容に置き換わります。
継承と表現されていますが、スロットの拡張版のようなイメージです。

  • templates_php/html/index.php
PHP:
  1. <?php $this->extend('php:/layout/basic') ?>
  2. <b><?php echo $name ?>さん、こんにちは</b>

  • templates_php/layout/basic.php
PHP:
  1. <html>
  2.   <body>
  3.   <h2>Basicレイアウトです</h2>
  4.   <hr />
  5. <?php $this->output('content') ?>
  6.   </body>
  7. </html>

*実行結果

継承元の内容を継承先から動的に書き換える

パンクズをslotの機能を用いて実現してみます。
継承元ではoutputメソッドを、継承先ではstart,stopメソッドを使います。
さきほどのindex.phpに'navi'という名前でスロットを定義します。

  • templates_php/html/index.php
PHP:
  1. <?php $this->extend('php:/layout/basic') ?>
  2. <?php $this->start('navi') ?>
  3. <p>TOPページ&gt; sample</p>
  4. <?php $this->stop() ?>
  5. <b><?php echo $name ?>さん、こんにちは</b>

これを継承元であるbasic.phpでoutputを用いて動的に変化させる領域を作成します。
outputは引数で渡したスロットが定義されていない場合はfalseを返すので、if構文で制御しておけば
標準で表示する内容を用意しておくことができます。

  • templates_php/layout/basic.php
PHP:
  1. <html>
  2.   <body>
  3. <div id="navi">
  4. <?php if (!$this->output('navi')): ?>
  5.   <p>TOPページ</p>
  6. <?php endif ?>
  7. </div>
  8.   <h2>Basicレイアウトです</h2>
  9.   <hr />
  10. <?php $this->output('content') ?>
  11.   </body>
  12. </html>

*実行結果

ヘッダー、フッターを部品化する

ヘッダーとフッターはレイアウト内で定義するようにします。
symfonyでいうパーシャル(partial)を使うイメージですが、Symfony Templatingではpartialは用意されていません。
ではどうするかというと、直接テンプレートを指定してrender処理すれば良いだけです。
なので、ヘッダーテンプレートとフッターテンプレートを読み込むためには以下のようなコードを書けばよいことになります。

PHP:
  1. テンプレートディレクトリpartsにあるheader.php,footer.phpを読み込み、
  2. phpレンダラーで処理します。
  3. <?php echo $this->render('php:parts/header') ?>
  4. <?php echo $this->render('php:parts/footer') ?>

というわけで、レイアウトを次のように書き換えます。

  • templates_php/layout/basic.php
PHP:
  1. <html>
  2.   <body>
  3. <?php echo $this->render('php:parts/header') ?>
  4. <div id="navi">
  5. <?php if (!$this->output('navi')): ?>
  6.   <p>TOPページ</p>
  7. <?php endif ?>
  8. </div>
  9.   <h2>Basicレイアウトです</h2>
  10.   <hr />
  11. <?php $this->output('content') ?>
  12. <?php echo $this->render('php:parts/footer') ?>
  13.   </body>
  14. </html>

あとは、読み込まれるヘッダーとフッターのテンプレートを用意します。

  • templates_php/parts/header.php
PHP:
  1. <div id="header">
  2. <hr />
  3. <p>HEADER</p>
  4. <hr />
  5. </div>

  • templates_php/parts/footer.php
PHP:
  1. <div id="footer">
  2. <hr />
  3. <p>FOOTER</p>
  4. <hr />
  5. </div>

*実行結果

ヘッダーに「○○さん、こんにちは!」を表示

○○さんの部分はhtml.phpの最後のrenderメソッドで第2引数で渡している配列の'name'を使います。
継承元のbasic.phpからさらにheader.phpをrender処理しているので、このメソッドに$nameを渡す必要があります。
その点のみ気をつければ次のようにbasic.phpを書き換えるだけです。

  • templates_php/layout/basic.php
PHP:
  1. ...
  2. <?php echo $this->render('php:parts/header', array('name' => $name)) ?>
  3. ...

あとはheader.phpでアサインされた変数($name)を出力するだけです。
ここでは、escapeメソッドを使い、XSS対策を行った文字列を出力しています。

  • templates_php/parts/header.php
PHP:
  1. <div id="header">
  2. <hr />
  3. <p>HEADER</p>
  4.    <p><?php echo $this->escape($name) ?>さん、こんにちは!</p>
  5. <hr />
  6. </div>

*実行結果

リクエストされたURIで表示するテンプレートを変化させる

これまでのサンプルはindex.phpを固定表示するコントローラー(html.php)でした。
なので、リクエストされるURIによってテンプレートを動的に変化させることができるようにしてみます。

apacheのMultiViewsを有効にする

まず、apacheの設定で、ディレクティブの設定でMultiViewsを有効にしておきます。

CODE:
  1. <Directory /path/to/webroot>
  2.   Options MultiViews
  3. </Directory>

こうすることで、
http://localhost/html.php/news/list
というリクエストの場合はhtml.phpが実行され、PATH_INFOの値がnews/listになります。
これを利用し動的に表示させるテンプレートを決定するようにします。

URIと表示するテンプレートの関係は以下のようなイメージです。

URI 表示するテンプレート
http://localhost/html.php/index /html/index.php
http://localhost/html.php/news/list /html/news/list.php

これを実装したhtml.phpは以下のようになりました。

  • htdocs/html.php
PHP:
  1. ...
  2. // for dynamic change
  3. $info = getenv('PATH_INFO');
  4. $filename = (isset($info))? $info : "index";
  5. try{
  6.   echo $engine->render('php:html/' . $filename, array('name' => "名無し"));
  7. }catch(Exception $e){
  8.   header("HTTP/1.1 404 Not Found");
  9.   echo "page is not found";
  10. }

もし、該当するテンプレートが存在しない場合は、renderメソッド処理後に例外が発生するので、例外発生時は404ステータスをheader関数で返すようにしています。
エラー用のテンプレートをrenderすればもっと良くなると思います。

リンク先のURLを開発、本番を意識しなくて済むようにする

よくある話で、サイト内リンクのパスをテンプレートに埋め込んでしまうと、開発から本番環境に移行する際にドメインが変わったりする場合に、変更作業が大変だったり、変更漏れがあったりするかもしれません。
Symfony TemplatingではAssetsヘルパーを使うことでこのパスの変化を吸収できます。

Assetsヘルパーを使う

ヘルパーを使うためには、engineにsetHelperSetしておかなくてはなりません。

  • htdocs/html.php
PHP:
  1. $helperSet = new sfTemplateHelperSet(array(
  2.   'inner_assets' => new sfTemplateHelperAssets('/sf-templating-sample/html1.php', 'http://localhost/'),
  3.   'outer_assets' => new sfTemplateHelperAssets('/', 'http://www.example.com/'),
  4. ));
  5. $engine->setHelperSet($helperSet);

sfTemplateHelperSetに配列で利用するヘルパーを定義します。
Assetsヘルパーは引数に共通で利用する'パス'と'ドメイン'を定義できます。
また、このときにキー名を与えておくことで、エイリアス名としてテンプレートで利用できます。
ここでは、assetsヘルパーを異なるエイリアス名を付ける事で2つ用意しています。
1つは、内部リンク用(inner_assets)、もう1つは外部リンク用(outer_assets)です。
これをテンプレートでgetUrlメソッドを用いて次のように利用することでドメインやパスの変更を吸収できます。

  • templates_php/html/index.php
PHP:
  1. <a href="<?php echo $this->inner_assets->getUrl('index') ?>">トップページ</a>
  2. // => http://localhost/sf-templating-sample/html1.php/index
  3. <a href="<?php echo $this->outer_assets->getUrl('index.html') ?>">トップページ</a>
  4. // => http://www.example.com/index.html

Smartyと連動させてみる

最後はSmartyと連動させて、テンプレートの一部をSmartyで処理させてみようと思います。
Smartyはrender処理を行う部分を担当するので、Smarty用のrendererを用意します。
結局はSmartyでfetchさせた結果をevaluateメソッドで返すだけです。

まずは、以下のようなSmarty用のレンダラーを用意します。

  • lib/sfTemplateRendererSmarty.php
PHP:
  1. <?php
  2. class sfTemplateRendererSmarty extends sfTemplateRenderer
  3. {
  4.   protected $smarty = null;
  5.   public function __construct(Smarty $smarty)
  6.   {
  7.     $this->smarty = $smarty;
  8.   }
  9.   public function evaluate($template, array $parameters = array())
  10.   {
  11.     // renderered by Smarty
  12.     foreach ($parameters as $key => $value) {
  13.       $this->smarty->assign($key, $value);
  14.     }
  15.     return $this->smarty->fetch($template);
  16.   }
  17. }

これを利用できるようにコントローラー(html.php)を変更します。
また、Smartyライブラリの読み込みとSmartyのためのディレクトリも用意しておきます。
Smarty用のテンプレートはPHPテンプレートとは別に用意し、templates_smartyディレクトリにしました。

  • htdocs/html.php
PHP:
  1. // smartyライブラリの読み込みとインスタンス作成
  2. require($base . '/vendor/smarty/libs/Smarty.class.php');
  3. $smarty = new Smarty();
  4. $smarty->template_dir = $base . '/data/smarty/templates';
  5. $smarty->compile_dir = $base . '/data/smarty/templates_c';
  6. $smarty->cache_dir = $base . '/data/smarty/cache';
  7. $smarty->config_dir = $base . '/data/smarty/configs';
  8. // smarty用のテンプレートをloaderのパスに追加
  9. $loader = new sfTemplateLoaderFilesystem(array(
  10.                                                $base. '/templates_smarty/%name%.tpl',
  11.                                                $base. '/templates_php/%name%.php',
  12.                                                ));
  13. // smarty用のレンダラーの読み込みとsmartyという名前でengineに定義
  14. require_once $base . '/lib/sfTemplateRendererSmarty.php';
  15. $engine = new sfTemplateEngine(
  16.                                $loader,
  17.                                array(
  18.                                      'php' => new sfTemplateRendererPhp(),
  19.                                      'smarty' => new sfTemplateRendererSmarty($smarty),
  20.                                      )
  21.                                );

これで準備は完了です。
これまでのサンプルのheader部分だけをSmartyを使うようにしてみます。
rendererの指定をphp:からsmarty:に変更し、smarty用のテンプレートを用意するだけです。

  • templates_php/layout/basic.php
PHP:
  1. ...
  2. <?php echo $this->render('smarty:parts/header', array('name' => $name)) ?>
  3. ...

  • templates_smarty/parts/header.tpl
PHP:
  1. <div id="header">
  2. <hr />
  3. <p>HEADER</p>
  4.    <p>{ $name|escape }さん、こんにちは!</p>
  5. <hr />
  6. </div>

header.tplは完全に見慣れたSmartyのテンプレートそのものです。
もちろん実行結果は同じです。

このように簡単に他のテンプレートエンジンを組み込み、一部のテンプレートのみ異なるテンプレートエンジンでレンダー処理ができるというのはかなり便利かと思います。
たとえば、デザイナーからの要望でSmartyを使う必要があるけど、全部Smartyはどうだろうなぁというような案件でも簡単に利用することができますね。

まとめ

  • symfonyのslotに慣れていれば仕組みは簡単。
  • renderメソッドがpartialの代わり。
  • SmartyやPHPTALなど他のテンプレートエンジンを一部で使うなどの連携が簡単
  • ヘルパーは健在。利用目的もこれまでのsymfonyでのヘルパーとほぼ同じ。ということはCakePHPとも同じ。
  • エスケープ処理はescapeメソッドで明示的に行わないと駄目。Smartyと同じ。

関連するその他の記事

Comments

Leave a Reply