【FuelPHP】Twigにアクセサクラスをsetしたら表示されなくてはまった

投稿日:

長男(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.phpwhitelisted_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外でやりたいです!
(でも、絶対事故るよね。。)

参考にさせて頂きました

作成者: shimabox

Web系のプログラマをやっています。 なるべく楽しく生きていきたい。

コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください