Smarty2をSmarty3に移行するには

Smartyとは

PHP Template Engine | Smarty
Smarty is a template engine for PHP.

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クラス

Smarty3Smartyクラスは、Smarty2Smartyクラスと互換性がありません。その代わり、Smarty3にはSmartyBCクラスが用意されています。BCは、Backward Compatibilityの略で、後方互換性という意味です。

Smarty2Smarty3互換性
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)

次の書き方をしても、Smartyplugins_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_dirsetConfigDirgetConfigDiraddConfigDir
template_dirsetTemplateDirgetTemplateDiraddTemplateDir
plugins_dirsetPluginsDirgetPluginsDiraddPluginsDir
compile_dirsetCompileDirgetCompileDir
cache_dirsetCacheDirgetCacheDir
cachingsetCaching
Smarty2のメソッドSmarty3のメソッド
assignassign同じ
displaydisplay同じ
config_loadconfigLoad
get_config_varsgetConfigVars
get_template_varsgetTemplateVars
register_functionregisterPlugin第1引数に "function"
register_modifierregisterPlugin第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.SideEffectsCode language: HTML, XML (xml)

プラグイン関数をクラス化して、registerPluginsで明示的に登録にする

コードのリファクタで、今回のSideEffectを解決できるでしょうか。

つまり、2つのrequire_onceをなくすことができるしょうか。

まず、autorun.phpのrequire_onceをなくすことはできました。

修正前は、MyTest.php単体で実行できていましたが、修正後は、autorun.phpを介して実行するようにしました。

これぐらいなら、大きな代償ではありません。

# 修正前
$ php MyTest.php

# 修正後
$ php autorun.php MyTest.phpCode 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を無視するのが一番ね

参考記事

[PHP]Smarty3.1にバージョンアップして困ったこと その3
ウチではPHPのテンプレートライブラリ(テンプレートエンジン)であるSmartyを使ってるんですが、昨年最新版の3.1系にバージョンアップしたら色々困ったことが起きたのでその記録の続きです。
タイトルとURLをコピーしました