タグ別アーカイブ: PHP

Search RegexプラグインでWordPress投稿を正規表現検索置換

※今回の記事の内容、特に自己責任で試してね。投稿記事の破損についてなど一切の責任は負いません。

そもそもの問題

過去にこのような投稿を上げていた。

小ネタ:WordPressがHTML5に対応して、過去記事の見出しに困る

WordPressがHTML5に対応した際、それまで記事部分での見出しレベルトップはh3であったのが、h2に繰り上げとなった。その影響で過去投稿をいちいち引っ張り出してきて、手動で修正するのが面倒だよねという話。
それに対して、見出しレベルを変更するこんなプラグインもありますよと紹介していたのがこの投稿。

WordPressテーマ変更に伴う記事のルート見出しレベルの変動を吸収するプラグイン WP hn Convert

記事内でも書いている通り、このプラグインを使ってWordPress関数the_content()をリプレースする形だと、以降も見出しトップレベルをh3にして投稿をし続けなければならず、この時は採用を断念していた。結局、見出しトップレベルの問題は全投稿の見出しを手動で書き換えることで解決した。

そんな貴方にオススメ。Search Regexプラグイン!

3年くらい経って、解決法に気付いた。全投稿を正規表現を用いて検索・置換するプラグインがあれば、この問題は容易に解決することが出来たのではないだろうか。そういう痒いところに手が届くプラグイン、きっとあるはず。
ありました。それがSearch Regexプラグイン。早速インストールして、具体的な指定方法を考えてみよう。

こんな感じで、インストール後は"ツール"メニューに出現

こんな感じで、インストール後は”ツール”メニューに出現

まず何より、左下にあるRegexチェックボックスにチェックを入れよう。これに気付かず、しばらく格闘してしまった(ココの表示がボールドになっているの、おかしくない?)。Search patternフィールドに検索文字列を、Replace patternフィールドに置換文字列を入力する。Replaceボタンを押すと、置換例を表示してくれるけれどデータベースに反映はしない。Replace & Saveボタンは押すと取り返しのつかないボタン。データベースに即時反映するヤツなので、前者のボタンで置換例を見て微調整しつつ、上手くいっているようだったら後者のボタンを押すようにしよう。

正規表現を使って見出しレベルを一段ずつ上げる

では実際にやってみよう。見出しレベルが一段ずつ上がるので、たとえば先にh6を置換してh5にして…というように下からやっていってしまうと、最終的に全部h2になってしまう(笑)。h3をh2に、その次にh4をh3に…という順序で手をつけよう。

 
Search pattern:/<h3>(.*?)<\/h3>/
Replace pattern:<h2>$1</h2>

このようなパターンで、h3の見出しが見出し語そのままh2に昇格する。Replace pattern側の$1というのが参照文字(preg_replaceの解説なども参照)で、h3タグに挟まれた文字を一旦預かり、置換パターン側に展開してくれる。もし預かる箇所が2ヶ所以上だったら、$2、$3…という形で指定が出来る。ちなみに$0にはh3タグも含んだ文字列全体が格納されている。

Search Regexによる置換イメージ

Search Regexによる置換イメージ

この置換を、次はh4…とどんどんやっていけば良い。

自サイトの内部リンクからtarget=”_blank”を外す

次の課題。昔のWEB制作ではひたすらリンクにtarget=”_blank”を付与していたものである。とりあえず付けられるなら付けておくか、といった感覚で、WordPressのエディタでもチェックボックス1つで付けられてしまうものだからかかさず付けていた。
それが時代の変遷とともに訪問者の閲覧環境がモバイルデバイス中心になると、モバイルブラウザ上でウィンドウやタブを何枚も開くことは難しく、即ユーザビリティの低下となるため、頻繁にtarget=”_blank”を使うサイトは忌避されユーザの離脱率が高くなるという問題が顕在化した。そのため、最近は内部リンクについてはtarget=”_blank”をつけないページ作りが正解になっているらしい。
では、過去の投稿の内部リンクからtarget=”_blank”をはがすパターンについて考えてみよう。

 
Search pattern:/<a\shref="http:\/\/akisi\.tabiyaku\.net([^>]*?)\starget="_blank">/
Replace pattern:<a href="http://akisi.tabiyaku.net$1>

