タグ別アーカイブ: Smarty

Smartyのforeachを使って商品一覧ページテンプレートなどを実現

Smartyユーザでもない人間が思い出しながらSmartyとテンプレートの良さについて解説するシリーズ(前回Smarty3.x編前々回Smarty2.x編)。前回までの例だと、いまいちテンプレートエンジンの必要性が伝わりにくい。そこで、今回はテンプレートエンジンが輝く例として、ありきたりではあるが商品の一覧ページを作る場合などについて解説していこう。

Smartyのforeachで、配列の要素数だけ繰り返し出力

オンラインショップを運営していて、商品の削除や追加があった場合、商品一覧ページから商品を消したり既存のリスト項目のhtmlをコピーして内容だけ変えて追記したりと、静的な仕組みの場合には都度の手間がかかってしまう。そこで、商品をデータベースに追加したら自動的に商品一覧ページに追加、表示してくれるような仕組みがあると有り難い。
勿論そうした仕組みを実現するには、データベースの知識などが必要である。まあ、その点はクリア出来たとして(きっと”SELECT * FROM table WHERE〜”とかなんとかSQLで取得したとして)、次に手元に出来た配列の要素数だけリスト項目を作ろうという段階になるわけだ。
データベースから帰ってくる結果の数については、もちろんテンプレートを用意する段階では知るよしもない。そこで、配列の要素数を読み取ってその回数だけループさせるための言語構造、foreachを使う。

 
//{foreach}から{/foreach}までの部分を、$arrayの要素数だけ繰り返す。
//配列のキー名はkey=で取り出す。
//値はitem=で取り出す。
//いずれも与えた文字列と同じ名前の変数({$key}、{$value}など)で取り出す。
 
<ul>
{foreach from=$array key="key" item="value"}
<li>キー値{$key}の値は{$value}</li>
{/foreach}
</ul>

たとえば$array = array(“山”,”川”,”谷”);という宣言の後に上のforeach部分が解釈されると、出力htmlは以下のようになる。

<ul>
<li>キー値1の値は山。</li>
<li>キー値2の値は川。</li>
<li>キー値3の値は谷。</li>
</ul>

このように、配列の要素数だけのリスト項目を出力してくれる。

Smartyではテンプレート自体に条件分岐を埋められる

ただ、上の例は実は要素数が0の場合に対応できていない。要素数が0だと、リスト項目をもたないただのulタグのみが出力されてしまい、htmlの文法エラーとなってしまう。
そこで、配列の要素数が0の場合ulタグも出力しないといったような条件分岐もつけたくなる。Smartyのテンプレート側でその対応は可能で、{if}内でPHP関数empty()を用いて出力の調整が出来る。

{if empty($array)}
<p>項目がありません。</p>
{else}
<ul>
{foreach from=$array key="key" item="value"}
<li>項目名{$key}の値は{$value}</li>
{/foreach}
</ul>
{/if}

このようにロジックを埋め込める。テンプレート側にロジックを埋め込むのはMVCの観点上あまりよろしいことではないのだが、こういった対応も可能ということで。

実際に商品一覧ページをテンプレートにしてみる

それでは、実際に例として商品一覧ページを作ってみよう。今回例に使うのはSmarty3.xなので、そこのところご注意を。

 
<!-- base.tpl -->
<!doctype html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="css/base.css" type="text/css">
{if $page.description != ""}
<meta name="description" content="{$page.description}">
{else}
<meta name="description" content="サンプル株式会社のホームページです">
{/if}
{if $page.keywords != ""}
<meta name="keywords" content="{$page.keywords}">
{else}
<meta name="keywords" content="サンプル株式会社,小売業,日本">
{/if}
<title>{$page.title} | サンプル株式会社</title>
{block name=head}
{/block}
</head>
<body>
<div id="wrapper">
<header>
<h1>サンプル株式会社</h1>
</header>
<div id="main">
{block name=main}
<article>
<h1>デフォルト文章</h1>
<p>mainのブロックに指定が無い場合のデフォルト文章です。</p>
.
.
</article>
{/block}
</div>
<footer>
<p>サンプル株式会社 2012-{$smarty.now|date_format:"%Y"} all rights reserved.</p>
</footer>
</div>
</body>
</html>
 
