こんにちは、かつコーチです。
前回はブラウザ側にデータを保存する「クッキー」を扱いました。
今回は視点を変えて、サーバー側の「ファイル」にデータを保存したり、読み込んだりする方法を解説します。
ログを記録したり、簡易的な設定ファイルを読み込んだりと、データベースを使うほどでもない場面で地味に活躍する処理なので、ぜひマスターしておきましょう。
PHPでファイル操作が必要になる場面
なぜファイル操作を学ぶのか
「データの保存」と聞くとデータベースを思い浮かべる人が多いと思いますが、実務では次のような場面でファイル操作がよく使われます。
- アクセスログやエラーログをテキストファイルに記録する
- 簡易的な設定値をファイルに保存しておく
- CSVファイルを読み込んでデータを一括登録する
- 一時的にアップロードされたファイルを処理する
私が初めて業務でPHPを触ったときも、最初に任されたのは「フォームの入力内容をログファイルに追記する」という、まさにファイル書き込みの処理でした。
データベース接続を学ぶ前段階として、ファイル操作を理解しておくと処理の流れがイメージしやすくなります。
2つのアプローチ:簡単な関数 vs 細かく制御できる関数
PHPのファイル操作には、大きく分けて2つのアプローチがあります。
| アプローチ | 代表的な関数 | 特徴 |
|---|---|---|
| シンプル系 | file_get_contents / file_put_contents | 1行で読み書きが完結。手軽だが細かい制御は苦手 |
| 標準系 | fopen / fread / fwrite / fclose | 少し手間はかかるが、1行ずつの処理や追記など細かい制御ができる |
「とりあえずファイルの中身を全部読みたい・書きたいだけ」ならシンプル系、「1行ずつ処理したい」「巨大なファイルを扱う」ならfopen系、と使い分けるのが基本方針です。
シンプルにファイルを読み書きする
手順1:ファイルを読み込む
ファイルの中身をまるごと読み込みたいだけなら、file_get_contents() が一番手軽です。
<?php
$content = file_get_contents("data/memo.txt");
if ($content !== false) {
echo $content;
} else {
echo "ファイルの読み込みに失敗しました";
}
?>
戻り値は文字列で、ファイルが存在しない場合などは false が返ってくるので、必ずチェックしましょう。
手順2:ファイルに書き込む
書き込みは file_put_contents() を使います。
<?php
$text = "これはメモの内容です。";
file_put_contents("data/memo.txt", $text);
?>
指定したファイルがすでに存在する場合は中身がすべて上書きされ、存在しない場合は新しくファイルが作成されます。
手順3:既存の内容に追記する
上書きではなく「追記」したい場合は、第3引数に FILE_APPEND を指定します。
<?php
$logLine = date("Y-m-d H:i:s") . " ログイン処理を実行しました" . PHP_EOL;
file_put_contents("data/access.log", $logLine, FILE_APPEND);
?>
PHP_EOL は、OSに応じた改行コードを自動で使ってくれる定数です。
ログファイルのように「1行ずつ追加していきたい」データには、この FILE_APPEND の指定が欠かせません。
fopenを使った細かい制御
手順1:モードを指定してファイルを開く
より細かく制御したい場合は fopen() を使います。
<?php
$handle = fopen("data/memo.txt", "r"); // "r" = 読み込みモード
if ($handle) {
while (($line = fgets($handle)) !== false) {
echo $line;
}
fclose($handle);
}
?>
fgets() で1行ずつファイルを読み込み、fclose() で必ずファイルを閉じます。
fopen() の第2引数(モード)は用途によって使い分けます。
| モード | 意味 |
|---|---|
"r" | 読み込み専用(ファイルがないとエラー) |
"w" | 書き込み専用(既存の内容は消えて上書きされる) |
"a" | 追記専用(末尾に追加していく) |
"r+" | 読み書き両方(ファイルの先頭から) |
手順2:1行ずつ書き込む
<?php
$handle = fopen("data/memo.txt", "a"); // 追記モード
if ($handle) {
fwrite($handle, "追加のメモです。" . PHP_EOL);
fclose($handle);
}
?>
fopen を使う場合は、開いたら必ず fclose() で閉じる、というセットを忘れないようにしましょう。
閉じ忘れても多くの場合PHPがスクリプト終了時に自動で閉じてくれますが、大量のファイルを扱う処理では、開けっぱなしのファイルが増えてリソース不足につながることがあります。
つまずきやすいポイント:モードの指定ミス
読み込みモードで書き込もうとして失敗した話
私が実際にやってしまった失敗が、fopen のモード指定ミスです。
❌ Before:読み込みモード("r")のまま書き込もうとする
<?php
$handle = fopen("data/memo.txt", "r"); // 読み込み専用で開いている
$result = fwrite($handle, "書き込みたい内容");
// Warning: fwrite(): Write of X bytes failed with errno=9 Bad file descriptor
?>
「ファイルは開けているのに、なぜか書き込みに失敗する」という状態になり、原因を探すのに時間がかかりました。
原因は単純で、"r" は読み込み専用モードなので、そのハンドルに対して書き込み処理をすること自体ができないという仕様でした。
✅ After:書き込み用のモード("a" や "w")で開く
<?php
$handle = fopen("data/memo.txt", "a"); // 追記用モードで開く
if ($handle) {
$result = fwrite($handle, "書き込みたい内容" . PHP_EOL);
fclose($handle);
}
?>
エラーメッセージに「Bad file descriptor」と出てきたら、モード指定が用途と合っているかをまず疑う、というのを覚えておくと解決が早くなります。
ファイルパスの相対・絶対で迷った話
もう一つよくあるつまずきが、ファイルパスの指定方法です。
fopen("data/memo.txt", "r") のような相対パスは、PHPを実行しているファイルの場所ではなく、実行時のカレントディレクトリを基準に解釈されることがあり、環境によって動いたり動かなかったりします。
私はローカル環境では動いていたのに、本番サーバーに上げたら「ファイルが見つかりません」というエラーになった経験があります。
こういうときは、__DIR__(このPHPファイルがあるディレクトリの絶対パス)を使うと安定します。
<?php
$path = __DIR__ . "/data/memo.txt";
$content = file_get_contents($path);
?>
__DIR__ を基準にパスを組み立てるようにしてから、環境差によるファイル操作系のエラーはほとんど無くなりました。
応用:ファイルの存在確認と安全な運用
ファイルの有無を確認してから処理する
書き込み処理の前には、ディレクトリの存在確認や書き込み権限のチェックをしておくと、思わぬエラーを防げます。
<?php
$dir = __DIR__ . "/data";
$path = $dir . "/access.log";
if (!is_dir($dir)) {
mkdir($dir, 0755, true); // ディレクトリがなければ作成
}
if (is_writable($dir) || !file_exists($path)) {
file_put_contents($path, "アクセスログ" . PHP_EOL, FILE_APPEND);
} else {
error_log("ログファイルへの書き込み権限がありません: " . $path);
}
?>
is_dir() でディレクトリの有無、is_writable() で書き込み権限を確認してから処理することで、本番環境で権限エラーが起きたときも気づきやすくなります。
CSVファイルを扱うときの関数
余談ですが、CSVファイルを扱う場合は fgetcsv() や fputcsv() という専用の関数も用意されています。
<?php
$handle = fopen("data/users.csv", "r");
if ($handle) {
while (($row = fgetcsv($handle)) !== false) {
echo $row[0] . " : " . $row[1] . PHP_EOL;
}
fclose($handle);
}
?>
カンマ区切りを自分で explode() して分解する必要がなく、CSVの仕様(ダブルクォートで囲まれた値など)にも対応してくれるので、CSVを扱うときはこちらを使うのがおすすめです。
まとめ
この記事のポイント
- 手軽に読み書きするなら
file_get_contents/file_put_contents - 1行ずつ処理したいときは
fopen/fgets/fwrite/fcloseを使う - 追記したいときは
FILE_APPENDやモード"a"を指定する fopenのモード指定ミス(読み込み専用で書き込もうとするなど)に注意する- パスは
__DIR__を基準に組み立てると環境差によるエラーを防げる
次に読むべき記事
次回は、文字列のパターンマッチングに使う「正規表現」を扱います。
入力チェックやデータの抽出など、実務でかなり出番の多いテーマです。
→ 次の記事:PHPの正規表現入門:preg_match・preg_replaceの使い方