このサイトのアドレスがhttp://akisi.tabiyaku.netから始まるので前方一致に入れているわけで、自身のサイトで流用する場合は適宜自サイトのアドレスに変えて欲しい。また、このパターンはtarget=”_blank”がaタグの最後に来るという性善説で考えているので、流用の際にはそこも注意である。

その他Search Regexの応用例として、Tabnabbing防止にtarget=”_blank”付の外部リンクに自動的にrel=”noopener”を付ける処理を出そうかとも思ったのだけれども、なんだか分岐の可能性が多そうなのでやめました(元々rel属性に何かの値が指定されていた場合など)。我こそはと思う方はやってみて、上手いやり方を教えて下さいな。


PHPテンプレートエンジンを使おう WordPress編

テンプレートエンジンの仕組みと、そのパラダイムについて何回かに分けて説明してきている(素のPHP編Smarty2.x編Smarty3.x編)。で、Smarty2.xのテンプレートエンジンのパラダイム、”閉じhead前切り”と同じく、テンプレートパーツを順番に呼び出していき最終的なhtmlとして成形するのが、かの有名なCMS、WordPressだ。WordPressのサイトがPHPプログラムからどのように生成されていくのか、入門編的な要素も絡めて解説しよう。

WordPressにおけるhtmlの切り分け方

WordPressにおけるテンプレートを確認するには、ダッシュボードの”外観”メニューから”テーマ編集”を選ぶ。初期画面で表示されているのはWordPressテーマのCSSだが、右側にある”テンプレート”と書かれたセクション、ここにあるのがテンプレートファイルということになる。ただし、このセクションはテンプレートファイルと呼び出し側のPHPスクリプトがごっちゃになっている状態(WordPressの場合、テンプレートファイルの拡張子も.phpになる)であり、また、”テーマ編集”で表示されるのは現在適用中のテーマのテンプレートファイルなので、テーマによって表示されている構成ファイルが異なるということに注意。とりあえず、本解説ではTwenty Twelveのものを基準として、どのテーマにも含まれそうなテンプレートファイルについて解説する。

テンプレートパーツ header.php

Smarty2.xの、”閉じhead前切り”では、html文書の頭の部分から、全ページ共通となる箇所を切り出してテンプレートを作成した。この頭からの部分に当たるのがheader.phpである。Smarty2.xの場合はこの部分に閉じheadタグを含めることができなかったが、これはmetaタグなどheadタグを閉じる前に挿入するページ固有の文言が必要な場合を鑑みてのことであった。
WordPressのheader.phpの場合は、呼び出し元のページ毎に必要な固有文言の挿入を、フィルターやアクションといった仕組みでキューに登録できる。登録した文言は、wp_head()というWordPress関数が呼ばれる際に一緒に呼ばれ挿入される仕組みになっている。したがって、head閉じタグの前で切らなくてもよい。大抵のテーマではヘッダー画像を内包して、mainやsidebarの直接の親要素の開始タグまでをまとめて、header.phpとしているようである(ソースコードの注釈にもそう書かれているのがわかるだろう)。

//Twenty Twelveのheader.php
 
<?php
/**
 * The Header template for our theme
 *
 * Displays all of the <head> section and everything up till <div id="main">
 *
 * @package WordPress
 * @subpackage Twenty_Twelve
 * @since Twenty Twelve 1.0
 */
?><!DOCTYPE html>
<!--[if IE 7]>
<html class="ie ie7" <?php language_attributes(); ?>>
<![endif]-->
<!--[if IE 8]>
<html class="ie ie8" <?php language_attributes(); ?>>
<![endif]-->
<!--[if !(IE 7) | !(IE 8)  ]><!-->
<html <?php language_attributes(); ?>>
<!--<![endif]-->
<head>
<meta charset="<?php bloginfo( 'charset' ); ?>" />
.
.
.
<?php wp_head(); ?>
</head>
<body <?php body_class(); ?>>
<div id="page" class="hfeed site">
	<header id="masthead" class="site-header" role="banner">
.
.
.
	</header><!-- #masthead -->
 
	<div id="main" class="wrapper">

このように、header.phpというテンプレートの終わりはidがmainであるdivの開始タグとなっている。実際に表示されたページで見ると、以下の部分だ。

header.phpの実際の表示

header.phpの実際の表示

テンプレートパーツ sidebar.php

サイドバー部分のテンプレートがsidebar.phpになる。とは言っても、サイドバーに表示するものはウィジェットという形でユーザが自由に追加していく仕組みなので、テンプレートsidebar.phpの内容は大体dynamic_sidebar()というWordPress関数の呼び出し程度である。

