読者です 読者をやめる 読者になる 読者になる

k-holyのPHPとか諸々メモ

Webで働くk-holyがPHP(スクリプト言語)とか諸々のことをメモしていきます。ソースコードはだいたいWindowsで動かしてます。

PHPTALテンプレート変数のパス参照でPHPTAL_VariableNotFoundExceptionがスローされる件

PHP PHPTAL

PHPTALにおけるテンプレート変数のパス記述に関するメモです。多分他の人にはあまり役に立たないと思います。

PHPTAL_Context::path() におけるパス参照時の PHPTAL_VariableNotFoundException発生条件

ソースはここ PHPTAL/classes/PHPTAL/Context.php

パスは / 区切りで分割されて終端に達するまで繰り返し処理される。

現在の要素がオブジェクトの場合

  1. オブジェクトがクロージャであれば、実行結果を取得して次の要素へ。 (実行結果がクロージャの場合、さらに実行を繰り返す)

  2. オブジェクトに現在の要素名のメソッドが存在し、呼び出し可能な場合は実行結果を取得して次の要素へ。

  3. オブジェクトに現在の要素名のプロパティが存在すれば、値を取得して次の要素へ。

  4. オブジェクトが ArrayAccess のインスタンスで現在の要素名のキーが有効であれば、値を取得して次の要素へ。

  5. オブジェクトが Countable のインスタンスで現在の要素名が length または size であれば要素を count() した結果を取得して次の要素へ。

  6. オブジェクトに __isset() メソッドが存在し、現在の要素名で __isset() が有効であれば、現在の要素名で値を取得して次の要素へ。

  7. オブジェクトに __get() メソッドが存在し、現在の要素名で参照した値がNULLでなければ、その値を取得して次の要素へ。

  8. オブジェクトに __call() メソッドが存在すれば、現在の要素名のメソッドで実行を試み、BadMethodCallException がスローされなければ、実行結果を取得して次の要素へ。

  9. 実行中の path() メソッドの第3引数 $nothrow が TRUE であれば(初期値は FALSE )NULLを返す。

  10. 上記の流れで次の要素に処理が移っていない場合 PHPTAL_Context::patherror() が実行され PHPTAL_VariableNotFoundException がスローされる。

現在の要素が配列の場合

  1. 現在の要素名のキーが存在すれば、値を取得して次の要素へ。

  2. 現在の要素名が length または size であれば要素を count() した結果を取得して次の要素へ。

  3. 実行中の path() メソッドの第3引数 $nothrow が TRUE であれば(初期値は FALSE)NULLを返す。

  4. 上記の流れで次の要素に処理が移っていない場合 PHPTAL_Context::patherror() が実行され PHPTAL_VariableNotFoundException がスローされる。

現在の要素が文字列の場合

  1. 現在の要素名が length または size であれば要素を strlen() した結果を取得して次の要素へ。

  2. 現在の要素名が数値(is_numeric() が真)の場合、現在の要素からその位置の文字を取得して次の要素へ。

これまでの流れで次の要素に処理が移っていない場合

  1. 実行中の path() メソッドの第3引数 $nothrow が TRUE であれば(初期値は FALSE)NULLを返す。

  2. PHPTAL_Context::patherror() が実行され PHPTAL_VariableNotFoundException がスローされる。

パスの要素を全て処理し終わったら、最終的に取得した値を返す。

なお、頻出する path() メソッドの第3引数 $nothrow についてはPHPDOCコメントにこう書かれています。

$nothrow is used by phptal_exists(). Prevents this function from throwing an exception when a part of the path cannot be resolved, null is returned instead.

コンパイル済みテンプレートファイルを検索してみたところ、後述するdefaultキーワードをパスに付与した場合、このフラグにTRUEが指定されるようでした。

isset() と __isset() と offsetExist() の実装内容によって PHPTAL_VariableNotFoundException がスローされるケース

たとえばテンプレート変数のパスをこう指定したとする。

<p tal:content="object/name"></p>
  1. $object->__isset('name') がFALSEを返した

  2. $object->__get('name') がNULLを返した

  3. $object->__call() が実装されていない、または $object->__call('name', array()) を実行した結果 BadMethodCallException がスローされた

これらの条件に該当する場合に PHPTAL_VariableNotFoundException がスローされる。

配列の場合は array_key_exists('name', $array) がTRUEを返していれば、値がNULLでも例外はスローされない。

これに対して、オブジェクトでプロパティ値がNULLの場合に __isset() がFALSEを返すような実装(isset()関数と同じ動作)をしていると、例外がスローされてしまう。

ArrayAccess を実装したオブジェクトで $object->offsetExists('name') がFALSEを返した場合も同様に例外がスローされる。

以前にもこれに似たような問題にはまって、こういう記事を書きました。

事ここに至って痛感したのは、property_exists($object, 'name') && $object->name !== nullと同じ意味で isset($object->name) と書くのをやめろということです。

ArrayAccess::offsetExists()__isset() は、そういう前提で実装しなくてはいけない。

<?php
/**
 * 悪い __isset()
 * @param mixed
 * @return bool
 */
public function __isset($name)
{
    return (property_exists($this, $name) && $this->{$name} !== null);
}

↑こう書いてはダメ!!

<?php
/**
 * 良い __isset()
 * @param mixed
 * @return bool
 */
public function __isset($name)
{
    return property_exists($this, $name);
}

↑こう書く。

これがどう影響するかというと、ユニットテストのコードを見ると一目瞭然です。

<?php
public function testIsset()
{
    $test = new EntityTraitTestData(array(
        'string' => 'Foo',
        'null'   => null,
    ));
    $this->assertTrue(isset($test->string));
    $this->assertTrue(isset($test->null)); // !! CAUTION !!
    $this->assertFalse(isset($test->undefined_property));
}

isset($object) はいいとして isset($object->name)isset($array['name']) という書き方は害でしかなくなってしまうわけで、Notice: Undefined index を親の仇のように憎悪して isset() を使いまくってきた自分にとって非常に困難を伴いますが…。

どうしても isset() を使いたい場合は isset($object->name) && $object->name !== null あるいは isset($array['name']) && $array['name'] !== null と書くしかありません。

PHPTALテンプレートで何とか対処する場合

オブジェクトの利用側コードで isset($objact->name) してて、むしろ出力時に対応した方が良さそうな場合の対応策。

defaultキーワード

PHPTALES の default キーワードを付けることで、値がNULLまたは PHPTAL_VariableNotFoundException がスローされた場合の代替値として、要素の中身(この場合は空文字)が出力されます。

<p tal:content="object/name|default"></p>

exists:式

代替値ではなく値の出力をスルーしたい、そしてより丁寧に書くのであれば PHPTALES の exists:式が使えます。

<p><span tal:condition="exists:object/name" tal:replace="object/name"></span></p>

普通にtal:condition

もちろん $object->hasName() みたいなメソッドが実装されているのであれば、こう書けます。

<p><span tal:condition="object/hasName" tal:replace="object/name"></span></p>

これは良くない例ですが、PHPTALの入力フォーム処理には、フォームオブジェクトみたいな物を導入して、入力値、バリデーション結果、エラーメッセージ等を項目単位でまとめて扱えるようにした方がシンプルに書けますね。

テンプレートの書きやすさだけでなく、一つのフォームの編集対象は一つのエンティティに限らないこと、エンティティの属性とフォームで扱う値の型は必ずしも一致しないこともありますし、何かしらレイヤ間のギャップを吸収する仕組みが必要になります。

具体的な実装については、まだ自分でも納得のいく物は書けてませんが…。