【PHP】「Cannot modify header information – headers already sent」エラーの原因と解決方法|全パターン完全解説

【PHP】「Cannot modify header information - headers already sent」エラーの原因と解決方法|全パターン完全解説 PHP

PHPで開発をしていると、ある日突然このようなエラーに遭遇することがあります。

エラーメッセージ
Warning: Cannot modify header information - headers already sent by
(output started at /var/www/html/index.php:5) in /var/www/html/redirect.php on line 12

このエラーは、PHPの header() 関数や setcookie()session_start() などを使ったときに発生する非常にポピュラーなエラーです。初心者の方が最初にぶつかる壁のひとつと言ってもよいでしょう。

この記事では、「Cannot modify header information – headers already sent」エラーのすべての原因パターン具体的な解決方法を、コード例付きで徹底的に解説します。WordPress や Laravel などのフレームワーク環境での対処法も網羅しています。

? この記事で学べること

  • 「headers already sent」エラーの根本原因(HTTPヘッダーの仕組み)
  • 全6パターンの原因と具体的な解決コード
  • output_buffering による回避テクニック
  • WordPress・Laravel での対処法
  • BOM の確認・除去方法
  • 実践的なデバッグテクニック
スポンサーリンク
  1. 「Cannot modify header information – headers already sent」エラーとは?
    1. エラーメッセージの読み方
    2. HTTPヘッダーの仕組み
    3. なぜ出力後にヘッダーを送れないのか
  2. 原因①:echo / print による出力が header() より先にある
    1. 最も基本的なパターン
    2. HTMLが先にあるパターン
    3. 条件分岐内の出力
    4. var_dump / print_r でのデバッグ出力
  3. 原因②:PHPタグの前の空白・BOM(バイトオーダーマーク)
    1. ファイル先頭の空白・改行
    2. UTF-8 BOM(バイトオーダーマーク)とは
    3. BOMの確認方法
      1. 方法1:コマンドラインで確認(Linux / Mac)
      2. 方法2:PHPスクリプトで確認
      3. 方法3:テキストエディタで確認
    4. BOMの除去方法
      1. コマンドラインでの除去
      2. PHPでBOMを除去
      3. VS Code での BOM 除去
  4. 原因③:PHP閉じタグ「?>」の後の空白・改行
    1. ?> の後に改行や空白がある
    2. 解決策:閉じタグ ?> を省略する
  5. 原因④:include / require での出力
    1. 読み込みファイル内の出力
    2. 設定ファイルの閉じタグ問題
    3. includeパスの全ファイルを一括チェックするスクリプト
  6. output_buffering による回避
    1. 出力バッファリングとは
    2. ob_start() / ob_end_flush() の使い方
    3. 出力バッファリング関数の一覧
    4. php.ini の output_buffering 設定
    5. バッファリングのネスト(入れ子)
    6. 実用例:テンプレートシステムでの活用
    7. gzip圧縮と出力バッファリングの組み合わせ
  7. WordPress での「headers already sent」エラー
    1. wp-config.php の BOM 問題
      1. 解決方法
    2. プラグイン / テーマの出力問題
    3. wp_redirect() でのエラー
    4. WordPress で使えるリダイレクト用フック
    5. functions.php の注意点
    6. WordPress プラグインが原因の場合の特定方法
    7. WordPress のデバッグモード
  8. フレームワークでの対処法
    1. Laravel での対処
    2. Laravel ミドルウェアでのヘッダー設定
    3. CakePHP での対処
    4. フレームワークが自動的にバッファリングする仕組み
  9. デバッグテクニック
    1. エラーメッセージの「output started at」の読み方(詳細)
    2. headers_sent() 関数でデバッグ
    3. デバッグ用ヘルパー関数
    4. Hex Editor で BOM を確認する方法
    5. よくある原因の確認チェックリスト
  10. 実践的なコードパターン集
    1. パターン1:ログイン → リダイレクトの正しい実装
    2. パターン2:ファイルダウンロード
    3. パターン3:API レスポンス(JSON)
    4. パターン4:Cookie ベースの言語切替
    5. パターン5:アクセス制御(認証チェック)
  11. よくある質問(FAQ)
    1. Q. output_buffering を On にすれば解決しますか?
    2. Q. header() の代わりに JavaScript でリダイレクトしてもいいですか?
    3. Q. PHP 8 でもこのエラーは発生しますか?
    4. Q. なぜ開発環境では発生せず、本番環境で発生するのですか?
    5. Q. 「output started at」にファイル名が表示されない場合は?
  12. まとめ
    1. 原因チェックリスト
    2. 解決フローチャート
    3. ベストプラクティスまとめ
  13. エラー発生の仕組みを図解で理解する
    1. 正常な処理フロー
    2. エラーが発生する処理フロー
    3. output_buffering 使用時の処理フロー
  14. PHPバージョン別の注意点
  15. サーバー環境別の設定
    1. Apache (.htaccess)
    2. nginx
    3. XAMPP(ローカル開発環境)
    4. レンタルサーバー(エックスサーバー、さくら、ロリポップ等)
  16. 自動テスト:headers already sent を事前に検出する
  17. 関連する PHP エラーとの違い
  18. エディタ・IDE の設定で予防する
    1. VS Code の推奨設定
    2. .editorconfig の設定
  19. PHPの内部処理:なぜこのエラーが起きるのか(上級者向け)
    1. PHPの出力フロー

「Cannot modify header information – headers already sent」エラーとは?

エラーメッセージの読み方

まず、このエラーメッセージを正確に読み解きましょう。エラーメッセージには重要な情報が含まれています。

エラーメッセージの構造
Warning: Cannot modify header information - headers already sent by
(output started at /var/www/html/index.php:5)
in /var/www/html/redirect.php on line 12
要素 内容 意味
output started at /var/www/html/index.php:5 出力が始まったファイルと行番号(原因箇所)
in ... on line /var/www/html/redirect.php:12 header() を呼び出した場所

ポイント:エラーメッセージの「output started at」の部分が最も重要です。ここに表示されているファイルと行番号を確認すれば、何が原因で出力が先に行われたかがわかります。

HTTPヘッダーの仕組み

このエラーを理解するには、HTTP通信の仕組みを知る必要があります。ブラウザとWebサーバーの通信は以下の順序で行われます。

順序 内容
1 HTTPヘッダー送信 Content-Type, Set-Cookie, Location など
2 空行(ヘッダーとボディの区切り) \r\n
3 HTTPボディ送信 HTML、JSON、画像データなど
HTTP通信の実際の流れ
// ① まずHTTPヘッダーが送信される
HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
Set-Cookie: PHPSESSID=abc123; path=/
                                         ← ② 空行
<!DOCTYPE html>                         ← ③ ボディ(HTML)の開始
<html>
...

重要なのは、HTTPヘッダーは必ずボディ(HTML出力)よりも先に送信されるという点です。PHPで何かを出力(echo、print、空白、改行など)すると、PHPは自動的にHTTPヘッダーを送信し、その後にボディの送信を開始します。

一度ヘッダーが送信されてしまうと、後からヘッダーを追加・変更することはできません。これが「headers already sent(ヘッダーは既に送信済み)」というエラーの正体です。

エラーが発生する流れ
// ① echoでHTMLを出力 → PHPがHTTPヘッダーを自動送信 → ボディ送信開始
echo 'Hello';  // この時点でヘッダーが確定・送信される

// ② その後でheader()を呼ぶ → もう遅い!
header('Location: /other-page.php');  // ← Warning発生!

