タイトル: $_FILES
SEOタイトル: PHP $_FILES でのファイルアップロード完全ガイド
| この記事の要点 |
|
基本: HTML フォームと PHP 側の対応
'photo.jpg', // 元のファイル名(クライアント任意)
'type' => 'image/jpeg', // クライアント申告の MIME(信用不可)
'tmp_name' => '/tmp/phpAbCdEf', // サーバ上の一時パス
'error' => UPLOAD_ERR_OK, // 0 なら成功
'size' => 123456, // バイト数
'full_path'=> 'photo.jpg', // PHP 8.1+ 追加(ディレクトリ含む元パス)
]
// 受信側
$file = $_FILES['avatar'] ?? null;
if (!$file || $file['error'] !== UPLOAD_ERR_OK) {
die('Upload failed: ' . ($file['error'] ?? 'no file'));
}
if (!is_uploaded_file($file['tmp_name'])) {
die('Possibly a forged upload'); // セキュリティ必須
}
$dest = __DIR__ . '/uploads/' . uniqid('img_') . '.jpg';
if (!move_uploaded_file($file['tmp_name'], $dest)) {
die('Move failed');
}
echo 'Uploaded to ' . $dest;
UPLOAD_ERR_* 定数一覧
| 値 | 定数 | 意味 |
|---|---|---|
| 0 | UPLOAD_ERR_OK | 成功 |
| 1 | UPLOAD_ERR_INI_SIZE | php.ini の upload_max_filesize 超過 |
| 2 | UPLOAD_ERR_FORM_SIZE | HTML の MAX_FILE_SIZE 超過 |
| 3 | UPLOAD_ERR_PARTIAL | 部分的にしかアップロードされていない |
| 4 | UPLOAD_ERR_NO_FILE | ファイルが選択されていない |
| 6 | UPLOAD_ERR_NO_TMP_DIR | 一時ディレクトリが無い |
| 7 | UPLOAD_ERR_CANT_WRITE | ディスク書込失敗 |
| 8 | UPLOAD_ERR_EXTENSION | 拡張モジュールにより中止 |
php.ini の関連設定
; /etc/php/8.3/fpm/php.ini
; 1 ファイルあたり最大サイズ
upload_max_filesize = 10M
; POST 全体の最大サイズ(複数ファイル合計 + フォームフィールド)
; ★ upload_max_filesize 以上にする
post_max_size = 12M
; 同時アップロード可能なファイル数
max_file_uploads = 20
; 一時ディレクトリ(未指定なら sys_get_temp_dir)
upload_tmp_dir = /tmp/phpuploads
; メモリ上限(大きなファイル処理時に注意)
memory_limit = 256M
; 実行時間
max_execution_time = 300
max_input_time = 300
変更後は systemctl restart php8.3-fpm。Nginx の client_max_body_size も合わせて設定しないと、PHP に到達する前に 413 で弾かれます。
複数ファイルのアップロード
['a.jpg', 'b.png'],
'type' => ['image/jpeg', 'image/png'],
'tmp_name' => ['/tmp/php1', '/tmp/php2'],
'error' => [0, 0],
'size' => [12345, 23456],
]
// 「行指向」に変換して扱いやすく
$count = count($_FILES['files']['name']);
for ($i = 0; $i < $count; $i++) {
if ($_FILES['files']['error'][$i] !== UPLOAD_ERR_OK) continue;
$tmp = $_FILES['files']['tmp_name'][$i];
$name = $_FILES['files']['name'][$i];
$safeName = preg_replace('/[^A-Za-z0-9._-]/', '_', $name);
$dest = __DIR__ . '/uploads/' . uniqid() . '_' . $safeName;
move_uploaded_file($tmp, $dest);
}
セキュリティ: 画像 MIME 検証
クライアント送信の type や拡張子は絶対に信用しないこと。example.php.jpg のようなファイルが PHP として実行される事故を防ぎます:
5 * 1024 * 1024) { // 5MB
die('File too large');
}
// 2. 画像であることを実体で確認
$info = @getimagesize($file['tmp_name']);
if ($info === false) {
die('Not an image');
}
// 3. 許可する MIME のホワイトリスト
$allowed = ['image/jpeg' => 'jpg', 'image/png' => 'png', 'image/gif' => 'gif'];
$mime = $info['mime'];
if (!isset($allowed[$mime])) {
die('Unsupported image type: ' . $mime);
}
// 4. finfo でも二重チェック
$finfo = new finfo(FILEINFO_MIME_TYPE);
$realMime = $finfo->file($file['tmp_name']);
if ($realMime !== $mime) {
die('MIME mismatch');
}
// 5. 拡張子は自前で付ける(クライアント名を使わない)
$ext = $allowed[$mime];
$dest = __DIR__ . '/uploads/' . bin2hex(random_bytes(16)) . '.' . $ext;
if (!move_uploaded_file($file['tmp_name'], $dest)) {
die('Move failed');
}
// 6. 保存先は Web 公開外(または .htaccess で php 実行禁止)
Laravel での書き方
use Illuminate\Http\Request;
public function upload(Request $request)
{
// 1. バリデーション(MIME・サイズも一括)
$request->validate([
'avatar' => 'required|image|mimes:jpg,jpeg,png|max:5120', // 5MB
]);
// 2. ファイルオブジェクト取得
$file = $request->file('avatar');
// $file は Illuminate\Http\UploadedFile
// 3. 保存(storage/app/public/avatars 配下)
$path = $file->store('avatars', 'public');
// または自分でファイル名指定
$path = $file->storeAs('avatars', 'user_' . auth()->id() . '.jpg', 'public');
// 4. 公開 URL
$url = Storage::url($path);
return response()->json(['url' => $url], 201);
}
よくあるトラブル
| 症状 | 原因 | 対処 |
|---|---|---|
| $_FILES が空 | form の enctype 未指定 / GET 送信 | multipart/form-data + POST |
| 413 Request Entity Too Large | Nginx の client_max_body_size | 該当値を増やす |
| error=1(INI_SIZE) | upload_max_filesize 超過 | php.ini で増やす |
| error=3(PARTIAL) | クライアント側で切断 | ネットワーク or タイムアウト確認 |
| error=6(NO_TMP_DIR) | upload_tmp_dir 不在 | ディレクトリ作成 / 権限付与 |
FAQ
Q: HTML の MAX_FILE_SIZE hidden は意味ある?
A: クライアントへのヒントにはなりますが、サーバ側で信用できないため、サーバでもチェック必須です。
Q: アップロードファイルを直接 DB に保存したい
A: BLOB として可能ですが、画像はファイルシステム / S3、メタデータのみ DB が定石。バックアップ容易性のため。
Q: チャンクアップロード(巨大ファイル分割送信)したい
A: PHP 単体機能では無いので、フロント側 (resumable.js / tus-js-client) と組み合わせるか、Laravel なら laravel-chunk-upload パッケージを使います。