PHP の file_get_contents() は、ファイルの中身を文字列として読み込む関数です。引数には、ファイルのパスのほか、http:// や https:// で始まる URL も渡せます。ところが、同じ PHP ファイルを読んでいるつもりでも、パスで読むか URL で読むかで、返ってくる内容がまったく違います。この違いと、本当にやりたいこと (処理結果を使うこと) に合った書き方を整理します。

パスで読むとソース、URL で読むと実行結果

次の 2 行は、どちらも header.php を読んでいるように見えます。

<?php
// ファイルのパスで読む: header.php の中身 (PHP のソース) がそのまま返る
$source = file_get_contents(__DIR__ . '/header.php');

// URL で読む: Web サーバーが header.php を実行した結果 (HTML) が返る
$html = file_get_contents('https://example.com/header.php');

パスで読んだときは、ディスク上のファイルをただのテキストとして読むだけです。<?php から始まるソースが、実行されないまま文字列になります。これをそのまま echo すると、画面には見えなくても、ページのソースを表示すれば PHP のコードが読めてしまいます。データベースのパスワードなどを書いたファイルなら、それが漏れることになります。

URL で読んだときは、PHP が HTTP のクライアントになって Web サーバーにリクエストを送ります。受け取った Web サーバーは、ふつうのアクセスと同じように header.php を実行し、その出力を返します。ブラウザーで開いたときと同じ結果になるのはこのためです。

つまり、file_get_contents() は「読む」だけで、「実行する」機能は持っていません。URL で読んだときに結果が返るのは、URL の先の Web サーバーが実行しているからです。

処理結果を使いたいなら require か関数

同じサーバーにある PHP の処理結果を使いたいなら、HTTP を通す必要はありません。include や require で読み込めば、そのファイルのコードが今のスクリプトの中で実行されます。require はファイルが無いと致命的なエラーで止まり、include は警告を出して先へ進みます。欠かせない部品には require (二重に読み込まないなら require_once) を使うのが一般的です。

出力を画面に出さずに、文字列として受け取りたいときは、出力バッファリングを使います。

<?php
// header.php が出力した内容を、文字列として受け取る
ob_start();
require __DIR__ . '/header.php';
$html = ob_get_clean();

もっと見通しをよくするなら、部品を関数にして、戻り値で文字列を返すようにします。何を渡すと何が返るかが、呼び出す側から読み取れます。

<?php
// parts.php
function render_header(string $title): string
{
    $safe = htmlspecialchars($title, ENT_QUOTES, 'UTF-8');
    return "<header><h1>{$safe}</h1></header>";
}
<?php
require_once __DIR__ . '/parts.php';
echo render_header('お知らせ');

設定ファイルのように値だけを読みたいときは、読み込まれる側のファイルの最後で return [...]; と配列を返し、$config = require __DIR__ . '/config.php'; のように受け取る書き方もできます。

URL で自分のサーバーを読む欠点

URL で読めば結果が取れるので、それで済ませたくなるかもしれません。しかし、自分のサーバーを URL で読む方法には、次のような欠点があります。

  • 遅い: 接続と HTTP のやり取りが毎回発生し、Web サーバーの処理も 2 回分 (元のリクエストと、内部のリクエスト) かかる
  • 設定に左右される: URL を開けるのは、php.ini の allow_url_fopen が有効なときだけ。サーバーによっては無効にされている
  • 待たされる: 相手が応答しないと、default_socket_timeout の秒数まで待つ。アクセスが集中すると、内部のリクエストどうしで処理の枠を取り合い、さらに遅くなることがある
  • 失敗に気づきにくい: 失敗すると警告とともに false が返るので、戻り値を確かめないと空のページを出してしまう

外部の API を読むなど、URL で読むのが正しい場面では、タイムアウトを短めに指定し、戻り値を確かめます。

<?php
$context = stream_context_create([
    'http' => ['timeout' => 5], // 5 秒で打ち切る
]);
$json = file_get_contents('https://api.example.com/news.json', false, $context);
if ($json === false) {
    // 取得に失敗したときの処理 (前回の結果を表示する、など)
}

利用者の入力を、そのまま URL やパスに使わない

file_get_contents($_GET['url']) のように、利用者から受け取った値をそのまま渡すのは危険です。サーバーが、攻撃者の指定した先にアクセスしてしまいます。これはサーバーサイドリクエストフォージェリ (SSRF) と呼ばれる攻撃で、外からは見えない内部向けの管理画面や、クラウドのメタデータ (サーバーの認証情報などを返す仕組み) を読まれるおそれがあります。値がパスとして扱われると、../ を使ってサーバー上の任意のファイルを読まれることもあります (ディレクトリトラバーサル)。

読み込む先は、利用者に URL を書かせるのではなく、決めておいた候補から選ばせます。

<?php
// 読み込んでよい先を、あらかじめ決めておく
$feeds = [
    'news'   => 'https://api.example.com/news.json',
    'events' => 'https://api.example.com/events.json',
];
$key = $_GET['feed'] ?? '';
if (!is_string($key) || !isset($feeds[$key])) {
    http_response_code(404);
    exit;
}
$json = file_get_contents($feeds[$key]);

まとめ

  • file_get_contents() はファイルを読むだけで、実行はしない。パスで読むと PHP のソースがそのまま返る
  • URL で読むと Web サーバーが実行した結果が返るが、遅く、allow_url_fopen やタイムアウトに左右される
  • 同じサーバーの処理結果を使うなら require や関数を使う。出力を文字列にしたいなら ob_start() と ob_get_clean() を使う
  • 利用者の入力を、そのまま URL やパスに使わない (SSRF やディレクトリトラバーサルの危険)

参考