なぜ出力後にヘッダーを送れないのか

これはHTTPプロトコルの仕様によるものです。HTTPレスポンスは以下のように構成されており、ヘッダーとボディの順序は厳格に決められています。

イメージ図
┌─────────────────────────────┐
│   HTTPステータスライン        │  HTTP/1.1 200 OK
├─────────────────────────────┤
│   HTTPヘッダー               │  Content-Type: text/html
│   (header() で設定する部分)  │  Set-Cookie: ...
│                             │  Location: ...
├─────────────────────────────┤
│   空行(区切り)             │
├─────────────────────────────┤
│   HTTPボディ                │  <html>...
│   (echo等で出力する部分)    │
└─────────────────────────────┘

※ 一度ボディの送信が始まると、ヘッダー部分には戻れない!

PHPは最初の出力が行われた時点で、蓄積していたHTTPヘッダーをクライアント(ブラウザ)に送信します。HTTPプロトコルの仕様上、一度送信したヘッダーを後から変更したり追加したりすることはできません。そのため、出力後に header() を呼ぶとWarningが発生するのです。

注意:「出力」とは echoprint だけではありません。PHPタグの外にある空白、改行、BOM(バイトオーダーマーク)、エラーメッセージなども「出力」に含まれます。これが初心者が混乱する大きな原因です。

原因①:echo / print による出力が header() より先にある

最も基本的なパターン

最もわかりやすい原因は、echoprint で何かを出力した後に header() を呼ぶケースです。

❌ NGコード:echo が header() より先にある
<?php
echo '<p>ログイン処理中...</p>';  // ← ここで出力が発生!

// ログイン処理...
if ($loginSuccess) {
    header('Location: /dashboard.php');  // ← Warning発生!
    exit;
}
?>
✅ OKコード:header() を出力より先に呼ぶ
<?php
// ログイン処理...
if ($loginSuccess) {
    header('Location: /dashboard.php');  // ← 出力前なのでOK
    exit;
}

// リダイレクトしなかった場合のみ出力
echo '<p>ログインに失敗しました</p>';
?>

HTMLが先にあるパターン

PHPファイルの先頭にHTMLを記述していて、その後でヘッダー操作をするケースも非常に多いです。

❌ NGコード:HTMLがPHP処理より先にある
<!DOCTYPE html>          <!-- ← ここで出力が始まる! -->
<html>
<head><title>サイト</title></head>
<body>
<?php
if (!$isLoggedIn) {
    header('Location: /login.php');  // ← Warning発生!
    exit;
}
?>
✅ OKコード:PHP処理をファイルの先頭に配置
<?php
// ★ ヘッダー操作はファイルの先頭で行う
if (!$isLoggedIn) {
    header('Location: /login.php');
    exit;
}
?>
<!DOCTYPE html>
<html>
<head><title>サイト</title></head>
<body>
<!-- ログイン済みユーザー向けのコンテンツ -->

条件分岐内の出力

条件分岐の中で一部だけ出力し、別の条件でリダイレクトするパターンも要注意です。

❌ NGコード:条件分岐内で出力後にリダイレクト
<?php
$action = $_GET['action'] ?? '';

if ($action === 'preview') {
    echo '<h1>プレビュー</h1>';  // ← action=preview の場合、ここで出力
}

if ($action === 'save') {
    // 保存処理...
    header('Location: /success.php');  // ← action=save ならOKだが...
    exit;
}
?>

上記のコードは、action=save の場合は正常に動作しますが、もし処理の流れやロジックにバグがあって、preview の分岐を通った後に save の分岐も通ってしまうとエラーになります。

✅ OKコード:elseif で排他的に処理
<?php
$action = $_GET['action'] ?? '';

// ★ ヘッダー操作が必要な処理を先に実行
if ($action === 'save') {
    // 保存処理...
    header('Location: /success.php');
    exit;
} elseif ($action === 'preview') {
    echo '<h1>プレビュー</h1>';
}
?>

var_dump / print_r でのデバッグ出力

開発中によくあるのが、デバッグ用の var_dump()print_r() を入れたまま header() を呼んでしまうケースです。

❌ NGコード:デバッグ出力が残っている
<?php
$userData = getUserData($userId);
var_dump($userData);  // ← デバッグ用の出力を消し忘れ!

if ($userData === null) {
    header('Location: /error.php');  // ← Warning発生!
    exit;
}
?>

ポイント:デバッグ出力は error_log() を使うとHTTP出力に影響を与えません。error_log(print_r($userData, true)); のようにすれば、出力をサーバーのエラーログに書き込めます。

✅ OKコード:error_log() でデバッグ
<?php
$userData = getUserData($userId);

// ★ error_log() ならHTTP出力に影響しない
error_log('User data: ' . print_r($userData, true));

if ($userData === null) {
    header('Location: /error.php');  // ← 正常に動作
    exit;
}
?>

原因②:PHPタグの前の空白・BOM(バイトオーダーマーク)

ファイル先頭の空白・改行

目に見えない原因として、PHPファイルの <?php タグの前に空白や改行が入っているケースがあります。テキストエディタでは見えにくいですが、これも「出力」としてカウントされます。

❌ NGコード:<?php の前に空行がある
                    ← この空行が出力になる!
<?php
session_start();  // ← Warning発生!
?>

上記の例では、1行目の空行(改行コード)がブラウザへの出力として送信されてしまいます。その結果、2行目の session_start() が実行される時点ではすでにヘッダーが送信済みとなり、エラーが発生します。

✅ OKコード:<?php がファイルの1行目1文字目
<?php
session_start();  // ← 正常に動作
?>

注意:エラーメッセージに output started at /path/to/file.php:1 と表示されている場合、ファイルの1行目に問題がある可能性が高いです。BOMか先頭の空白を疑ってください。

UTF-8 BOM(バイトオーダーマーク)とは

BOM(Byte Order Mark)は、ファイルの文字エンコーディングを示すための特殊なバイト列です。UTF-8 の場合、ファイルの先頭に 0xEF 0xBB 0xBF の3バイトが挿入されます。

エンコーディング BOMバイト列 PHPへの影響
UTF-8(BOMなし) なし 問題なし ✅
UTF-8(BOM付き) EF BB BF headers already sent エラー ❌
UTF-16 LE FF FE PHP自体が正常に動作しない
UTF-16 BE FE FF PHP自体が正常に動作しない

BOMは通常のテキストエディタでは目に見えません。そのため、ファイルを開いても一見何も問題がないように見えますが、PHPから見ると <?php タグの前に3バイトの出力があることになります。

BOMの確認方法

いくつかの方法でBOMが含まれているかを確認できます。

方法1:コマンドラインで確認(Linux / Mac)

bash – hexdump でBOMを確認
# ファイルの先頭数バイトを16進数で表示
hexdump -C -n 10 config.php

# BOMがある場合の出力例:
00000000  ef bb bf 3c 3f 70 68 70  |...<?php|
#         ^^^^^^^^ これがBOM!

# BOMがない場合の出力例:
00000000  3c 3f 70 68 70 0a 0a 73  |<?php..s|
#  先頭から <?php で始まっている → 正常

方法2:PHPスクリプトで確認

PHP – BOM検出スクリプト
<?php
// 指定ディレクトリ内の全PHPファイルでBOMを検出
function checkBom($directory) {
    $files = new RecursiveIteratorIterator(
        new RecursiveDirectoryIterator($directory)
    );

    foreach ($files as $file) {
        if ($file->getExtension() !== 'php') {
            continue;
        }

        $content = file_get_contents($file->getPathname());

        // UTF-8 BOM: 0xEF 0xBB 0xBF
        if (substr($content, 0, 3) === 'xEFxBBxBF') {
            echo 'BOM detected: ' . $file->getPathname() . '
';
        }
    }
}

