Hatena::ブログ(Diary)

do_akiの徒然想記 RSSフィード

2013-12-18

PHP 文字列リテラルにおける変数展開ノ全テ

はじめに

本記事は、PHP Advent Calendar 2013 18日目です。

前日は @oasynnoum さんの Just Another チラシの裏: Raspberry Pi と PHP で遊ぼう でした。

ラズパイ x php ってあまり見ない組み合わせだったので、なかなかに新鮮でした。


さて、まず初めに大事な前提を。

この記事に書かれている内容は php-5.5.7 のコードをベースにしています。それ以前のバージョンでもだいたい同様に動くと思いますが、今後のバージョンで同じように動作するかどうかは分かりません。



世にも奇妙な php

早速ですが問題です。

code.1 は、何が出力されるでしょうか?

code.1
<?php

echo "${" . " . " . "}";

ぱっと見では "${ . }" が出力されそうに見えるこのコード、正解は「何も出力されない」です。

(環境によっては Notice が発生しますが。)



では次、 code.2 において、関数 echo_yield_key の戻り値は何でしょう?

code.2
<?php

function echo_yield_key(array $_) {
   echo "${_[yield]}";
}

$what_is_this = echo_yield_key(['yield' => '']);

php は、関数に return が無い場合、その戻り値は NULL になります。ですので、普通に考えると $what_is_this は NULL になるように思えます。

が、正解は Generator。

とてもとても不思議な挙動ではありますが、echo_yield_key の呼び出しによって、 Generator が生成されます。



これらの奇妙な挙動ですが、 php の変数展開をきちんと知れば納得……するかどうかは別として、きちんと説明できます。

本記事では、PHP の文字列リテラルにおける変数展開について解説します。

3種の変数展開