<!-- item.tpl -->
<li>
<section>
<h1>{$value.name}</h1>
<p>{$value.description}</p>
<p>{$value.price|number_format:0}円</p>
<p><a href="http://xxxxxxxx.xxx/item.php?id={$value.id}" title="{$value.name}の商品詳細">>>商品詳細へ</a></p>
</section>
</li>
 
<!-- itemlist.tpl -->
{extends file="base.tpl"}
{block name=main}
<article>
<h1>サンプル株式会社の商品一覧</h1>
{if empty($page.items)}
<p>現在取り扱っている商品はありません</p>
{else}
<p>当社が取り扱う商品の一覧です。</p>
<ul id="itemlist">
{foreach from=$page.items item="value"}
{include file="item.tpl"}
{/foreach}
</ul>
{/if}
</article>
{/block}

base.tplはどのページにも共通するベースのテンプレート。item.tplが商品情報と個別ページへと飛ぶリンクをのせる枠組みで、itemlist.tplというのは商品一覧ページitemlist.phpが呼ばれた時に読み込むテンプレート。このitemlist.tplがbase.tplの子テンプレートになっているわけだ。というわけで、呼び出し側のPHP。Smartyのパス等はdefinesmarty.phpという別ファイルにまとめたという設定で。

//itemlist.php
 
require_once("script/definesmarty.php");
$items = array();
 
//本来$itemsにデータベースから取ってきた商品情報を入れる処理が入る。
//ここでは以下の情報がかえってきたものとする。
 
$items = array(
array("id" => "1","name" =>"商品A","description" => "さわやかなミントの香りの牛車です","price" => "100000"),
array("id" => "2","name" =>"商品B","description" => "官位です。これがあれば思いのまま(返品不可)","price" => "3000"),
array("id" => "10","name" =>"商品C","description" => "古今和歌集です。お買い得セール!後の世に国宝になりますよ","price" => "160000")
);
$page = array(
"description" => "サンプル株式会社の商品一覧ページです。",
"keywords" => "サンプル株式会社,ネットショップ",
"title" => "当社商品の一覧",
"items" => $items
);
$smarty->assign("page",$page);
$smarty->display("itemlist.tpl");

色々と新しい表現などが登場しているが、とりあえず出力はこんな感じだ。

itemlist.phpへアクセスした結果

itemlist.phpへアクセスした結果

テンプレート内で使える、Smartyの便利な機能

前回までの例と異なり、今回はassignで連想配列を与えている。連想配列のキーで値にアクセスするためには、変数の後ろにピリオドをつける。これはまあ、説明しなくてもなんとなく分かってもらえるだろう。オブジェクトのメンバの場合には、標準的なPHPの場合と同じく”->”でアクセスする。
次に、所々に出てくる、変数の後ろに”|”がついているもの。これは後ろに続くのが修飾子名で、変数を展開した後のテキスト処理を行ってくれる。例で挙げているもののうち、”number_format:0″という修飾子。これは数字の3桁毎にカンマを入れてくれている。したがって出力後の数字は100,000円などとなっているはずだ。この修飾子のさらなる使い方は、PHPマニュアルでも見てほしい。
そして、フッタ部クレジットにある$smarty.nowというのは、Smartyが標準で備える予約変数で、プログラム動作時のタイムスタンプを得る。例のように書いておけば、年が明けてもクレジットの年号を変更する必要が無くなるわけだ。

如何だっただろうか。Smartyを使えばこのように簡単にテンプレートを用いた開発が出来る。究極足りないところはインラインPHPで補いながら開発ができるので、Dreamweaverのテンプレートより便利だろう。
こんなに便利なSmartyだが、惜しむらくはやはりヴァージョン2と3のユーザが混在しており、WEBで調べながら開発していく方法においてはリファレンスがどちらのヴァージョンに基づいたものなのか分かりにくいということ。あと、公式マニュアル(Bitcoinがドネートできる!)のデザインが無骨。確かTwigに乗り換えた理由の1つは、マニュアルがオシャレだったから、とどうでも良いことを思い出したり。


PHPテンプレートエンジンを使おう Smarty3.x編