checkBom('/var/www/html');
?>

方法3:テキストエディタで確認

エディタ 確認方法
VS Code 右下のステータスバーに「UTF-8 with BOM」と表示される
サクラエディタ ファイル → 名前を付けて保存 → BOMの有無を選択可能
Notepad++ エンコード → 「UTF-8」か「UTF-8-BOM」かを確認
Vim :set bomb? で確認、:set nobomb で除去

BOMの除去方法

コマンドラインでの除去

bash – BOM除去コマンド
# sed で BOM を除去
sed -i '1s/^xEFxBBxBF//' config.php

# ディレクトリ内の全PHPファイルから一括除去
find . -name '*.php' -exec sed -i '1s/^xEFxBBxBF//' {} +

PHPでBOMを除去

PHP – BOM除去スクリプト
<?php
function removeBom($filePath) {
    $content = file_get_contents($filePath);

    // UTF-8 BOM を検出して除去
    if (substr($content, 0, 3) === 'xEFxBBxBF') {
        $content = substr($content, 3);
        file_put_contents($filePath, $content);
        echo 'BOM removed: ' . $filePath . '
';
        return true;
    }

    return false;
}

// 使用例
removeBom('/var/www/html/config.php');
?>

VS Code での BOM 除去

VS Code を使っている場合、以下の手順で BOM を除去できます。

VS Code での BOM 除去手順

  1. 右下のステータスバーで「UTF-8 with BOM」をクリック
  2. 「Save with Encoding」を選択
  3. 「UTF-8」(BOM なし)を選択
  4. ファイルを保存

VS Code の設定で、新規ファイルにBOMを付けないようにすることもできます。

settings.json
{
    "files.encoding": "utf8",
    "files.autoGuessEncoding": false
}

原因③:PHP閉じタグ「?>」の後の空白・改行

?> の後に改行や空白がある

PHPの閉じタグ ?> の後に改行や空白がある場合、それもHTTP出力として扱われます。特に、includerequire で読み込まれるファイルの末尾に ?> と改行がある場合に問題になります。

❌ NGコード:config.php の ?> の後に改行がある
<?php
// config.php
$dbHost = 'localhost';
$dbName = 'mydb';
$dbUser = 'root';
$dbPass = 'password';
?>
                    ← この空行が出力になる!

この config.php を require すると、?> の後の改行がそのまま出力されます。

❌ NGコード:読み込んだファイルの末尾改行が出力に
<?php
require 'config.php';  // ← config.phpの末尾改行で出力が発生

session_start();  // ← Warning発生!
?>

解決策:閉じタグ ?> を省略する

PHPには公式のベストプラクティスがあります。PHPのみのファイルでは閉じタグ ?> を省略することが推奨されています。これはPSR-12(コーディング規約)でも明確に規定されています。

✅ OKコード:閉じタグを省略(推奨)
<?php
// config.php
$dbHost = 'localhost';
$dbName = 'mydb';
$dbUser = 'root';
$dbPass = 'password';
// ★ 閉じタグ ?> を省略することで、末尾の空白・改行問題を完全に回避

PSR-12 コーディング規約(抜粋)

  • PHPのみのファイルでは閉じタグ ?> を省略しなければならない(MUST)
  • ファイルの末尾には空行を1行だけ入れる
  • これにより、意図しない出力を完全に防止できる

注意:HTMLとPHPが混在するテンプレートファイル(例: template.php)では、閉じタグ ?> が必要です。省略するのはPHPのみのファイル(クラスファイル、設定ファイルなど)に限ります。

原因④:include / require での出力

読み込みファイル内の出力

includerequire で読み込んだファイル内で出力が行われるケースです。これは原因の特定が難しく、特に複数のファイルを読み込んでいる場合にデバッグが大変です。

❌ NGコード:読み込みファイル内で出力がある
// ── functions.php ──
<?php
function getUser($id) {
    echo 'Loading user...';  // ← デバッグ出力が残っている!
    // ...
    return $user;
}
?>

// ── index.php ──
<?php
require 'functions.php';
$user = getUser(1);  // ← ここで functions.php 内の echo が実行される

if (!$user) {
    header('Location: /login.php');  // ← Warning発生!
    exit;
}
?>

設定ファイルの閉じタグ問題

先ほども説明した通り、設定ファイルの ?> の後に空白があるケースです。複数のファイルを読み込む場合、どのファイルが原因かの特定が重要です。

❌ NGコード:複数ファイル読み込みで原因が分かりにくい
<?php
require 'config/database.php';     // どれかのファイルの
require 'config/constants.php';    // 末尾に空白があるかも?
require 'lib/functions.php';       // 
require 'lib/auth.php';            // 

session_start();  // ← どのファイルが原因かわからない...

ポイント:エラーメッセージの「output started at」部分を確認すれば、どのファイルで出力が始まったかがわかります。例えば output started at /var/www/html/config/database.php:15 と表示されていれば、database.php の15行目を確認すればOKです。

includeパスの全ファイルを一括チェックするスクリプト

PHP – 全PHPファイルの問題チェック
<?php
/**
 * PHPファイルの「headers already sent」原因を一括チェック
 * - BOM検出
 * - 閉じタグ後の空白検出
 * - 先頭空白検出
 */
function checkHeaderIssues($directory) {
    $issues = [];
    $iterator = new RecursiveIteratorIterator(
        new RecursiveDirectoryIterator($directory)
    );

    foreach ($iterator as $file) {
        if ($file->getExtension() !== 'php') continue;

        $content = file_get_contents($file->getPathname());
        $path = $file->getPathname();

        // 1. BOM チェック
        if (substr($content, 0, 3) === 'xEFxBBxBF') {
            $issues[] = ['file' => $path, 'type' => 'BOM'];
        }

        // 2. <?php の前に空白がないかチェック
        if (preg_match('/^s+<?php/', $content)) {
            $issues[] = ['file' => $path, 'type' => '先頭空白'];
        }

        // 3. 閉じタグ後に空白があるかチェック
        if (preg_match('/?>s+$/', $content)) {
            $issues[] = ['file' => $path, 'type' => '閉じタグ後の空白'];
        }
    }

    return $issues;
}

// 実行
$issues = checkHeaderIssues('/var/www/html');

if (empty($issues)) {
    echo '問題は見つかりませんでした。
';
} else {
    echo count($issues) . ' 件の問題が見つかりました:
';
    foreach ($issues as $issue) {
        echo '  [' . $issue['type'] . '] ' . $issue['file'] . '
';
    }
}

output_buffering による回避

出力バッファリングとは

PHPには「出力バッファリング」という機能があります。通常、echo などで出力されたデータは即座にブラウザに送信されますが、出力バッファリングを有効にすると、出力をいったんメモリに蓄積し、後でまとめて送信することができます。

出力バッファリングのイメージ
【バッファリングなし(通常)】
echo "A"  →  即座に送信 → ブラウザ(ヘッダー送信済み)
echo "B"  →  即座に送信 → ブラウザ
header()  →  エラー!(ヘッダー送信済み)

【バッファリングあり】
ob_start()
echo "A"  →  バッファに蓄積(まだ送信しない)
echo "B"  →  バッファに蓄積(まだ送信しない)
header()  →  OK!(まだ何も送信していない)
ob_end_flush()  →  バッファの内容をまとめて送信

