PHPのファイル操作入門|ファイルの読み込み・書き込み・アップロードの実装方法


はじめに

前回の記事ではPHPのセッションを使ってフォームの選択状態を保持する方法を解説しました。今回はPHPのファイル操作を解説します。「テキストファイルにログを書き出したい」「CSVを読み込んでデータを処理したい」「お問い合わせフォームから画像をアップロードしたい」という場面はWeb制作でよく登場します。ファイルの読み込み・書き込み・アップロードの基本をひとつずつ丁寧に解説します。


1. ファイルの読み込み

1.1 ファイルの内容を文字列で読み込む

<?php

// file_get_contents:ファイル全体を文字列で読み込む
// → 最もシンプルでよく使う方法

$content = file_get_contents( 'data/message.txt' );

if ( $content === false ) {
    echo 'ファイルの読み込みに失敗しました';
} else {
    echo $content;
}

// ============================================

// 絶対パスで指定する場合
// __DIR__ は現在のファイルがあるディレクトリのパス
$content = file_get_contents( __DIR__ . '/data/message.txt' );

/*
    __DIR__ を使うと
    どこから実行しても正しいパスになるので
    相対パスより安全
*/

1.2 ファイルを1行ずつ読み込む

<?php

// file:ファイルを1行ずつ配列で読み込む
// → CSVや設定ファイルの処理に便利

$lines = file( __DIR__ . '/data/users.txt', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES );
/*
    FILE_IGNORE_NEW_LINES  → 各行末の改行を除去する
    FILE_SKIP_EMPTY_LINES  → 空行をスキップする
*/

if ( $lines === false ) {
    echo 'ファイルの読み込みに失敗しました';
} else {
    foreach ( $lines as $line ) {
        echo htmlspecialchars( $line ) . '<br>';
    }
}

// ============================================

// fopen・fgets:大きなファイルを少しずつ読み込む
// → メモリを節約できる

$handle = fopen( __DIR__ . '/data/large.txt', 'r' );

if ( $handle === false ) {
    echo 'ファイルを開けませんでした';
} else {
    while ( ( $line = fgets( $handle ) ) !== false ) {
        // 1行ずつ処理する
        echo htmlspecialchars( $line ) . '<br>';
    }
    fclose( $handle ); // 必ずファイルを閉じる
}

1.3 CSVファイルを読み込む

<?php

/*
    CSVファイルの読み込み:
    fgetcsv を使うと
    カンマ区切りのデータを配列で読み込める
*/

$handle = fopen( __DIR__ . '/data/products.csv', 'r' );

if ( $handle === false ) {
    echo 'CSVファイルを開けませんでした';
} else {
    $products = [];
    $is_first_row = true;

    while ( ( $row = fgetcsv( $handle ) ) !== false ) {
        // 1行目はヘッダーなのでスキップする
        if ( $is_first_row ) {
            $is_first_row = false;
            continue;
        }
        $products[] = [
            'id'    => $row[0],
            'name'  => $row[1],
            'price' => $row[2],
        ];
    }

    fclose( $handle );
}

// 読み込んだデータをHTMLで表示する
foreach ( $products as $product ) {
    echo '<tr>';
    echo '<td>' . htmlspecialchars( $product['id'] )    . '</td>';
    echo '<td>' . htmlspecialchars( $product['name'] )  . '</td>';
    echo '<td>' . htmlspecialchars( $product['price'] ) . '</td>';
    echo '</tr>';
}

/*
    CSVの例(products.csv):
    id,name,price
    1,ノートPC,89800
    2,マウス,3800
    3,キーボード,12800
*/

2. ファイルへの書き込み

2.1 ファイルに文字列を書き込む

<?php

// file_put_contents:ファイルに文字列を書き込む
// → 最もシンプルでよく使う方法

$content = "こんにちは、PHPのファイル操作です。\n";
$result  = file_put_contents( __DIR__ . '/data/output.txt', $content );

if ( $result === false ) {
    echo '書き込みに失敗しました';
} else {
    echo $result . ' バイト書き込みました';
}

// ============================================

// 追記する場合は FILE_APPEND フラグを使う
$new_line = date( 'Y-m-d H:i:s' ) . " アクセスがありました\n";

