html属性指定の罠

htmlタグ出力ヘルパー関数の属性指定引数で、トラブルの元になりそうな挙動を見つけました。

■おさらい

symfonyでは、htmlタグ出力を助けるために様々なヘルパー関数が用意され、ビューのテンプレート内で使うことができます。 ほとんどのhtmlタグ出力ヘルパー関数は、属性(attribute)を引数によって制御できるようになっています。

たとえば、最も基本的なhtmlタグ出力ヘルパー関数として、tag 関数があります。

tag($name, $options = array(), $open = false)

tag 関数では、第二引数 $options によって属性を指定できるようになっています。 $options の初期値は空の配列になっていますが、$options には配列だけではなく文字列も入れることができます。 以下の例を見ればわかるように、同じhtmlタグを得られます。

echo tag('hoge', array('attr_name1'=>'attr_val1','attr_name2'=>'attr_val2'));
echo tag('hoge', 'attr_name1=attr_val1 attr_name2=attr_val2');
// output_both: <hoge attr_name1="attr_val1" attr_name2="attr_val2" />

これからは、説明がわかりやすいように、配列での属性指定を「配列型指定」、文字列での属性指定を「文字型指定」と呼ぶことにします。

//配列型指定:
 array('attr_name1'=>'attr_val1','attr_name2'=>'attr_val2')
 
//文字列型指定:
 'attr_name1=attr_val1 attr_name2=attr_val2'

■トラブル内容

さて、ここに Yes! と描かれた fuga.png という画像があります。 これを image_tag 関数を使ってimgタグで表示させます。属性にはtitleを設定してみましょう。 image_tag 関数の属性指定をする引数は、二番目になります。 (title は alt 以外の適当な属性だったというだけで、とくに深い理由はありません。)

echo image_tag('fuga.png', array('title'=>'yes'));
// output: <img title="yes" src="/images/fuga.png" alt="Fuga" />
 
echo image_tag('fuga.png', 'title=yes');
// output: <img title="1" src="/images/fuga.png" alt="Fuga" />

どちらも同じように title 属性に yes を指定しているだけなのに、出力結果が違います。 配列型指定は、title属性が yes にちゃんとなっています。 しかし、文字列型指定では yes と指定したはずの属性値が 1 となってしまいました。

こうなってしまうのは、関数に文字列型指定で渡された際、配列型に変換をしているからです。

Tag ヘルパー内の _parse_attributes 関数が変換処理をしているのですが、 コードを追っていくと、文字列型指定には sfToolkit::literalize というメソッドが適用されています。

sfToolkit::literalize の中では、属性値に特定の文字列が入ってくるとデータ型を変えるような変換処理を行います。 yes はその特定の文字列にあたるため、出力時に値が変わってしまったのです。

'null', '~', ''NULL(つまり 空文字)
 
'true', 'on', '+', 'yes'
→ boolean true(つまり 1)
 
'false', 'off', '-', 'no'
→ boolean false(つまり 空文字)
 
ctype_digit()trueとなる文字列
→ intに型キャスト
 例: '123465'(int) 123465
 
is_numeric()trueとなる文字列
→ floatに型キャスト
 例: '7E55'(float) 7.0E+55

is_numericはtrueなる範囲が思ったよりも広いので、注意しましょう。 ディノオープンラボラトリで hnw がこの件を記事にしていますので、参考にしてください。

ディノオープンラボラトリ: そのis_numeric()は適切ですか?

■回避方法

これらの文字列を属性値にとりたい場合にどうしたらよいか考えたところ、 3つ思いつきましたので参考にしてください。ただし、下に行くほど見栄えが悪くなり、お勧め度が下がります。

  1. 配列型指定で書く 文字列型指定にすると問答無用で変換されてしまうので、変換されないように配列型指定にします。

  2. 文字列型指定 + 配列型指定で書く もし、どうしても一部属性を文字列指定したいのであれば、以下のように、変換したくない属性値だけ配列で書きます。 文字列指定部分は、_parse_attributes を使って配列に変換します。 (下記は + で配列をマージしていますが、属性が被った場合上書きされるので注意してください。)

  3. 実体参照で書く 文字列型指定しか使いたくないのであれば、属性値の部分を文字実体参照で書けば変換されてしまうことはありません。 htmlソースを見たときに何が書いてあるのか、すぐにはわからないのが難点です。

//回避方法 2
<?php echo tag('fuga', array('not_converted'=>'true')+ _parse_attributes('converted=true')) ?>
// output: <fuga not_converted="true" converted="1" />
#実体参照変換表
null&#110;&#117;&#108;&#108;
~(チルダ)&#126;
(空文字)(そのまま)
true&#116;&#114;&#117;&#101;
false&#102;&#97;&#108;&#115;&#101;
on → &#111;&#110;
off → &#111;&#102;&#102;
yes → &#121;&#101;&#115;
no → &#110;&#111;
+&#43;
-&#45;
//回避方法 3
<?php echo tag('fuga', 'not_converted=&#121;&#101;&#115; converted=yes') ?>
// output: <fuga not_converted="&#121;&#101;&#115;" converted="1" />

■変換された値を利用している例

textarea_tagの入力にWYSIWYGエディタを使いたい場合、rich属性(正確にいうと属性ではないが)を指定するように The Definitive Guide to symfony の Chapter 10 – Forms Listing 10-3 – Input Date Helpers で書かれています。

ここで ‘rich=true’ と文字列型指定で書かれているのは、書くのが簡単というわけではなく、ちゃんと理由があります。 textarea_tag 関数へは richに対応する属性値に boolean型の true が来るように書かれているからです。

#textarea_tag 抜粋
if (true === $rich)
{
  $rich = sfConfig::get('sf_rich_text_editor_class', 'TinyMCE');
}

‘rich=true’であれば、TinyMCEを使うというコード部分です。 具体的には sfRichTextEditor$rich、 つまり sfRichTextEditorTinyMCE クラスを読み込んでいろいろ処理します。

echo textarea_tag('foo', null, 'rich=true');
// sfRichTextEditorTinyMCE 呼び出し → OK

配列型指定で書いてしまうと、$rich が true 文字列になってしまうので sfRichTextEditortrue という 存在しないクラスを読み込み、エラーになってしまいます。

echo textarea_tag('bar', null, array('rich'=>'true')); 
// sfRichTextEditortrue 呼び出し → エラー

なので、’rich=true’ と文字列型指定しなければならないのです。

■おわり

属性値に boolean の true/false を返してもらってもあまり意味が無いために、一見この仕様はバグのような気がします。 しかし、textarea_tag 関数をみるかぎり、どうやらこの仕様で正しいようです。

Leave a Reply

You must be logged in to post a comment.