前回紹介したSmarty2.xの”閉じhead前切り”パラダイム。htmlを変なところでブツ切りにしなければならない事情は理解できるけれど、やはり少し格好悪い。Dreamweaverのテンプレートと比べると、どうも直感的ではない。また、完成後htmlから切り分けてテンプレートを作っていく形だと、どうしても切り損なってしまい、後から部品テンプレートの数を増やす/減らすなどで、結構な手間が生じる可能性がある。

Smarty3.xのパラダイム テンプレート継承

そこでSmarty3からはDreamweaverのような、テンプレート中の挿入部分をタグで囲み明示する形の仕組みが取り入れられた。この仕組はそして、オブジェクト指向におけるクラスの継承と実に似通ったものとなっている。

<!-- base.tpl -->
<!DOCTYPE html>
<html lang="ja">
<head>
.
.
<title>{$title} | サンプル株式会社</title>
{block name=head}
{/block}
</head>
<body>
<header>
<h1>サンプル株式会社</h1>
</header>
<div id="wrapper">
<div id="main">
{block name=main}
<article>
<h1>デフォルト文章</h1>
<p>mainのブロックに指定が無い場合のデフォルト文章です。</p>
.
.
</article>
{/block}
</div>
{block name=sidebar}
{/block}
</div>
<footer>
.
.
</footer>
</body>
</html>
 
<!-- sidebar.tpl -->
<div id="sidebar">
<aside>
.
.
</aside>
</div>

{$title}の部分は既に説明済ということで、今回新たに出てきたのは{block}である。{block}で始まり、{/block}で終わる部分は、DreamweaverにおけるTemplateBeginEditableと同じく、ここに記述が追加される可能性がありますよという表示である。では、インデックスページのテンプレートindex.tplをbase.tplの子テンプレートとして作成し、head部にメタタグを1行加えた上でサイドバーのテンプレートsidebar.tplを組み込むといったことを行ってみよう。

<!-- index.tpl -->
{extends file="base.tpl"}
{block name=head}
<meta name="description" content="…">
{/block}
{block name=sidebar}
{include file="sidebar.tpl"}
{/block}

親テンプレートの指定は{extends}で行い、親テンプレート中の{block}に挿入する内容を、子テンプレート内の{block}で囲んでいる。
また、親テンプレート中で{block name=main}で囲まれていた部分は、子テンプレートで指定がされていないため、親テンプレート中で囲まれていたデフォルト値が採用される。
{include}については、テンプレートをテンプレート中で読み込む仕組みになる。勿論サイドバーを読み込まないページについては、サイドバーのブロックを子テンプレート中で無指定にすれば良い。このように部品テンプレートの増減にも柔軟に対応できる。

また実際index.phpから呼び出す際には、子テンプレート名だけ指定すれば、自動的に親テンプレートを継承して表示してくれる。

//index.php
require_once("../bin/php/php5.4.4/lib/php/smarty/libs/Smarty.class.php");
$smarty = new Smarty();
$smarty->template_dir = "../bin/php/php5.4.4/lib/php/smarty/templates";
$smarty->compile_dir = "../bin/php/php5.4.4/lib/php/smarty/templates_c";
$smarty->config_dir = "../bin/php/php5.4.4/lib/php/smarty/configs";
$smarty->cache_dir = "../bin/php/php5.4.4/lib/php/smarty/cache";
$smarty->assign("title","トップページ");
$smarty->display("index.tpl");

Smarty3.xの注意点

Smarty2.xに比べて格段に直感的になったSmarty3.xだが、先述の通り依然Smarty2.xに留まっているユーザも多くいる。その理由としては、PHP5.2以上が必要要件であること、速度面でSmarty2.xにかなり劣るということ、メソッド名の命名規則がキャメルケースになったこと(例:$smarty->clear_all_cache()というメソッドが、$smarty->clearAllCache()に)を始めとして文法の見直しが多少あるということなど挙げられる。
一応、Smarty2.xの書き方と互換をもった、Smarty3.xのBC(Backwards Compatibility Wrapper)というものもあり、Smarty.class.phpの代わりにSmartyBC.class.phpを読み込むことで適用することも可能ではある。

SmartyはJavaScriptの扱いで少し面倒くさい