sidebar.phpが担当する部分

sidebar.phpが担当する部分

テンプレートパーツ footer.php

footer.phpでは、ご想像の通りheader.phpで開いたid=”main”のdivの閉じタグから始まる。フッター部分のパーツ(クレジットなど)を表示し、bodyタグやhtmlタグも閉じる。bodyタグを閉じる前にwp_footer()という関数を呼んでいるが、これもまたこの位置で挿入されるスクリプト等のキューを参照、表示する。

//Twenty Twelveのsidebar.php
 
<?php
/**
 * The template for displaying the footer
 *
 * Contains footer content and the closing of the #main and #page div elements.
 *
 * @package WordPress
 * @subpackage Twenty_Twelve
 * @since Twenty Twelve 1.0
 */
?>
	</div><!-- #main .wrapper -->
	<footer id="colophon" role="contentinfo">
		<div class="site-info">
			<?php do_action( 'twentytwelve_credits' ); ?>
			<a href="<?php echo esc_url( __( 'http://wordpress.org/', 'twentytwelve' ) ); ?>" title="<?php esc_attr_e( 'Semantic Personal Publishing Platform', 'twentytwelve' ); ?>"><?php printf( __( 'Proudly powered by %s', 'twentytwelve' ), 'WordPress' ); ?></a>
		</div><!-- .site-info -->
	</footer><!-- #colophon -->
</div><!-- #page -->
 
<?php wp_footer(); ?>
</body>
</html>

実際に表示される部分としては、以下の部分。

footer.phpの担当部分

footer.phpの担当部分

WordPressにおけるテンプレートパーツの呼び出し方

この3種類の基本的なパーツを、個々のページファイルから呼び出して表示するわけである。ブログの表玄関である、index.phpでの呼び出され方を見てみよう。

//Twenty Twelveのindex.php
 
<?php
/**
 * The main template file
.
.
.
 */
 
get_header(); ?>
 
	<div id="primary" class="site-content">
		<div id="content" role="main">
		<?php if ( have_posts() ) : ?>
 
			<?php /* Start the Loop */ ?>
			<?php while ( have_posts() ) : the_post(); ?>
				<?php get_template_part( 'content', get_post_format() ); ?>
			<?php endwhile; ?>
 
			<?php twentytwelve_content_nav( 'nav-below' ); ?>
 
		<?php else : ?>
.
.
.
		<?php endif; // end have_posts() check ?>
 
		</div><!-- #content -->
	</div><!-- #primary -->

<?php get_sidebar(); ?>
<?php get_footer(); ?>

get_header()、get_sidebar()、get_footer()というWordPress関数で、それぞれheader.php、sidebar.php、footer.phpを呼び出して表示している。つまり最低限この3テンプレートと、3関数のコールがあればページの枠はできているのである。
あとは、get_header()とget_sidebar()の間に書かれている部分が、ページのメイン部分の内容になる。ここで、もう一種類のテンプレート読み込み関数、get_template_part()というものが登場している。

get_template_part()

get_template_part()という関数は、任意の名称のテンプレートを読み込む関数である。第1引数のみ指定すると(たとえば、”content”という文字列を与えよう)、第1引数.phpというテンプレートファイルを読み込む(例であれば、content.php)。第2引数を添えると(たとえば、”content”,”aside”という2つの文字列を与えよう)、第1引数-第2引数.phpというテンプレートファイルを読み込む(content-aside.php)。
上記のTwenty Twelveの例では、”content”とget_post_format()という2つの引数を与えている。get_post_formatは投稿のフォーマットを返してくるので、contentの後に投稿フォーマットのついた複数のテンプレートファイルにここで振り分けを行っているのだ。確かに、Twenty Twelveの構成テンプレート一覧の中に”content-XXX.php”というファイルが多数ある。

content.phpの一族

content.phpの一族

このようにして、いちいち投稿フォーマット毎にswitch文で分岐を作らなくても、適切なテンプレートを取得してくれるようになっているのである。

WordPressはテンプレートエンジン

