PHPでファイルを開いて読み込む

PHP初心者のときに混乱したし、未だに初心者が困ってるのをよく見掛ける。

## はやわかり

* ファイル全体をまるごと読み込みたい→ **`file_get_contents()`**
* ファイルの中身をまるごと出力したい→ **`readfile()`**
* 行単位のテキストファイルを配列として読み込みたい→ **`file()`**
* ファイルをバイト単位で読み込みたい→ **`fopen()`**+**`fread()`**
* CSVを読み込みたい→ **`SplFileObject`クラス**
* オブジェクト指向的に操作したい→ **`SplFileObject`クラス**
* クラウドとかFTPとかにあるファイルを読み込みたい→ **`League\Flysystem`ライブラリ**

バイナリファイルを操作したいとか事情がない限り、`feof()`とか`fclose()`とかの出番はない。

## `file_get_contents()`

*([PHP: file_get_contents - Manual](http://php.net/file_get_contents)より抜萃、2016年1月8日閲覧)*

> ### 説明
> ```php
> string file_get_contents ( string $filename [, bool $use_include_path = false [, resource $context [, int $offset = -1 [, int $maxlen ]]]] )
> ```
> この関数は`file()`と似ていますが、`offset`で指定した場所から開始し`maxlen`バイト分だけファイルの内容を文字列に読み込むという点が異なります。 失敗した場合、`file_get_contents()` は `FALSE` を返します。
>
> `file_get_contents()`はファイルの内容を文字列に読み込む 方法として好ましいものです。もしOSがサポートしていればパフォーマンス向上のためにメモリマッピング技術が使用されます。

`file_get_contents()`はファイル全体を読み込むための簡潔な方法です。また、この函数はバイナリセーフなので、バイナリフォーマットなファイル(たとえば画像など)を読み込んでも、破損することはありません。

引数にはファイル名を指定します。また、`$offset`と`$maxlen`を指定することで、ファイル途中を切り出して取得することもできます。

典型的なユースケースは、JSONファイルのパースなどです。

```php
$json = file_get_contents(__DIR__ . '/game-list.json');
if ($json === false) {
    throw new \RuntimeException('file not found.');
}
$data = json_decode($json, true);
```

デメリットは、ファイル全体を読み込んでしまうため巨大なファイルを扱ふには非効率だったり、そもそもメモリに乗らないことがあることです。

## `readfile()`

*([PHP: readfile - Manual](http://php.net/readfile)より抜萃、2016年1月8日閲覧)*

> ### 説明
> ```php
>  int readfile ( string $filename [, bool $use_include_path = false [, resource $context ]] )
> ```
> ファイルを読んで標準出力に書き出します。

典型的なユースケースは、Webアプリケーションでファイルをダウンロードさせる場合です。

小さなファイルであれば、以下のようなPHPスクリプトにブラウザなどでアクセスさせれば画像をダウンロードさせることができます。

```image.php
<?php
$image = file_get_contents('/path/to/sample.png');
header('Content-Type: image/png');
header('Content-Length: ' . count($image));
header('Content-Disposition: attachment; filename=sample.png')

echo $image; // バイナリファイルの出力も、これでok
```

上記のサンプルコードの問題点は、巨大なファイルであっても一度ファイル全体を読み込む必要があることです。数十〜数百メガバイト程度のファイルをわざわざ読み込むのはメモリの無駄づかひだし、ギガバイト単位になると現実的ではありません。

そんなときに役立つのが`readfile()`です。

```download.php
<?php
$filename = '/path/to/sample.iso';
header('Content-Type: application/octet-stream');
header('Content-Length: ' . filesize($filename));
header('Content-Disposition: attachment; filename=sample.iso')

readfile($filename);
```

注意として、Webアプリケーションフレームワークを利用する場合`readfile()`を直接利用する方法では上手く動作しない場合があります。フレームワークのお作法に則りましょう。

また、後述する`fopen()`を利用する場合は`fpassthru()`を利用します。

## `file()`

> ### 説明
> ```php
> array file ( string $filename [, int $flags = 0 [, resource $context ]] )
> ```
> ファイル全体を配列に読み込みます。

`file()`はテキストファイルを改行区切りの配列で読み込みます。典型的なユースケースはログファイルの解析や、改行区切りのテキストで表現されたリストのインポートです。

```a.txt
りんご
ばなな

みかん
```

```php
<?php
var_dump(file(__DIR__ . '/a.txt'));
// array(4) {
//   [0]=>
//   string(10) "りんご
// "
//   [1]=>
//   string(10) "ばなな
// "
//   [2]=>
//   string(1) "
// "
//   [3]=>
//   string(10) "みかん
// "
// }

var_dump(file(__DIR__ . '/a.txt', FILE_IGNORE_NEW_LINES));
// array(4) {
//   [0]=>
//   string(9) "りんご"
//   [1]=>
//   string(9) "ばなな"
//   [2]=>
//   string(0) ""
//   [3]=>
//   string(9) "みかん"
// }

var_dump(file(__DIR__ . '/a.txt', FILE_SKIP_EMPTY_LINES));
// array(4) {
//   [0]=>
//   string(10) "りんご
// "
//   [1]=>
//   string(10) "ばなな
// "
//   [2]=>
//   string(1) "
// "
//   [3]=>
//   string(10) "みかん
// "
// }

var_dump(file(__DIR__ . '/a.txt', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES));
// array(3) {
//   [0]=>
//   string(9) "りんご"
//   [1]=>
//   string(9) "ばなな"
//   [2]=>
//   string(9) "みかん"
// }
```

第二引数に`FILE_IGNORE_NEW_LINES`を指定しないと、配列の各要素の末尾には改行コードがついたままです。また、`FILE_SKIP_EMPTY_LINES`を指定すると空行を無視できます。

行指向のテキストデータを一気に取り込むのに便利ですが、`file_get_contents()`や`readfile()`と違ってバイナリファイルには対応しないので気をつけてください。

また、行指向のデータでもCSVの場合は、後述する`SplFileObject`を使った方がべんりです。

## `fopen()`+`fread()`

*[PHP: fopen - Manual](http://php.net/fopen)より抜萃、2016年1月8日閲覧)*

> ### 説明
> ```php
> resource fopen ( string $filename , string $mode [, bool $use_include_path = false [, resource $context ]] )
> ```
> fopen() は、filename で指定されたリソースをストリームに結び付けます。

*[PHP: fread - Manual](http://php.net/fread)より抜萃、2016年1月8日閲覧)*

> ### 説明
> ```php
> string fread ( resource $handle , int $length )
> ```
> `fread()` は、handle が指すファイルポインタから最高 length バイト読み込みます。 以下のいずれかの条件を満たしたら、読み込みを終了します。
>
> * length バイトぶん読み込んだ
> * EOF (ファイルの終端) に達した
> * パケットが利用可能になるか、あるいは ソケットのタイムアウト が発生した (ネットワークストリームの場合)
> * バッファつきで読み込まれた、プレーンなファイルでないストリームの場合に、 一回の読み込みバイト数がチャンクサイズ (通常は 8192) に達した。 それまでにバッファされていたデータの内容によって、返されるデータのサイズはチャンクサイズより大きくなることがあります。

`fopen()`と`fread()`はこれまで紹介した函数と違って、ファイルリソースを利用する低レベルなAPIです。つまり、バイナリフォーマットのファイルを効率よく解析しようとする場合は基本的に不要です。ただし、`fopen()`を利用した便利なテクニックについては後述します。

バイナリフォーマットを解析する場合でも、固定位置の数バイトを取得すればことたりるならば`file_get_contents()`を使った方が簡潔に書けることがあります。[ZipArchiveで日本語ファイル名を扱えない場合があって困った話](http://qiita.com/t_cyrill/items/13a9eac1646e365691da)のような感じ。

逆にいへば、固定ではないバイナリフォーマットでは`fopen()`と`fread()`のほか`fseek()`や`feof()`などをうまく利用する必要があります。また、これらの函数と同等の機能は`SplFileObject`でも可能なので、好みで使ひやすい方を選んでください。

PHPでファイルを読み込む処理を調べるためにぐぐると`fopen()`と`fgets()`を組合せたサンプルコードが出てくるのは罠。**特別な事情がなければ`file_get_contents()`を使った方がいいです**。

## SplFileObject

*([PHP: SplFileObject - Manual](http://php.net/splfileobject)より抜萃、2016年1月8日閲覧)*

> ```php
> SplFileObject extends SplFileInfo implements RecursiveIterator , SeekableIterator {
> /* 定数 */
> const integer DROP_NEW_LINE = 1 ;
> const integer READ_AHEAD = 2 ;
> const integer SKIP_EMPTY = 4 ;
> const integer READ_CSV = 8 ;
> 
> /* メソッド */
> public string|array current ( void )
> public bool eof ( void )
> public bool fflush ( void )
> public string fgetc ( void )
> public array fgetcsv ([ string $delimiter = "," [, string $enclosure = "\"" [, string $escape = "\\" ]]] )
> public string fgets ( void )
> public string fgetss ([ string $allowable_tags ] )
> public bool flock ( int $operation [, int &$wouldblock ] )
> public int fpassthru ( void )
> public int fputcsv ( array $fields [, string $delimiter = "," [, string $enclosure = '"' [, string $escape = "\" ]]] ) // "
> public string fread ( int $length )
> public mixed fscanf ( string $format [, mixed &$... ] )
> public int fseek ( int $offset [, int $whence = SEEK_SET ] )
> public array fstat ( void )
> public int ftell ( void )
> public bool ftruncate ( int $size )
> public int fwrite ( string $str [, int $length ] )
> public void getChildren ( void )
> public array getCsvControl ( void )
> public int getFlags ( void )
> public int getMaxLineLen ( void )
> public bool hasChildren ( void )
> public int key ( void )
> public void next ( void )
> public void rewind ( void )
> public void seek ( int $line_pos )
> public void setCsvControl ([ string $delimiter = "," [, string $enclosure = "\"" [, string $escape = "\\" ]]] )
> public void setFlags ( int $flags )
> public void setMaxLineLen ( int $max_len )
> public void __toString ( void )
> public bool valid ( void )
> 
> /* 継承したメソッド */
> public SplFileInfo::__construct ( string $file_name )
> public int SplFileInfo::getATime ( void )
> public string SplFileInfo::getBasename ([ string $suffix ] )
> public int SplFileInfo::getCTime ( void )
> public string SplFileInfo::getExtension ( void )
> public SplFileInfo SplFileInfo::getFileInfo ([ string $class_name ] )
> public string SplFileInfo::getFilename ( void )
> public int SplFileInfo::getGroup ( void )
> public int SplFileInfo::getInode ( void )
> public string SplFileInfo::getLinkTarget ( void )
> public int SplFileInfo::getMTime ( void )
> public int SplFileInfo::getOwner ( void )
> public string SplFileInfo::getPath ( void )
> public SplFileInfo SplFileInfo::getPathInfo ([ string $class_name ] )
> public string SplFileInfo::getPathname ( void )
> public int SplFileInfo::getPerms ( void )
> public string SplFileInfo::getRealPath ( void )
> public int SplFileInfo::getSize ( void )
> public string SplFileInfo::getType ( void )
> public bool SplFileInfo::isDir ( void )
> public bool SplFileInfo::isExecutable ( void )
> public bool SplFileInfo::isFile ( void )
> public bool SplFileInfo::isLink ( void )
> public bool SplFileInfo::isReadable ( void )
> public bool SplFileInfo::isWritable ( void )
> public SplFileObject SplFileInfo::openFile ([ string $open_mode = "r" [, bool > $use_include_path = false [, resource $context = NULL ]]] )
> public void SplFileInfo::setFileClass ([ string $class_name = "SplFileObject" ] )
> public void SplFileInfo::setInfoClass ([ string $class_name = "SplFileInfo" ] )
> public void SplFileInfo::__toString ( void )
> }
> ```
>

`SplFileObject`はいたれり尽せりなオブジェクトで、実はバイナリファイルをバイト単位で制御しながら操作するなど、`fopen()`などでできることは一通り可能です。

また、ファイルを行単位で読み込んで操作するような処理は`SplFileObject`を利用することで圧倒的に簡潔に書けます。

下記は、空行を読み飛ばしながらテキストファイルの内容を行番号付きで出力する例です。

```php
<?php

$file = new SplFileObject(__DIR__ . '/a.txt', 'r');
$file->setFlags(SplFileObject::SKIP_EMPTY | SplFileObject::DROP_NEW_LINE);

foreach ($file as $n => $line) {
    if ($line === false) continue;
    echo "$n $line", PHP_EOL;
}
```

また冒頭にも書いた通り、`SplFileObject`はCSVの読み込みにもとても便利です。

```a.csv
くだもの,こすう
りんご,100
みかん,200
ばなな,114514
```

```php
<?php
$csv = new SplFileObject(__DIR__ . '/a.csv', 'r');
$csv->setFlags(SplFileObject::READ_CSV);

$header = [];
foreach ($csv as $row) {
    if ($row === [null]) continue; // 最終行の処理
    if (empty($header)) {
        $header = $row;
        continue;
    }
    $data[] = array_combine($header, $row);
}
var_dump($data);
// array(3) {
//   [0]=>
//   array(2) {
//     ["くだもの"]=>
//     string(9) "りんご"
//     ["こすう"]=>
//     string(3) "100"
//   }
//   [1]=>
//   array(2) {
//     ["くだもの"]=>
//     string(9) "みかん"
//     ["こすう"]=>
//     string(3) "200"
//   }
//   [2]=>
//   array(2) {
//     ["くだもの"]=>
//     string(9) "ばなな"
//     ["こすう"]=>
//     string(6) "114514"
//   }
// }
```

べんり。

## Flysystem

このライブラリについては、以前に個人のblogで紹介しました。

* [さまざまな環境で統一的にファイルを操作する [league/flysystem] - 超PHPerになろう](http://cho-phper.hateblo.jp/entry/2015/11/11/032710)
* [Flysystem - Multiple Filesystems, One API](http://flysystem.thephpleague.com/)

本稿の執筆時点で公式サイトに載ってるAdapter一覧は以下の通り。

```
Local (ローカルのファイルシステム)
Azure (WindowsAzure)
AWS S3 V2
AWS S3 V3
Copy.com
Dropbox
FTP
GridFS
Memory
Null / Test (いはゆる /dev/null)
Rackspace
ReplicateAdapter
SFTP
WebDAV
PHPCR (JCRのPHP版)
ZipArchive (.zipファイル)
```

いろいろあってべんり。

## 落穂拾ひ

ここまでで適当に流してきたこととか、いろいろ。

### ファイル名

ここまでしれっと“ファイル名”と書いてきましたが、実はローカルのファイルパス以外にもいろいろ書けます。

*[PHP: サポートするプロトコル/ラッパー - Manual](http://php.net/wrappers)より抜萃、2016年1月8日閲覧)*

> * `file://` — ローカルファイルシステムへのアクセス
> * `http://` — HTTP(s) URL へのアクセス
> * `ftp://` — FTP(s) URL へのアクセス
> * `php://` — さまざまな入出力ストリームへのアクセス
> * `zlib://` — 圧縮ストリーム
> * `data://` — データ (RFC 2397)
> * `glob://` — パターンにマッチするパス名の検索
> * `phar://` — PHP アーカイブ
> * `ssh2://` — Secure Shell 2
> * `rar://` — RAR
> * `ogg://` — オーディオストリーム
> * `expect://` — 対話的プロセスストリーム

つまり、`file_get_contents("http://example.com/hoge.json")`とか書いて、直接HTTPでファイルの取得もできます。

が、このままでは先程の巨大ファイルを出力するのと同様に、巨大ファイルをダウンロードするときもメモリに載せなければいけないので大変。

そんなときはこうする。

```php
<?php
$fp = fopen('http://example.com/huge.iso', 'r');
file_put_contents('/path/to/save.iso', $fp);
```

もっと縮めて書くこともできるけれど、おぼえやすい方で良い。

```php
<?php
copy('http://example.com/huge.iso', '/path/to/save.iso');
```

`file_put_contents()`に文字列を渡してやるとデータを書き込めるのだけれど、ファイルポインタとかストリームリソースを渡してやってもよしなにやってくれます。ちょうべんりですね。

ちなみにFTPは[PHP: ftp:// - Manual](http://php.net/manual/ja/wrappers.ftp.php)を使っても読み書きすることができる。

## あとがき

[【PHP入門講座】 目次](http://qiita.com/mpyw/items/752e19a578cf7d96fc5f)とか[[PHP] 自分の主要な記事のまとめ](http://qiita.com/mpyw/items/b2a2b0e8e60ed4680f06)を書いてる @mpyw 師が既にまとめてないかなー、と思ったけど、そんなことはなかったぜ！

今回はPHPについて書いたけど、Rubyでもファイルの適切な取り扱ひかたを知ってるひとは多くない気がするので誰か書いてください。