以上、とりあえずSmartyの基本を説明したわけだが、最初に宣言した通り、個人的にはPHPテンプレートエンジンはSmartyでなくTwigを使っている。その理由については、実は解説を書いている内に思い出したのだが、Smartyの場合、波括弧({})に囲まれた部分は言語構造と見なされてしまうため、JavaScriptとの相性が悪い。JavaScriptを書く部分を{literal}{/literal}で囲んでエスケープするか、あるいはSmarty側の設定をいじって、波括弧でないデリミタをあてがわないといけない。

$smarty->left_delimiter = '<!--{';
$smarty->right_delimiter = '}-->';

たとえばこのようにすると、波括弧の代わりに競合しない括弧の定義が出来るわけである。ただし、競合を避けるためだけに通常より多めの記号を打たされている感がしてあまり心証もよくない。また、熟達したsmarty使いとの引き継ぎの際にやはり口論が起きる。

(追記&訂正:コメントでご指摘いただいたとおり、Smarty3では直前ないし直後に空白文字・改行文字・タブなどを伴う波括弧はデリミタとみなされないという仕様ができたらしい。したがって、CSS、JavaScriptともに波括弧使用の後に改行を行うコーディングスタイルであれば、自然にSmartyの言語構造とみなされることを回避してくれる。

CSSの場合

 
/* 前後に改行文字があるためSmartyの言語構造とみなされない */
* {
margin : 0;
}

JavaScriptの場合

 
//前後に改行文字があるためSmartyの言語構造とみなされない
function someFunction(){
return 0;
}
 
//前後に改行文字があるためSmartyの言語構造とみなされない
while(1){
someFunction();
}

ただ、人によっては無名関数の処理部分など改行せずに記述したいという場合もあるので、その場合には少し手直しが必要になるだろう

JavaScript

 
//こんな記述はアウト
var someFunction = function(){return 0;};
 
//せめてこう変えるべき(スペースが入るのでセーフ)
var someFunction = function(){ return 0; };

追記終わり)

twigの場合だと、Smartyの波括弧にあたるのが”{%”と”%}”なので、特別に設定しなくても競合はそう起こらない。起こる可能性があるとしたら、jquery.tmplの言語構造を囲む二重波括弧({{}})が変数とみなされてしまうケースが考えられるけれど、相対的には少ないので、jquery.tmplを使うところだけ{% raw %}{% endraw %}で囲っておくという形で対処する。熟達したtwig使い(そもそもいないのでは?)との間に口論も起きないだろう。


PHPテンプレートエンジンを使おう Smarty2.x編

テンプレートの仕組みは、前回のPHP編で説明したとおり。それでは今度は、テンプレートエンジンという便利なモノについて見ていこう。

自作PHPテンプレートの欠点

前回紹介したPHPテンプレートには、いくらでも改善の余地がある。たとえば本格的に使用するのであれば、プレースホルダを置き換える値として何が来ても大丈夫なよう、エスケープをする必要があるだろうし、商品紹介ページのようなプレースホルダと変数が一対一対応になるページではなく、一対多の関係になるであろう商品一覧ページのようなものを実現するためには、配列からループで値を取り出しプレースホルダ部分にあてはめる仕組みが必要となる。もちろん、熟達した腕をもつPHP使いにとっては朝飯前の作業だろうが、担当者が変わり引き継ぎの可能性がある場合など、前任PHP使いのクセを読み取る無駄な作業ができてしまう(そして熟達したPHP使い同士による引き継ぎだと、口論が起きるだろう(笑))。

オープンソースのテンプレートエンジンを使おう

そこでSmartyなどのオープンソーステンプレートエンジンだ。機能の改善余地となりうるところはコミュニティが既に改善してくれている可能性が高いし、メジャーなテンプレートエンジンであれば記法を一度覚えればつぶしが効く。
また、ポピュラーなテンプレートエンジンというものはMVCを念頭に置いて設計されている可能性が高い。MVCというのは、モデル・ビュー・コントローラのそれぞれの頭文字をとった単語で、プログラムを処理とデータを担当する部分(モデル)、見た目の部分(ビュー)、ユーザインターフェースの部分(コントローラ)に分けて制作するという考え方だ。前回の記事の最後に言及したデザイナー・htmlコーダとの連携は、MVCを意識したプログラムを使うとより容易になるわけで、たとえばモデルの部分の開発作業が滞ってしまったとしても、ビューやコントローラの作業の進行には影響が出なくなる。

Smartyの利用方法(2.x編)

