デザイナーとの協業での工夫 Smartyプリフィルタの活用法

いま行なっている案件で、社外のデザイナーさんが作ったデザインをシステムに取り込むという件があり、お互いに労力の少なく出来る方法を考えてみたのでここに残しておく。前提として、システムばりばりなものではなく、デザインがメインだが、フォームがあるページや投稿系でシステムで出すべき一覧ページがあったりするようなサイトの場合です。

基本的な思想

基本的には、デザイナーさんが作ったhtmlファイルに極力プログラマ側で手を入れない。逆にプログラム上必要なタグ等を埋め込んだ場合には、そのマージ後のファイルを修正してもらう。
今回は、フレームワークにEthna、テンプレートエンジンにはSmartyを使ってあります。
最近、Smartyよくないという風潮ですが、プリフィルタなどのプラグイン機能は有用だと思います。

仕組みとして作ったもの

  • 1. .htmlファイルをエントリポイントにする
  • 2. Smartyのデリミタを変える
  • 3. Smartyのデリミタの種類増やす
  • 4. http<->httpsに絶対パスで書かなくても動くようにする
  • 5. 部品のincludeを出来るようにする
1. .htmlファイルをエントリポイントにする

デザイナーさんは、当然htmlファイルとしてデザインをします。それをシステム側に取り込むことになるのだが、その段階でファイルの階層がずれたり、URLが変わってしまうと、各ページからのリンクなども修正する必要が出てしまいます。
逆にページのURLをシステム側で決めた論理的なもの(物理ファイルが無いもの)にしていまうと、デザイン作成時にリンクが正しいかなどの確認が煩雑になります。


そこで、今回は、.htmlの拡張子が付いたファイルを phpとして動かし、さらにそれをエントリポイントとします。また同時に、テンプレートにもなるようにします。

例)
http://example.com/page/request_form.html
↓
request_form アクションが起動する

テンプレートとして使用する仕組みは、Ethnaの場合には、Viewの forward_path をいじってあげることで実現させています。
.htmlファイルを php として動かすには、以下のような .htaccess などで設定をしています。

#php settings
AddType application/x-httpd-php .html
AddType application/x-httpd-php .htm
<FilesMatch "\.html?$">
    php_value auto_prepend_file "/path/to/_action.php"
    # AcceptPathInfo Apache 2.0 only
    AcceptPathInfo Off
</FilesMatch>
2. Smartyのデリミタを変える

Smartyのデリミタは、次のようにしています。

<?php //色づけ
$smarty->left_delimiter = '<{';
$smarty->right_delimiter = '}>';

html内でこの 「 <{ 」 という書き方は通常出てくることはありえません。なぜなら、文章中で書きたいのならば、 「 &lt;{ 」 と書くべきで、htmlタグとしては、< の次には、htmlタグとして有効なアルファベットか、htmlコメント用の ! しか出てくることはありえません。


これは、以前こちらの記事でも書いたことがありました。
「Smartyのデリミタとescape - maru.cc@はてな」

3. Smartyのデリミタの種類増やす

2の「 <{ 」この形式のデリミタを使用した場合の弊害と出てくるのは、htmlタグ内の属性値にSmartyタグを入れたい場合に、htmlとしておかしな状態になってしまいます。

例)
<a href="sample.html?id=<{$id}>">link</a>
<input type="text" name="sample" value="<{$form.sample}>" />


これを回避するために、Smartyのデリミタの種類を増やしてみました。正確にはデリミタとして認識される文字列を増やしてみました。また、htmlタグ内で使うためにurlencodeをした文字列を使うようにしてみます。
今回採用した方法としては、次のようなルールです。
デリミタとして次の文字を追加

  • %3C%7B
  • %7D%3E

それぞれ 「 <{ 」 「 }> 」 を urlencodeしたものです。また、このタグ内は urldecodeされるようにしました。
実際に記述する場合には次のようになります。

例)
<a href="sample.html?id=%3C%7B%24id%7D%3E">link</a>
<input type="text" name="sample" value="%3C%7B%24form.sample%7D%3E" />

「$」は、urldecodeしてもそのままなので、次のように書いてもOK。

例)
<a href="sample.html?id=%3C%7B$id%7D%3E">link</a>
<input type="text" name="sample" value="%3C%7B$form.sample%7D%3E" />


これを有効にするには、以下のような Smartyのプリフィルタプラグインを使用します。

<?php
/**
 * Smartyプリフィルタプラグイン
 */
/**
 * タグ内の属性値等にSmartyタグを入れるための拡張
 *
 * <pre>
 * SmartyのデリミタのURLエンコード値の範囲内をurldecodeする
 * ex.)
 * left_delimiter = '<{'
 * right_delimiter = '}>'
 * <input type="text" name="sample" value="%3C%7B%24form.sample%7D%3E" />
 * ↓
 * <input type="text" name="sample" value="<{$form.sample}>" />
 * </pre>
 */
