【PHP】ファイルの先頭にデータを追加する＆ファイル読み書きのためのシンプルな関数を作ってみる

ファイルを読み込んだり書き込んだりする時には、`file_get_contents()` や `file()` や `file_put_contents()` が非常に便利です。

ただ、引数に色んなものを渡せるせいで、「サクッと書きたい時にまだるっこい（まどろっこしい）」という事がままありますので…

- 引数にあれこれ渡さなくても、よく使うであろう処理をサクっと書ける
    - URL の処理等は考慮しない
- ファイルのロック処理にもある程度気をつけてみる

あたりを意識して、コードを書いてみます。PHP 5.2.6 ～ PHP 7.3.X まで、幅広い環境で動作すると思います。

--------------------------------------------------------------------------------

## ファイルを読み込む

```php
function file_get($path, $offset = -1, $maxLength = -1)
{
    if (!$fp = fopen($path, 'rb')) { return false; }

    flock($fp, LOCK_SH);
    $data = stream_get_contents($fp, $maxLength, $offset);
    flock($fp, LOCK_UN);
    fclose($fp);

    return $data;
}
```

[stream_get_contents()](https://www.php.net/manual/ja/function.stream-get-contents.php) を使えば、`while` でループさせる必要すらないので楽ですが、**`offset` `length` の引数の順番が、[file_get_contents()](https://www.php.net/manual/ja/function.file-get-contents.php) とは逆**なところが、いかにも PHP らしい罠です(汗)[^1]

`substr()` 等でも、引数は `offset` `length` の順に渡しますので、その仕様に合わせています。


[^1]: 歴史的に、offset パラメータが後から追加されたため、このような順番になってしまったようです。


### 使い方
```php
//-- 単純にファイルの中身が欲しい場合
$data = file_get('/path/to/file.txt');

//-- 5byte目以降を全て読み込む
$data = file_get('/path/to/file.txt', 5);

//-- 最初の5byteだけ読み込む
$data = file_get('/path/to/file.txt', 0, 5);
```

--------------------------------------------------------------------------------

## ファイルへ上書きする

`file_put_contents($path, $data, LOCK_EX)` に該当する処理です。既に `$path` の中身がある場合でも、`$data` で上書きされます。

```php
function file_put($path, $data)
{
    if (!$fp = fopen($path, 'cb')) { return false; }

    flock($fp, LOCK_EX);
    ftruncate($fp, 0);

    $result = fwrite($fp, $data);
    fflush($fp);
    flock($fp, LOCK_UN);
    fclose($fp);

    return $result;
}
```

### 使い方
```php
file_put('/path/to/file.txt', $data);
```

--------------------------------------------------------------------------------

## ファイルの末尾へ追記する

`file_put_contents($path, $data, FILE_APPEND | LOCK_EX)` に該当する処理です。既に `$path` の中身がある場合は、`$data` がファイルの末尾に追記されます。

```php
function file_append($path, $data)
{
    if (!$fp = fopen($path, 'ab')) { return false; }

    flock($fp, LOCK_EX);

    $result = fwrite($fp, $data);
    fflush($fp);
    flock($fp, LOCK_UN);
    fclose($fp);

    return $result;
}
```

### 使い方
```php
file_append('/path/to/file.txt', $data);
```

--------------------------------------------------------------------------------

## ファイルの先頭へ追加する

`file_put_contents()` だけではできない処理です。既に `$path` の中身がある場合は、`$data` がファイルの先頭に追加されます。

```php
function file_prepend($path, $data)
{
    if (!$fp = fopen($path, 'c+b')) { return false; }

    flock($fp, LOCK_EX);
    $data = $data . stream_get_contents($fp);
    rewind($fp);

    $result = fwrite($fp, $data);
    fflush($fp);
    flock($fp, LOCK_UN);
    fclose($fp);

    return $result;
}
```

### 使い方
```php
file_prepend('/path/to/file.txt', $data);
```

#### 注意点

[軽くググってみる](https://www.google.co.jp/search?q=PHP+%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB+%E5%85%88%E9%A0%AD+%E8%BF%BD%E5%8A%A0&ie=UTF-8) と見つかるコードは、ファイルロックを全く考慮していないものばかりのようですので、ご注意ください。

--------------------------------------------------------------------------------

## ファイルを配列に読み込む

UTF-8 以外の文字コードにも対応させたものです。

`file()` 関数では、`FILE_IGNORE_NEW_LINES`（各行の最後の改行を省略する）を指定する事が圧倒的に多いと思いますので、そこは指定しなくて済むようにしています。

また、`FILE_SKIP_EMPTY_LINES`（空行を読み飛ばす）を指定する事も多いと思いますので、空行を読み飛ばす動作がデフォルトになっています。

```php
function file_get_array($path, $isSkipEmptyLines = true)
{
    if (($data = file_get($path)) === false) { return false; }

    $data = mb_convert_encoding($data, 'UTF-8', mb_detect_encoding($data, mb_detect_order(), true));

    return ($isSkipEmptyLines) ? preg_split('/\r\n|\n|\r/', $data, -1, PREG_SPLIT_NO_EMPTY)
                               : preg_split('/\r\n|\n|\r/', $data);
}
```

### 使い方
```php
$array = file_get_array('/path/to/file.txt');

//-- 空行を読み飛ばさない時
$array = file_get_array('/path/to/file.txt', false);
```

--------------------------------------------------------------------------------

### 昔話

perl や PHP4 の頃には、`file_get_contents()` や `file_put_contents()` なんて関数はありませんでしたので、PHP 5 で初めてこれらを使った時は、その便利さに感動したものです。

ところが…

- `file_get_contents()` ってファイルロック処理がないよね
    - きちんとロックしようとすると、ファイルオープン 1 回の中で読み込み～書き込みする必要があるので、この関数の用途的に、不要といえば不要なのかも。

- `file_put_contents()` のロック処理ってきちんと働いてなくない！？
    - ※PHP5.2.6 で修正されました。`c` モードも、このバージョンで実装されました。

- ファイルロック解除の際は、`flock($fp, LOCK_UN)` を書いたら駄目らしいよ！？
    - ※PHP5.3.2 で修正されました。アンロック処理を明示するのが正しいです。

- `file_get_contents()` の第 2 引数って邪魔じゃない？
    - 特に、ファイルの一部を読みたい時に、第 2 第 3 引数に `false, null` と渡すのが…。

- `file_get_contents()` や `file()` には URL も渡せるけど、色々とイケてないよね…。
    - タイムアウト処理とか `$http_response_header` とか…。

…という、何ともむず痒い…「痒いところまで手は届くのだけど、背中を掻きたい時に、足の裏を掻いてしまう感じ」…なので、結局こういったものを用意して使ってました。[^2]


今から 10 年ぐらい前？の話です(遠い目)

大昔のコードを元に書いてますので、おかしなところがありましたら、ご指摘ください。


[^2]: `file_get_contents()` で、HTTP の POST 処理なんかまで書ける実用主義？なところは、とても PHP らしいと思います。ただ、[Beauty Is in Simplicity](https://www.google.com/search?q=%E7%BE%8E%E3%81%AF%E3%82%B7%E3%83%B3%E3%83%97%E3%83%AB%E3%81%95%E3%81%AB%E5%AE%BF%E3%82%8B&ie=utf-8) とも言いますので…。