file_put_contents(
    __DIR__ . '/data/access.log',
    $new_line,
    FILE_APPEND | LOCK_EX  // FILE_APPEND:追記  LOCK_EX:排他ロック
);

/*
    LOCK_EX を使う理由:
    複数のリクエストが同時に書き込もうとすると
    ファイルが壊れることがある
    → LOCK_EX で書き込み中のファイルをロックして
      他のプロセスの書き込みを待たせる
*/

2.2 ログファイルに書き込む

<?php

/*
    実案件でよく使う:ログの記録
    エラーや操作履歴をファイルに残す
*/

function write_log( string $message, string $level = 'INFO' ): void {
    $log_dir  = __DIR__ . '/logs';
    $log_file = $log_dir . '/' . date( 'Y-m' ) . '.log';

    // logsディレクトリがなければ作成する
    if ( !is_dir( $log_dir ) ) {
        mkdir( $log_dir, 0755, true );
        /*
            0755 : ディレクトリのパーミッション
            true : 途中のディレクトリも再帰的に作成する
        */
    }

    $line = sprintf(
        "[%s] [%s] %s\n",
        date( 'Y-m-d H:i:s' ),
        $level,
        $message
    );

    file_put_contents( $log_file, $line, FILE_APPEND | LOCK_EX );
}

// 使用例
write_log( 'ユーザーがログインしました' );
write_log( 'DBへの接続に失敗しました', 'ERROR' );
write_log( '注文が完了しました', 'INFO' );

/*
    ログファイルの例(2024-03.log):
    [2024-03-15 10:30:00] [INFO]  ユーザーがログインしました
    [2024-03-15 10:31:05] [ERROR] DBへの接続に失敗しました
    [2024-03-15 10:32:10] [INFO]  注文が完了しました
*/

2.3 CSVファイルに書き込む

<?php

// fputcsv でCSVファイルを書き出す

$products = [
    [ 'id' => 1, 'name' => 'ノートPC',   'price' => 89800 ],
    [ 'id' => 2, 'name' => 'マウス',     'price' => 3800  ],
    [ 'id' => 3, 'name' => 'キーボード', 'price' => 12800 ],
];

$handle = fopen( __DIR__ . '/data/export.csv', 'w' );

if ( $handle === false ) {
    echo 'ファイルを開けませんでした';
} else {
    // BOMを書き込む(Excelで文字化けを防ぐ)
    fwrite( $handle, "\xEF\xBB\xBF" );

    // ヘッダー行を書き込む
    fputcsv( $handle, [ 'ID', '商品名', '価格' ] );

    // データを書き込む
    foreach ( $products as $product ) {
        fputcsv( $handle, [
            $product['id'],
            $product['name'],
            $product['price'],
        ] );
    }

    fclose( $handle );
    echo 'CSVファイルを書き出しました';
}

3. ファイルのアップロード

3.1 HTMLフォームの準備

<!--
    ファイルアップロードフォームの注意点:
    ① method="post" にする(getは不可)
    ② enctype="multipart/form-data" を付ける(必須)
-->

<form
    method="post"
    action="upload.php"
    enctype="multipart/form-data"
>
    <label for="upload-file">ファイルを選択してください</label>
    <input
        type="file"
        id="upload-file"
        name="upload_file"
        accept="image/jpeg,image/png,image/gif,image/webp"
    >

    <!--
        accept 属性で選択できるファイルの種類を制限する
        → ブラウザ上での制限なのでPHP側でも必ず検証する
    -->

    <button type="submit">アップロードする</button>
</form>

3.2 アップロード処理の基本

<?php
// upload.php

session_start();

// アップロードされたファイルの情報
// $_FILES['フォームのname属性']['プロパティ']

/*
    $_FILES['upload_file'] の中身:
    [name]     → アップロードされたファイルの元の名前
    [type]     → MIMEタイプ(信頼しない・必ず自分で検証する)
    [tmp_name] → サーバーの一時ファイルのパス
    [error]    → エラーコード(0 = エラーなし)
    [size]     → ファイルサイズ(バイト)
*/

// アップロードディレクトリ
$upload_dir = __DIR__ . '/uploads/';

