タイトル: $_FILES
SEOタイトル: PHP $_FILES でのファイルアップロード完全ガイド
この本文には実行可能なタグ等が含まれるため、安全のためHTMLソースとして表示しています。本文データは変更していません。
<table border="1" cellpadding="6" cellspacing="0" style="border-collapse:collapse;background:#fffbea;border:1px solid #f4c430;margin:1em 0;width:100%"><tr><td style="background:#f4c430;color:#000;font-weight:bold;padding:8px">この記事の要点</td></tr><tr><td style="padding:10px"><ul><li><strong><code>$_FILES</code></strong> は <code>enctype="multipart/form-data"</code> を付けた form の POST でのみ生成される</li><li><strong>キー</strong>: <code>name</code> 元ファイル名、<code>type</code> MIME、<code>size</code> バイト数、<code>tmp_name</code> 一時パス、<code>error</code> エラーコード</li><li><strong>必ず</strong> <code>is_uploaded_file()</code> + <code>move_uploaded_file()</code> でセキュアに移動</li><li><code>php.ini</code> の <code>upload_max_filesize</code> / <code>post_max_size</code> / <code>max_file_uploads</code> を確認</li><li>Laravel なら <code>$request->file("avatar")->store("avatars")</code> で MIME 検証 / 保存まで一発</li></ul></td></tr></table>
<h2>基本: HTML フォームと PHP 側の対応</h2>
<pre style="background:#1e293b;color:#e2e8f0;padding:1em;border-radius:6px;overflow-x:auto;line-height:1.5"><code class="language-html"><!-- ★ enctype と method を必ず指定 -->
<form action="/upload.php" method="POST" enctype="multipart/form-data">
<input type="hidden" name="MAX_FILE_SIZE" value="2097152"> <!-- 2MB ヒント -->
<input type="file" name="avatar">
<button type="submit">Upload</button>
</form></code></pre><pre style="background:#1e293b;color:#e2e8f0;padding:1em;border-radius:6px;overflow-x:auto;line-height:1.5"><code class="language-php"><?php
// $_FILES['avatar'] の構造
[
'name' => '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;</code></pre>
<h2>UPLOAD_ERR_* 定数一覧</h2>
<table border="1" cellpadding="6" cellspacing="0" style="border-collapse:collapse;width:100%">
<thead><tr style="background:#e3f2fd"><th>値</th><th>定数</th><th>意味</th></tr></thead>
<tbody>
<tr><td>0</td><td><code>UPLOAD_ERR_OK</code></td><td>成功</td></tr>
<tr><td>1</td><td><code>UPLOAD_ERR_INI_SIZE</code></td><td>php.ini の <code>upload_max_filesize</code> 超過</td></tr>
<tr><td>2</td><td><code>UPLOAD_ERR_FORM_SIZE</code></td><td>HTML の <code>MAX_FILE_SIZE</code> 超過</td></tr>
<tr><td>3</td><td><code>UPLOAD_ERR_PARTIAL</code></td><td>部分的にしかアップロードされていない</td></tr>
<tr><td>4</td><td><code>UPLOAD_ERR_NO_FILE</code></td><td>ファイルが選択されていない</td></tr>
<tr><td>6</td><td><code>UPLOAD_ERR_NO_TMP_DIR</code></td><td>一時ディレクトリが無い</td></tr>
<tr><td>7</td><td><code>UPLOAD_ERR_CANT_WRITE</code></td><td>ディスク書込失敗</td></tr>
<tr><td>8</td><td><code>UPLOAD_ERR_EXTENSION</code></td><td>拡張モジュールにより中止</td></tr>
</tbody>
</table>
<h2>php.ini の関連設定</h2>
<pre style="background:#1e293b;color:#e2e8f0;padding:1em;border-radius:6px;overflow-x:auto;line-height:1.5"><code class="language-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</code></pre>
<p>変更後は <code>systemctl restart php8.3-fpm</code>。<strong>Nginx の <code>client_max_body_size</code></strong> も合わせて設定しないと、PHP に到達する前に 413 で弾かれます。</p>
<h2>複数ファイルのアップロード</h2>
<pre style="background:#1e293b;color:#e2e8f0;padding:1em;border-radius:6px;overflow-x:auto;line-height:1.5"><code class="language-html"><form action="/upload.php" method="POST" enctype="multipart/form-data">
<input type="file" name="files[]" multiple>
<button>Upload</button>
</form></code></pre><pre style="background:#1e293b;color:#e2e8f0;padding:1em;border-radius:6px;overflow-x:auto;line-height:1.5"><code class="language-php"><?php
// $_FILES['files'] は ↓ のような「列指向」になる
[
'name' => ['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);
}</code></pre>
<h2>セキュリティ: 画像 MIME 検証</h2>
<p>クライアント送信の <code>type</code> や拡張子は<strong>絶対に信用しない</strong>こと。<code>example.php.jpg</code> のようなファイルが PHP として実行される事故を防ぎます:</p>
<pre style="background:#1e293b;color:#e2e8f0;padding:1em;border-radius:6px;overflow-x:auto;line-height:1.5"><code class="language-php"><?php
$file = $_FILES['avatar'];
// 1. サイズ上限を別途チェック(php.ini 任せにしない)
if ($file['size'] > 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 実行禁止)</code></pre>
<h2>Laravel での書き方</h2>
<pre style="background:#1e293b;color:#e2e8f0;padding:1em;border-radius:6px;overflow-x:auto;line-height:1.5"><code class="language-php">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);
}</code></pre>
<h2>よくあるトラブル</h2>
<table border="1" cellpadding="6" cellspacing="0" style="border-collapse:collapse;width:100%">
<thead><tr style="background:#fff3e0"><th>症状</th><th>原因</th><th>対処</th></tr></thead>
<tbody>
<tr><td>$_FILES が空</td><td>form の <code>enctype</code> 未指定 / GET 送信</td><td><code>multipart/form-data</code> + POST</td></tr>
<tr><td>413 Request Entity Too Large</td><td>Nginx の <code>client_max_body_size</code></td><td>該当値を増やす</td></tr>
<tr><td>error=1(INI_SIZE)</td><td>upload_max_filesize 超過</td><td>php.ini で増やす</td></tr>
<tr><td>error=3(PARTIAL)</td><td>クライアント側で切断</td><td>ネットワーク or タイムアウト確認</td></tr>
<tr><td>error=6(NO_TMP_DIR)</td><td>upload_tmp_dir 不在</td><td>ディレクトリ作成 / 権限付与</td></tr>
</tbody>
</table>
<h2>FAQ</h2>
<p><strong>Q: HTML の <code>MAX_FILE_SIZE</code> hidden は意味ある?</strong><br />
A: クライアントへのヒントにはなりますが、<strong>サーバ側で信用できない</strong>ため、サーバでもチェック必須です。</p>
<p><strong>Q: アップロードファイルを直接 DB に保存したい</strong><br />
A: BLOB として可能ですが、画像は<strong>ファイルシステム / S3</strong>、メタデータのみ DB が定石。バックアップ容易性のため。</p>
<p><strong>Q: チャンクアップロード(巨大ファイル分割送信)したい</strong><br />
A: PHP 単体機能では無いので、フロント側 (resumable.js / tus-js-client) と組み合わせるか、Laravel なら <code>laravel-chunk-upload</code> パッケージを使います。</p>