このように、特定テンプレートパーツを呼び出すget_header()などの関数、あるいはファイル名を指定して呼び出すget_template_part()関数があるため、WordPressはテンプレートエンジンそのものであると言える。
肝心のプレースホルダについてはどうであるか。WordPressは投稿(Post)を単位として読み込み、投稿のタイトル、本文などの情報にWordPress関数(ゲッタメソッド)経由でアクセス・表示させる。たとえばタイトルと本文をテンプレート中で呼び出す場合には、以下のような具合になる。

<h1 class="title"><?php the_title(); ?></h1>
<div class="honbun"><?php the_content(); ?></div>

Smartyなどのプレースホルダと異なり、関数をそのまま実行という少し無骨な形ではあるが、WordPressサイトの制作に慣れたデザイナー・コーダであれば、大体どの関数が何を出力しているか理解するし、またこの出力部分以外のデザインを変更すればよいと理解するのである。

ということで、WordPressでのサイト制作が現在主流となっているのは、そのCMS機能に期待したものではなく、デファクトスタンダードのテンプレートエンジンとしての需要であるという見方も可能である筈。


Conceptual:WEBサービスページの共通部分をtwigテンプレート化してみる

twigの実践編というわけでもないが、これまでConceptualのページに載せていたWEBサービスページの共通部分(ヘッダー、広告、フッターなど)をtwigテンプレートとして抽出する作業を行ったので、その過程など紹介してみようと思う。

共通部分の抽出

公開しているプログラムのうち、OnlyCSViewEXIFviewerについてはページ構造がほぼ同じだ。現在の2プログラムの画面を並べてみると、下の画像のような感じになる。共通部分と書いてある所はほぼソースが同じであり、真ん中の白い部分(コントローラ部分)とスクリプトの部分だけが違うということになる。

共通部分を持つ2ページ

共通部分を持つ2ページ

テンプレートエンジンで共通部分を抽出する場合、Smarty2.xのような”閉じhead前切り”パラダイムのものだと、ヘッダー部分、メイン部分、フッター部分と必要ファイルが3つになってしまうだろう。今回はテンプレートの継承機能のあるTwigを用いるため、ヘッダーとフッターを含んだ基礎部分のテンプレート(base.html.twig)にメイン部分に対応するブロックを作り、子テンプレートでブロック部分をオーバーライドするという、2ファイルでの解決を図ることにする。

ロジックとビューの分離

加えて、今回手を入れるプログラムは相当昔に作ったものなので、PHP入門書の最初のサンプルにありがちな、ロジックとビューが混在している状態になっている。テンプレートエンジンらしく、これの分離も同時に行ってみたい。

EXIFviewer手入れ前のソース

ロジックとビューの分離の説明に丁度良かったので、今回例として出すのはEXIFviewerの方にする。

 
//手入れ前index.php(1ファイル)
 
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="Description" content="EXIFviewerはローカルのJPEGファイルをアップロードすることなくサイズ、位置情報、撮影情報等のEXIFデータを確認できるWEBサービスです。">
<meta name="Keywords" content="PHP,EXIF,WEBサービス,JPEG,画像,位置情報">
<link rel="stylesheet" href="base.css" type="text/css">
.
.
<script>
$(function(){
$("#filesel").change(function(){
var reader = new FileReader();
reader.readAsDataURL(document.getElementById("filesel").files[0]);
reader.onload = function(e){
$("#path").val(e.target.result);
}
if(window.createObjectURL){
var ourl = window.createObjectURL(document.getElementById("filesel").files[0]);
} else if(window.URL) {
var ourl = window.URL.createObjectURL(document.getElementById("filesel").files[0]);
} else {
var ourl = window.webkitURL.createObjectURL(document.getElementById("filesel").files[0]);
}
$("#viewer").empty().html('<img src="' + ourl + '">');
$("#blob").val(ourl);
});//end of fileselchange
 
//Googlemap関連コード(省略)
 
});//end of jQueryready
</script>
<title>EXIFviewer JPEGファイルのEXIF情報をチェックするWEBサービス</title>
.
.
</head>
<body>
<div id="main">
	<nav><div id="back"><a href="http://akisi.tabiyaku.net/" title="AkisiのWEB制作日記に戻る">AkisiのWEB制作日記に戻る</a></div></nav>
<div id="upperad">
<script type="text/javascript">
 
//adsenseコード
 
