PHP

フレキシブルなheredocがやってくる

PHPはRFCと呼ばれる1機能提案と投票の制度2によって機能の追加や仕様変更が議論されます。ここでAcceptされた提案は将来のPHPに反映されることになります。

私はあまり頻繁にRFCをウォッチしてるわけではないのでこれを知ったのは昨日のことなのですが、PHP RFC: Flexible Heredoc and Nowdoc Syntaxesが昨年2017年の11月15日まで投票され、受理されました。現状Pending Implementationではありますが、おそらく次のメジャーバージョン3で実装されることでせう。

heredoc/nowdocとは何か

ヒアドキュメントはシェルやPerlなどの言語に存在する構文で、複数行にわたる文字列を記述することができます。

PHPにおけるヒアドキュメントにはheredocnowdocと呼ばれる二種類の記法が存在します。

以下のようなコードがheredocとnowdocの典型例です。

// heredoc
$srtr1 = <<<EOS
りんご
→ごりら
→→らっぱ
→→→ぱんだ
EOS;

// nowdoc
$srtr2 = <<<'EOS'
りんご
→ごりら
→→らっぱ
→→→ぱんだ
EOS;

heredocは"で囲った文字列と同様に変数展開が可能、nowdocは'で囲った文字列と同様に変数展開できず特定の文字以外の\も展開しません。

つまり以下のような差が生じます。

<?php

$a = "A";
$v = [];
$v[] = <<<EOS
$a\nb
EOS;
$v[] = <<<'EOS'
a\nb
EOS;

// array(2) {
//   [0]=>
//   string(3) "A
// b"
//   [1]=>
//   string(4) "a\nb"
// }

この差をPHPマニュアルでは以下のように説明します。

Nowdoc はヒアドキュメントと似ていますが、 ヒアドキュメントがダブルクォートで囲んだ文字列として扱われるのに対して、 Nowdoc はシングルクォートで囲んだ文字列として扱われます。 Nowdoc の使用方法はヒアドキュメントとほぼ同じですが、 その中身について パース処理を行いません。 PHP のコードや大量のテキストを埋め込む際に、 エスケープが不要になるので便利です。この機能は、SGML の <![CDATA[ ]]> (ブロック内のテキストをパースしないことを宣言する) と同じようなものです。

しかしながら、このheredocとnowdocですが、不遇なことに実際のコードではあまり人気がありません

なぜ従来のheredoc/nowdocが不遇だったか

文法が厳密すぎる

ここまでのサンプルコードではあまり不自然さは感じなかったかもしれませんが、従来のheredoc/nowdocは以下のように説明されます。

終端 ID は、その行の最初のカラムから始める必要があります。 使用するラベルは、PHP の他のラベルと同様の命名規則に従う必要があります。 つまり、英数字およびアンダースコアのみを含み、 数字でない文字またはアンダースコアで始まる必要があります。

警告
非常に重要なことですが、終端 ID がある行には、セミコロン (;) 以外の他の文字が含まれていてはならないことに注意しましょう。 これは、特に ID はインデントしてはならないということ、 セミコロンの前に空白やタブを付けてはいけないことを意味します。 終端 ID の前の最初の文字は、使用するオペレーティングシステムで定義された 改行である必要があることにも注意を要します。これは、例えば、Macintoshでは \r となります。 最後の区切り文字の後にもまた、改行を入れる必要があります。

この規則が破られて終端 ID が "clean" でない場合、 終端 ID と認識されず、PHP はさらに終端 ID を探し続けます。 適当な終了 ID がみつからない場合、 スクリプトの最終行でパースエラーが発生します。

(PHPマニュアル http://php.net/manual/ja/language.types.string.php#language.types.string.syntax.heredoc より引用、2018年2月6日閲覧)

この説明にはいくらか実際の実装と齟齬がある4のですが、重要なのは終端 ID がある行には、セミコロン (;) 以外の他の文字が含まれていてはならないことに注意しましょうです。

この説明通り、以下のようなコードはsyntax errorです。

var_dump(<<<EOS
いろはにほへと
 ちりぬるを
わかよたれそ
 つねねらむ
うゐのおくやま
 けふこえて
あさきゆめみし
 ゑひもせす
EOS);

文法違反の咎は、終端IDがある行にID以外の文字 ) が含まれることです。 えっ、そんなことで怒ってくるの……?

var_dump(<<<EOS
いろはにほへと
 ちりぬるを
わかよたれそ
 つねねらむ
うゐのおくやま
 けふこえて
あさきゆめみし
 ゑひもせす
EOS
); // 別の行に書く必要がある

もちろん、引数や配列要素の区切り文字の,も書いてはいけませんでした。

そもそも通常の引用符が改行を受理する

C言語やJavaScriptは複数行の文字列を作るにはめんどくさい行継続の記号などが必要ですが、PHPでは特に工夫なく"'で括られた文字列リテラルに改行を書いて問題ありません

<?php

$a = [
    <<<EOS

いろはにほへと
 ちりぬるを
わかよたれそ
 つねねらむ

EOS
    ,
    <<<EOS

うゐのおくやま
 けふこえて
あさきゆめみし
 ゑひもせす

EOS
];

