PHP で最後の終了タグがあると何が起きるか

PHP はテンプレート言語ですので、コードは PHP タグで囲みますが、ファイル末尾の終了タグは省略することが推奨されています。

```php
<?php
if(...) {
}
...
?> // ←省略を推奨
```

[PSR-2](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md) には 

> The closing ?> tag MUST be omitted from files containing only PHP.

とありますし、PHP マニュアルの [PHP タグ](https://www.php.net/manual/ja/language.basic-syntax.phptags.php) にも

> ファイルが PHP コードのみを含む場合は、ファイルの最後の終了タグは省略するのがおすすめです。 終了タグの後に余分な空白や改行があると、予期せぬ挙動を引き起こす場合があるからです。 余分な空白や改行のせいで PHP が出力バッファリングを開始し、その時点の内容を意図せず出力してしまうことになります。

とあります。では実際に終了タグを省略しない場合には何が起きるのでしょうか。

## 前提知識

### PHP タグの外側の扱い

PHP はテンプレート言語ですので、PHP タグの外側は print 文が書かれているのと同等の扱いになります。

```php
hoge
<?php
print "fuga\n";
```

は

```php
<?php
print "hoge\n";
print "fuga\n";
```

と同等です。終了タグのあとでも、空行であっても print と等価になります。

```php
<?php
print "hoge\n";
?>
  // ←空行を入れた
```

は

```php
<?php
print "hoge\n";
print "\n";
```

と解釈されるというわけです。

### レスポンスヘッダの自動送信

PHP は print 文を処理した際に HTTP レスポンスヘッダが未送信の場合、HTTP ステータス 200 と `Content-Type: text/html; charset=UTF-8` ヘッダが送信されます。

```php
<?php
print "hoge\n";
```

は

```php
header("HTTP/1.1 200 OK");
header("Content-Type: text/html; charset=UTF-8");
print "hoge\n";
```

と同等です。

## 実験環境

Apache 2.4.46 + PHP 7.4.11 という環境で実験を行いました。

## 不具合例 : header 関数が誤動作する

```php:test.php
<?php
require('lib.php');
header("Location: https://qiita.com");
?>
```

```php:lib.php
<?php
?>
  // ←ここに空行
```

`test.php` にアクセスすると qiita.com にリダイレクトするはずですが、実際にアクセスすると

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86075/dfb7cf91-6125-ef28-c34b-8c2f7ff920f8.png)

`Warning: Cannot modify header information - headers already sent by (output started at /usr/local/www/apache24/data/lib.php:3) in /usr/local/www/apache24/data/test.php on line 3` というエラーが発生してしまいました。

telnet コマンドでも確認してみます。

```shell
$ (echo "GET /test.php HTTP/1.0"; echo ""; sleep 1) | telnet 192.168.1.253 80
Trying 192.168.1.253...
Connected to 192.168.1.253.
Escape character is '^]'.
HTTP/1.1 200 OK
Date: Fri, 13 Nov 2020 05:27:08 GMT
Server: Apache/2.4.46 (FreeBSD) PHP/7.4.11
X-Powered-By: PHP/7.4.11
Content-Length: 214
Connection: close
Content-Type: text/html; charset=UTF-8


<br />
<b>Warning</b>:  Cannot modify header information - headers already sent by (output started at /usr/local/www/apache24/data/lib.php:3) in <b>/usr/local/www/apache24/data/test.php</b> on line <b>3</b><br />
Connection closed by foreign host.
```

HTTP ステータス 302 を返して欲しいところですが、200 が返ってきています。具体的には以下のようなことが起こっています。

1. `test.php` から `lib.php` が読み込まれる。
2. `lib.php` の末尾の終了タグ後に空行があることから print 文処理が行われる。
3. print 文を処理するにあたり、レスポンスヘッダが未送信であることから HTTP ステータス 200 と `Content-Type: text/html; charset=UTF-8` が送信される。
4. `test.php` で header 関数を処理するが、既にレスポンスヘッダが送信済であるため無効となり、警告が出力される。

