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

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

# 問題点

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

# コード例と解説

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

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


## `file_get_contents()` で書いた場合

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

```php
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;
}
```

参考：

*  [PHP の file\_get\_contents でステータスコードを取得する](https://blog.manabusakai.com/2013/02/file-get-contents-status-code/)

## 上記のコードの問題点

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

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

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

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

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

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

参考 :

* [file\_get\_contents を使った HTTPリクエストでタイムアウトが正しく動かない \- PSI Labs](http://www.psi-net.co.jp/blog/?p=1123)
* [PHP: stream\_context\_create \- Manual](http://php.net/manual/ja/function.stream-context-create.php)
* [PHP: HTTP コンテキストオプション \- Manual](http://php.net/manual/ja/context.http.php)

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

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

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

参考：

* [file\_get\_contentsで$http\_response\_headerを使用するときの注意点 \- \[PHP \+ PHP\] ぺんたん info](http://pentan.info/php/file_get_contents_http_response_header.html)


## 代替案としての `curl`

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

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

```php: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` をオススメします。

参考：

* [PHPメモ : file\_get\_contentsの代替にcURLを使う \- Mach3\.laBlog](http://blog.mach3.jp/2010/12/21/use-curl-for-filegetcontents.html)
* [PHP\+cURLのエラーハンドリング \- Qiita](http://qiita.com/Hiraku/items/bd17a3bea7960bc15037)
* [libcurl \- Error Codes](https://curl.haxx.se/libcurl/c/libcurl-errors.html)


# まとめ

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