マニュアルによれば、文字列における変数の展開方法は大きく分けて2つ。単純な構文と複雑な構文があります。(http://www.php.net/manual/ja/language.types.string.php#language.types.string.parsing)

しかしここでは、複雑な構文については、より細かく分類し、以下の3種類にわけて説明していきます。

  1. 単純な構文
  2. 複雑な構文1 ({$variable} タイプ)
  3. 複雑な構文2 (${expr} タイプ)


単純な構文

単純な といいつつ、実はあまり単純ではありません。単純な構文の変数しか解釈することができないというだけです。

この方式では以下のルールによって変数展開されます。


  1. $ に続く LABEL *1 を変数名として採用します。 *2
  2. [] による 配列(あるいは文字列)の要素指定は可能ですが、添字を引用符で囲うことは''できません''。また、多次元配列も指定できません。 *3
  3. プロパティにアクセス可能ですが、プロパティの配列要素やプロパティのプロパティは指定できません。 *4

code.3 に例を挙げました。

code.3
<?php

/*** 1 ***/
$hoge = 'HOGE';
$fuga = 'FUGA';
echo "$hoge is not $fuga!" . PHP_EOL;  // HOGE is not FUGA!


/*** 2 ***/
$a = [
  0      => 'ZERO',
  'hoge' => 'HOGE',
  99     => ['ARRAY'],
];

echo "$a[0]" . PHP_EOL;       // ZERO
echo "$a[hoge]" . PHP_EOL;    // HOGE (ここでは、ベアワードを使うことに対する Notice は発生しません)
echo "$a['hoge']" . PHP_EOL;  // syntax error!
echo "$a[99][0]" . PHP_EOL;   // Array[0]  ($a[99] が評価されて Notice:  Array to string conversion が発生。その後に続く [0] は素の文字列としてそのまま出力されます)


/*** 3 ***/
$b = new stdClass();
$b->prop = 'property';
$b->self = &$b;

echo "$b->prop" . PHP_EOL;       // property
echo "$b->prop[0]" . PHP_EOL;    // property[0] ($b->prop までが評価されて、[0] は素の文字列としてそのまま出力されます)
echo "$b->self->prop" . PHP_EOL; // (Catchable fatal error:  Object of class stdClass could not be converted to string が発生)


複雑な構文1 ({$variable} タイプ)

基本的に、 通常の PHP 構文の中で $ から始まる変数参照はすべて使えます。

また、単純な構文と異なり、配列の要素指定では引用符が利用でき、ベアワードを使うと通常通り Notice が発生します (code.4)

code.4
<?php

$a = array(
  0      => 'ZERO',
  'hoge' => 'HOGE',
  99     => ['ARRAY'],
);

echo "{$a[0]}" . PHP_EOL;       // zero
echo "{$a[hoge]}" . PHP_EOL;    // HOGE (Notice:  Use of undefined constant hoge - assumed 'hoge')
echo "{$a['hoge']}" . PHP_EOL;  // HOGE
echo "{$a[99][0]}" . PHP_EOL;   // ARRAY

もちろん、プロパティも使えますし、プロパティのプロパティも参照可能です。

さらにメソッド呼び出しも可能であるため、メソッドチェインもOKですし、 Array デリファレンスも有効です (code.5)

code.5
<?php

class X {
  public $prop = 'PROPERTY';
  public $self = null;

  public function self() {
    return $this;
  }
  public function get() {
  	return __CLASS__;
  }
  public function getArray() {
  	return array(__CLASS__);
  }
}

$x = new X();
$x->self = $x;

echo "{$x->prop}" . PHP_EOL;        // PROPERTY
echo "{$x->self->prop}" . PHP_EOL;  // PROPERTY

echo "{$x->self()->self()->get()}" . PHP_EOL;          // X
echo "{$x->self()->self()->getArray()[0]}" . PHP_EOL;  // X

変数の参照はできますが、その変数を使った演算はできません (code.6)

code.6
<?php

$one = 1;

echo "{$one + 2}" . PHP_EOL; // PHP Parse error:  syntax error, unexpected '+', expecting '}'

Closure の呼び出しも $ で始まるので、当然呼び出し可能です。

呼び出しの中だけは、演算可能で、関数も呼べます。 (code.7)

code.7
<?php

$f = function($a) { return "received values is " . $a; };
$one = 1;

echo "{$f($one + 1)}" . PHP_EOL;   // received values is 2
echo "{$f(abs(-1))}" . PHP_EOL;    // received values is 1

複雑な構文2 (${expr} タイプ)

通常の変数参照については、単純な構文よりも貧弱です。

変数そのものか、あるいは配列の要素しか参照できません。プロパティでさえ。 (code.8)

code.8
<?php

$hoge = 'HOGE';
echo "${hoge}" . PHP_EOL; // HOGE

$ary = ["ZERO"];
echo "${ary[0]}" . PHP_EOL; // ZERO

$obj = new stdClass();
$obj->prop = "PROPERTY";
echo "${obj->prop}" . PHP_EOL; // PHP Parse error:  syntax error, unexpected -> (T_OBJECT_OPERATOR)

しかし、この構文の強力かつ変態的なところは、上記以外のケースです。

"${" から "}" あるいは "[" までの間の文字が変数名として有効でない場合*5は、"${" から "}" の間が PHP の式として評価され、その評価値が示す変数の値が参照されます。 (code.9)

code.9
<?php

$alpha = 'abcdefghijklmn...';
$abc = "abc's value";
echo "${substr($alpha, 0, 3)}" . PHP_EOL; // substr($alpha, 0, 3) が評価されて、 "abc" となり、 $abc が参照される

$dim0 = 'hoge';
$dim1 = 'fuga';
$dim2 = 'bar';
for ($i=0; $i < 3; ++$i) {
    // "dim$i" が評価されて、 "dim0"、"dim1"、"dim2" となり、それぞれの変数が参照される
    echo "${"dim$i"}" . PHP_EOL; // hoge, fuga,bar
}

ダブルクォテーションの中にダブルクォテーションが入っても、"${" と "}" の間なので、文字列終了とは見なされないあたりがとても深淵ですね!


冒頭のコードを解説

冒頭のコードですが、奇妙な挙動は全て、複雑な構文 ${expr} タイプ の文字列によって引き起こされています。

code.1 の 「"${" . " . " . "}"」 は、${ の後がダブルクォテーションで、これは変数名ではないので式として評価され、 「" . " . " . "」 つまり、 「" . . "」 (space dot space space dot space) という名前の変数の値が出力されることになります。

通常、この変数には何も代入されていないため NULL と評価(このとき未代入変数の使用による Notice が発生します) され、空の文字列が生成されます。

余談ですが、" . . " という名前の変数は通常の変数表現で表すことはできませんが、 code.10 の (A) のように代入することができ、 (B) のように参照することができます。

続く echo がどれも同じ変数を参照することは、もう分かりますね。

code.10
<?php

${" .  . "} = 'hoge';  // (A)
echo  ${" .  . "};     // (B)

echo "${" .  . "}";      // hoge
echo "${" . " . " . "}"; // hoge

この形式での変数代入(及び参照)の仕方を使うと、バイナリも変数名として使えるようになります。

2日目の記事 PHP の配列を使った手品とその種明かし と同じ事が、配列のキーではなく変数名でも可能だったりします。 (code.11)

code.11
<?php

${hex2bin('00')} = 'value_0';
${hex2bin('01')} = 'value_1';
${hex2bin('02')} = 'value_2';
var_dump($GLOBALS);
  /*
  array(13) {
    (中略)
    '\0' =>
    string(7) "value_0"
    '' =>                  //  表示されてないけど 0x01
    string(7) "value_1"
    '' =>                  //  表示されてないけど 0x02
    string(7) "value_2"
    'GLOBALS' =>
    &array
  }
  */


閑話休題

code.2 ですが、まず "${" から "[" の間の文字 "_" が変数名として解釈されます。

そして、その要素を参照することになる訳ですが、この "[" と "]" に囲われた文字についても、 php の式として評価されます。

ただし、変数として有効な文字列だと変数参照される "${" ... "}" と異なり、こちらには無条件で php の式を入れることができます。

つまりどういう事かというと、一部のキーワード、例えば yield が記述可能なのです(code.12)

これにより、yield が評価され、Generator が生成されます。

code.12
<?php
echo "${yield}" .PHP_EOL;     // "yield" が変数名として有効なので、 $yield が参照される
echo "${_[yield]}" .PHP_EOL;  // yield 式として評価される

そして、その''一部のキーワード'''には、 exit や die も含まれます。

code.13 では、台詞を聞くことなく終了します。

code.13
<?php

"${_[die]}";

echo <<<EOM
あ…ありのまま 今 起こった事を話すぜ!
「俺は文字列を書いていたと思ったらいつの間にか終了していた」
な… 何を言っているのか わからねーと思うが 
おれも 何をされたのか わからなかった…
EOM;


恐ろしや!



まとめ

歴史的な事情もあるのでしょうが、php の文字列における変数展開はとてつもない闇を秘めています。

この闇を使いこなせるかどうかは、php を利用しているあなた次第です。



自分としては、変数展開するなら、複雑な構文1 ({$variable} タイプ) をオススメしたいところ。

単純な構文 を使った場合、変数を表す文字列とそれ以外の文字列の区別がわかりにくくなるケースがあるし、複雑な構文2 (${expr} タイプ) は、黒魔術的な側面が強いです。

とはいえ、実は、複雑な構文1 ({$variable} タイプ) でも、メソッドやクロージャ呼び出しの引数については、php の式を記述可能だったりします。

これはつまり、 code.14 で突然死するということです。

code.14
<?php
$f = function(){};
"{$f(exit)}";      // 突然の死

echo "php まじカオス";

変数展開は、用法用量を守って正しく使いましょう。



明日は @ne_sachirou さんです。

*1:LABEL [a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]* / Zend/zend_language_scanner.l@1013

*2: Zend/zend_language_scanner.l@1889

*3: Zend/zend_language_scanner.l@1881

*4: Zend/zend_language_scanner.l@1871

*5: Zend/zend_language_scanner.l@1490

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証

トラックバック - http://d.hatena.ne.jp/do_aki/20131218/1387335279
リンク元