function smarty_prefilter_delimiter_urldecode($source, &$smarty)
{
    $pattern = sprintf('!(%s.*%s)!msueU'
        , preg_quote(urlencode($smarty->left_delimiter), '!')
        , preg_quote(urlencode($smarty->right_delimiter), '!')
        );
    $buf = preg_replace($pattern, "urldecode('\$1')", $source);
    return $buf;
}


このファイルを Smartyプラグインディレクトリとして登録したディレクトリ内に 「prefilter.delimiter_urldecode.php」というファイル名で保存し、Smartyの読み込みプラグインに登録します。

<?php //色づけ
$smarty->autoload_filters['pre'][] = 'delimiter_urldecode';


とりあえず、今回はこの程度で済ませましたが、if や foreach をhtmlコメント形式にしたデリミタ形式を追加するのもありだと思います。

4. http<->httpsに絶対パスで書かなくても動くようにする

htmlを作るうえで、環境依存になりがちなのが、http領域とSSL領域を行き来するリンクです。URLに http または https から書かねばならず、絶対パスになってしまうので環境依存になります。こちらも Smartyのプリフィルタを使用して置換処理をしてしまいます。
私の会社ではドキュメントルートは次のような名前を使用しています。

  • htdocs   <-http領域のドキュメントルート
  • htdocs.ssl <- https領域のドキュメントルート

こちらをそれぞれ相対パスで書いてもらって置換する仕組みです。

例)
http://example.com/index.html -> https://example.com/form.html へのリンク
<a href="../htdocs.ssl/form.html">リンク</a>

https://example.com/sample1/page1.html -> http://example.com/sample2/page2.html へのリンク
<a href="../../htdocs/sample2/page2.html">リンク</a>


phpのコードは割愛しますが、上記のデリミタ置換と同様の形式です。実際に使用しているプリフィルタでは、htmlのタグと属性まで識別し、単なる文字列置換ではなく、aタグなどのタグとhrefなどの属性まで見るようにしています。

5. 部品のincludeを出来るようにする

htmlをデザインする時に、実際にはヘッダーやフッターなどは部品化して作成しているので、それをそのまま使えるようにします。
デザイン時点では部品化し、SSIのインクルードで確認などをして、こちらのシステムへの組み込みはそのSSIのタグのままテンプレートとして使用し、SSIのインクルードタグを Smartyのインクルードタグに置換するプリフィルタを作成しました。

例)
html上の記述
<!--#include virtual="/include/header.html" -->
↓置換後
<{include file="/path/to/include/header.html"}>


Smartyプリフィルタプラグイン

<?php
/**
 * Smartyプリフィルタプラグイン
 */
/**
 * SSI形式のincludeタグを認識させるための拡張
 *
 * <pre>
 * ex.)
 * <!--#include virtual="/include/header.html" -->
 * ↓
 * <{include file="/path/to/include/header.html"}>
 * </pre>
 */
function smarty_prefilter_ssiparts_include($source, &$smarty)
{
    $pattern = sprintf('/<!--#include +virtual *= *"\/([^"]+)" +-->/msuU'
        );
    $path = $_SERVER['DOCUMENT_ROOT'];
    $buf = preg_replace($pattern, "<{include file='{$path}/\$1'}>", $source);
    return $buf;
}


このファイルを Smartyプラグインディレクトリとして登録したディレクトリ内に 「prefilter.ssiparts_include.php」というファイル名で保存し、Smartyの読み込みプラグインに登録します。

<?php //色づけ
$smarty->autoload_filters['pre'][] = 'ssiparts_include';

最後に

これはあくまでも仕組みで吸収できるであろう部分のみなので、実際にはデザイナーさんが SSIで部品を分けてくれるかとか、マージ後の文字列を壊さないかなどの問題はありますし、意識をあわせる必要もあります。
今回、SSIタグ方式などは、元々は独自タグを書いてもらうつもりでしたが、ミーティング時にSSIで部品化するような話が出てきたのでその場で決めた仕様だったりします。
うまくいかない場面も出てくるとは思いますが、随時やり方をアップデートしていきたいと思います。


最近のphp界隈の人たちと話すと、テンプレートエンジンがどうこうという話がよく出てくると思います。いまどきSmartyも無いよねというのもよく聞きます。
確かに、システムバリバリの作りこんだ管理画面に、上記のような仕組みを導入しようとは思いません。そこは開発者が楽になるように作ればいいと思いますが、サイトのユーザ側はどうしてもデザインの要素がとても大きくなります。サイトにもよりますが。
その場合に、デザイナーさんはタグを理解してくれないとか、開発側はデザイン変更時に手をわずらわされたくないとか。。
どちらか一方に負担をかけるやり方ではなく、お互いが少しの努力でより良く楽になるような仕組みにしていければと常々考えたりしています。


で、Smartyの是非ですが、個人的には使い勝手がいいし、Ethnaのデフォルトテンプレートエンジンということで多用しています。
上記のような置換をさせたとして、プリフィルタであれば、Smartyのコンパイル時に1度だけ動くことになりますし。
Smartyの話がでてきた時に気になるのは、このような Smartyプラグインをいろいろそろえてこその Smartyだと思いますので、タグ形式などだけで話すのは少し不毛な気がします。