ob_start() / ob_end_flush() の使い方

PHP – ob_start() で出力バッファリング
<?php
// ★ 出力バッファリングを開始
ob_start();

// これらの出力はバッファに蓄積される(まだブラウザに送信されない)
echo ''<!DOCTYPE html>'';
echo ''<html><head><title>テスト</title></head>'';
echo ''<body>'';

// 何かの条件でリダイレクトが必要になった場合
if ($needRedirect) {
    // バッファをクリア(今までの出力を破棄)
    ob_end_clean();

    // ヘッダーはまだ送信されていないのでリダイレクト可能!
    header(''Location: /other.php'');
    exit;
}

echo ''<p>コンテンツ</p>'';
echo ''</body></html>'';

// ★ バッファの内容をまとめて送信
ob_end_flush();

出力バッファリング関数の一覧

関数 動作 バッファ
ob_start() 出力バッファリング開始 作成
ob_end_flush() バッファの内容を出力して閉じる 送信&削除
ob_end_clean() バッファの内容を破棄して閉じる 破棄&削除
ob_get_contents() バッファの内容を取得(送信しない) 維持
ob_get_clean() バッファの内容を取得して閉じる 取得&削除
ob_flush() バッファの内容を出力(閉じない) 送信&クリア
ob_get_level() ネストレベルを取得 変更なし
ob_get_length() バッファの長さを取得 変更なし

php.ini の output_buffering 設定

php.inioutput_buffering 設定で、全PHPスクリプトに自動的に出力バッファリングを適用することができます。

php.ini
; 出力バッファリングの設定

; Off: バッファリング無効(デフォルト)
output_buffering = Off

; On: バッファリング有効(バッファサイズ無制限)
output_buffering = On

; 4096: 4KBのバッファを使用(推奨)
output_buffering = 4096
設定値 動作 メリット デメリット
Off バッファリング無効 メモリ使用量が最小 headers already sent エラーが発生しやすい
4096 4KBまでバッファ 小さな出力の場合にheader問題を回避 4KBを超えるとヘッダーが送信される
On 無制限バッファ header問題がほぼ発生しない メモリ使用量が増加する可能性

注意:output_buffering は「headers already sent」エラーを回避する便利な設定ですが、根本的な解決策ではありません。コードの構造自体を改善し、出力前にヘッダー操作を完了させるのがベストプラクティスです。

バッファリングのネスト(入れ子)

出力バッファは入れ子にすることも可能です。

PHP – バッファのネスト
<?php
ob_start();  // レベル 1
echo ''A'';

ob_start();  // レベル 2
echo ''B'';

$innerContent = ob_get_clean();  // レベル 2 の内容を取得("B")

echo ''C'';
$outerContent = ob_get_clean();  // レベル 1 の内容を取得("AC")

echo ''Inner: '' . $innerContent;  // "Inner: B"
echo ''Outer: '' . $outerContent;  // "Outer: AC"

実用例:テンプレートシステムでの活用

PHP – テンプレートエンジン風の実装
<?php
/**
 * シンプルなテンプレートレンダリング
 * 出力バッファリングを使ってテンプレートファイルの出力を文字列として取得
 */
function render($template, $variables = []) {
    // 変数を展開
    extract($variables);

    // 出力バッファリング開始
    ob_start();

    // テンプレートファイルを読み込み(出力はバッファへ)
    include $template;

    // バッファの内容を文字列として取得
    return ob_get_clean();
}

// 使用例
$html = render(''templates/header.php'', [''title'' => ''マイページ'']);

// この時点ではまだ何も出力していないので、ヘッダー操作が可能
header(''Content-Type: text/html; charset=UTF-8'');
echo $html;

gzip圧縮と出力バッファリングの組み合わせ

出力バッファリングは gzip 圧縮と組み合わせることもできます。ob_start() にコールバック関数を渡すことで、バッファの内容を加工してから送信できます。

PHP – gzip圧縮付き出力バッファリング
<?php
// gzip圧縮付きでバッファリング開始
ob_start(''ob_gzhandler'');

// 通常通りコーディング
header(''Content-Type: text/html; charset=UTF-8'');
echo ''<h1>Hello World</h1>'';

// スクリプト終了時に自動的にバッファがフラッシュされ、gzip圧縮される

ob_start() のコールバック関数

  • ob_start(''ob_gzhandler'') – gzip圧縮
  • ob_start(function($buffer) { return strtoupper($buffer); }) – カスタム加工
  • ob_start(null, 4096) – バッファサイズを指定(4KB)

WordPress での「headers already sent」エラー

WordPress を使っている場合、「headers already sent」エラーは特に頻繁に発生します。WordPress は内部的に wp_redirect()setcookie()session_start() などを多用しているため、わずかな不注意でこのエラーが発生します。

wp-config.php の BOM 問題

最も多いケースが、wp-config.php にBOM(バイトオーダーマーク)が含まれているパターンです。特にWindowsのメモ帳(Notepad)で編集した場合に起きやすいです。

エラー例
Warning: Cannot modify header information - headers already sent by
(output started at /var/www/html/wp-config.php:1)
in /var/www/html/wp-includes/pluggable.php on line 1299

ポイント:「output started at wp-config.php:1」と表示されている場合は、ほぼ間違いなくBOMが原因です。ファイルの1行目から出力が始まっているのは、目に見えないBOMバイトが先頭にあることを意味します。

解決方法

  1. wp-config.php を VS Code や Notepad++ で開く
  2. エンコーディングが「UTF-8 with BOM」になっていないか確認
  3. 「UTF-8」(BOMなし)で保存し直す
bash – wp-config.php の BOM を除去
# BOM があるか確認
hexdump -C -n 3 wp-config.php
# ef bb bf が表示されたらBOMあり

# BOM を除去
sed -i ''1s/^xEFxBBxBF//'' wp-config.php

プラグイン / テーマの出力問題

プラグインやテーマのファイルに問題がある場合もあります。特に以下のケースが多いです。

ファイル よくある原因 解決方法
functions.php ファイル末尾の ?> の後に空行がある ?> を削除する
functions.php ファイル先頭に BOM がある BOM なし UTF-8 で保存
プラグインファイル プラグイン内で直接 echo している プラグインを無効化して確認
wp-config.php BOM、先頭/末尾の空白 BOM除去、空白削除

wp_redirect() でのエラー

WordPress の wp_redirect() 関数は内部的に header() を使っているため、同じエラーが発生します。

❌ NGコード:WordPress テンプレート内でリダイレクト
// テーマの header.php の途中でリダイレクトしようとする
<!DOCTYPE html>
<html>
<head>
<?php
if (!is_user_logged_in()) {
    wp_redirect(wp_login_url());  // ← Warning発生!
    exit;
}
?>
✅ OKコード:template_redirect フックを使用
// functions.php に記述
<?php
// ★ template_redirect フックなら出力前に実行される
add_action(''template_redirect'', function() {
    if (is_page(''members-only'') && !is_user_logged_in()) {
        wp_redirect(wp_login_url(get_permalink()));
        exit;
    }
});

WordPress で使えるリダイレクト用フック

フック名 実行タイミング 用途
template_redirect テンプレート読み込み前 フロント側のリダイレクト
admin_init 管理画面の初期化時 管理画面でのリダイレクト
wp_loaded WordPress完全読み込み後 初期化後のリダイレクト
init WordPress初期化時 早い段階でのリダイレクト
send_headers ヘッダー送信時 カスタムヘッダーの追加