$b = [
    '
いろはにほへと
 ちりぬるを
わかよたれそ
 つねねらむ
',
    '
うゐのおくやま
 けふこえて
あさきゆめみし
 ゑひもせす
'];

// $a と $b の中身は同じ
var_dump($a === $b);

少々わざとらしい例ではありますが、どちらがすっきり見えるかは議論があるところです。

インデント位置

かなりわざとらしい例になるのですが、PHPスクリプトにクラスを書くと、すぐに3段程度のインデントのネストは生じます。

<?php

class Hoge
{
    public function a(iterable $xs)
    {
        foreach ($xs as $x) {
            $db->query(<<<'EOS'
            SELECT
              *
            FROM
              `hoge`
            WHERE
              1 = 1
EOS
            );
        }
    }
}

ループの中にSQL文なんか書くなよ、とのつっこみは妥当ですが、これは飽くまで例です。この位置で丁寧にインデントして書かれたSQL文は以下のように展開されます。

            SELECT
              *
            FROM
              `hoge`
            WHERE
              1 = 1

そう。これは単なる文字列なので間延びしたインデントの空白が漏れなくついてきます。

それを避けるためにはインデントを下げるしかありません。

<?php

class Hoge
{
    public function a(iterable $xs)
    {
        foreach ($xs as $x) {
            $db->query(<<<'EOS'
SELECT
  *
FROM
  `hoge`
WHERE
  1 = 1
EOS
            );
        }
    }
}

なんか… べこっとしてますね…

何が改善されるのか

インデント

PHP: rfc:flexible_heredoc_nowdoc_syntaxesでは次のように説明されます。

The indentation of the closing marker dictates the amount of whitespace to strip from each line within the heredoc/nowdoc. So let's demonstrate these semantics with a few examples:

ざっくり「終端IDのインデントは各行から取り除くホワイトスペースの量を指示します」ですね。続いて以下の例です。

echo <<<END
      a
     b
    c
    END;
/*
  a
 b
c
*/

ENDのインデント位置に注目してください。cEND は同じインデントの深さですから、cの前にはホワイトスペースは出力されません。ここで気になるのは、インデントの量がちぐはぐである場合、特にENDよりもインデントが浅い行があるときの挙動です。

それに関しては次のように説明されます。

If the closing marker is indented further than any lines of the body, then a ParseError will be thrown:

echo <<<END
  a
 b
c
 END;

// Parse error: Invalid body indentation level (expecting an indentation at least 5) in %s on line %d

このような場合はきちんと Parse error によって検出されます。つまりphp -lで検出できるはずです。

Tabs are supported as well, however, tabs and spaces must not be intermixed regarding the indentation of the closing marker and the indentation of the body (up to the closing marker). In any of these cases, a ParseError will be thrown:

「タブも同様にサポートされますが、終端IDと(終端IDまでの)本体のインデントにはタブとスペースを混在させてはなりません (must not)。これらのどんな場合でも Parse error が送出されます。」はい。

終端ID

この提案では目玉のインデントだけではなく、終端IDの融通の利かなさも改善されます。

以下のような例が許容されると説明されます。

stringManipulator(<<<END
   a
  b
 c
END);

$values = [<<<END
a
b
c
END, 'd e f'];

これらは現行のPHP(7.2以下)では、すべてsyntax errorになります。

つまり、どうなるの…?

べこっとしなくなります。

<?php

class Hoge
{
    public function a(iterable $xs)
    {
        foreach ($xs as $x) {
            $db->query(<<<'EOS'
            SELECT
              *
            FROM
              `hoge`
            WHERE
              1 = 1
            EOS);
        }
    }
}

$db->query()に渡される文字列は、当然に無様なインデントが存在しない文字列になるはずです。

SELECT
  *
FROM
  `hoge`
WHERE
  1 = 1

最高では。

まとめ

次のPHPのメジャーバージョンが楽しみですね!

おまけPhpStormにおけるheredoc

PhpStormは賢いので、heredoc/nowdocの終端IDを言語名にすると、中身の文字列をその言語で構文ハイライト(SQLなどでは入力補完も)してくれます5。GitHubやQiitaのMarkdownの言語指定にも似てるので、PhpStormを使ってなかったとしてもわかりやすくてよいですね!!!! (もっともPhpStormは律儀に言語名を指定しなくても勝手に推測してハイライトしてくれるんだけどさ)

<?php

class Hoge
{
    public function a(iterable $xs)
    {
        foreach ($xs as $x) {
            $db->query(<<<'SQL'
            SELECT
              *
            FROM
              `hoge`
            WHERE
              1 = 1
            SQL);
        }
    }
}

スクリーンショット 2018-02-07 午前0.47.00.png

PhpStormがサポートしてるはずの言語でも表示してくれたりくれなかったり変な感じになるのはよくわかんない5です ヾ(〃><)ノ゙

脚注


  1. 当然ながら、インターネット標準として知られるIETFのRFCとは別物です 

  2. PythonにおけるPEPと同様の制度です 

  3. 本記事の記述時点でPHPの次のメジャーリリースが7.3になるか8.0になるかはよくわかりません 

  4. 実際には終了IDなどのシンボルには、ASCIIの英数字以外の文字、日本語なども利用できます。 

  5. 詳細はUsing Language Injections - Help | PhpStormを見てほしいのですが、当該記事では// language=SQLのようなコメントを使って言語指定する方法が指定され、heredoc/nowdocの終端IDを使って言語名指定する方法は紹介すらされてないのですが、なんとなく動いたり動かなかったりするので、みんな積極的に使っていこうぜ……!