Location 以外にも、HTTP ステータス 4xx や 5xx を返せませんし、Content-Type として image/jpeg や application/pdf などを返したくても返せません。

warning 出力を抑制し(本番環境では大抵そうでしょう)、HTTP クライアントが HTTP ステータスや Content-Type を無視して実データからある程度いい感じに処理してくれたりすると、テストでも発覚しなかったりしますので余計にやっかいです。

## 不具合例 : HTML の解釈が変わる

HTTP ステータス 200 と text/html を返す場合は、先に header() が実行されていても問題はありません。そして HTML は基本的に改行を無視する言語ですので空行が余計に入ったところで問題は無いのですが、場合によっては HTML の解釈が変わることがあります。

### DOCTYPE が無視される

ブラウザによっては DOCTYPE 宣言が1行目にないと無視されます。その場合 DOCTYPE 宣言が省略されたものとみなされデフォルトの文書タイプが適用され、レンダリングが大幅に崩れてしまうことがあります。

ということが古い Internet Explorer ではよくあったのですが、ほとんどのブラウザがデフォルトで HTML5 でレンダリングするようになった現代ではあまり問題になることはないでしょう。

### textarea や pre で余計な改行が入る

HTML ではほとんどの場合改行を含むホワイトスペースは意味を持ちませんが、textarea や pre 要素の中では別です。新規のフォームを開いたのにテキストエリアに謎の改行が入っているということがたまにありますが、犯人はこいつの可能性があります。

### 謎の空白が入る

HTML ではホワイトスペースは意味を持たないのですが、全くレンダリングされないわけでもありません。多くのブラウザでは連続するホワイトスペースは一つにまとめて、一つだけホワイトスペースがレンダリングされます。結果として意図せぬ空白が入って表示がちょっとだけずれるということが起こります。

## まとめ

実際のところ、終了タグを省略しなくてもそれだけでは問題は発生しません。問題となるのは、終了タグのあとに空行を入れてしまった場合です。

大抵のプログラム言語のソースコードでは空行は意味を持ちませんので、ファイルの末尾に空行を入れても何の問題も起きません。PHP でもコード部分であれば空行は無視されますが、テンプレート部分の場合は print 文と等価になってしまうわけです。

終了タグを書いてもそのあとに空行は絶対に入れないことを守れるのなら終了タグを書いてもいいのですが、実際には守れないこともあるだろうから、それならば終了タグをそもそも書かないようにした方がいいのではないかというのが PSR-2 などの主張ということになります。

## CLI の場合(2020/11/26追記)

コメントをいただいて、CLI の場合について言及していないことに気が付きました。

例えば以下のようなスクリプトがあったとします。

```php:test.php
#!/usr/bin/env php
<?php
// do something
?>
  // ←ここに空行
```

コマンドラインから実行します。

```shell
$ ./test.php

$
```

CLI では header() はそもそも実行されませんので、終了タグのあとに空行があった場合、標準出力に空行だけが出力されます。コマンドラインで操作した場合は余計な空行が表示されて~~邪魔だなという程度で済みます。~~、単なるメッセージならともかくフィルタコマンドとかなら大惨事です。

また、このスクリプトを cron で実行した場合はどうでしょうか。

```shell:crontab
* * * * * /foo/bar/test.php
```

毎分 test.php が実行されるわけですが、cron は実行したタスクが標準出力に出力した場合は、その内容を MAILTO で設定したメールアドレスにメール送信します。

流儀はいろいろあるでしょうが、一般的なところとしては

* タスクが正常終了した場合、標準出力に何も出力しない。MAILTO へのメールも送信されない。
* タスクが異常終了した場合、標準出力もしくはエラー出力にメッセージを出力する。メッセージ内容は cron によって MAILTO のアドレスにメール送信され、異常通知を受け取る。

ということが多いかと思います。終了タグのあとに空行があると標準出力への出力が発生し、正常終了しているのにメール送信が行われてしまうというわけです。