100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
PHP

File Uploads in PHP

Learn how PHP handles multipart form uploads through the $_FILES superglobal, including validation, error codes, and safely moving uploaded files on disk.

Web Development with PHPIntermediate9 min readJul 9, 2026
Analogies

File Uploads in PHP

PHP has built-in support for handling file uploads submitted through HTML forms using the multipart/form-data encoding. When a browser submits such a form, PHP automatically parses the request body and populates the $_FILES superglobal with metadata about each uploaded file, while the raw bytes are written to a temporary location on disk. Your script's job is to validate that upload and move it to a permanent, safe location before the request ends and PHP cleans up the temp file.

🏏

Cricket analogy: When a player submits their kit bag at the stadium gate, ground staff log its contents and stash it in a temporary holding room until match officials formally clear it for the dressing room — just as PHP parks an upload in a temp file until your script clears it.

The $_FILES superglobal structure

For a form field named avatar, $_FILES['avatar'] is an associative array with keys name (original client-side filename), type (client-supplied MIME type, which is untrustworthy), tmp_name (the server-side temp path), error (an UPLOAD_ERR_* constant), and size (bytes). If the field allows multiple files via avatar[], each of these becomes a nested array indexed by position, which requires careful reshaping if you want one struct per file.

🏏

Cricket analogy: A scorer's entry sheet for a single batter lists name, declared batting style, actual delivery footage, dismissal code, and runs scored — and if multiple batters submit at once, each field becomes a list indexed by batting order, just like $_FILES['avatar'][] for multiple uploads.

php
<?php
declare(strict_types=1);

enum UploadStatus {
    case Ok;
    case TooLarge;
    case BadType;
    case UploadError;
}

function handleAvatarUpload(array $file, string $destDir): UploadStatus
{
    if ($file['error'] !== UPLOAD_ERR_OK) {
        return UploadStatus::UploadError;
    }

    $maxBytes = 2 * 1024 * 1024; // 2MB
    if ($file['size'] > $maxBytes) {
        return UploadStatus::TooLarge;
    }

    // Never trust $file['type'] — inspect the real content instead.
    $finfo = finfo_open(FILEINFO_MIME_TYPE);
    $mime = finfo_file($finfo, $file['tmp_name']);
    finfo_close($finfo);

    $allowed = ['image/png' => 'png', 'image/jpeg' => 'jpg', 'image/webp' => 'webp'];
    if (!isset($allowed[$mime])) {
        return UploadStatus::BadType;
    }

    $ext = $allowed[$mime];
    $safeName = bin2hex(random_bytes(16)) . '.' . $ext;

    if (!is_uploaded_file($file['tmp_name'])) {
        return UploadStatus::UploadError;
    }

    move_uploaded_file($file['tmp_name'], rtrim($destDir, '/') . '/' . $safeName);

    return UploadStatus::Ok;
}

Validating size, type, and origin

Robust upload handling checks four things independently: the error code, the size against both php.ini limits and your own application limit, the actual file content type via fileinfo rather than the client-supplied MIME string, and that the temp file was genuinely produced by PHP's upload mechanism. That last check matters because tmp_name is just a string — without verifying it, a maliciously crafted request could reference an arbitrary server path.

🏏

Cricket analogy: Before certifying a bat for match use, officials independently check its willow grade, physical dimensions against the rulebook, an actual edge-thickness measurement rather than the manufacturer's claim, and a genuine certification stamp — four separate checks, just like robust upload validation.

PHP enforces two separate size ceilings from php.ini: upload_max_filesize caps a single file, while post_max_size caps the entire request body (all fields and files combined). If post_max_size is smaller than upload_max_filesize, large uploads silently fail — always keep post_max_size comfortably larger.

Moving and storing files safely

Always use move_uploaded_file() rather than rename() or copy() for the initial relocation — it internally verifies the source path is a genuine PHP upload before touching the filesystem, which rename() does not. Generate a new random filename instead of trusting the client's original name, since filenames can contain path traversal sequences like ../../etc/passwd or null bytes. Store uploads outside the web root when possible, or in a directory with script execution disabled, so an uploaded .php file can never be requested and executed directly.

🏏

Cricket analogy: A ground's official kit inspector uses a certified scale that verifies a bat is genuinely stamped for match use before it enters the field, rather than just glancing at a label — just as move_uploaded_file() verifies genuine PHP-uploaded origin, unlike rename().

Never store uploaded files inside a publicly served directory that also executes PHP. If an attacker can upload a file with a .php extension (or trick an extension check) and then request it via URL, the web server will execute it as code — a classic remote-code-execution path known as an unrestricted file upload vulnerability.

Understanding UPLOAD_ERR_* codes

PHP populates $file['error'] with one of several constants: UPLOAD_ERR_OK (0) means success, UPLOAD_ERR_INI_SIZE and UPLOAD_ERR_FORM_SIZE mean the file exceeded configured or form-specified limits, UPLOAD_ERR_PARTIAL means only part of the file arrived, UPLOAD_ERR_NO_FILE means no file was submitted, and UPLOAD_ERR_NO_TMP_DIR or UPLOAD_ERR_CANT_WRITE indicate server-side filesystem problems. Checking this code first, before inspecting size or tmp_name, avoids acting on a failed upload.

🏏

Cricket analogy: A match official checks the toss result code first — rain delay, no result, or completed — before even looking at the scoreboard, because acting on a score from an abandoned match would be meaningless, just as PHP checks $file['error'] before size or tmp_name.

  • $_FILES is populated automatically for multipart/form-data requests; tmp_name points to a temporary server file that is deleted at request end.
  • Always check $file['error'] against UPLOAD_ERR_OK before doing anything else with the upload.
  • Never trust the client-supplied 'type' or original 'name' — verify content type with fileinfo and generate a new random filename.
  • Use move_uploaded_file() (not rename/copy) so PHP verifies the file genuinely came through its upload mechanism.
  • post_max_size must be >= upload_max_filesize or large uploads fail silently.
  • Store uploads outside the web root or in a non-executable directory to prevent uploaded scripts from being run.

Practice what you learned

Was this page helpful?

Topics covered

#PHP#PHPProgrammingStudyNotes#Programming#FileUploadsInPHP#File#Uploads#FILES#Superglobal#StudyNotes#SkillVeris