// ① ファイルが送られてきたか確認する
if ( !isset( $_FILES['upload_file'] ) || $_FILES['upload_file']['error'] === UPLOAD_ERR_NO_FILE ) {
    die( 'ファイルが選択されていません' );
}

$file = $_FILES['upload_file'];

// ② エラーコードを確認する
if ( $file['error'] !== UPLOAD_ERR_OK ) {
    $error_messages = [
        UPLOAD_ERR_INI_SIZE   => 'ファイルサイズがphp.iniの制限を超えています',
        UPLOAD_ERR_FORM_SIZE  => 'ファイルサイズがフォームの制限を超えています',
        UPLOAD_ERR_PARTIAL    => 'ファイルが一部しかアップロードされませんでした',
        UPLOAD_ERR_NO_TMP_DIR => '一時フォルダが見つかりません',
        UPLOAD_ERR_CANT_WRITE => 'ディスクへの書き込みに失敗しました',
    ];
    $message = $error_messages[ $file['error'] ] ?? '不明なエラーが発生しました';
    die( htmlspecialchars( $message ) );
}

// ③ ファイルサイズを確認する(2MB以下)
$max_size = 2 * 1024 * 1024; // 2MB
if ( $file['size'] > $max_size ) {
    die( 'ファイルサイズは2MB以下にしてください' );
}

// ④ MIMEタイプを確認する($_FILES['type'] は信頼しない)
$finfo     = new finfo( FILEINFO_MIME_TYPE );
$mime_type = $finfo->file( $file['tmp_name'] ); // 実際のファイル内容から判定する

$allowed_types = [
    'image/jpeg',
    'image/png',
    'image/gif',
    'image/webp',
];

if ( !in_array( $mime_type, $allowed_types, true ) ) {
    die( '許可されていないファイル形式です(JPEG・PNG・GIF・WebPのみ)' );
}

// ⑤ 拡張子を確認する
$extension_map = [
    'image/jpeg' => 'jpg',
    'image/png'  => 'png',
    'image/gif'  => 'gif',
    'image/webp' => 'webp',
];

$extension = $extension_map[ $mime_type ];

// ⑥ 安全なファイル名を生成する(元のファイル名は使わない)
// uniqid() + ランダム文字列でユニークなファイル名を作る
$new_filename = uniqid( '', true ) . '_' . bin2hex( random_bytes( 8 ) ) . '.' . $extension;
$upload_path  = $upload_dir . $new_filename;

// ⑦ アップロードディレクトリが存在するか確認する
if ( !is_dir( $upload_dir ) ) {
    mkdir( $upload_dir, 0755, true );
}

// ⑧ 一時ファイルを移動する
if ( move_uploaded_file( $file['tmp_name'], $upload_path ) ) {
    echo 'アップロードに成功しました:' . htmlspecialchars( $new_filename );
} else {
    echo 'ファイルの移動に失敗しました';
}

💡 元のファイル名は絶対に使わない: アップロードされたファイルの元の名前($_FILES['name'])には危険な文字列が含まれる場合があります。必ず uniqid() などで新しいファイル名を生成して保存しましょう。

⚠️ $_FILES['type'] は信頼しない: MIMEタイプはブラウザが送ってくる値なので偽装できます。必ず finfo クラスでファイルの実際の中身から判定します。


4. 複数ファイルのアップロード

4.1 HTMLフォーム

<!-- multiple 属性で複数ファイルを選択できる -->
<form method="post" action="upload_multiple.php" enctype="multipart/form-data">

    <label for="upload-files">画像を選択してください(最大5枚)</label>
    <input
        type="file"
        id="upload-files"
        name="upload_files[]"
        accept="image/jpeg,image/png,image/webp"
        multiple
    >
    <!--
        name に [] を付けると複数ファイルを配列で受け取れる
        multiple 属性で複数選択を許可する
    -->

    <button type="submit">アップロードする</button>

</form>

4.2 PHP処理

<?php
// upload_multiple.php

$upload_dir   = __DIR__ . '/uploads/';
$allowed_types = [ 'image/jpeg', 'image/png', 'image/webp' ];
$max_size      = 2 * 1024 * 1024; // 2MB
$max_count     = 5; // 最大5枚

$uploaded_files = [];
$errors         = [];