</script>
</div>
<div id="instruction">
<h1>セットした画像のEXIFデータを表示</h1>
</div>
<div id="controlls">
<form>
<p><input type="file" id="filesel" type="image/jpeg"></p>
</form>
<form action="index.php" method="post">
<div id = "viewer">
<?php
if(!empty($_POST["path"])){
echo '<img src="' . $_POST["path"] . '">';
}
?>
</div>
<input type="hidden" name="blob" id ="blob">
<input type="hidden" name="path" id ="path">
<p><input type="submit" value="表示" class="btn btn-primary"></p>
</form>
</div>
<div id="tablearea">
<?php
if(!empty($_POST["blob"])){
if($exif = exif_read_data($_POST["blob"],NULL,true)){
echo "<h2>EXIF情報</h2>";
echo "<table class='exiftable table table-striped'><thead><tr><th>キー名</th><th>値</th></tr></thead><tbody>";
foreach ($exif as $key => $section) {
    foreach ($section as $name => $val) {
	if(is_array($val)){
	foreach ($val as $valname => $vval) {
	echo "<tr><td>" . htmlentities($key,ENT_QUOTES,"UTF-8") . ' ' . htmlentities($name,ENT_QUOTES,"UTF-8") . ' ' .  htmlentities($valname,ENT_QUOTES,"UTF-8") . "</td><td id=" . '"' . htmlentities($name . $valname,ENT_QUOTES,"UTF-8") . '">' . "$vval</td></tr>";
	}
	} else {
	echo "<tr><td>" . htmlentities($key,ENT_QUOTES,"UTF-8") . ' ' . htmlentities($name,ENT_QUOTES,"UTF-8") . "</td><td id=" . '"' . htmlentities($name,ENT_QUOTES,"UTF-8") . '">' . htmlentities($val,ENT_QUOTES,"UTF-8") . "</td></tr>";
	}
    }
}
echo "</tbody></table>";
} else {
echo "画像のEXIFデータが見つかりませんでした。";
}
} else {
echo "画像のEXIFデータが見つかりませんでした。";
}
?>
</div>
<div id="googlemapdivarea">
<div id="googlemapdiv"></div>
</div>
<div id="lowerad">
<div class="loweradbox">
<script>
 
//adsenseコード
 
</script>
</div>
<div class="loweradbox">
<script>
 
//adsenseコード
 
</script>
</div>
</div>
</div>
</body>
</html>

いわゆる、初回アクセス時にはPOSTの値(”blob”と”path”)が空であるため例外処理として初期画面を吐いているタイプのページだ。1ファイルでまとまってくれているので扱い易いが、echo命令によってテーブルを構成するhtmlタグの細切れを吐いている部分は一見して訳が分からない。html修正時にミスも起こり易いだろう。

EXIFviewer手入れ後のソース

index.phpにはテンプレートの呼び出しとロジックを押し込み、base.html.twigとindex.html.twigの2ファイルに値だけ渡す

 
//index.php
 
require_once("Twig/Autoloader.php");
Twig_Autoloader::register();
$loader = new Twig_Loader_Filesystem("twigtmp/");
$twig = new Twig_Environment($loader, array("cache" => "cache/"));
$blob = "";
$exif = array();
if(isset($_POST["blob"])){
$blob = $_POST["blob"];
}
if(!empty($_POST["path"])){
$exif = exif_read_data($_POST["path"],NULL,true);
}
$page = array(
"description" => "EXIFviewerはローカルのJPEGファイルをアップロードすることなくサイズ、位置情報、撮影情報等のEXIFデータを確認できるWEBサービスです。",
"keywords" => "PHP,EXIF,WEBサービス,JPEG,画像,位置情報",
"title" => "EXIFviewer JPEGファイルのEXIF情報をチェックするWEBサービス",
"blob" => $blob,
"exif" => $exif
);
$template = $twig->loadTemplate("index.html.twig");
$template->display($page);

続いて呼ばれる側のテンプレート2ファイル(実際名指しされるのはindex.html.twigの方だけだけれど)。

 
<!-- base.html.twig -->
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="css/base.css" type="text/css">
{% if description is not empty %}
<meta name="description" content="{{ description }}">
{% else %}
<meta name="description" content="AkisiのWEB制作日記の提供するウェブサービス">
{% endif %}
{% if keywords is not empty %}
<meta name="keywords" content="{{ keywords }}">
{% else %}
<meta name="keywords" content="ウェブサービス,WEB">
{% endif %}
.
.
<title>{{ title }}</title>
{% block head %}
{% endblock head %}
.
.
</head>
<body>
<div id="main">
	<nav><div id="back"><a href="http://akisi.tabiyaku.net/" title="AkisiのWEB制作日記に戻る">AkisiのWEB制作日記に戻る</a></div></nav>
