PHP 手冊明確建議:純 PHP 檔案應省略結束標籤 ?>。這不是風格偏好,而是為了避免「輸出緩衝區過早發送」導致的 Cannot modify header information - headers already sent 錯誤。本文深入剖析原理、實際案例與團隊規範落地。
PHP 處理流程:
<?php 進入 PHP 模式?> 退出 PHP 模式,進入 HTML 輸出模式?> 之後的字元(空白、換行、BOM、不可見字元)都會被直接輸出到瀏覽器header()、setcookie()、session_start() 皆會失敗<?php
// config.php
$config = ['db' => 'mysql'];
?>
<?php
// index.php
require 'config.php';
session_start(); // 錯誤!Header already sent
?>
甚至使用 IDE 自動格式化、FTP 傳輸時自動轉換換行符、Git checkout 時行尾轉換,都可能在 ?> 後引入不可見字元。
| 來源 | 規範內容 |
|---|---|
| PHP 官方手冊 | 「PHP 閉合標籤在純 PHP 檔案中是可選的,建議省略以避免輸出問題」 |
| Zend Framework / Laminas | 「所有 PHP 檔案必須省略閉合標籤,並在檔案末尾加上註解標示路徑」 |
| PSR-12 | 雖未強制,但社群共識為「純 PHP 檔案省略 ?>」 |
| Symfony / Laravel 核心碼 | 全專案無任何閉合標籤 |
<?php
// src/Service/UserService.php
namespace App\Service;
class UserService {
public function getUser(int $id): array {
// ...
}
}
// 結尾註解:標示檔案結束與相對路徑
// EOF
// Path: src/Service/UserService.php
若檔案包含 HTML 輸出(如 .phtml、.blade.php、.twig),必須保留 閉合標籤以正確切換輸出模式:
<?php $title = '頁面標題'; ?>
<h1><?= htmlspecialchars($title) ?></h1>
<p>內容...</p>
<?xml version="1.0"?>
雖無內建規則,可編寫自訂規則掃描 T_CLOSE_TAG token 後是否存在非空白內容。
#!/bin/bash
# .git/hooks/pre-commit
files=$(git diff --cached --name-only -- '*.php')
for f in $files; do
if grep -q '^.*?>\s*$' "$f" && ! grep -q '<\?php' "$f" | head -1 | grep -q 'html\|echo\|print'; then
echo "Warning: $f contains closing tag in pure PHP file"
# exit 1 # 嚴格模式可阻止提交
fi
done
| 誤區 | 事實 |
|---|---|
| 「不寫 ?> 會導致解析錯誤」 | PHP 解析器以 EOF 為隱性結束,完全合法 |
| 「只影響 include/require」 | 任何被引入的檔案、自動載入的類檔案、Composer 產生的 autoload 皆受影響 |
| 「ob_start() 可以解決」 | 輸出緩衝只能延遲症狀,無法解決根本原因,且增加記憶體開銷 |
find . -name '*.php' -exec sed -i '/^.*?>\s*$/d' {} \;(謹慎備份後執行)省略 ?> 是零成本、高收益的防禦性編程習慣,建議所有 PHP 專案立即落地執行。