functions.php の注意点

WordPress の functions.php は特に注意が必要なファイルです。以下のポイントを確認してください。

✅ functions.php のベストプラクティス
<?php                           // ← 1行目1文字目から開始(BOM・空白なし)
/**
 * Theme functions and definitions
 */

// テーマサポート
add_action(''after_setup_theme'', function() {
    add_theme_support(''title-tag'');
    add_theme_support(''post-thumbnails'');
});

// スタイルシートの読み込み
add_action(''wp_enqueue_scripts'', function() {
    wp_enqueue_style(''theme-style'', get_stylesheet_uri());
});

// ★ 閉じタグ ?> は書かない!(末尾の空白問題を防止)

注意:functions.php を FTP でアップロードする際に、文字化けやエンコーディングの変換が起きることがあります。FTPクライアントの転送モードを「バイナリ」に設定してアップロードしてください。

WordPress プラグインが原因の場合の特定方法

プラグインが原因の場合、以下の手順で特定します。

プラグイン原因特定の手順

  1. すべてのプラグインを無効化する
  2. エラーが消えるか確認する
  3. プラグインを1つずつ有効化する
  4. エラーが再発するプラグインを特定する
  5. そのプラグインのファイルを確認する(BOM、末尾空白など)
WP-CLI でプラグインを一括無効化
# 全プラグインを無効化
wp plugin deactivate --all

# 1つずつ有効化してテスト
wp plugin activate plugin-name

# エラーログを確認
tail -f /var/log/apache2/error.log

WordPress のデバッグモード

WordPress には独自のデバッグ機能があります。wp-config.php に以下の設定を追加することで、エラーの詳細をログファイルに記録できます。

wp-config.php – デバッグ設定
// デバッグモードを有効化
define(''WP_DEBUG'', true);

// エラーを画面に表示しない(本番環境向け)
define(''WP_DEBUG_DISPLAY'', false);

// エラーをログファイルに記録
define(''WP_DEBUG_LOG'', true);

// ログファイル: wp-content/debug.log に出力される

フレームワークでの対処法

Laravel での対処

Laravel では、フレームワーク自体が出力バッファリングとレスポンスオブジェクトを管理しているため、通常は「headers already sent」エラーが発生しにくい設計になっています。しかし、以下のような場合にエラーが起きることがあります。

原因 説明
設定ファイルのBOM config/*.php や routes/*.php にBOMが含まれている
直接 echo している コントローラー内で Response を返さず echo している
ミドルウェアでの出力 ミドルウェア内でデバッグ出力している
サービスプロバイダーでの出力 boot() メソッド内で echo している
❌ NGコード:Laravel コントローラーで直接 echo
<?php

namespace AppHttpControllers;

class UserController extends Controller
{
    public function login(Request $request)
    {
        echo ''Debug: login method called'';  // ← 直接 echo はNG!

        if (Auth::attempt($request->only(''email'', ''password''))) {
            return redirect()->intended(''/dashboard'');  // ← エラー発生の可能性
        }
    }
}
✅ OKコード:Laravel の Response オブジェクトを使用
<?php

namespace AppHttpControllers;

use IlluminateSupportFacadesLog;

class UserController extends Controller
{
    public function login(Request $request)
    {
        // ★ デバッグはログに出力
        Log::debug(''login method called'');

        if (Auth::attempt($request->only(''email'', ''password''))) {
            // ★ Response オブジェクトを return
            return redirect()->intended(''/dashboard'');
        }

        return back()->withErrors([''email'' => ''認証に失敗しました'']);
    }
}

Laravel ミドルウェアでのヘッダー設定

PHP – Laravel ミドルウェアでカスタムヘッダーを追加
<?php

namespace AppHttpMiddleware;

class SecurityHeaders
{
    public function handle($request, Closure $next)
    {
        // ★ レスポンスオブジェクトを通じてヘッダーを設定
        $response = $next($request);

        $response->headers->set(''X-Frame-Options'', ''SAMEORIGIN'');
        $response->headers->set(''X-Content-Type-Options'', ''nosniff'');
        $response->headers->set(''X-XSS-Protection'', ''1; mode=block'');

        return $response;
    }
}

CakePHP での対処

CakePHP でも同様に、レスポンスオブジェクトを通じてヘッダーを設定します。

PHP – CakePHP でのリダイレクト
<?php
// CakePHP 4.x
class UsersController extends AppController
{
    public function login()
    {
        if ($this->request->is(''post'')) {
            $user = $this->Auth->identify();
            if ($user) {
                $this->Auth->setUser($user);
                // ★ レスポンスオブジェクトを return
                return $this->redirect($this->Auth->redirectUrl());
            }
            $this->Flash->error(''ログインに失敗しました'');
        }
    }
}

フレームワークが自動的にバッファリングする仕組み

モダンなPHPフレームワークは、以下のような仕組みで「headers already sent」問題を解消しています。

フレームワークの処理フロー
1. リクエスト受信2. フレームワークの初期化(ob_start() が暗黙的に実行される)
   ↓
3. ルーティング → コントローラー実行4. Response オブジェクトの構築
   - ヘッダー情報の蓄積
   - ボディ(HTML/JSON)の蓄積
   ↓
5. Response の送信
   - まずヘッダーを送信
   - 次にボディを送信6. 完了

フレームワークでの鉄則

  • コントローラーでは直接 echo を使わない
  • 必ず Response オブジェクト(またはビュー)を return する
  • デバッグ出力には専用のロガー(Log::debug() 等)を使う
  • ヘッダーの設定はミドルウェアまたは Response オブジェクトで行う

デバッグテクニック

エラーメッセージの「output started at」の読み方(詳細)

エラーメッセージには、問題を特定するための重要な情報が含まれています。もう一度、詳しく見てみましょう。

エラーメッセージの解読
Warning: Cannot modify header information - headers already sent by
(output started at /var/www/html/includes/config.php:42)
in /var/www/html/login.php on line 15

解読:/var/www/html/includes/config.php の42行目 で出力が開始された
  → この場所を確認すれば原因がわかる

・/var/www/html/login.php の15行目 で header() を呼んだ
  → ここで header() / setcookie() / session_start() が呼ばれている
行番号のパターン 可能性が高い原因
output started at file.php:1 BOM、またはファイル先頭の空白
output started at file.php:最終行 閉じタグ ?> の後の空白・改行
output started at file.php:中間行 echo / print / HTML出力
output started at error_handler 先行するエラーメッセージが出力に

headers_sent() 関数でデバッグ

headers_sent() 関数を使うと、ヘッダーがすでに送信されたかどうかを確認できます。さらに、どのファイルの何行目で送信されたかも取得できます。

PHP – headers_sent() の使い方
<?php
// 基本的な使い方
if (headers_sent()) {
    echo ''ヘッダーは送信済みです'';
} else {
    echo ''ヘッダーはまだ送信されていません'';
}

// ★ 詳細情報付き(ファイル名と行番号を取得)
if (headers_sent($file, $line)) {
    echo ''ヘッダーが送信された場所: '' . $file . '' 行 '' . $line;
} else {
    // ヘッダー未送信 → header() が使える
    header(''Location: /redirect.php'');
    exit;
}

デバッグ用ヘルパー関数

PHP – headers already sent デバッグヘルパー
<?php
/**
 * ヘッダー送信状況をデバッグ出力する関数
 * 開発環境でのみ使用すること
 */