<div id="upperad">
<script>
 
//adsenseコード
 
</script>
</div>
{% block main %}
{% endblock main %}
<div id="lowerad">
<div class="loweradbox">
<script>
 
//adsenseコード
 
</script>
</div>
<div class="loweradbox">
<script>
 
//adsenseコード
 
</script>
</div>
</div>
</div>
</body>
</html>
 
<!-- index.html.twig -->
{% extends "base.html.twig" %}
{% block head %}
<link rel="stylesheet" href="css/index.css" type="text/css">
<script type="text/javascript" src="https://maps.googleapis.com/maps/api/js?v=3.exp&sensor=false"></script>
<script type="text/javascript">
$(function(){
 
$("#filesel").change(function(){
var reader = new FileReader();
reader.readAsDataURL(document.getElementById("filesel").files[0]);
reader.onload = function(e){
$("#path").val(e.target.result);
}
if(window.createObjectURL){
var ourl = window.createObjectURL(document.getElementById("filesel").files[0]);
} else if(window.URL) {
var ourl = window.URL.createObjectURL(document.getElementById("filesel").files[0]);
} else {
var ourl = window.webkitURL.createObjectURL(document.getElementById("filesel").files[0]);
}
$("#viewer").empty().html('<img src="' + ourl + '">');
$("#blob").val(ourl);
});//end of fileselchange
 
//Googlemap関連コード(省略)
 
});//end of jQueryready
</script>
{% endblock head %}
{% block main %}
<div id="instruction">
<h1>セットした画像のEXIFデータを表示</h1>
</div>
<div id="controlls">
<form>
<p><input type="file" id="filesel" type="image/jpeg"></p>
</form>
<form action="index.php" method="post">
<div id = "viewer">
{% if blob is not empty %}
<img src="{{ blob }}">
{% endif %}
</div>
<input type="hidden" name="blob" id ="blob">
<input type="hidden" name="path" id ="path">
<p>
<span><input type="submit" value="表示" class="btn btn-primary"></span>
</p>
</form>
</div>
<div id="tablearea">
{% if exif is not empty %}
<h2>EXIF情報</h2>
<table class='exiftable table table-striped'><thead><tr><th>キー名</th><th></th></tr></thead><tbody>
{% for key, section in exif %}
{% for name, val in section %}
{% if val is iterable %}
{% for valname, vval in val %}
<tr><td>{{ key }} {{ name }} {{ valname }}</td><td id="{{ name }}{{ valname }}">{{ vval }}</td></tr>
{% endfor %}
{% else %}
<tr><td>{{ key }} {{ name }}</td><td id="{{ name }}">{{ val }}</td></tr>
{% endif %}
{% endfor %}
{% endfor %}
</tbody></table>
{% else %}
画像のEXIFデータが見つかりませんでした。
{% endif %}
</div>
<div id="googlemapdivarea">
<div id="googlemapdiv"></div>
</div>
{% endblock main %}

テンプレートのcssは、共通部分のためのbase.cssと、index.cssに分ける。手入れ前のソースでは色付きハイライトされているPHP部分がなくなり、その代わりにtwigのfor inで配列を回している。テンプレート関係で新しく登場したのは、for inで回す際にキー値にもアクセスする場合の、{% for key, value in array %}という書き方。それから、配列に要素があるかどうか調べるのには{% if array is iterable %}のように書く、といったことくらいだろうか。
テンプレートの使い分け判断に使う値が変数の中にあるようであれば(あるいは変数自身の存在で判断できるのであれば)変数のみをそのままテンプレートに渡し、そうでないようならロジック側で切り替え判断のためのフラグを値で用意し、変数とフラグを合わせてテンプレートに渡す、このようにしてロジックとビューの分離をはかるわけである。

あとは、テンプレート中のJavaScriptに波括弧が含まれる場合{% raw %}{% endraw %}で囲むのを忘れずに。

TwigをLinuxの公開サーバに上げる段になってハマり易いこと

Twigで個人的にハマったこと。MAMPなどのローカル環境で動作したプログラムを公開サーバにアップロードしてみると、以下のようなエラーが出て動かないということがあった。

Class ‘Twig_Loader_Filesystem’ not found

