<?php...?>タグの外側の文字列はどういう扱いなのか

# はじめに

[phpでswitch文の「switch($hoge)★ここ★{case0:」に改行いれるとエラーになる。](https://qiita.com/matsuyoro/items/38d1fd073c8a4c63a637)を読んでこれどうなってるんだろうと思ったので調べて見ました。

PHPのマニュアルの[PHPタグ](http://jp2.php.net/manual/ja/language.basic-syntax.phptags.php)のところには

> つまり、開始タグと終了タグで囲まれている 箇所以外のすべての部分は、PHP パーサに無視されます。

と書いてあるので何を挟んでも良さそうに読めるのですが...

# 結論

`<?php`...`?>`の外側の文字列は、内部的にはその文字列を出力するecho文のように扱われます。ただし`?>`の直後の改行は除きます。パーサに無視されてなどいなかったのです。

したがって、echo文が許されないところで`?>`でタグを終了して次の`<?php`との間に1文字でも挟むと構文エラーになります。

先の記事で駄目だった例の

```php
<?php
$i = 0;
?>

<?php
switch($i){
?>

<?php
 case 0:
?>
<p>aaa</a>

<?php
break;
}
```

は

```php
<?php
$i = 0;
echo "\n";
switch($i){
 echo "\n";
 case 0:
echo "<p>aaa</a>\n";
break;
}
```

と書くのと一緒なので、`switch`の直後の最初の`case`の前には`echo`文を書いてはいけないのと同じ意味でNGということになります。

# 詳しい話

マニュアルを読んでもわからないので該当部分のソースを見てみました。

## 字句解析

字句解析は`Zend/zend_language_scannar.l`で行っています。

### phpタグの外側

`INITIAL`はphpタグの外側にいる状態で、これより前に`<?php`などは処理されているのでそれらに当てはまらない文字列がここで処理されます。最終的には`T_INLINE_HTML`というトークンになります。

```
<INITIAL>{ANY_CHAR} {
        // 長いので略
	RETURN_TOKEN(T_INLINE_HTML);
}

```

### `?>`の直後の改行

以下のようになっており、直後の文字が改行なら閉じタグの一部として食われます。だから次に来る`<?php`との間に改行一つだけ挟むのはセーフ。

```
<ST_IN_SCRIPTING>"?>"{NEWLINE}? {
	BEGIN(INITIAL);
	if (yytext[yyleng-1] != '>') {
		CG(increment_lineno) = 1;
	}
	RETURN_TOKEN(T_CLOSE_TAG);  /* implicit ';' at php-end tag */
}
```

## 構文解析

構文解析は`Zend/zend_language_parser.y`で行っています。`T_INLINE_HTML`が出てくるのは`statement`のところです。ひとつの文として扱われていることがわかります。

```
statement:
		'{' inner_statement_list '}' { $$ = $2; }
	|	if_stmt { $$ = $1; }
	|	alt_if_stmt { $$ = $1; }
	|	T_WHILE '(' expr ')' while_statement
			{ $$ = zend_ast_create(ZEND_AST_WHILE, $3, $5); }
	|	T_DO statement T_WHILE '(' expr ')' ';'
			{ $$ = zend_ast_create(ZEND_AST_DO_WHILE, $2, $5); }
	|	T_FOR '(' for_exprs ';' for_exprs ';' for_exprs ')' for_statement
			{ $$ = zend_ast_create(ZEND_AST_FOR, $3, $5, $7, $9); }
	|	T_SWITCH '(' expr ')' switch_case_list
			{ $$ = zend_ast_create(ZEND_AST_SWITCH, $3, $5); }
	|	T_BREAK optional_expr ';'		{ $$ = zend_ast_create(ZEND_AST_BREAK, $2); }
	|	T_CONTINUE optional_expr ';'	{ $$ = zend_ast_create(ZEND_AST_CONTINUE, $2); }
	|	T_RETURN optional_expr ';'		{ $$ = zend_ast_create(ZEND_AST_RETURN, $2); }
	|	T_GLOBAL global_var_list ';'	{ $$ = $2; }
	|	T_STATIC static_var_list ';'	{ $$ = $2; }
	|	T_ECHO echo_expr_list ';'		{ $$ = $2; }
	|	T_INLINE_HTML { $$ = zend_ast_create(ZEND_AST_ECHO, $1); }
	|	expr ';' { $$ = $1; }
	|	T_UNSET '(' unset_variables ')' ';' { $$ = $3; }
	|	T_FOREACH '(' expr T_AS foreach_variable ')' foreach_statement
			{ $$ = zend_ast_create(ZEND_AST_FOREACH, $3, $5, NULL, $7); }
	|	T_FOREACH '(' expr T_AS foreach_variable T_DOUBLE_ARROW foreach_variable ')'
		foreach_statement
			{ $$ = zend_ast_create(ZEND_AST_FOREACH, $3, $7, $5, $9); }
	|	T_DECLARE '(' const_list ')'
			{ zend_handle_encoding_declaration($3); }
		declare_statement
			{ $$ = zend_ast_create(ZEND_AST_DECLARE, $3, $6); }
	|	';'	/* empty statement */ { $$ = NULL; }
	|	T_TRY '{' inner_statement_list '}' catch_list finally_statement
			{ $$ = zend_ast_create(ZEND_AST_TRY, $3, $5, $6); }
	|	T_THROW expr ';' { $$ = zend_ast_create(ZEND_AST_THROW, $2); }
	|	T_GOTO T_STRING ';' { $$ = zend_ast_create(ZEND_AST_GOTO, $2); }
	|	T_STRING ':' { $$ = zend_ast_create(ZEND_AST_LABEL, $1); }
;
```