function debugHeaders() {
    if (headers_sent($file, $line)) {
        error_log(sprintf(
            ''[DEBUG] Headers already sent in %s on line %d'',
            $file,
            $line
        ));
        return false;
    }

    // 現在設定されているヘッダーを確認
    $headers = headers_list();
    error_log(''[DEBUG] Current headers: '' . implode('', '', $headers));
    return true;
}

// 使用例
if (debugHeaders()) {
    header(''Location: /next.php'');
    exit;
}

Hex Editor で BOM を確認する方法

GUIのHex Editor を使えば、ファイルのバイト列を視覚的に確認できます。

ツール プラットフォーム 入手方法
VS Code Hex Editor 全プラットフォーム 拡張機能「Hex Editor」をインストール
HxD Windows 無料ダウンロード
Hex Fiend macOS 無料ダウンロード
xxd(コマンド) Linux / macOS 標準インストール済み
bash – xxd コマンドで確認
# xxd でファイルの先頭を確認
xxd -l 20 config.php

# 出力例(BOMあり)
00000000: efbb bf3c 3f70 6870 0a24  ...<?php.$
#         ^^^^^^ BOM(EF BB BF)

# 出力例(BOMなし - 正常)
00000000: 3c3f 7068 700a 2464 6248  <?php.$dbH

よくある原因の確認チェックリスト

「headers already sent」エラーが発生した場合、以下のチェックリストを上から順に確認してください。

デバッグチェックリスト
□ 1. エラーメッセージの「output started at」を確認した
□ 2. 指定されたファイルの指定行を確認した
□ 3. echo / print / var_dump がないか確認した
□ 4. PHPタグの前に空白・BOMがないか確認した
□ 5. PHPタグの後(?>の後)に空白がないか確認した
□ 6. include / require しているファイルも確認した
□ 7. エラーメッセージ(Notice/Warning)が出力されていないか確認した
□ 8. HTMLが header() より前にないか確認した

実践的なコードパターン集

パターン1:ログイン → リダイレクトの正しい実装

PHP – ログイン処理の安全な実装
<?php
// ★ ファイルの先頭でセッションとヘッダー操作を完了させる
session_start();