オープンソーステンプレートエンジンの筆頭、Smartyであるが、ヴァージョン2.x系と3.x系では少し使い勝手も異なり、2.x系の速度面での優位も大きいので、いまだ2.x系にとどまっているユーザも多い。2.xのパラダイムから見ていった方が面白いので、今回はSmarty2.xについての紹介のみということにしよう。

まずSmartyの実体とは何かというと、PHPプログラム。したがってPHPプログラムの動作するディレクトリに置けばその機能を利用することができるようになる。インストール方法であるが、公式サイトから2.x系の最新ヴァージョンを落としてきて解凍、libsというディレクトリを取り出してアップロードする。
libsディレクトリと同階層には、4つの空ディレクトリを作る(cache、configs、templates、templates_c)。パブリックディレクトリでこれをやってしまうと散らかってしまいみっともないので、適当にsmartyディレクトリでも作って、その中にlibsを含めた5つのディレクトリを放り込んでおく。

Smartyのディレクトリを作成

Smartyのディレクトリを作成

もちろんパブリックより上の階層にアクセスする権限があるのなら、このsmartyディレクトリも上の階層に設置して、勝手にアクセスされる危険性を無くしておいた方がよい。MAMPだったらMAMP/bin/php/(使用中のphpのヴァージョン)/lib/phpあたりに入れておけば良いだろう。
ディレクトリの内のcacheとtemplates_cについては、書き込み権限が必要となるので設定する。権限は「770」とか「775」あたりにしておく。OSXの場合は、「情報を見る」から「共有とアクセス権」などで設定できるはずだ。

Smartyによるテンプレート読み込みの基本

Smartyはクラスとして定義されており、利用する際にはSmarty.class.phpをrequireしたあとインスタンスを作り、メソッドを使ってテンプレートと変数の対応関係をつけていくことになる。まずインスタンスの作成,そして各種ディレクトリへのパスを宣言しておく。

//MAMPのMAMP/bin/php/bin/php/php5.4.4/lib/phpディレクトリに
//smartyディレクトリを投げ込んだ場合の例
//htdocs直下のindex.phpで実行すると仮定
 
require_once("../bin/php/php5.4.4/lib/php/smarty/libs/Smarty.class.php");
$smarty = new Smarty();
$smarty->template_dir = "../bin/php/php5.4.4/lib/php/smarty/templates";
$smarty->compile_dir = "../bin/php/php5.4.4/lib/php/smarty/templates_c";
$smarty->config_dir = "../bin/php/php5.4.4/lib/php/smarty/configs";
$smarty->cache_dir = "../bin/php/php5.4.4/lib/php/smarty/cache";

実際パスをいちいち書いていたら冗長に過ぎるので、その辺りは適宜定数として宣言して対応できるだろう。
テンプレートファイルは.tplという拡張子をつけて、templatesディレクトリに放り込んでおく。たとえばdefault.tplというテンプレートファイルを読み込む場合、displayメソッドを使う。

$smarty->display("default.tpl");

テンプレートのプレースホルダに値をセットする

displayメソッドを呼ぶ前に、テンプレート内のプレースホルダに対して値をセットすることが出来る。たとえば、default.tplのページタイトル部分にプレースホルダを置く場合は、{}で変数名を囲んでおく。

//default.tplの記述
 
<!doctype html>
<html lang="ja">
<head>
<title>{$title} | サンプル株式会社</title>
</head>
<body>
.
.
</body>
</html>

このテンプレートに対して、プレースホルダの変数名を指定してassignメソッドを使い値を入れる。

//トップページ index.phpから呼び出すとき
 
$smarty->assign("title","トップページ");
$smarty->display("default.tpl");
 
//会社概要ページ company.phpから呼び出すとき
 
$smarty->assign("title","会社概要");
$smarty->display("default.tpl");

このようにすることで、ユーザがindex.phpのアドレスを指定してアクセスしてきたときにはページタイトルが”トップページ | サンプル株式会社”となり、company.phpにアクセスするときには”会社概要 | サンプル株式会社”となる。そして、全てのページのタイトルにおいてサンプル株式会社の部分を変更したくなったとき、修正するファイルはdefault.php一つだけで済む(なんというテンプレートエンジン!)。

Smarty2.xのパラダイム ”閉じhead前切り”

