PHP: File Uploads in PHP
Profile pictures, résumé attachments, image sharing — every real web application handles file uploads. PHP makes it straightforward, but a single overlooked security detail can open a serious hole.
1. Prerequisites
(1) Required HTML Form Setup
HTML
<!-- The three requirements for a file upload form -->
<form method="POST" enctype="multipart/form-data">
<input type="file" name="avatar">
<button type="submit">Upload</button>
</form>
| Requirement | Why |
|---|---|
method="POST" |
Files must be sent via POST |
enctype="multipart/form-data" |
Required! Without it, only the filename is sent |
type="file" |
Tells the browser to show a file picker |
🔥 Common Mistake: Forgetting
enctype="multipart/form-data" is the number-one file upload mistake — PHP receives the filename but never the file contents.
(2) Create the Upload Directory
Inside myphp/, create an uploads/ folder and make sure PHP can write to it:
PHP
<?php
// Automatically create the upload directory from your script
$uploadDir = __DIR__ . '/uploads/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
?>
2. $_FILES In Depth
PHP
<?php
// After uploading a file, $_FILES is structured like this:
print_r($_FILES['avatar']);
/*
Array (
[name] => photo.jpg // Original filename
[type] => image/jpeg // MIME type reported by the browser
[tmp_name] => C:\xampp\tmp\php123.tmp // Temporary path on the server
[error] => 0 // Error code (0 = success)
[size] => 85469 // File size in bytes
)
*/
?>
| Key | Meaning | Heads-up |
|---|---|---|
name |
Original filename | Not trustworthy — users can spoof it |
type |
MIME type | Browser-reported, not trustworthy |
tmp_name |
Server temporary file | Automatically deleted when the script ends |
error |
Error code | 0 = success, anything else = problem |
size |
Size in bytes | Useful for enforcing size limits |
(1) Error Codes
| Code | Constant | Meaning |
|---|---|---|
| 0 | UPLOAD_ERR_OK |
✅ Upload successful |
| 1 | UPLOAD_ERR_INI_SIZE |
Exceeds php.ini's upload_max_filesize |
| 2 | UPLOAD_ERR_FORM_SIZE |
Exceeds the form's MAX_FILE_SIZE |
| 3 | UPLOAD_ERR_PARTIAL |
File was only partially uploaded |
| 4 | UPLOAD_ERR_NO_FILE |
No file was selected |
| 6 | UPLOAD_ERR_NO_TMP_DIR |
No temporary folder configured |
| 7 | UPLOAD_ERR_CANT_WRITE |
Disk write failure |
3. move_uploaded_file() — The Core Function
PHP stores uploaded files in a temporary directory. You must explicitly move them to a permanent location, or they're lost when the script ends:
PHP
<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$file = $_FILES['avatar'];
$uploadDir = __DIR__ . '/uploads/';
if ($file['error'] !== UPLOAD_ERR_OK) {
echo "Upload failed. Error code: " . $file['error'];
} else {
$targetPath = $uploadDir . $file['name'];
if (move_uploaded_file($file['tmp_name'], $targetPath)) {
echo "Upload successful! File saved at: {$targetPath}";
} else {
echo "Failed to move the uploaded file";
}
}
}
?>
▶ サンプル: Basic File Upload with Error Handling
PHP
<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$file = $_FILES['avatar'];
$uploadDir = __DIR__ . '/uploads/';
if ($file['error'] === UPLOAD_ERR_OK) {
$targetPath = $uploadDir . $file['name'];
if (move_uploaded_file($file['tmp_name'], $targetPath)) {
echo "<p style='color:green'>Upload successful!</p>";
}
} elseif ($file['error'] === UPLOAD_ERR_NO_FILE) {
echo "<p style='color:red'>Please select a file</p>";
} else {
echo "<p style='color:red'>Upload failed. Error code: {$file['error']}</p>";
}
}
?>
<form method="POST" enctype="multipart/form-data">
<input type="file" name="avatar">
<button type="submit">Upload</button>
</form>
4. Security Validation (Critical!)
(1) 4.1 Limit File Size
PHP
<?php
$maxSize = 2 * 1024 * 1024; // 2MB
if ($file['size'] > $maxSize) {
die("File too large — maximum 2MB allowed");
}
?>
(2) 4.2 Extension Whitelist
PHP
<?php
$allowed = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if (!in_array($ext, $allowed)) {
die("File type not allowed. Accepted: " . implode(', ', $allowed));
}
?>
💡 Tip: Always use a whitelist (only allow known-safe types), never a blacklist (blocking .php, .exe, etc.). Attackers will always find an extension your blacklist missed.
(3) 4.3 Generate a Safe Filename
PHP
<?php
// ❌ Using the user-supplied filename directly is dangerous
// $targetPath = $uploadDir . $file['name'];
// ✅ Generate a unique, safe filename
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
$safeName = uniqid('img_') . '.' . $ext;
$targetPath = $uploadDir . $safeName;
?>
▶ サンプル: Complete Secure Upload Function
PHP
<?php
function handleUpload(array $file, string $uploadDir): array {
$maxSize = 2 * 1024 * 1024; // 2MB
$allowed = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
// 1. Check for upload errors
if ($file['error'] !== UPLOAD_ERR_OK) {
return ['success' => false, 'message' => 'Upload failed'];
}
// 2. Check file size
if ($file['size'] > $maxSize) {
return ['success' => false, 'message' => 'File must not exceed 2MB'];
}
// 3. Check extension (whitelist)
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if (!in_array($ext, $allowed)) {
return ['success' => false, 'message' => 'Only image files are allowed'];
}
// 4. Verify the real file type (Magic Bytes — far more reliable than extension)
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mimeType = finfo_file($finfo, $file['tmp_name']);
finfo_close($finfo);
$allowedMimes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
if (!in_array($mimeType, $allowedMimes)) {
return ['success' => false, 'message' => 'File type mismatch'];
}
// 5. Generate a safe filename
$safeName = uniqid('upload_') . '.' . $ext;
$targetPath = $uploadDir . $safeName;
// 6. Move the file
if (move_uploaded_file($file['tmp_name'], $targetPath)) {
return [
'success' => true,
'message' => 'Upload successful',
'path' => $targetPath,
'name' => $safeName,
];
}
return ['success' => false, 'message' => 'Failed to save the file'];
}
// Usage
$result = handleUpload($_FILES['avatar'], __DIR__ . '/uploads/');
echo $result['message'];
?>
5. Multiple File Uploads
PHP
<form method="POST" enctype="multipart/form-data">
<input type="file" name="photos[]" multiple>
<button type="submit">Upload Multiple</button>
</form>
PHP
<?php
$uploadDir = __DIR__ . '/uploads/';
$results = [];
foreach ($_FILES['photos']['name'] as $i => $name) {
// Reassemble into a single-file array
$singleFile = [
'name' => $_FILES['photos']['name'][$i],
'type' => $_FILES['photos']['type'][$i],
'tmp_name' => $_FILES['photos']['tmp_name'][$i],
'error' => $_FILES['photos']['error'][$i],
'size' => $_FILES['photos']['size'][$i],
];
$results[] = handleUpload($singleFile, $uploadDir);
}
foreach ($results as $r) {
echo $r['message'] . "<br>";
}
?>
💡 Tip: PHP stores multiple files in a "columnar" structure — all names in one array, all sizes in another, etc. You need to manually reassemble them into per-file arrays for processing.
6. Displaying Uploaded Images
PHP
<?php
// List images in the uploads directory
$images = glob(__DIR__ . '/uploads/*.{jpg,jpeg,png,gif,webp}', GLOB_BRACE);
?>
<h3>Uploaded Images</h3>
<div style="display:flex;flex-wrap:wrap;gap:10px">
<?php foreach ($images as $img): ?>
<img src="uploads/<?= basename($img) ?>" width="150">
<?php endforeach; ?>
</div>
<form method="POST" enctype="multipart/form-data">
<input type="file" name="avatar">
<button type="submit">Upload</button>
</form>
❓ よくある質問
Q What's the difference between
move_uploaded_file and copy?A
move_uploaded_file verifies that the file genuinely arrived via HTTP POST (safe). Plain copy() could be exploited to duplicate arbitrary files on the server. Always use move_uploaded_file() for uploads.Q Large files keep failing. What's wrong?
A Check three limits: (1)
upload_max_filesize in php.ini (default 2M); (2) post_max_size (must be larger than upload_max_filesize); (3) the optional MAX_FILE_SIZE hidden field in the form. After changing php.ini, restart Apache/Nginx.Q Is
$_FILES['avatar']['type'] reliable?A No. It's the MIME type reported by the browser, which is trivial to forge. Use
finfo_file() to inspect the file's actual Magic Bytes for accurate detection.📖 まとめ
- The form must include
enctype="multipart/form-data" $_FILES['field']['tmp_name']is the temp file — move it withmove_uploaded_file()- Four validation steps: size limit → extension whitelist → MIME check → safe rename
finfo_file()inspects the real file type via Magic Bytes — far more reliable than extension- Multiple file uploads require reassembling the
$_FILESstructure - Security rule: whitelist > blacklist, generate new filenames, enforce size limits
📝 練習問題
- Build an avatar upload feature: limit to 1MB, allow only jpg/png/gif, and display a preview after uploading.
- Add complete error feedback to the upload feature (file too large, wrong type, no file selected) and preserve the form state on failure.
- Implement a bulk uploader: upload up to 5 images and display them in a thumbnail grid after upload.