Smartyとは
PHPのテンプレートエンジンです。
Smarty 2.1.0のリリースは2002年4月、まだPHP 4.1のころです。とても長い歴史があります。

Smartyが使われていたら、運用歴の長いアプリケーションかもしれないわね。

いまだとLaravel標準のBladeのほうが人気なのかな

だいぶ昔2008年に「Smarty動的サイト構築入門」っていう本を共著で書いたって。
https://www.ninton.co.jp/archives/98
ステップ1:PHP側:Smarty3のSmartyBCクラス
composer未導入。
Smartyの最新バージョンは、3.1.36 です。
PHP 5.2や PHP 5.3の場合は、3.1.34または3.1.29を選んでください。
確認中は、頻繁に templates_c/* を削除しましょう。gitでブランチを切り替えながら作業していると、もう一方のブランチでコンパイルしたファイルを返してしまい、なかなか問題に気づけないことがあります。
SmartyBCクラス
Smarty3
のSmarty
クラスは、Smarty2
のSmarty
クラスと互換性がありません。その代わり、Smarty3
にはSmartyBCクラスが用意されています。BCは、Backward Compatibilityの略で、後方互換性という意味です。
Smarty2 | Smarty3 | 互換性 |
---|---|---|
Smartyクラス | Smartyクラス | なし |
Smartyクラス | SmartyBCクラス | あり |
修正前のコード
require_once 'xxx/yyy/smarty-2/Smarty.class.php';
class MySmarty extends Smarty
{
public function __construct()
{
parent::__construct();
省略
}
}
Code language: PHP (php)
修正後のコード
まず、Smarty.class.phpではなく、SmaryBC.class.phpを require_once します。
次に、Smartyクラスではなく、SmartyBCクラスをextendsします。
require_once 'xxx/yyy/smarty-3/libs/SmartyBC.class.php';
class MySmarty extends SmartyBC
{
public function __construct()
{
parent::__construct();
省略
}
}
Code language: PHP (php)

互換性のある SmartyBCクラスがあって、ほっとしたよ
plugins_dir
次に、自作プラグインがある場合です。
smarty-2/plugins/の下に自作プラグインを置いている場合は、自作プラグインファイルだけを smarty-2/ の外に移動します。標準のプラグインファイルは元の場所に残しておきます。
移動先は、configs/ や templates/ と同列の plugins/ がわかりやすいでしょう。
例:
app/
smarty/configs/
smarty/plugins/
smarty/templates/
修正前
自作プラグイン用ディレクトリだけを指定すればよかったのですが、
require_once 'xxx/yyy/smarty-3/libs/SmartyBC.class.php';
class MySmarty extends SmartyBC
{
public function __construct()
{
parent::__construct();
$this->template_dir = '/xxx/yyy/app/smarty/templates/';
$this->config_dir = '/xxx/yyy/app/smarty/configs/';
$this->plugins_dir[] = '/xxx/yyy/app/smarty/plugins/'; // ★コレです
省略
}
}
Code language: PHP (php)
修正後
Smartyディレクトリ下の plugins/ 、自作プラグイン用ディレクトリの2つとも指定する必要があります。どちらも絶対パスで指定してください。
require_once 'xxx/yyy/smarty-3/libs/SmartyBC.class.php';
class MySmarty extends SmartyBC
{
public function __construct()
{
parent::__construct();
$this->template_dir = '/xxx/yyy/app/smarty/templates/';
$this->config_dir = '/xxx/yyy/app/smarty/configs/';
$plugins_dir = $this->plugins_dir;
$plugins_dir[] = '/xxx/yyy/smarty-3/libs/plugins/'; // ★コレと
$plugins_dir[] = '/xxx/yyy/app/smarty/plugins/'; // ★コレです
$this->plugins_dir = $plugins_dir;
省略
}
}
Code language: PHP (php)
次の書き方をしても、Smarty
のplugins_dir
には反映されません。
$this->plugins_dir[] = '/xxx/yyy/smarty-3/libs/plugins/';
$this->plugins_dir[] = '/xxx/yyy/app/smarty/plugins/';
Code language: PHP (php)
Notice
表示で、そのようなことを言われます。
Notice: Indirect modification of overloaded property Smarty::$plugins_dir has no effect

has no effect
は効果がないってことだよね、なぜなの?

plugins_dir
プロパティは、protected
だから外部から見えないのよ

でも$this->plugins_dir
でアクセスできるよ?

マジックメソッド __get()
が、plugins_dir
のコピーを返しているのよ
さきほどのコードは、擬似的に書くと、次のような処理をしています。
$tmp1 = $this->__get('plugins_dir');
$tmp1[] = '/xxx/yyy/smarty-3/libs/plugins/';
$tmp2 = $this->__get('plugins_dir');
$tmp2[] = '/xxx/yyy/app/smarty/plugins/';
Code language: PHP (php)

これならわかるよ、コピーした配列に追加してるだけで、コピー元のplugins_dir
プロパティは変化しないんだね。
ステップ2:テンプレート側
いくつか変更する必要があります。
配列のダブルクォートを削除する
Smarty3では、配列をダブルクォートすると、1になってしまいました。ダブルクォートをしないで渡します。
修正前
{html_options options="$my_options"}
Code language: plaintext (plaintext)
修正後
{html_options options=$my_options}
Code language: plaintext (plaintext)
変数とパイプの間のスペースを削除する
読みやすいように、スペースで パイプ|
の位置をそろえていることがありました。
変数とパイプの間にスペースがあると、エラーになってしまうので、スペースを削除します。
修正前
{$rcd.a |escape:html}
{$rcd.bb |escape:html}
{$rcd.ccc|escape:html}
Code language: plaintext (plaintext)
修正後
{$rcd.a|escape:html}
{$rcd.bb|escape:html}
{$rcd.ccc|escape:html}
Code language: plaintext (plaintext)
変数名内で、バッククォートで変数を展開しない
PHPからテンプレートへ、
$img0_href、$img0_src、$img0_alt
$img1_href、$img1_src、$img1_alt
を渡しています。
$img0_xxx、$img1_xxxとで、共通のサブテンプレートを使うために、次のような実装をしていました。
修正前
page.tpl
{include file=element.tpl n="0"}
{include file=element.tpl n="1"}
Code language: plaintext (plaintext)
element.tpl
<a href=""{$img`$n`_href}""><img src="{$img`$n`_src}" alt="{$img`$n`_src}"></a>
Code language: plaintext (plaintext)

$nの部分は配列にしたほうが自然ね
さて、引数として、n="0"を設定した場合は、
<a href="{$img0_href}"><img src="{$img0_src}" alt="{$img0_alt}"></a>
Code language: plaintext (plaintext)
引数として、n="1"を設定した場合は、
<a href="{$img1_href}"><img src="{$img1_src}" alt="{$img1_alt}"></a>
Code language: plaintext (plaintext)
と同じ意味となります。
しかし、Smarty3では、エラーでした。

$img0_hrefや$img0_srcをサブテンプレートに渡していないけど、いいの?

PHPからテンプレートに渡した変数は、テンプレート内ではグローバルスコープだから、どこからでもアクセスできるのよ
修正後
PHPからテンプレートへ渡すとき、$img[0]['href']、$img[0]['src']、$img[0]['alt']、
$img[1]['href']、$img[1]['src']、$img[1]['alt']、
変数をこのような構造にすれば、テンプレート側は
page.tpl(変更なし)
{include file=element.tpl n="0"}
{include file=element.tpl n="1"}
Code language: plaintext (plaintext)
element.tpl
<a href=""{$img[$n].href}""><img src="{$img[$n].src}" alt="{$img[$n].src}"></a>
Code language: plaintext (plaintext)
自然に書くことができます。
PHP側は変更したくない、テンプレート側だけで対応したい場合は、
page.tpl
{include file=element.tpl img_href=$img0_href img_src=$img0_src img_alt=$img0_alt}
{include file=element.tpl img_href=$img1_href img_src=$img1_src img_alt=$img1_alt}
Code language: plaintext (plaintext)
element.tpl
<a href="{$img_href}"><img src="{$img_src}" alt="{$img_alt}"></a>
Code language: plaintext (plaintext)
ステップ3:PHP側:SmartyBCクラスからSmartyクラスへ
Smarty2のプロパティは、Smarty3ではメソッド化されました。対応するメソッドで置き換えます。
Smarty2のメソッド名は、Smarty3では、キャメルケース(小文字始まり)にリネームされました。対応するメソッドで置き換えます。
多くの箇所で使われていそうな、assign
メソッド、display
メソッドは、同じままです。置換作業は予想よりは大変ではありません。
例えば、PhpStormなら、[Replace in Path...
]で config_load
を検索し、置換文字列に configLoad
を入力しておきます。検索結果の一覧を確認しながら、[Replace]ボタンをクリックして、一つづつ置換していきます。
Smarty2のプロパティ | Smarty3のメソッド set系 | Smarty3のメソッド get系 | Smarty3のメソッド add系 |
---|---|---|---|
config_dir | setConfigDir | getConfigDir | addConfigDir |
template_dir | setTemplateDir | getTemplateDir | addTemplateDir |
plugins_dir | setPluginsDir | getPluginsDir | addPluginsDir |
compile_dir | setCompileDir | getCompileDir | |
cache_dir | setCacheDir | getCacheDir | |
caching | setCaching |
Smarty2のメソッド | Smarty3のメソッド | |
---|---|---|
assign | assign | 同じ |
display | display | 同じ |
config_load | configLoad | |
get_config_vars | getConfigVars | |
get_template_vars | getTemplateVars | |
register_function | registerPlugin | 第1引数に "function" |
register_modifier | registerPlugin | 第1引数に "modifier" |
require_once 'xxx/yyy/smarty-3/libs/Smarty.class.php';
class MySmarty extends Smarty
{
public function __construct()
{
parent::__construct();
$this->setTemplateDir('/xxx/yyy/app/smarty/templates/');
$this->setConfigDir('/xxx/yyy/app/smarty/configs/');
$this->addPluginsDir('/xxx/yyy/app/smarty/plugins/');
省略
}
}
Code language: PHP (php)
番外編:PHP側:プラグイン関数にnamespaceを導入するには
独自のSmartyプラグインがあり、SimpleTestでテストケースを作りました。
<?php
namespace Ninton\Tests;
require_once __DIR__ . '/../vendor/simpletest/simpletest/autorun.php';
require_once __DIR__ . '/../app/smarty/plugins/function.hoge.php';
class SmartyPluginTest
{
public function testHoge()
{
// 省略
$actual = smarty_function_hoge($params, $smarty);
// 省略
}
}
Code language: PHP (php)
これを phpcs
にかけると、SideEffect
の警告が表示されました。
----------------------------------------------------------------------
FOUND 0 ERRORS AND 1 WARNING AFFECTING 1 LINE
----------------------------------------------------------------------
1 | WARNING | A file should declare new symbols (classes, functions,
| | constants, etc.) and cause no other side effects, or
| | it should execute logic with side effects, but should
| | not do both. The first symbol is defined on line 9 and
| | the first side effect is on line 5.
----------------------------------------------------------------------
Code language: plaintext (plaintext)
SideEffect
は「クラスや関数の定義」と「それらを実行するコード」は同じファイルに書かないほうがいい、というプラクティスです。
例えば、MyApp.phpのなかで、class MyAppの定義と、インスタンス化やechoなどの実行をするのではなく、
<?php
// MyApp.php
class MyApp
{
public function main()
{
}
}
$myApp = new MyApp();
echo "Hello";
Code language: PHP (php)
インスタンス化やechoなどの実行は、別ファイルに分けたほうがいい、というものです。
<?php
// MyApp.php
class MyApp
{
public function main()
{
}
}
Code language: PHP (php)
<?php
// main.php
$myApp = new MyApp();
echo "Hello";
Code language: PHP (php)
require_once
も「コードの実行」として扱われるので、同じファイルでrequire_onceとクラス定義をすると、このSideEffect警告が表示されるんですね。
phpcsのSideEffect無視する
妥当な理由がある場合は、無視しましょう。
ファイル先頭に次の1行をいれておくと、SideEffect
の警告はでなくなります。
<?php
// phpcs:disable PSR1.Files.SideEffects
Code language: HTML, XML (xml)
プラグイン関数をクラス化して、registerPluginsで明示的に登録にする
コードのリファクタで、今回のSideEffect
を解決できるでしょうか。
つまり、2つのrequire_onceをなくすことができるしょうか。
まず、autorun.phpのrequire_onceをなくすことはできました。
修正前は、MyTest.php単体で実行できていましたが、修正後は、autorun.phpを介して実行するようにしました。
これぐらいなら、大きな代償ではありません。
# 修正前
$ php MyTest.php
# 修正後
$ php autorun.php MyTest.php
Code language: PHP (php)
次に、独自のプラグイン function.hoge.php のrequire_onceです。
関数のままでは、autoloadできません。autoloadはクラスが対象だからです。つまり、関数のままでは、どこかでrequire_onceする必要があるので、require_onceをなくすことができません。
そこでプラグイン関数をクラスにして、namespaceを設定しました。SmartyFunctionHogeクラスのsmarty_function_hogeメソッドです。
すると、MyTest.phpでuseを使えるようになり、require_onceをなくすことができました。
しかし、Smartyのプラグイン自動読み込みで、該当するプラグイン関数がないエラーになってしまいました。それはそうですね、グローバル空間のsmarty_function_hoge関数はなくなってしまったので。
そこで、plugins_dirの自動読み込みはやめて、AppSmartyクラス内で、registerPluginsメソッドで、明示的にプラグインを登録することにしました。
SmartyFunctionHogeクラスのsmarty_function_hogeメソッドは、その名前である必要がなくなり、SmartyFunctionHogeクラスのhogeメソッドにしました。

結構たいへんだね。修正する価値はあるの?

今回のケースは、phpcsのSideEffectを無視するのが一番ね