// ファイルが送られてきたか確認する
if ( empty( $_FILES['upload_files']['name'][0] ) ) {
    die( 'ファイルが選択されていません' );
}

// アップロードされたファイルの数を確認する
$file_count = count( $_FILES['upload_files']['name'] );

if ( $file_count > $max_count ) {
    die( "一度にアップロードできるのは{$max_count}枚までです" );
}

// アップロードディレクトリを確認する
if ( !is_dir( $upload_dir ) ) {
    mkdir( $upload_dir, 0755, true );
}

// 複数ファイルを1件ずつ処理する
for ( $i = 0; $i < $file_count; $i++ ) {
    $error    = $_FILES['upload_files']['error'][$i];
    $tmp_name = $_FILES['upload_files']['tmp_name'][$i];
    $size     = $_FILES['upload_files']['size'][$i];

    // エラー確認
    if ( $error !== UPLOAD_ERR_OK ) {
        $errors[] = ( $i + 1 ) . '枚目のアップロードに失敗しました';
        continue;
    }

    // ファイルサイズ確認
    if ( $size > $max_size ) {
        $errors[] = ( $i + 1 ) . '枚目のファイルサイズが大きすぎます';
        continue;
    }

    // MIMEタイプ確認
    $finfo     = new finfo( FILEINFO_MIME_TYPE );
    $mime_type = $finfo->file( $tmp_name );

    if ( !in_array( $mime_type, $allowed_types, true ) ) {
        $errors[] = ( $i + 1 ) . '枚目のファイル形式が許可されていません';
        continue;
    }

    // 拡張子を取得する
    $extension_map = [
        'image/jpeg' => 'jpg',
        'image/png'  => 'png',
        'image/webp' => 'webp',
    ];
    $extension = $extension_map[ $mime_type ];

    // 安全なファイル名を生成する
    $new_filename = uniqid( '', true ) . '_' . bin2hex( random_bytes( 8 ) ) . '.' . $extension;
    $upload_path  = $upload_dir . $new_filename;

    // ファイルを移動する
    if ( move_uploaded_file( $tmp_name, $upload_path ) ) {
        $uploaded_files[] = $new_filename;
    } else {
        $errors[] = ( $i + 1 ) . '枚目の保存に失敗しました';
    }
}

// 結果を表示する
if ( !empty( $uploaded_files ) ) {
    echo count( $uploaded_files ) . '枚のファイルをアップロードしました<br>';
    foreach ( $uploaded_files as $filename ) {
        echo htmlspecialchars( $filename ) . '<br>';
    }
}

if ( !empty( $errors ) ) {
    echo 'エラーがありました:<br>';
    foreach ( $errors as $error ) {
        echo htmlspecialchars( $error ) . '<br>';
    }
}

5. ファイルの存在確認・削除・コピー

<?php

$file_path = __DIR__ . '/data/sample.txt';

// ===== ファイルの存在確認 =====

// file_exists:ファイルまたはディレクトリの存在確認
if ( file_exists( $file_path ) ) {
    echo 'ファイルが存在します';
}

// is_file:ファイルかどうか確認する(ディレクトリはfalse)
if ( is_file( $file_path ) ) {
    echo 'これはファイルです';
}

// is_dir:ディレクトリかどうか確認する
if ( is_dir( __DIR__ . '/uploads' ) ) {
    echo 'uploadsディレクトリが存在します';
}

// is_readable:読み込み可能か確認する
if ( is_readable( $file_path ) ) {
    echo '読み込み可能です';
}

// is_writable:書き込み可能か確認する
if ( is_writable( $file_path ) ) {
    echo '書き込み可能です';
}

// ===== ファイルサイズ・情報の取得 =====

// filesize:ファイルサイズをバイトで取得する
$size = filesize( $file_path );
echo number_format( $size ) . ' バイト';

// filemtime:最終更新日時を取得する
$mtime = filemtime( $file_path );
echo date( 'Y-m-d H:i:s', $mtime );

// pathinfo:ファイルパスの情報を取得する
$info = pathinfo( $file_path );
echo $info['dirname'];   // ディレクトリ
echo $info['basename'];  // ファイル名(拡張子あり)
echo $info['filename'];  // ファイル名(拡張子なし)
echo $info['extension']; // 拡張子

