PHP

APIなどにfile_get_contents()を使うのはオススメしない理由と代替案

More than 1 year has passed since last update.

file_get_contents() はファイルパスを指定してデータを取得するだけではなく
URLを指定すればそのURLの情報を手軽にとれる関数ですが
対API等に使うと色々とハマるポイントが多いので、それを簡単に解説したものです。
このページ向け に書いていたものですが、コード量が多いので分割しました。

問題点

file_get_contents()
ヘッダ情報の保持ルールやタイムアウト処理に癖があるため
返却されるステータスコードや、タイムアウト時に再リクエストなどを行うような
対APIの処理では、それらを知らないと想定していない事態に陥る。

コード例と解説

以下の要件でfunctionを書くとします。

  • jsonが返ってくるAPIにアクセスして、jsonをarray化する。
  • タイムアウトは3秒とし、そうなった場合は空の配列が返ってくる。
  • ステータスコードが200以外で返ってきた場合は空の配列が返ってくる。

file_get_contents() で書いた場合

上記を満たすコードを file_get_contents() で記述すると以下のようになります。

private function getApiData($url)
{
    $options = [
        'http' => [
            'method'  => 'GET',
            'timeout' => 3, // タイムアウト時間
        ]
    ];

    $json = file_get_contents($url, false, stream_context_create($options));

    // もしFalseが返っていたらエラーなので空白配列を返す
    if ($json === false) {
        return [];
    }

    // 200以外のステータスコードは失敗とみなし空配列を返す
    preg_match('/HTTP\/1\.[0|1|x] ([0-9]{3})/', $http_response_header[0], $matches);
    $statusCode = (int)$matches[1];
    if ($statusCode !== 200) {
        return [];
    }

    // 文字列から変換
    $jsonArray = json_decode($json, true);

    return $jsonArray;
}

参考:

上記のコードの問題点

パッと見たところでは問題ないコードですし、正常系で使う場合には問題がありません。
ただし以下のようなケースでおかしな挙動をします。

  • 3秒以上リクエストが返って来なくても処理が終了せず、タイムアウトしない
    →タイムアウトが設定値通りに動いていない
  • 複数回リクエストして、ときどき成功しているはずなのに空で結果が返ってくる
    →過去リクエストの結果が引き継がれるているものがある

以下、各々のケースにおける問題点に関して説明します。

タイムアウトが設定値通りに動かない

stream_context_creatで設定したタイムアウト時間でタイムアウトするわけではなく
実質約2倍の時間が経過してからタイムアウトとして処理される。

また、file_get_contents の場合、false の場合はエラーで判断するしかなく
それがどんなエラー由来かが特定しにくく、明確にタイムアウトと判断もし難いです。

参考 :

過去リクエストの結果が引き継がれる

$http_response_headerfile_get_contents
最後に成功したリリクエストのレスポンスヘッダを保持している。

そのため、リクエストが失敗した場合は、過去の成功したヘッダ情報を返してしまうため
うまく処理をしておかないとただしく情報を取得することができません。

参考:

代替案としての curl

シェルでもよく使う curl もPHP上に存在するので
環境等の制約がない場合はこちらのほうが使い勝手がよいので
対APIなどの取得処理の場合はこちらのほうが便利かと思います。

curl を使って
上記と処理内容を対比しやすいような形に書き換えてみました。

curlを使ったfunction例
private function getApiDataCurl($url)
{
    $option = [
        CURLOPT_RETURNTRANSFER => true, //文字列として返す
        CURLOPT_TIMEOUT        => 3, // タイムアウト時間
    ];

    $ch = curl_init($url);
    curl_setopt_array($ch, $option);

    $json    = curl_exec($ch);
    $info    = curl_getinfo($ch);
    $errorNo = curl_errno($ch);

    // OK以外はエラーなので空白配列を返す
    if ($errorNo !== CURLE_OK) {
        // 詳しくエラーハンドリングしたい場合はerrorNoで確認
        // タイムアウトの場合はCURLE_OPERATION_TIMEDOUT
        return [];
    }

    // 200以外のステータスコードは失敗とみなし空配列を返す
    if ($info['http_code'] !== 200) {
        return [];
    }

    // 文字列から変換
    $jsonArray = json_decode($json, true);

    return $jsonArray;
}

file_get_contents よりも…

  • curlで想定されたエラーが拾えるためエラーハンドリングがしやすい。
  • curl_getinfo でステータスコード以外も取得できるので便利。
  • ヘッダ情報など明確にリクエスト単位で結果を取得できるので、わかりやすい。

というような利点があるので、代替案としては curl をオススメします。

参考:

まとめ

  • file_get_contents はURLを指定すれば手軽にデータをとれるが、利用する上で知っておかないといけない癖がある
  • 癖を知らなくても素直に使えるのが curl なので、代替案としてはオススメ