$error = '''';

if ($_SERVER[''REQUEST_METHOD''] === ''POST'') {
    $email = $_POST[''email''] ?? '''';
    $password = $_POST[''password''] ?? '''';

    // 認証処理
    $user = authenticate($email, $password);

    if ($user) {
        // セッションにユーザー情報を保存
        $_SESSION[''user_id''] = $user[''id''];
        $_SESSION[''user_name''] = $user[''name''];

        // ★ 出力前にリダイレクト → header() は問題なく動作
        header(''Location: /dashboard.php'');
        exit;
    }

    $error = ''メールアドレスまたはパスワードが正しくありません。'';
}
// ★ ここから HTML 出力開始(リダイレクトが不要な場合のみ到達)
?>
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>ログイン</title>
</head>
<body>
    <?php if ($error): ?>
        <p class="error"><?= htmlspecialchars($error) ?></p>
    <?php endif; ?>

    <form method="post">
        <input type="email" name="email" required>
        <input type="password" name="password" required>
        <button type="submit">ログイン</button>
    </form>
</body>
</html>

パターン2:ファイルダウンロード

PHP – ファイルダウンロードの正しい実装
<?php
// ★ ファイルダウンロード処理は出力の前に完結させる

$filePath = ''/var/www/files/document.pdf'';

if (!file_exists($filePath)) {
    http_response_code(404);
    echo ''ファイルが見つかりません'';
    exit;
}

// ★ すべてのヘッダーを出力前に設定
header(''Content-Type: application/pdf'');
header(''Content-Disposition: attachment; filename="document.pdf"'');
header(''Content-Length: '' . filesize($filePath));
header(''Cache-Control: no-cache, must-revalidate'');

// ★ ヘッダー設定完了後にファイル内容を出力
readfile($filePath);
exit;

パターン3:API レスポンス(JSON)

PHP – JSON APIレスポンスの正しい実装
<?php
/**
 * JSON レスポンスを返すヘルパー関数
 */
function jsonResponse($data, $statusCode = 200) {
    // ★ ヘッダーを先に設定
    http_response_code($statusCode);
    header(''Content-Type: application/json; charset=UTF-8'');
    header(''Access-Control-Allow-Origin: *'');

    // ★ ヘッダー設定後に JSON 出力
    echo json_encode($data, JSON_UNESCAPED_UNICODE);
    exit;
}

// 使用例
$users = getUsers();

if (empty($users)) {
    jsonResponse([''error'' => ''ユーザーが見つかりません''], 404);
}

jsonResponse([''data'' => $users]);

パターン4:Cookie ベースの言語切替

PHP – 言語切替の安全な実装
<?php
// ★ Cookie設定はファイルの先頭で
$allowedLanguages = [''ja'', ''en'', ''zh''];

if (isset($_GET[''lang'']) && in_array($_GET[''lang''], $allowedLanguages)) {
    // ★ 出力前に Cookie を設定してリダイレクト
    setcookie(''language'', $_GET[''lang''], [
        ''expires'' => time() + 86400 * 365,
        ''path'' => ''/'',
        ''httponly'' => true,
        ''samesite'' => ''Lax'',
    ]);

    // 言語パラメータを除いてリダイレクト
    $redirectUrl = strtok($_SERVER[''REQUEST_URI''], ''?'');
    header(''Location: '' . $redirectUrl);
    exit;
}

// 現在の言語を取得
$currentLang = $_COOKIE[''language''] ?? ''ja'';
// ここから HTML 出力...

パターン5:アクセス制御(認証チェック)

PHP – 認証チェック共通ファイル
<?php
// auth_check.php - 認証が必要なページの先頭で require する

session_start();

function requireLogin($redirectTo = ''/login.php'') {
    if (!isset($_SESSION[''user_id''])) {
        // 現在のURLをセッションに保存(ログイン後にリダイレクト用)
        $_SESSION[''redirect_after_login''] = $_SERVER[''REQUEST_URI''];

        // ★ ヘッダー操作はこの関数内で完結
        header(''Location: '' . $redirectTo);
        exit;
    }
}

// ★ このファイルは閉じタグを書かない
PHP – 認証が必要なページでの使い方
<?php
// ★ 必ずファイルの先頭で require
require_once ''auth_check.php'';
requireLogin();

// ここに到達 = ログイン済み
?>
<!DOCTYPE html>
<html>
<head><title>会員ページ</title></head>
<body>
    <h1>ようこそ、<?= htmlspecialchars($_SESSION[''user_name'']) ?>さん</h1>
</body>
</html>

よくある質問(FAQ)

Q. output_buffering を On にすれば解決しますか?

output_buffering = On にすれば多くの場合エラーは出なくなりますが、根本的な解決策ではありません。出力バッファリングはあくまで一時的な回避策です。コードの構造を改善し、ヘッダー操作を出力前に行うようにするのがベストプラクティスです。

Q. header() の代わりに JavaScript でリダイレクトしてもいいですか?

緊急回避策としては使えますが、以下のデメリットがあります。

方法 メリット デメリット
header() リダイレクト 高速、SEO対応、サーバーサイド完結 出力前に実行が必要
JavaScript リダイレクト 出力後でも使える JS無効環境で動作しない、SEO的に不利、一瞬ページが表示される
meta refresh リダイレクト JS無効でも動作 SEO的に不利、一瞬ページが表示される、遅延がある

Q. PHP 8 でもこのエラーは発生しますか?

はい、PHP 8 でも同様に発生します。これはPHP自体のバージョンではなく、HTTPプロトコルの仕様に基づくエラーです。PHPのバージョンに関係なく、出力後にヘッダーを送信しようとすればエラーになります。

Q. なぜ開発環境では発生せず、本番環境で発生するのですか?

開発環境の php.inioutput_buffering = 4096 が設定されている場合があります。この場合、4KB以内の出力であればバッファに蓄積されるため、エラーが発生しません。本番環境で output_buffering = Off の場合、同じコードでもエラーが発生します。

PHP – output_buffering の設定を確認
<?php
// 現在の output_buffering 設定を確認
echo ''output_buffering: '' . ini_get(''output_buffering'');

// phpinfo() でも確認可能
phpinfo();

Q. 「output started at」にファイル名が表示されない場合は?

まれに (output started at) とだけ表示され、ファイル名が省略される場合があります。これは、Apache や nginx のモジュールが出力を開始した場合や、php.ini の設定で出力が行われた場合に起きます。この場合は、headers_sent($file, $line) を使ってプログラム内で原因を特定してください。

まとめ

原因チェックリスト

チェック項目 確認方法 解決策
echo/print が header() より前にある コードを確認 header() を echo より前に移動
ファイル先頭に BOM がある hexdump / VS Code BOMなしUTF-8で保存
ファイル先頭に空白/改行がある エディタで確認 <?php を1行目1文字目に
閉じタグ ?> の後に空白がある エディタで確認 ?> を省略する(推奨)
include ファイル内で出力がある エラーメッセージ確認 該当ファイルの出力を修正
エラーメッセージが先に出力される エラーログ確認 display_errors=Off + エラー修正
HTML が PHP処理より前にある コードを確認 PHP処理をファイルの先頭に移動

解決フローチャート

解決フローチャート
エラー発生
    │
    ▼
「output started at」のファイル名と行番号を確認
    │
    ├── 行番号 = 1
    │   ├── BOM があるか確認 → BOM除去
    │   └── 先頭に空白があるか確認 → 空白を削除
    │
    ├── 行番号 = 最終行
    │   └── ?> の後に空白があるか確認 → ?> を省略
    │
    ├── 行番号 = 中間行
    │   ├── echo / print / var_dump がある → 移動または削除
    │   ├── HTML出力がある → PHP処理を先頭に移動
    │   └── エラーメッセージが出力 → エラーの原因を修正
    │
    └── include先のファイル
        └── 上記と同じチェックを include ファイルに対して実行

ベストプラクティスまとめ

「headers already sent」を防ぐためのベストプラクティス

  1. PHPのみのファイルでは閉じタグ ?> を省略する
  2. ファイルは BOM なし UTF-8 で保存する
  3. ヘッダー操作(header, setcookie, session_start)はファイルの先頭で行う
  4. 「処理」→「出力」の順にコードを構成する
  5. デバッグ出力には echo ではなく error_log() を使う
  6. フレームワークでは Response オブジェクトを return する
  7. 本番環境では display_errors = Off にする
  8. コードレビュー時に include ファイルの末尾空白もチェックする

「Cannot modify header information – headers already sent」は、PHP開発において避けては通れないエラーです。しかし、このエラーの仕組みを正しく理解し、上記のベストプラクティスを実践すれば、もう二度と悩まされることはないでしょう。

エラーが発生した場合は、まず「output started at」の情報を確認することから始めてください。ほとんどの場合、そこに解決のヒントがあります。

エラー発生の仕組みを図解で理解する

ここまでの内容を図解で整理しましょう。PHPの処理フローにおいて、どのタイミングでヘッダーが送信され、いつエラーが発生するのかを視覚的に理解することが大切です。

正常な処理フロー

正常な処理フロー

┌─────────────────────────────────────────────┐
│  PHPスクリプト実行開始                       │
│                                             │
│  ① session_start();                         │
│     → セッションCookie をヘッダーリストに追加  │
│                                             │
│  ② header(''Content-Type: text/html'');      │
│     → Content-Type をヘッダーリストに追加     │
│                                             │
│  ③ setcookie(''token'', ''abc123'');          │
│     → Set-Cookie をヘッダーリストに追加       │
│                                             │
│  ④ echo ''<html>...'';                        │
│     → ここでヘッダーリストが一括送信される     │
│     → 続いてボディ(HTML)の送信が開始        │
│                                             │
│  ⑤ echo ''...</html>'';                      │
│     → 追加のボディを送信                     │
│                                             │
│  スクリプト終了                               │
└─────────────────────────────────────────────┘

結果:✅ 正常にレスポンスが送信される

エラーが発生する処理フロー

エラーが発生する処理フロー

┌─────────────────────────────────────────────┐
│  PHPスクリプト実行開始                       │
│                                             │
│  ① echo ''Hello'';                            │
│     → デフォルトヘッダーが自動送信される       │
│     → ボディの送信が開始される               │
│     → ★ この時点でヘッダーは確定・変更不可    │
│                                             │
│  ② session_start();                         │
│     → セッションCookie を送信しようとする     │
│     → ❌ Warning: headers already sent!      │
│                                             │
│  ③ header(''Location: /other.php'');         │
│     → Location ヘッダーを送信しようとする     │
│     → ❌ Warning: headers already sent!      │
│                                             │
│  スクリプト終了                               │
└─────────────────────────────────────────────┘

結果:❌ リダイレクトが機能しない、セッションが開始できない

output_buffering 使用時の処理フロー

output_buffering 使用時

┌─────────────────────────────────────────────┐
│  ob_start()  ← バッファリング開始             │
│                                             │
│  ① echo ''Hello'';                            │
│     → バッファに蓄積(まだ送信しない)        │
│     → ヘッダーはまだ未送信                  │
│                                             │
│  ② session_start();                         │
│     → ✅ OK!ヘッダーはまだ送信されていない   │
│                                             │
│  ③ header(''Location: /other.php'');         │
│     → ✅ OK!ヘッダーリストに追加            │
│                                             │
│  ob_end_flush() or スクリプト終了            │
│     → ヘッダーを送信                         │
│     → バッファの内容を送信                    │
└─────────────────────────────────────────────┘

結果:✅ ヘッダーが正しく送信される

PHPバージョン別の注意点

PHPバージョン 関連する変更点
PHP 5.x output_buffering のデフォルトが環境によって異なる。XAMPP では 4096 がデフォルトのことが多い。
PHP 7.x session_start() のオプションが追加。session_start([''cookie_lifetime'' => 86400]) のような配列形式が使用可能。
PHP 8.0 多くの Notice が Warning に昇格。undefined variable が Warning になるため、display_errors=On の場合に headers already sent が発生しやすくなった。
PHP 8.1+ Fibers が追加されたが、headers already sent の動作に変更なし。readonly プロパティの関連エラーが Warning を出す可能性あり。

サーバー環境別の設定

Apache (.htaccess)

Apache を使っている場合、.htaccess で PHP の設定を変更できます。

.htaccess
# 出力バッファリングを有効化
php_value output_buffering 4096

# エラー表示を無効化(本番環境)
php_flag display_errors Off

# エラーログを有効化
php_flag log_errors On
php_value error_log /var/log/php/error.log

nginx

nginx を使っている場合は、.htaccess は使えません。php.ini または php-fpm.conf で設定します。

php-fpm.conf
; PHP-FPM プールの設定
[www]
php_admin_value[output_buffering] = 4096
php_admin_flag[display_errors] = off
php_admin_flag[log_errors] = on
php_admin_value[error_log] = /var/log/php-fpm/error.log

XAMPP(ローカル開発環境)

XAMPP の php.ini の場所と設定
# XAMPP の php.ini の場所(Windows)
C:xamppphpphp.ini

# XAMPP の php.ini の場所(Mac)
/Applications/XAMPP/etc/php.ini

# 設定内容
output_buffering = 4096    ; XAMPP のデフォルト
display_errors = On        ; 開発用にON
error_reporting = E_ALL    ; 全エラーを報告

ポイント:XAMPP では output_buffering = 4096 がデフォルトで設定されていることが多いため、開発環境ではエラーが出ないのに本番環境(output_buffering = Off)ではエラーが出る、というケースが頻繁にあります。開発と本番で同じ設定にするか、コード側で対策するのが安全です。

レンタルサーバー(エックスサーバー、さくら、ロリポップ等)

レンタルサーバーでは php.ini を直接編集できないことが多いですが、以下の方法で設定を変更できます。

サーバー 設定方法
エックスサーバー サーバーパネル → php.ini設定、または .user.ini ファイル
さくらサーバー コントロールパネル → PHP設定、または .user.ini
ロリポップ ユーザー専用ページ → PHP設定
ConoHa WING コントロールパネル → PHP設定
.user.ini(レンタルサーバー用)
; .user.ini ファイルをドキュメントルートに配置
output_buffering = 4096
display_errors = Off
log_errors = On

自動テスト:headers already sent を事前に検出する

CI/CD パイプラインに組み込んで、デプロイ前に問題を自動検出するスクリプトを作成できます。

PHP – 自動チェックスクリプト
<?php
/**
 * PHPファイルの headers already sent 原因を自動検出するスクリプト
 * CI/CDパイプラインに組み込んで使用
 *
 * 使い方: php check_headers.php /path/to/project
 */

$exitCode = 0;
$directory = $argv[1] ?? ''.'';

$iterator = new RecursiveIteratorIterator(
    new RecursiveDirectoryIterator(
        $directory,
        RecursiveDirectoryIterator::SKIP_DOTS
    )
);

foreach ($iterator as $file) {
    if ($file->getExtension() !== ''php'') continue;

    // vendor ディレクトリはスキップ
    if (strpos($file->getPathname(), ''vendor'') !== false) continue;

    $content = file_get_contents($file->getPathname());
    $path = $file->getPathname();

    // Check 1: BOM
    if (substr($content, 0, 3) === '''') {
        echo ''[ERROR] BOM detected: '' . $path . ''
