[PHP] ファイルオープンモードに関するマニュアルの記述は間違っている

# **間違った記述**

**[fopen](http://php.net/manual/ja/function.fopen.php)** 関数のマニュアルは、完全に間違っているとも言い難いが、 **「極めて不親切」** であるとは言える。その記述が以下のものである。

![ss (2013-10-31 at 01.56.32).png](https://qiita-image-store.s3.amazonaws.com/0/25060/93ddf592-db44-6f80-9bb6-3f52f8d7f47e.png)

実際には、ファイルポインタを **先頭** に置いているとしか思えない挙動をする。

```php
$fp = fopen('new_file.txt', 'w');
fwrite($fp, 'abcde');
fclose($fp);
$fp = fopen('new_file.txt', 'a');
echo ftell($fp);
fclose($fp);
```

としてみると、 **[ftell](http://php.net/manual/ja/function.ftell.php)** 関数の返り値は **`0`** となっていることが分かる。マニュアルを見ると、

![ss (2013-10-31 at 03.30.22).png](https://qiita-image-store.s3.amazonaws.com/0/25060/f7a0b9e8-5906-47ad-af5c-c8f3d3c53803.png)

確かに **未定義** であるとされているため、この場合は誤りでは無いのかもしれない。しかし、 **`a+`** モードは<ins>読み出しも可能</ins>なストリームでもあるのに、未定義で片づけてしまうのは間違っているのではないかと思う。混乱を招く要因にもなり得るだろう。

# **内部構造を意識した正しい認識**

## 早見表

**`x`** **`x+`** は PHP4.3.2、 **`c`** **`c+`** はPHP5.2.6以降でのみサポートされている。

![ss (2013-10-31 at 03.40.00).png](https://qiita-image-store.s3.amazonaws.com/0/25060/f62d508d-32d6-8de5-3cac-f0d8d3f2c202.png)

内部構造を意識し、 **`a+`** モードのことも考慮すると、<ins>書き込み時にファイルポインタを終端に移動する</ins>という認識が一番適切であろう。

## 「仮NULLバイト」の導入

説明のために **「仮NULLバイト」** という言葉を導入する。これはバッファの範囲内と範囲外で区別する。以下、加味されるものを **○** 、加味されないものを **×** で示す。

![ss (2013-11-01 at 11.05.04).png](https://qiita-image-store.s3.amazonaws.com/0/25060/5aafccdf-3640-047d-9ed6-e49515be3a97.png)

※ 外部バッファとは、読み出された後の値 ( **[stream_get_contents](http://php.net/manual/ja/function.stream-get-contents.php)** 関数などの返り値 ) のことを意味する。

## **[fseek](http://php.net/manual/ja/function.fseek.php)** 関数 について

**[fseek](http://php.net/manual/ja/function.fseek.php)** 関数のマニュアルを見ると、 

```
一般的に、ファイルの終端より先の位置に移動することも許されています。
```

と書かれているので、ここでは便宜的に

![ss (2013-11-01 at 11.24.17).png](https://qiita-image-store.s3.amazonaws.com/0/25060/d8356fec-4d4d-0f41-b17a-f1a5694b826d.png)

という5バイトのファイル `data.txt` があれば、先ほど提示した「範囲外の仮NULLバイト」を用いて

![ss (2013-11-01 at 11.17.54).png](https://qiita-image-store.s3.amazonaws.com/0/25060/cf9d4eec-98e2-8c98-e310-3ebb537cc8a7.png)

と表記する。

```php
$fp = fopen('data.txt', 'c');
fseek($fp, 7);
```

とすると、

![ss (2013-11-01 at 11.26.03).png](https://qiita-image-store.s3.amazonaws.com/0/25060/f0dd23ae-4d44-7c2a-3292-c51e64a7b7c2.png)

この位置に移動することが出来る。更に

```php
fwrite($fp, 'f');
```

として、ここから<ins>1バイト以上</ins>の書き込みを行うと…

#### 一般的なモードでの挙動

その位置より左側にある「範囲外の仮NULLバイト」の実体化が起こる。

![ss (2013-11-01 at 11.56.38).png](https://qiita-image-store.s3.amazonaws.com/0/25060/cfd3b911-327b-0142-4b3f-c19c5f53e2da.png)

#### `a` `a+` モードでの挙動

そのまま追記される。

![ss (2013-11-01 at 11.57.15).png](https://qiita-image-store.s3.amazonaws.com/0/25060/6e0223e3-f94f-9cb2-0f0c-0d563efa788d.png)

## **[ftruncate](http://php.net/manual/ja/function.ftruncate.php)** 関数について

この関数はファイルポインタを指定したバイト数に丸める関数である。

### 概要

- ポインタの位置は<ins>変更されない</ins>。
- 超過している部分は切り捨てられる。
- 不足している部分は<ins>実体の</ins>NULLバイトで埋められる。

**[ftruncate](http://php.net/manual/ja/function.ftruncate.php)** 関数コール直後に **[fwrite](http://php.net/manual/ja/function.fwrite.php)** 関数をコールしてしまうと、ポインタの位置が変更されていないが故に、そこからの書き込みとなり、当然そこまではNULLバイトで埋められてしまう。下記に例を示す。

```php
$fp = fopen('data.txt', 'c');
fseek($fp, 2);
ftruncate($fp, 0);
fwrite($fp, 'f');
fseek($fp, 0);
```

としたときに

![ss (2013-11-01 at 11.17.54).png](https://qiita-image-store.s3.amazonaws.com/0/25060/cf9d4eec-98e2-8c98-e310-3ebb537cc8a7.png)

がどうなるかを下に示す。

#### 一般的なモードでの挙動

![ss (2013-11-01 at 11.54.16).png](https://qiita-image-store.s3.amazonaws.com/0/25060/d17e52ff-92d8-f280-2d97-e93f87215a88.png)

NULLバイトで先頭が埋められるのを回避したければ、 **[fwrite](http://php.net/manual/ja/function.fwrite.php)** 関数のコール直前または直後に **[rewind](http://php.net/manual/ja/function.rewind.php)** 関数を実行することにより回避できる。

#### `a` `a+` モードでの挙動

![ss (2013-11-01 at 11.46.51).png](https://qiita-image-store.s3.amazonaws.com/0/25060/3c704c9e-8499-c2e4-1649-60c5d4a821f7.png)

最後の **[fseek](http://php.net/manual/ja/function.fseek.php)** 関数が先頭の仮NULLバイトを無視していることに注意。

## [fseek](http://php.net/manual/ja/function.fseek.php)関数の非常にややこしい挙動

最初の表において[fseek](http://php.net/manual/ja/function.fseek.php)関数は「範囲内の仮NULLバイト」の存在を加味しないとしたが、実際には<ins>現在位置より左側にある「範囲内の仮NULLバイト」の個数分だけ指定値に加算</ins>することで差を打ち消しているとみられる。 **`SEEK_SET`** フラグ(デフォルト)のときはこれで最初に述べたような自然な挙動になるが、 **`SEEK_CUR`** フラグを指定したときにかなり気持ち悪い結果を引き起こす。具体例を下に挙げる。

![ss (2013-11-02 at 12.26.37).png](https://qiita-image-store.s3.amazonaws.com/0/25060/e206906b-9ff1-efcb-828f-8f3a2d073d1a.png)

このとき、

```php
fseek($fp, 1, SEEK_SET); // 1+2=3
```

と

```php
fseek($fp, -4, SEEK_CUR); // -4+2=-2
```

は、どちらも

![ss (2013-11-02 at 12.35.40).png](https://qiita-image-store.s3.amazonaws.com/0/25060/b6ac3983-0fbc-c858-6bdd-1ac74a850eff.png)

となる。

# **その他**

## テキストモード(t)とバイナリモード(b)

マニュアルより引用。

![ss (2013-10-31 at 06.27.06).png](https://qiita-image-store.s3.amazonaws.com/0/25060/40009a83-6069-7a83-c9dc-933f9c9b4a39.png)

本記事にて `r` `r+` などとしてきたが、実際に使うときは **`rb`** **`r+b`** のようにすべきであろう。

## **[file_get_contents](http://php.net/manual/ja/function.file-get-contents.php)** / **[file_put_contents](http://php.net/manual/ja/function.file-put-contents.php)** の是非

これらは、ファイルの読み出し/書き込みに関わる一連の処理を簡略化することが出来る非常に有用な関数であるが・・・

まず最初に **[file_put_contents](http://php.net/manual/ja/function.file-put-contents.php)** 関数について触れる。この関数のマニュアルを見ると

![ss (2013-10-31 at 05.01.23).png](https://qiita-image-store.s3.amazonaws.com/0/25060/aca2bb1d-9f2b-4230-6ffe-1c8a5f54c558.png)

第3引数に排他ロックフラグ **`LOCK_EX`** を指定できるようであるが、この関数はC言語レベルで実際には下記のような実装になっている。 **[「php-5.2.5以前のfile_put_contentsではLOCK_EXによる排他ロックは動かない(5.2.6でFix)」](http://kannokanno.hatenablog.com/entry/20120624/1340532542)** より引用。

```c
PHP_FUNCTION(file_put_contents)
{
  // (省略：初期化)

	if (flags & PHP_FILE_APPEND) {
		mode[0] = 'a';
	} else if (flags & LOCK_EX) {
		/* check to make sure we are dealing with a regular file */
		if (php_memnstr(filename, "://", sizeof("://") - 1, filename + filename_len)) {
			if (strncasecmp(filename, "file://", sizeof("file://") - 1)) {
				php_error_docref(NULL TSRMLS_CC, E_WARNING, "Exclusive locks may only be set for regular files");
				RETURN_FALSE;
			}
		}
		mode[0] = 'c';
	}
	mode[2] = '\0';

	stream = php_stream_open_wrapper_ex(filename, mode, ((flags & PHP_FILE_USE_INCLUDE_PATH) ? USE_PATH : 0) | REPORT_ERRORS, NULL, context);
	if (stream == NULL) {
		RETURN_FALSE;
	}

	if (flags & LOCK_EX && (!php_stream_supports_lock(stream) || php_stream_lock(stream, LOCK_EX))) {
		php_stream_close(stream);
		php_error_docref(NULL TSRMLS_CC, E_WARNING, "Exclusive locks are not supported for this stream");
		RETURN_FALSE;
	}

	if (mode[0] == 'c') {
		php_stream_truncate_set_size(stream, 0);
	}

  // (省略：たぶん書き込み処理)
}
```

**`c`** モードでオープンを行っているので、ロックが取得できなくてもファイルのデータが失われてしまうことは無い。ところがバージョン5.2.5以前のソースを見ると…

```c
PHP_FUNCTION(file_put_contents)
{
  // (省略)
	stream = php_stream_open_wrapper_ex(filename, (flags & PHP_FILE_APPEND) ? "ab" : "wb", 
			((flags & PHP_FILE_USE_INCLUDE_PATH) ? USE_PATH : 0) | ENFORCE_SAFE_MODE | REPORT_ERRORS, NULL, context);
  // (省略)
}
```

**`w`** モードでのオープンを行っているので、<ins>オープンには成功したがロックには失敗した</ins>場合にデータが失われてしまう。もしPHPバージョン5.2.5以下を使う必要があるならば、下記のような関数を実装して使うのが賢明である。 このバージョンでは **`c`** モードを使えないため、一見不便に思えるかもしれないが、 **`a`** モードで対応することができ、更にこの「仮NULLバイト」の性質を利用すれば **[rewind](http://php.net/manual/ja/function.rewind.php)** 関数のコールを省略することさえ出来てしまう。

```php
function safe_file_put_contents($filename, $data, $flags = 0, $context = null) {
    $args = array(
        $filename,
        'ab',
        $flags & FILE_USE_INCLUDE_PATH,
    );
    if (func_num_args() > 3) {
        $args[] = $context;
    }
    if (!$fp = call_user_func_array('fopen', $args)) {
        return false;
    }
    if ($flags & LOCK_EX && !flock($fp, LOCK_EX)) {
        fclose($fp);
        return false;
    }
    if (!($flags & FILE_APPEND)) {
        ftruncate($fp, 0);
    }
    $ret = fwrite($fp, is_array($data) ? implode('', $data) : $data);
    flock($fp, LOCK_UN);
    fclose($fp);
    return $ret;
}
```

次に **[file_get_contents](http://php.net/manual/ja/function.file-get-contents.php)** 関数について触れるが、この関数はそもそも<ins>ファイルロックをサポートしてない</ins>。書き込み時の **排他ロック(LOCK_EX)** に比べれば 読み込み時の **共有ロック(LOCK_SH)** の重要性は低いかもしれないが、ロックをかけないと読み出し中に他のプロセスによる書き込みが行われた際、想定外の挙動を示す恐れがあるため、 **ユーザーが書き換え可能なデータにアクセスが集中する場合** 、ロックは行っておいた方が無難である。

また、掲示板等のスクリプトで、読み出しと書き込みを順次行う場合について触れる。

```
読み出しのみでファイルオープン(失敗したら中止)
↓
共有ロック(失敗したら中止)
↓
読み出し
↓
ロック解除
↓
ファイルクローズ
↓
＜ブランク＞
↓
[ if (.....): ]
↓
書き込みのみでファイルオープン(失敗したら中止)
↓
排他ロック(失敗したら中止)
↓
書き込み
↓
ロック解除
↓
ファイルクローズ
↓
[ endif; ]
```

ファイルロックをきちんと行っていたとしても、 **`＜ブランク＞`** で<ins>他のプロセスに書き込み割り込みされて困る</ins>場合には、ファイルオープン回数を1回にする工夫をしなければならない。下記のように実装するのが理想である。

```
読み出し/書き込みでファイルオープン(失敗したら中止)
↓
共有ロック(失敗したら中止)
↓
読み出し
↓
[ if (.....): ]
↓
排他ロックに切り替える(失敗したら中止)
↓
書き込み
↓
[ endif; ]
↓
ロック解除
↓
ファイルクローズ
```

## さまざまなファイル読み出し関数

![ss (2013-10-31 at 07.00.01).png](https://qiita-image-store.s3.amazonaws.com/0/25060/2c338081-836d-9914-ea44-d1dde1823b93.png)

```php
echo file_get_contents($filename);
```

よりは

```php
readfile($filename);
```

の方がパフォーマンスはいい。

# 関連

### [[シリアル版] ページング処理を採り入れた掲示板サンプル](http://qiita.com/items/e7f3f60b687053b7832a)