topic: file-io (read / write / CSV / paths / upload概念) / ch06
ch06 — ファイルアップロードの概念
学習目標
- HTML フォームのアップロード送信に必要な
enctype="multipart/form-data"を言える - PHP 側に届く
$_FILESの構造 (name/tmp_name/size/type/error) を読める move_uploaded_file($tmp, $dest)で 一時ファイルを 保存先に移す 流れが分かる- アップロード処理での セキュリティ最低限 (拡張子ホワイトリスト / サイズ上限 / 保存ディレクトリの分離) を言える
所要時間
スライドのみ = 7〜8 分
ドリル
このチャプターにドリルはありません。
理由: ファイルアップロードは HTML フォーム → HTTP multipart/form-data → PHP の $_FILES という Web ランタイム前提の機能で、本コースの CLI 採点ランナーでは再現できない。$_FILES を CLI で擬似的に埋めるスタブは書けるが、move_uploaded_file が 「本物のアップロード由来のファイルしか動かさない」 ように保護されているため、意味のあるドリルにならない。
代わりに 「概念と落とし穴」 をスライドで頭に入れ、実機 (php -S + ブラウザ) で 任意で試す案内とする。
本物の体験 (任意)
php -S でローカルサーバーを立ち上げてブラウザから試すと、5 分で全体像が掴める。スライド末尾の最小サンプル (form.html + upload.php) を写経して、ブラウザで localhost:8000/form.html を開いて画像を 1 枚アップしてみると分かりやすい。
次のステップ
ファイルアップロードを本格的に扱うのは Laravel など Web フレームワーク (バリデーション / Storage ドライバ / 画像変換ライブラリ) を学ぶ時。本コースは 「生 PHP で何が起きているか」を理解する ところまでに留める。
🎞 解説スライド
教材スライド(Marp)を1枚ずつ表示します。矢印キー(←→)・スワイプ・ナビボタンで移動できます。
-
<!-- LLM_CONTEXT: Lesson 14 / Chapter 6 目的: ファイルアップロードの全体像 (multipart form / $_FILES / move_uploaded_file) と最低限のセキュリティを言葉にする 扱わない: 画像加工 / Storage ドライバ / バリデーションフレームワーク (Web フレームワークで覚える領域) 読み上げ時間目安: 7〜8 分 -->
ファイルアップロードの概念
Lesson 14 / Chapter 6
-
なぜこのチャプターは「概念だけ」か
- アップロードは ブラウザ → HTTP
multipart/form-data→ PHP の$_FILESという Web 前提の機能 - 本コースの CLI 採点ランナーでは再現できない (
move_uploaded_fileが「本物のアップロード由来か」を内部チェックして弾く) - そこで本チャプターは 「全体像と落とし穴」 を言葉と絵で覚え、実機で試したい人だけ
php -Sで体感してもらう構成
→ ドリルはなし。スライドを読み終わったら L14 完了。
- アップロードは ブラウザ → HTTP
-
HTML フォーム側に必要なもの
<!DOCTYPE html> <form action="/upload.php" method="post" enctype="multipart/form-data"> <input type="file" name="avatar"> <button type="submit">アップロード</button> </form>要素 何のため method="post"ファイルは大きい / URL に乗せられないので 必ず POST enctype="multipart/form-data"バイナリも送れる multipart 形式 を指示 (これを忘れると $_FILESが空になる No.1 原因)<input type="file" name="avatar">ファイル選択 UI。 name属性が PHP 側の$_FILES['avatar']になる→ 「enctype 付け忘れ」が初学者の事故 No.1。テンプレ化して覚える。
-
PHP 側に届く
$_FILESの構造<?php // /upload.php print_r($_FILES); // 出力 (avatar に hello.png をアップロードした場合): // Array ( // [avatar] => Array ( // [name] => hello.png ← 元のファイル名 // [type] => image/png ← MIME タイプ (ブラウザ申告: 信用しない) // [tmp_name] => /tmp/phpAbCdEf ← サーバー側で一時保存されたパス // [error] => 0 ← UPLOAD_ERR_OK = 0 // [size] => 12345 ← バイト数 // ) // )$_FILES['<name>']は 連想配列 (name/type/tmp_name/error/size)tmp_name= PHP が 自動で一時ディレクトリに保存したパス。リクエスト終了時に消える- 受け取った側の役目は
tmp_nameを恒久保存先に移す こと
-
move_uploaded_fileで恒久保存先に移す<?php // /upload.php if (($_FILES['avatar']['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) { http_response_code(400); exit("アップロード失敗"); } $tmp = $_FILES['avatar']['tmp_name']; $dest = __DIR__ . '/uploads/' . basename($_FILES['avatar']['name']); if (move_uploaded_file($tmp, $dest)) { echo "OK: $dest に保存"; } else { http_response_code(500); exit("保存失敗"); }move_uploaded_file($tmp, $dest)= アップロード由来の一時ファイルを 保存先に移すrenameではなくmove_uploaded_fileを使う のが鉄則 (アップロード由来かを内部検証してくれる = 任意ファイル移動の悪用を弾く)errorが 0 (=UPLOAD_ERR_OK) かを 先に確認
-
セキュリティ最低限 4 つ
守ること 何をする 拡張子ホワイトリスト pathinfo($name, PATHINFO_EXTENSION)を取り、許可リスト (['jpg','png','gif']等) に 入っていれば通すサイズ上限 $_FILES[..]['size']を見て、上限超過なら 413 / 400 で拒否保存ディレクトリの分離 公開ディレクトリ ( public/) に PHP 実行可能な状態で置かない。uploads/を.htaccessなどで PHP 実行禁止にファイル名のサニタイズ ユーザー名そのままを使わない。 uniqid()などで サーバー側生成 にする (../../etc/passwd攻撃の防止)→ 「ブラックリスト方式」(
.phpを弾く) は破られやすい。「ホワイトリスト方式」(.jpgだけ通す) を defult に。 -
$_FILES['x']['error']のエラーコード定数 値 意味 UPLOAD_ERR_OK0 成功 UPLOAD_ERR_INI_SIZE1 php.iniのupload_max_filesize超過UPLOAD_ERR_FORM_SIZE2 HTML form の MAX_FILE_SIZE超過UPLOAD_ERR_PARTIAL3 一部しか届かなかった UPLOAD_ERR_NO_FILE4 ファイル未選択 UPLOAD_ERR_NO_TMP_DIR6 一時ディレクトリ無し UPLOAD_ERR_CANT_WRITE7 ディスクに書けない UPLOAD_ERR_EXTENSION8 拡張モジュールが拒否 → 「0 かどうか」だけで判断しない。ユーザーに 「何が原因か」 を 1 行返せると親切。
-
本物の動作確認 (任意)
mkdir -p /tmp/upload-demo && cd /tmp/upload-demo mkdir uploadsform.html:<form action="/upload.php" method="post" enctype="multipart/form-data"> <input type="file" name="avatar"> <button type="submit">送る</button> </form>upload.php:<?php if (($_FILES['avatar']['error'] ?? 4) !== 0) exit("err: ".$_FILES['avatar']['error']); $dest = __DIR__ . '/uploads/' . basename($_FILES['avatar']['name']); move_uploaded_file($_FILES['avatar']['tmp_name'], $dest); echo "saved to: $dest";php -S localhost:8000 # ブラウザで http://localhost:8000/form.html を開いて 画像を 1 枚送信 -
このチャプターでできるようになること
✅ HTML form で
enctype="multipart/form-data"が必要だと言える ✅$_FILES['<name>']の構造 (name/tmp_name/size/type/error) を読める ✅move_uploaded_fileで 一時ファイルを保存先に移す流れを 図で説明できる ✅ セキュリティの最低限 (拡張子ホワイトリスト / サイズ上限 / 保存ディレクトリ分離 / ファイル名サニタイズ) を言える ✅errorコードを見て 「何が原因か」 をユーザーに返す重要性を理解した→ L14 完了。お疲れさまでした。
スライドを全部一気に読む(縦表示)
<!-- LLM_CONTEXT: Lesson 14 / Chapter 6 目的: ファイルアップロードの全体像 (multipart form / $_FILES / move_uploaded_file) と最低限のセキュリティを言葉にする 扱わない: 画像加工 / Storage ドライバ / バリデーションフレームワーク (Web フレームワークで覚える領域) 読み上げ時間目安: 7〜8 分 -->
ファイルアップロードの概念
Lesson 14 / Chapter 6
なぜこのチャプターは「概念だけ」か
- アップロードは ブラウザ → HTTP
multipart/form-data→ PHP の$_FILESという Web 前提の機能 - 本コースの CLI 採点ランナーでは再現できない (
move_uploaded_fileが「本物のアップロード由来か」を内部チェックして弾く) - そこで本チャプターは 「全体像と落とし穴」 を言葉と絵で覚え、実機で試したい人だけ
php -Sで体感してもらう構成
→ ドリルはなし。スライドを読み終わったら L14 完了。
HTML フォーム側に必要なもの
<!DOCTYPE html>
<form action="/upload.php" method="post" enctype="multipart/form-data">
<input type="file" name="avatar">
<button type="submit">アップロード</button>
</form>
| 要素 | 何のため |
|---|---|
method="post" |
ファイルは大きい / URL に乗せられないので 必ず POST |
enctype="multipart/form-data" |
バイナリも送れる multipart 形式 を指示 (これを忘れると $_FILES が空になる No.1 原因) |
<input type="file" name="avatar"> |
ファイル選択 UI。name 属性が PHP 側の $_FILES['avatar'] になる |
→ 「enctype 付け忘れ」が初学者の事故 No.1。テンプレ化して覚える。
PHP 側に届く $_FILES の構造
<?php
// /upload.php
print_r($_FILES);
// 出力 (avatar に hello.png をアップロードした場合):
// Array (
// [avatar] => Array (
// [name] => hello.png ← 元のファイル名
// [type] => image/png ← MIME タイプ (ブラウザ申告: 信用しない)
// [tmp_name] => /tmp/phpAbCdEf ← サーバー側で一時保存されたパス
// [error] => 0 ← UPLOAD_ERR_OK = 0
// [size] => 12345 ← バイト数
// )
// )$_FILES['<name>']は 連想配列 (name/type/tmp_name/error/size)tmp_name= PHP が 自動で一時ディレクトリに保存したパス。リクエスト終了時に消える- 受け取った側の役目は
tmp_nameを恒久保存先に移す こと
move_uploaded_file で恒久保存先に移す
<?php
// /upload.php
if (($_FILES['avatar']['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
http_response_code(400);
exit("アップロード失敗");
}
$tmp = $_FILES['avatar']['tmp_name'];
$dest = __DIR__ . '/uploads/' . basename($_FILES['avatar']['name']);
if (move_uploaded_file($tmp, $dest)) {
echo "OK: $dest に保存";
} else {
http_response_code(500);
exit("保存失敗");
}move_uploaded_file($tmp, $dest)= アップロード由来の一時ファイルを 保存先に移すrenameではなくmove_uploaded_fileを使う のが鉄則 (アップロード由来かを内部検証してくれる = 任意ファイル移動の悪用を弾く)errorが 0 (=UPLOAD_ERR_OK) かを 先に確認
セキュリティ最低限 4 つ
| 守ること | 何をする |
|---|---|
| 拡張子ホワイトリスト | pathinfo($name, PATHINFO_EXTENSION) を取り、許可リスト (['jpg','png','gif'] 等) に 入っていれば通す |
| サイズ上限 | $_FILES[..]['size'] を見て、上限超過なら 413 / 400 で拒否 |
| 保存ディレクトリの分離 | 公開ディレクトリ (public/) に PHP 実行可能な状態で置かない。uploads/ を .htaccess などで PHP 実行禁止に |
| ファイル名のサニタイズ | ユーザー名そのままを使わない。uniqid() などで サーバー側生成 にする (../../etc/passwd 攻撃の防止) |
→ 「ブラックリスト方式」(.php を弾く) は破られやすい。「ホワイトリスト方式」(.jpg だけ通す) を defult に。
$_FILES['x']['error'] のエラーコード
| 定数 | 値 | 意味 |
|---|---|---|
UPLOAD_ERR_OK |
0 | 成功 |
UPLOAD_ERR_INI_SIZE |
1 | php.ini の upload_max_filesize 超過 |
UPLOAD_ERR_FORM_SIZE |
2 | HTML form の MAX_FILE_SIZE 超過 |
UPLOAD_ERR_PARTIAL |
3 | 一部しか届かなかった |
UPLOAD_ERR_NO_FILE |
4 | ファイル未選択 |
UPLOAD_ERR_NO_TMP_DIR |
6 | 一時ディレクトリ無し |
UPLOAD_ERR_CANT_WRITE |
7 | ディスクに書けない |
UPLOAD_ERR_EXTENSION |
8 | 拡張モジュールが拒否 |
→ 「0 かどうか」だけで判断しない。ユーザーに 「何が原因か」 を 1 行返せると親切。
本物の動作確認 (任意)
mkdir -p /tmp/upload-demo && cd /tmp/upload-demo
mkdir uploads
form.html:
<form action="/upload.php" method="post" enctype="multipart/form-data">
<input type="file" name="avatar">
<button type="submit">送る</button>
</form>
upload.php:
<?php
if (($_FILES['avatar']['error'] ?? 4) !== 0) exit("err: ".$_FILES['avatar']['error']);
$dest = __DIR__ . '/uploads/' . basename($_FILES['avatar']['name']);
move_uploaded_file($_FILES['avatar']['tmp_name'], $dest);
echo "saved to: $dest";php -S localhost:8000
# ブラウザで http://localhost:8000/form.html を開いて 画像を 1 枚送信
このチャプターでできるようになること
✅ HTML form で enctype="multipart/form-data" が必要だと言える
✅ $_FILES['<name>'] の構造 (name / tmp_name / size / type / error) を読める
✅ move_uploaded_file で 一時ファイルを保存先に移す流れを 図で説明できる
✅ セキュリティの最低限 (拡張子ホワイトリスト / サイズ上限 / 保存ディレクトリ分離 / ファイル名サニタイズ) を言える
✅ error コードを見て 「何が原因か」 をユーザーに返す重要性を理解した
→ L14 完了。お疲れさまでした。