'';
        $exitCode = 1;
    }

    // Check 2: 先頭空白
    if (preg_match(''/^s+<?php/'', $content)) {
        echo ''[WARN] Leading whitespace before <?php: '' . $path . ''
'';
        $exitCode = 1;
    }

    // Check 3: 閉じタグ後の空白(PHPのみのファイル)
    if (preg_match(''/?>s+$/'', $content)) {
        // HTMLテンプレートファイルは除外
        if (strpos($content, ''<html'') === false) {
            echo ''[WARN] Trailing whitespace after ?>: '' . $path . ''
'';
            $exitCode = 1;
        }
    }
}

if ($exitCode === 0) {
    echo ''All checks passed! No header issues found.
'';
}

exit($exitCode);
bash – CI/CDでの使用例(GitHub Actions)
# .github/workflows/check.yml
name: PHP Header Check
on: [push, pull_request]

jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Check for header issues
        run: php check_headers.php src/

関連する PHP エラーとの違い

「headers already sent」と混同しやすいエラーがあります。それぞれの違いを理解しておきましょう。

エラー 原因 対処法
Cannot modify header information 出力後にヘッダーを変更しようとした 本記事で解説した方法
Session already started session_start() を2回呼んだ session_status() でチェック
Cannot send session cookie session_start() 時にヘッダー送信済み 本記事の session_start() セクション参照
Cannot send session cache limiter session_start() 時にヘッダー送信済み 同上
PHP – session_start() の安全な呼び出し
<?php
// ★ セッションが既に開始されているか確認してから開始
if (session_status() === PHP_SESSION_NONE) {
    session_start();
}

// PHP 7.0+ では session_start() にオプションを渡せる
if (session_status() === PHP_SESSION_NONE) {
    session_start([
        ''cookie_lifetime'' => 86400,
        ''cookie_httponly'' => true,
        ''cookie_secure'' => true,
        ''cookie_samesite'' => ''Lax'',
    ]);
}

エディタ・IDE の設定で予防する

エディタやIDEの設定を適切に行うことで、「headers already sent」の原因を事前に防ぐことができます。

VS Code の推奨設定

settings.json – VS Code の推奨設定
{
    // ファイルのデフォルトエンコーディング(BOMなし)
    "files.encoding": "utf8",

    // エンコーディングの自動推測を無効化(BOM付きファイルの意図しない変換を防ぐ)
    "files.autoGuessEncoding": false,

    // ファイル末尾に改行を挿入
    "files.insertFinalNewline": true,

    // ファイル末尾の余分な改行を削除
    "files.trimFinalNewlines": true,

    // 行末の空白を削除
    "files.trimTrailingWhitespace": true
}

.editorconfig の設定

プロジェクトに .editorconfig を配置することで、チームメンバー全員のエディタ設定を統一できます。

.editorconfig
# .editorconfig
root = true

[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

[*.php]
indent_style = space
indent_size = 4

ポイント:charset = utf-8 を指定することで、BOMなしのUTF-8がデフォルトになります。trim_trailing_whitespace = true で末尾の空白も自動的に除去されるため、閉じタグ後の空白問題も防げます。

PHPの内部処理:なぜこのエラーが起きるのか(上級者向け)

最後に、PHPの内部処理レベルでこのエラーがなぜ発生するのかを解説します。

PHPの出力フロー

PHPは以下のレイヤーを通じて出力を行います。

PHPの出力レイヤー

PHP スクリプト
    │ echo, print, var_dump, etc.
    ▼
ユーザーレベルの出力バッファ(ob_start() で作成)
    │ ob_flush() / ob_end_flush()
    ▼
デフォルト出力バッファ(php.ini output_buffering で制御)
    │
    ▼
SAPI(Server API)出力ハンドラー
    │ ← この時点でヘッダーが送信される
    ▼
Webサーバー(Apache / nginx)
    │
    ▼
クライアント(ブラウザ)

PHPの内部では、最初のバイトが SAPI 出力ハンドラーに到達した時点で、sapi_send_headers() が呼ばれてHTTPヘッダーが送信されます。以降の header() 呼び出しは、すでにヘッダーが送信済みであるため失敗します。

PHP C言語ソースコード(概念的な疑似コード)
// PHP内部の header() 関数の実装(概念)
PHP_FUNCTION(header) {
    // ヘッダーが既に送信されているか確認
    if (SG(headers_sent)) {
        // 送信済みなら Warning を出して return
        php_error_docref(
            NULL,
            E_WARNING,
            "Cannot modify header information - "
            "headers already sent by (output started at %s:%d)",
            SG(output_start_filename),
            SG(output_start_lineno)
        );
        return;
    }

    // ヘッダーリストに追加
    sapi_header_op(SAPI_HEADER_REPLACE, &sapi_header);
}

この記事で解説した全パターンと対処法を押さえておけば、「Cannot modify header information – headers already sent」エラーにもう悩むことはありません。エラーメッセージをよく読み、原因を特定して、適切な方法で解決しましょう。