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";
        }
    }
}
?>

▶ Example: Basic File Upload with Error Handling

Output:

TEXT 📖 Display only
<p style='color:green'>Upload successful!</p>
<p style='color:red'>Please select a file</p>
<p style='color:red'>Upload failed. Error code: {/var/www/html['error']}</p>
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>

Output:

TEXT 📖 Display only
Output displayed

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;
?>

▶ Example: Complete Secure Upload Function

Output:

TEXT 📖 Display only
25
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'];
?>

Output:

TEXT 📖 Display only
Upload successful

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>

▶ Example: File Upload Security Checklist

Output:

TEXT 📖 Display only
(no visible output - function defined)
PHP
<?php
function validateUpload(array $file): string {
    if ($file['error'] !== UPLOAD_ERR_OK) return "Upload error";
    if ($file['size'] > 2 * 1024 * 1024) return "File exceeds 2MB limit";
    $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
    if (!in_array($ext, ['jpg', 'jpeg', 'png', 'gif'])) return "Extension not allowed";
    $finfo = finfo_open(FILEINFO_MIME_TYPE);
    $mime = finfo_file($finfo, $file['tmp_name']);
    finfo_close($finfo);
    if (!in_array($mime, ['image/jpeg', 'image/png', 'image/gif'])) return "MIME type mismatch";
    return "File is safe to upload";
}

Output:

TEXT 📖 Display only
Output displayed

❓ FAQ

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.

📖 Summary

📝 Exercises

  1. Build an avatar upload feature: limit to 1MB, allow only jpg/png/gif, and display a preview after uploading.
  2. Add complete error feedback to the upload feature (file too large, wrong type, no file selected) and preserve the form state on failure.
  3. Implement a bulk uploader: upload up to 5 images and display them in a thumbnail grid after upload.
Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