// ===== ファイルの削除 =====

if ( file_exists( $file_path ) ) {
    if ( unlink( $file_path ) ) {
        echo 'ファイルを削除しました';
    } else {
        echo 'ファイルの削除に失敗しました';
    }
}

// ===== ファイルのコピー =====

$source      = __DIR__ . '/data/original.txt';
$destination = __DIR__ . '/data/copy.txt';

if ( copy( $source, $destination ) ) {
    echo 'コピーしました';
} else {
    echo 'コピーに失敗しました';
}

// ===== ファイルの移動・名前変更 =====

$old_path = __DIR__ . '/data/old_name.txt';
$new_path = __DIR__ . '/data/new_name.txt';

if ( rename( $old_path, $new_path ) ) {
    echo '名前を変更しました';
}

6. アップロードファイルのダウンロード

<?php
// download.php
// アップロードされたファイルをダウンロードさせる

// ダウンロードするファイル名をGETパラメーターで受け取る
$filename = $_GET['file'] ?? '';

// ファイル名のバリデーション
// → ディレクトリトラバーサル攻撃を防ぐ
// → ../ や / などのパス記号を除去する
$filename     = basename( $filename ); // パス部分を取り除く
$upload_dir   = __DIR__ . '/uploads/';
$file_path    = $upload_dir . $filename;

// ファイルの存在確認
if ( !file_exists( $file_path ) || !is_file( $file_path ) ) {
    http_response_code( 404 );
    die( 'ファイルが見つかりません' );
}

// MIMEタイプを取得する
$finfo     = new finfo( FILEINFO_MIME_TYPE );
$mime_type = $finfo->file( $file_path );

// ダウンロードのヘッダーを送信する
header( 'Content-Type: ' . $mime_type );
header( 'Content-Disposition: attachment; filename="' . $filename . '"' );
header( 'Content-Length: ' . filesize( $file_path ) );
header( 'Cache-Control: no-cache, no-store, must-revalidate' );

// ファイルの内容を出力する
readfile( $file_path );
exit;

7. 実践チェックリスト

✓ file_get_contents() でファイルを読み込んで戻り値が false でないか確認しているか

✓ ファイルパスは __DIR__ を使って絶対パスで指定しているか

✓ ファイルへの書き込みは FILE_APPEND | LOCK_EX で追記・ロックを設定しているか

✓ アップロードフォームに enctype="multipart/form-data" を付けているか

✓ $_FILES['type'] は使わず finfo クラスで実際のMIMEタイプを確認しているか

✓ アップロードされたファイルの元の名前は使わず uniqid() で新しいファイル名を生成しているか

✓ ファイルサイズの上限を設定して大きすぎるファイルを拒否しているか

✓ HTMLに出力するファイル名は htmlspecialchars() でエスケープしているか

✓ ダウンロード機能では basename() でディレクトリトラバーサル攻撃を防いでいるか


まとめ

今回はPHPのファイル操作として読み込み・書き込み・アップロードの実装方法を解説しました。ポイントをまとめると:

  • file_get_contents() でファイル全体を・file() で1行ずつ・fopen + fgets で大きなファイルをメモリ効率よく読み込める
  • CSVの読み込みは fgetcsv・書き出しは fputcsv を使うとカンマや改行を自動でエスケープしてくれる
  • file_put_contents() で書き込み・FILE_APPEND で追記・LOCK_EX で排他ロックをかけて安全に書き込む
  • アップロードフォームには enctype="multipart/form-data" が必須
  • $_FILES['type'] は信頼せず finfo クラスでファイルの実際の中身からMIMEタイプを判定する
  • アップロードされたファイルの元の名前は使わず uniqid() + random_bytes() で安全なファイル名を生成する
  • ファイルのサイズ上限・MIMEタイプ・拡張子の3つをすべて検証してから move_uploaded_file() で移動する
  • ダウンロード機能では basename() を使ってディレクトリトラバーサル攻撃(../ で上位ディレクトリにアクセスする攻撃)を防ぐ

次の記事では、PHPでメールを送信する方法を解説します。mail関数の基本からPHPMailerを使ったGmail送信・お問い合わせフォームの実装まで丁寧にお届けします。お楽しみに!

コメント

タイトルとURLをコピーしました