Smartyのテンプレート表示の基本は上の通り。配列をループで出力したり…といった使い方はまた別の機会に実践編として説明しよう(するかもしれない)。
PHPは命令の実行結果を逐次標準出力に吐き出していくことが出来るので、なにもdisplayメソッドが一ファイル中一度しか使えないわけではない。そのため、htmlファイルを部分部分に分けてそれぞれテンプレート化し、必要に応じて呼び出すと言った方法が取られる。そのため、テンプレートエンジンを使う場合でもまず完成系のhtmlファイルを作成してしまって、部分部分に切っていくという手順が一般的である。前回の例を使って、部分部分に切りわけていこう。

まず元となるhtmlファイルがこれ。

 
<!DOCTYPE html>
<html lang="ja">
<head>
.
.
<title>トップページ | サンプル株式会社</title>
</head>
<body>
<header>
<h1>サンプル株式会社</h1>
</header>
<div id="wrapper">
<div id="main">
<article>
<h1>ごあいさつ</h1>
<p>ようこそサンプル株式会社ホームページへ!</p>
.
.
</article>
</div>
<div id="sidebar">
<aside>
.
.
</aside>
</div>
</div>
<footer>
.
.
</footer>
</body>
</html>

タイトル部分やメインコンテンツ部分を各ページ毎に変えたいというのはもちろんのこと、ページ内容によってはサイドバーのない画面いっぱいのデザインも使いたい。ヘッダ画像部分は全ページ共通でよい。という要請の場合、bodyタグの直前部分までと、ヘッダ画像とメイン部分、サイドバー部分、フッター以降html閉じタグまでという4つのテンプレートに分けて作成する。

 
<!-- head.tpl -->
<!DOCTYPE html>
<html lang="ja">
<head>
.
.
<title>{$title} | サンプル株式会社</title>
 
<!-- main.tpl -->
</head>
<body>
<header>
<h1>サンプル株式会社</h1>
</header>
<div id="wrapper">
<div id="main">
{$article}
</div>
 
<!-- sidebar.tpl -->
<div id="sidebar">
<aside>
.
.
</aside>
</div>
 
<!-- footer.tpl -->
</div>
<footer>
.
.
</footer>
</body>
</html>

まあ、こんな形で、呼び出し側ではこのようになる。

//index.php
 
require_once("../bin/php/php5.4.4/lib/php/smarty/libs/Smarty.class.php");
$smarty = new Smarty();
$smarty->template_dir = "../bin/php/php5.4.4/lib/php/smarty/templates";
$smarty->compile_dir = "../bin/php/php5.4.4/lib/php/smarty/templates_c";
$smarty->config_dir = "../bin/php/php5.4.4/lib/php/smarty/configs";
$smarty->cache_dir = "../bin/php/php5.4.4/lib/php/smarty/cache";
$smarty->assign("title","トップページ");
$smarty->display("head.tpl");
$smarty->assign("article","<article>
<h1>ごあいさつ</h1>
<p>ようこそサンプル株式会社ホームページへ!</p>
.
.
</article>");
$smarty->display("main.tpl");
$smarty->display("sidebar.tpl");
$smarty->display("footer.tpl");

切り慣れていないので、本当はもっと効率の良い切り方はあるのだろうけど。

で、強調したいところはmain.tplが何故bodyタグから始まらずに、閉じheadタグから始まるかというところ。htmlコーダの人は、この閉じheadタグの所在をかなり不気味に思うだろう。
実はこれ、各ページによってcssファイルの読み込みやscriptタグでのjavascriptの記述、あるいはそのページ特有のmetaタグ(meta descriptionなど)が入り込む可能性を考慮してこの切り方になっている。その場合header.tplとmain.tplのdisplayの間で、また別のテンプレートを呼び出すか、あるいはインラインで書くか。とにかく急に1ページだけライブラリの読み込みが必要となった場合などに、全ページ共通読み込み部分のhead.tplの記述を増やさなくてよいような配慮となっている。

このような配慮を考える必要があるのは、部品化はできても部品同士の上下関係はPHPスクリプトにおける呼び出し順に依拠するためだ。ということで、こういうテンプレートエンジンのパラダイムを、勝手に”閉じhead前切り”パラダイムと命名。Smarty 2.xは”閉じhead前切り”パラダイム!