長男(6歳)に「外でポケモンGOをやったら絶対駄目!危ないからね!」と忠告を受けて、大人としてどう対応すべきか悩んでいます。
さて、前回 【FuelPHP】PresenterのViewをTwigにする | Shimabox Blog を書きました。
PresenterのViewをTwigに置き換えるのは簡単に出来たのですが、一つだけ思わぬ落とし穴があったのでメモしておきます。
アクセサクラスをsetしても表示されない
はい。アクセサクラスって何?って話ですが、簡単に言うと__set()
と__get()
だけのクラスです。自分はAPIとかの結果をオブジェクトに格納する時によく使ったりします。
(プロパティをいちいち定義するのめんどくさいし。。)
そういうクラスをアクセサクラスって勝手に言っています。
そのアクセサクラスを定義してTwigにsetした時に、__get()
が呼ばれずにTwigがNULLしか返さないという現象に出くわしました。
それを解決というか、自分の認識不足だった部分が埋められたので以下に書き連ねていきます。
サンプルのソースは前回のソースを使います。
アクセサクラスを定義する
こんな感じでアクセサクラスを定義したとします。
fuel/app/classes/presenter/twig/parameter.php
<?php /** * Twig Sample * * Viewにsetするパラメータ格納用 */ class Presenter_Twig_Parameter implements \Sanitization { // Sanitization 実装 trait use \Model_TraitSanitization; /** * 動的プロパティ管理配列 * @var array */ protected $vars = array(); /** * コンストラクタ */ public function __construct(){} /** * マジックメソッド__get * * @param string $name プロパティ名 * @return mixed */ public function __get($name) { $value = ''; if (isset($this->vars[$name])){ $value = $this->vars[$name]; } if ($this->_sanitization_enabled) { return Security::htmlentities($value); } return $value; } /** * マジックメソッド__set * * @param string $name プロパティ名 * @param mixed $value 値 */ public function __set($name, $value) { $this->vars[$name] = $value; } }
ポイント
オブジェクトをviewで扱うには、fuel/app/config/config.php
のwhitelisted_classes
に該当クラスを書くか、Sanitizationインターフェイス
を実装しなくてはいけません。
この辺は前に書きました。
それと、なんで$varsに詰めてるの? property_exists()
使わないの?って思うかもしれませんが、
property_exists() 関数は、マジックメソッド __get を使ってアクセスするプロパティを検出することはできません。
だからです。
プレゼンター側
プレゼンター側fuel/app/classes/presenter/twig/sample.php
でこう書きます。
<?php /** * Twig Sample Presenter */ class Presenter_Twig_Sample extends Presenter { /** * レンダリング */ public function view() { // パラメータ $params = new Presenter_Twig_Parameter(); $params->hello = '<strong>Hello</strong>'; $params->world = '<strong>World</strong>'; $this->set('params', $params, false); } }
ビュー側
ビューfuel/app/views/twig/sample.twig
はこうします。
dump()
は前回の記事を参考にしてください。
{% extends "layout.twig" %} {% block content %} <body> <h3>オブジェクトのdump</h3> {{ dump(params) }} <h3>オブジェクトのプロパティ表示</h3> <p>{{ params.hello|e }} {{ params.world|e }}</p> </body> {% endblock %}
確認
では確認してみます。
なんという事でしょう。
dumpでは表示されるのに、肝心のプロパティが表示されていません。
アクセサクラスでデバッグしたところ、__get()
が呼ばれていないようです。
※ Twigではなく、普通のView(.php)にセットしたところきちんと表示されました
これは困った。分からぬ。。
おもむろにphp __get 呼ばれない
でググッたところ、この記事を見つけます。
isset関数とempty関数のときは__issetメソッドが呼ばれているから
これは目から鱗。知らなかったわぁ。なんて思っていたところ、僕はこんなディレクトリを見つけます。
fuel/app/cache/twig
そう、Twigはコンパイル後のテンプレートをキャッシュしているのです。
僕はその中のキャッシュファイルを見ました。
// 一部抜粋 <h3>オブジェクトのプロパティ表示</h3> <p>"; // line 9 echo twig_escape_filter($this->env, $this->getAttribute((isset($context["params"]) ? $context["params"] : null), "hello", array())); echo " "; echo twig_escape_filter($this->env, $this->getAttribute((isset($context["params"]) ? $context["params"] : null), "world", array())); echo "</p>
(isset($context["params"])
で変数の存在見てるんだぁ。へぇ。。って、これやないかい!
そうなんです。Twigは変数の存在をisset()を使って見ていたのです!
解決
そうとなれば、後は改修あるのみです。
先のアクセサクラスでマジックメソッド__isset()
を実装します。
<?php /** * Twig Sample * * Viewにsetするパラメータ格納用 */ class Presenter_Twig_Parameter implements \Sanitization { // Sanitization 実装 trait use \Service_TraitSanitization; /** * 動的プロパティ管理配列 * @var array */ protected $vars = array(); /** * コンストラクタ */ public function __construct(){} /** * マジックメソッド__get * * @param string $name プロパティ名 * @return mixed */ public function __get($name) { $value = ''; if (isset($this->vars[$name])){ $value = $this->vars[$name]; } if ($this->sanitization_enabled) { return Security::htmlentities($value); } return $value; } /** * マジックメソッド__set * * @param string $name プロパティ名 * @param mixed $value 値 */ public function __set($name, $value) { $this->vars[$name] = $value; } /** * マジックメソッド__isset * * @param string $name プロパティ名 * @param boolean */ public function __isset($name) { return isset($this->vars[$name]); } }
再度確認
では再度確認してみます。
なんという事でしょう。
プロパティが見事に表示されています。
他に得た知見
この過程を経て色々知見を得たので展開します。
クロージャは?
はい。その通り。このままではクロージャが使えません。
そんな時は、先のアクセサクラスでマジックメソッド__call()
を実装します。
<?php /** * Twig Sample * * Viewにsetするパラメータ格納用 */ class Presenter_Twig_Parameter implements \Sanitization { // Sanitization 実装 trait use \Service_TraitSanitization; /** * 動的プロパティ管理配列 * @var array */ protected $vars = array(); /** * コンストラクタ */ public function __construct(){} /** * マジックメソッド__get * * @param string $name プロパティ名 * @return mixed */ public function __get($name) { $value = ''; if (isset($this->vars[$name])){ $value = $this->vars[$name]; } if ($this->sanitization_enabled) { return Security::htmlentities($value); } return $value; } /** * マジックメソッド__set * * @param string $name プロパティ名 * @param mixed $value 値 */ public function __set($name, $value) { $this->vars[$name] = $value; } /** * マジックメソッド__isset * * @param string $name プロパティ名 * @param boolean */ public function __isset($name) { return isset($this->vars[$name]); } /** * マジックメソッド__call * * @param string $name 関数名 * @param array $arguments 引数 */ public function __call($name, $arguments) { if (!isset($this->vars[$name])) { return; } return call_user_func_array($this->vars[$name], $arguments); } }
さらに確認の為、プレゼンターとビューを修正します。
fuel/app/classes/presenter/twig/sample.php
を以下の様に修正します。
// これを追加 $params->func = function ($arg1, $arg2) { return 'arg1=' . (string)$arg1 . ', arg2=' . (string)$arg2; }; $this->set('params', $params, false);
ビューfuel/app/views/twig/sample.twig
はこうします。
{% extends "layout.twig" %} {% block content %} <body> <h3>オブジェクトのプロパティ表示</h3> <p>{{ params.hello|e }} {{ params.world|e }}</p> <h3>クロージャの確認</h3> <p>{{ params.func("abc", "efg") |e }}</p> </body> {% endblock %}
再々確認
では、再々確認してみます。
なんという事で(ry
ここから先は必要に応じて
以下にforeach()
, json_encode()
, count()
の対応も書きますが、この辺は必要に応じてでいいと思います。
foreach()は?
はい。その通り。このままではforeach()
してもぐるぐる回りません。
そんな時は、先のアクセサクラスでIteratorAggregate::getIterator
の実装をします。
<?php /** * Twig Sample * * Viewにsetするパラメータ格納用 */ class Presenter_Twig_Parameter implements \Sanitization, \IteratorAggregate { // Sanitization 実装 trait use \Model_TraitSanitization; /** * 動的プロパティ管理配列 * @var array */ protected $vars = array(); /** * コンストラクタ */ public function __construct(){} /** * マジックメソッド__get * * @param string $name プロパティ名 * @return mixed */ public function __get($name) { $value = ''; if (isset($this->vars[$name])){ $value = $this->vars[$name]; } if ($this->_sanitization_enabled) { return Security::htmlentities($value); } return $value; } /** * マジックメソッド__set * * @param string $name プロパティ名 * @param mixed $value 値 */ public function __set($name, $value) { $this->vars[$name] = $value; } /** * マジックメソッド__isset * * @param string $name プロパティ名 * @param boolean */ public function __isset($name) { return isset($this->vars[$name]); } /** * マジックメソッド__call * * @param string $name 関数名 * @param array $arguments 引数 */ public function __call($name, $arguments) { if (!isset($this->vars[$name])) { return; } return call_user_func_array($this->vars[$name], $arguments); } /* |---------------------------------------------------------------- | implements |---------------------------------------------------------------- */ /** * 外部イテレータを取得する * * @return \ArrayIterator */ public function getIterator() { return new \ArrayIterator($this->vars); } }
ビューfuel/app/views/twig/sample.twig
はこうします。
Twigではfor 〜 in
構文がforeachに該当します。
※ for文もfor 〜 in
で書きます
{% extends "layout.twig" %} {% block content %} <body> <h3>オブジェクトのforeach</h3> <ul> {% for key, param in params %} <li>{{ key }} => {{ dump(param) }}</li> {% endfor %} </ul> </body> {% endblock %}
json_encodeは?
はい。その通り。このままではjson_encode()
してもプロパティが列挙されません。
そんな時は、先のアクセサクラスでJsonSerializable::jsonSerialize
の実装をします。
<?php /** * Twig Sample * * Viewにsetするパラメータ格納用 */ class Presenter_Twig_Parameter implements \Sanitization, \IteratorAggregate, \JsonSerializable { // Sanitization 実装 trait use \Model_TraitSanitization; /** * 動的プロパティ管理配列 * @var array */ protected $vars = array(); /** * コンストラクタ */ public function __construct(){} /** * マジックメソッド__get * * @param string $name プロパティ名 * @return mixed */ public function __get($name) { $value = ''; if (isset($this->vars[$name])){ $value = $this->vars[$name]; } if ($this->_sanitization_enabled) { return Security::htmlentities($value); } return $value; } /** * マジックメソッド__set * * @param string $name プロパティ名 * @param mixed $value 値 */ public function __set($name, $value) { $this->vars[$name] = $value; } /** * マジックメソッド__isset * * @param string $name プロパティ名 * @param boolean */ public function __isset($name) { return isset($this->vars[$name]); } /** * マジックメソッド__call * * @param string $name 関数名 * @param array $arguments 引数 */ public function __call($name, $arguments) { if (!isset($this->vars[$name])) { return; } return call_user_func_array($this->vars[$name], $arguments); } /* |---------------------------------------------------------------- | implements |---------------------------------------------------------------- */ /** * 外部イテレータを取得する * * @return \ArrayIterator */ public function getIterator() { return new \ArrayIterator($this->vars); } /** * json_encode() でシリアライズするデータを返します * @return array */ public function jsonSerialize() { return $this->vars; } }
ビューfuel/app/views/twig/sample.twig
はこうします。
Twigではjson_encode
がfilterで使えます。
※ こういうのfilterっていうんですね
{% extends "layout.twig" %} {% block content %} <body> <h3>json_encodeの確認</h3> <p>{{ params|json_encode|e }}</p> </body> {% endblock %}
余談ですが、クロージャは空のオブジェクトとして展開されます。
countは?
はい。その通り。このままではcount()
しても 1 のままです。
そんな時は、先のアクセサクラスでCountable::count
の実装をします。
実装は上の例に習えばすぐ出来ると思いますので割愛します。
Twigではlength
がfilterで使えます。
※ countじゃない!!
{% extends "layout.twig" %} {% block content %} <body> <h3>lengthの確認</h3> <p>{{ params|length }}</p> </body> {% endblock %}
まとめ
というわけで、Twigで陥った罠とその対処方法を書きました。
- viewにsetしたいアクセサクラスは
__set()
,__get()
,__isset()
,__call()
を実装しておくとハマリ辛い- (
IteratorAggregate::getIterator
,JsonSerializable::jsonSerialize
,Countable::count
は必要に応じて実装する)
- (
- コンパイル後のテンプレートを見るとTwigで何をしているのか大体わかる
基本的な部分の方が多かったと思いますが、自分の認識不足だった部分が埋められました。
何か参考になれば幸いです。
他に注意点とかあれば教えてください。
あと、ポケモンGO外でやりたいです!
(でも、絶対事故るよね。。)