Reader Stacks

How to Upload a File in Plain PHP, Without a Framework

The $_FILES superglobal and move_uploaded_file() are what Laravel's own file upload handling is built on top of — worth understanding directly, even in a framework-based project.

Handling a file upload in plain PHP, without any framework, is built on the $_FILES superglobal and move_uploaded_file() — understanding this directly is genuinely useful even for framework-based work, since it's exactly what Laravel's own upload handling wraps under a friendlier API.

The upload form

enctype="multipart/form-data" is required on the form itself — without it, the browser doesn't actually encode and send the file content at all, only its filename as plain text.

The $_FILES superglobal's structure

print_r($_FILES);
/*
[document] => Array
(
    [name] => report.pdf
    [type] => application/pdf
    [tmp_name] => /tmp/phpXXXXXX
    [error] => 0
    [size] => 245678
)
*/

tmp_name is where PHP has already saved the uploaded file temporarily — it needs to be explicitly moved to a permanent location before the request finishes, or it's automatically deleted.

Validating the upload before moving it

 $maxSize) {
    die('File too large.');
}

Checking the error field first matters — a value other than UPLOAD_ERR_OK (0) means something went wrong during upload (exceeded size limit, partial upload, no file selected), and attempting to process tmp_name without this check first is a common source of confusing failures.

Why checking the client-reported MIME type isn't fully trustworthy

The type field in $_FILES comes from the browser and can be spoofed by anyone crafting a request directly — for a genuine security check (not just a UX convenience), verifying the file's actual content with finfo_file() or by checking file signature bytes directly is more reliable than trusting this client-supplied value alone.

Moving the uploaded file to a permanent location

$uploadDir = __DIR__ . '/uploads/';
$filename = uniqid() . '_' . basename($_FILES['document']['name']);
$destination = $uploadDir . $filename;

if (move_uploaded_file($_FILES['document']['tmp_name'], $destination)) {
    echo "File uploaded successfully to $filename";
} else {
    echo "Failed to move uploaded file.";
}

move_uploaded_file(), not a generic rename() or copy(), is specifically required here — it performs an extra security check confirming the file genuinely was uploaded via HTTP POST (rather than, say, an attacker's crafted path pointing at an arbitrary server file), which a generic file-move function doesn't verify.

Generating a unique filename to avoid overwrites and directory traversal

Using uniqid() (or a similar random component) rather than the original uploaded filename directly avoids both accidentally overwriting an existing file with the same name and any directory-traversal risk from a maliciously crafted filename like ../../etc/passwd in the original name field.

Checking php.ini's upload limits

Following the guidance covered elsewhere on this site, upload_max_filesize and post_max_size both cap what PHP will accept before your own code even runs — a file larger than these limits produces an error in $_FILES before any application-level validation gets a chance to run at all.