原因は、ざっくり言ってしまうとLinuxが大文字小文字の扱いにうるさいからだった。たとえばAutoloader.phpへのパスが”twig/Twig/Autoloader.php”だった場合、”Twig/Twig/Autoloader.php”というように指定してしまうと、OSXやWindowsなどの環境では理解してくれるものの、Linuxサーバ上では理解をしてくれない。
また、Autoloader.phpの上位のディレクトリ名も、”Twig”でないといけない。思う所あって”twig”というように小文字のディレクトリにしてアップロードしていたから、”twig/Autoloader.php”という正しいパスを指定してもエラーが出てしまっていることに当惑した。”Twig”ディレクトリは勝手にリネームしてはいけない。それが今回の失敗の教訓。


PHPテンプレートエンジンを使おう Twig編

PHPテンプレートエンジンについて解説・紹介していく企画、素のPHPSmarty2.xSmarty3.xと紹介してきて、満を持してのTwig登場である。

PHPテンプレートエンジンTwigとは

そもそも、Twigとは何なのだろうか。疑問を解決するためにお手元の検索エンジンで検索しても、小枝の画像ばっかり出てくる(笑)。Twigとは、フランスのSensioLabsというソフトウェア会社にスポンサードされている、オープンソースのWEBアプリケーションフレームワークSymfonyを構成する標準バンドルパッケージの一つである(Symfony公式サイトTwig公式サイト)。
WEBアプリケーションフレームワークという言葉が出てきたが、WEBアプリケーションを作るための”概して便利な”枠組みのことで、Symfonyは企業サイトやらAPIやらのWEB上に展開するサービスを作成する際に、ヴァージョンの管理やライブラリの管理、データ構造など様々なことを、MVCの考え方に基づいて整理・管理してくれる仕組みだ。
MVCにのっとっているため、Symfonyは多人数が関わるプロジェクトなどにおいて強みを発揮する。逆に言うと、小規模で作ったら作りっぱなしのようなプロジェクトに導入する意義はそれほど無い。でもそうした小規模プロジェクトでも、Symfonyの特定機能だけ利用したいという需要があるかもしれないので、Symfonyでは機能の多くをモジュール化しており、Symfony本体を導入しなくてもかいつまんで使えるようになっている。そのモジュールの一つが、PHPテンプレートエンジンのTwigというわけだ。

Twigの利用方法

Twig単体で利用する場合、先程紹介したTwig公式サイトより本体を落としてきて、libディレクトリ内のTwigディレクトリを適当な場所に配置する。あとはキャッシュ用のディレクトリと、テンプレートファイル格納用のディレクトリを必要に応じて作っておけばよい。なおインストールにはcomposerというSymfonyのモジュールを使う方法もあり、ファイルの依存関係を管理するというcomposer自体の機能も使いたい場合には、一緒に導入しておいても良いだろう。
で、ひとまずcomposerを使用しないインストールが済んだとして、呼び出し側のPHPスクリプトではこういった感じに書く。Smarty2.xの説明で出した例と同じことをやっているので、適宜比較などしてほしい。

//index.php
 
require_once("Twig/Autoloader.php");
Twig_Autoloader::register();
$loader = new Twig_Loader_Filesystem("twigtmp/");
$twig = new Twig_Environment($loader, array("cache" => "cache/", "debug" => false));
$template = $twig->loadTemplate("default.html.twig");
$template->display(array("title" => "トップページ"));

まず、先程のTwigディレクトリにあるAutoloader.phpを読み込む。これは関連ファイルを全て読み込むためのクラスで、Twig_Autoloader::registerメソッドを呼ぶ。
次に$loaderに入れているのはテンプレートをロードするクラスのインスタンスで、Twig_Loader_Filesystemの場合コンストラクタにテンプレートファイルのあるディレクトリへのパスを引数として与える。ファイルを読み込むのでなくインラインのStringでテンプレートを用意する場合はTwig_Loader_Stringクラスなど、いくつか種類があるのだが、とりあえずTwig_Loader_Filesystemだけ覚えておけば良い。

次のTwig_Environmentというクラスのインスタンス作成時に、$loaderと配列にした各種設定を渡している。”cache”はキャッシュを入れるディレクトリへのパス、”debug”はデバッグの有無など。キャッシュを使わずデバッグモードもオフならば、第一引数のみで良い。

$templateはTwig_environmentのメソッドであるLoadTemplate()で読み込むテンプレートファイル名を与える。そして$templateのdisplay()メソッドの引数に、変数の値をラベル付き配列で与える。これで標準出力へと出力されることになる。
テンプレート側の記述はこのように。変数の先頭に$をつけず、二重波括弧({{}})で変数名を囲む。

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

商品一覧ページをTwigで作ってみる

それでは、Smarty3.xを用いて作った商品一覧ページを、Twigで作り直してみよう。まずはテンプレートファイル側。

 
<!-- base.html.twig -->
<!doctype html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="css/base.css" type="text/css">
{% if description is not empty %}
<meta name="description" content="{{ description }}">
{% else %}
<meta name="description" content="サンプル株式会社のホームページです">
{% endif %}
{% if keywords is not empty %}
<meta name="keywords" content="{{ keywords }}">
{% else %}
<meta name="keywords" content="サンプル株式会社,小売業,日本">
{% endif %}
<title>{{ title }} | サンプル株式会社</title>
{% block head %}
{% endblock head %}
</head>
<body>
<div id="wrapper">
<header>
<h1>サンプル株式会社</h1>
</header>
<div id="main">
{% block main %}
<article>
<h1>デフォルト文章</h1>
<p>mainのブロックに指定が無い場合のデフォルト文章です。</p>
.
.
</article>
{% endblock main %}
</div>
<footer>
<p>サンプル株式会社 2012-{{ "now"|date("Y") }} all rights reserved.</p>
</footer>
</div>
</body>
</html>
 
<!-- item.html.twig -->
<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.html.twig -->
{% extends "base.html.twig" %}
{% block main %}
<article>
<h1>サンプル株式会社の商品一覧</h1>
{% if items is empty %}
<p>現在取り扱っている商品はありません</p>
{% else %}
<p>当社が取り扱う商品の一覧です。</p>
<ul id="itemlist">
{% for value in items %}
{% include "item.html.twig" %}
{% endfor %}
</ul>
{% endif %}
</article>
{% endblock main %}

テンプレートファイル側でのSmartyとの違いは、プレースホルダの書き方は先程説明の通り。ifなどの制御構造は括弧だけでなく、end〜という閉じタグになるところや、foreachの書き方がfor inになるところ、blockのname=でなく直接block名指定になるところ、比較演算子がis notのような自然言語的な書き方になることなど、細かい違いが結構あると言える。配列のラベルへのアクセスは同じだが、Twigの場合value[“name”]のようなアクセスも可能。そして、オブジェクトのメンバへのアクセスも同じ書き方で出来る。
修飾子が”|”の後に続くのも同じ。あとは、オシャレなマニュアルでチェックしてほしい。

呼び出し側はこのように。

//itemlist.php
 
require_once("Twig/Autoloader.php");
Twig_Autoloader::register();
$loader = new Twig_Loader_Filesystem("twigtmp/");
$twig = new Twig_Environment($loader, array("cache" => "cache/", "debug" => false));
$items = array();
 
//例の如く、下記の配列が帰ってきたと仮定
 
$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" => "16000")
);
$page = array(
"description" => "サンプル株式会社の商品一覧ページです。",
"keywords" => "サンプル株式会社,ネットショップ",
"title" => "当社商品の一覧",
"items" => $items
);
$template = $twig->loadTemplate("itemlist.html.twig");
$template->display($page);

まあ既に説明した内容である。displayメソッドに持たせる引数が標準で配列であるため、テンプレート側からそのままラベルのみでアクセスしていることに注意。
あと、この例だとキャッシュを有効化しているので、いじってテストなどする際には、結果確認の前にキャッシュディレクトリの中身を廃棄しておこう(これをわざわざやらなくて良いためにデバッグモードがあるらしい。先程知った)。

SmartyとTwigはどちらが良いのか

以上、Twigについても見てきた。仕組みはそれほどSmartyと変わらないので、乗り換えは楽だろう。ただ、乗り換え後過去資産を活用しようとすると変換は面倒臭いので、そこはあまり期待しない方が良い。
良く言われるSmarty3.xとTwigの速度面の差だが、確かに体感などTwigの方がキビキビ動いている印象がある(Smarty2.xには劣るけど)。速度比較については厳密度は分からないけれど、こんなデータもある。
あとはマニュアルのオシャレさなんかを基準にどちらを使うか決めれば良いだろう。


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前切り”パラダイム!