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
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
1. Which function should be used to relocate a file from PHP's temporary upload location to a permanent directory?
2. Why is $_FILES['field']['type'] considered untrustworthy?
3. What does UPLOAD_ERR_INI_SIZE indicate?
4. If post_max_size is smaller than upload_max_filesize, what happens to a large upload?
5. Why should uploaded files be stored outside the web root or in a non-executable directory?
Was this page helpful?
You May Also Like
Form Handling and Validation
Learn a reliable pattern for processing submitted HTML forms in PHP: reading input safely, validating and sanitizing each field, and reporting errors back to the user.
Superglobals and Request Data
Explore PHP's built-in superglobal arrays that expose incoming HTTP request data, server information, and environment variables to every scope without needing to be passed or imported.
PHP Security Basics
Core security practices every PHP developer must apply — input validation, output escaping, safe database access, and secure session handling.
Sessions and Cookies
Learn how PHP maintains state across stateless HTTP requests using cookies for client-side storage and sessions for server-side storage keyed by a session identifier.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics