web php
File Uploads
File uploads let a browser send file content to PHP as part of a form submission. They are common for avatars, documents, imports, attachments, and images.
Uploads are also one of the riskier web features because the application receives content chosen by the user. The first job is to handle the upload mechanics correctly: form encoding, PHP limits, error codes, temporary files, and final storage.
The form must use multipart encoding
A normal form submission does not send files. The form needs method="post" and enctype="multipart/form-data".
<?php
declare(strict_types=1);
$html = '<form method="post" enctype="multipart/form-data">'
. '<input type="file" name="avatar">'
. '<button type="submit">Upload</button>'
. '</form>';
echo $html . PHP_EOL;
// Prints:
// <form method="post" enctype="multipart/form-data"><input type="file" name="avatar"><button type="submit">Upload</button></form>
In a real template, write the HTML normally. The PHP string here only keeps the example runnable.
What PHP puts in $_FILES
For a file input named avatar, PHP creates $_FILES['avatar']. The important keys are:
name: original filename from the browsertype: browser-supplied MIME type, not trustedtmp_name: temporary path on the servererror: upload status codesize: uploaded file size in bytes
Always check error before using tmp_name.
<?php
declare(strict_types=1);
$file = [
'name' => 'avatar.jpg',
'type' => 'image/jpeg',
'tmp_name' => '/tmp/php-upload-123',
'error' => UPLOAD_ERR_OK,
'size' => 82_000,
];
echo 'Temporary file: ' . $file['tmp_name'] . PHP_EOL;
echo 'Original name: ' . $file['name'] . PHP_EOL;
// Prints:
// Temporary file: /tmp/php-upload-123
// Original name: avatar.jpg
The original filename is useful for display, but it should not be used as the final storage path. A user can submit names that collide, contain odd characters, or reveal misleading extensions.
Handle upload errors deliberately
UPLOAD_ERR_OK means PHP received the file. Other error codes explain what went wrong.
<?php
declare(strict_types=1);
function uploadErrorMessage(int $error): string
{
return match ($error) {
UPLOAD_ERR_OK => 'Upload received.',
UPLOAD_ERR_INI_SIZE, UPLOAD_ERR_FORM_SIZE => 'The file is too large.',
UPLOAD_ERR_PARTIAL => 'The file was only partly uploaded.',
UPLOAD_ERR_NO_FILE => 'No file was selected.',
default => 'The upload could not be processed.',
};
}
echo uploadErrorMessage(UPLOAD_ERR_NO_FILE) . PHP_EOL;
// Prints:
// No file was selected.
Do not treat a missing file the same as a successful upload with an empty name. The error code is the source of truth.
Move uploaded files into controlled storage
PHP stores uploaded files in a temporary location. If the upload is accepted, move it into a directory your application controls.
Use a server-generated filename. Store the original display name separately if you need it.
<?php
declare(strict_types=1);
function storedUploadName(string $extension): string
{
$extension = strtolower(ltrim($extension, '.'));
return bin2hex(random_bytes(8)) . '.' . $extension;
}
echo storedUploadName('jpg') . PHP_EOL;
// Output will look like:
// 8f7c2a11e9b004ac.jpg
In a real upload handler, use move_uploaded_file($file['tmp_name'], $destination). That function checks that the source path came from PHP's upload mechanism. Do not use a normal rename as a replacement for real HTTP uploads.
Basic upload flow
This example models the decision process without moving a real uploaded file:
<?php
declare(strict_types=1);
/**
* @param array{name: string, tmp_name: string, error: int, size: int} $file
*/
function describeUpload(array $file, int $maxBytes): string
{
if ($file['error'] !== UPLOAD_ERR_OK) {
return uploadErrorMessage($file['error']);
}
if ($file['size'] > $maxBytes) {
return 'The file is larger than this feature allows.';
}
return 'The upload is ready for validation and storage.';
}
$file = [
'name' => 'avatar.jpg',
'tmp_name' => '/tmp/php-upload-123',
'error' => UPLOAD_ERR_OK,
'size' => 82_000,
];
echo describeUpload($file, 500_000) . PHP_EOL;
// Prints:
// The upload is ready for validation and storage.
The next lesson covers deeper validation and scanning. This lesson is about getting the upload from the browser to a controlled server-side path safely.
Storage location matters
Avoid storing untrusted uploads directly inside a public web directory unless the files are meant to be publicly downloadable and the server is configured safely.
For private files, store them outside the document root and stream them through an authorised controller. That gives the application a chance to check permissions before sending the file.
For public files, still use generated names, restricted file types, correct response headers, and a storage area that cannot execute PHP code.
Size limits
Uploads are affected by several limits:
- form-level
MAX_FILE_SIZE, which is not a security control upload_max_filesizepost_max_size- web server request body limits
- application-specific size checks
The application should still check file size after PHP receives the upload, because configuration limits and feature limits are not always the same.
What you should be able to do
After this lesson, you should be able to build a multipart upload form, read the $_FILES structure, handle upload error codes, avoid trusting original filenames, move accepted uploads into controlled storage, and explain why upload storage location matters.
Practice
Task: Describe An Upload Decision
Write a small PHP script that turns a file upload array into a clear decision message.
Requirements
- Use
declare(strict_types=1);. - Accept an array shaped like one item from
$_FILES. - Check the upload error code before checking anything else.
- Reject files over a feature-specific byte limit.
- Return a message for a successful upload.
- Include one successful case, one missing-file case, and one too-large case.
- Print the results.
Check Your Work
Run the script and confirm that each case returns a different, useful message.
Show solution
This solution checks the upload status first, then applies an application size limit.
<?php
declare(strict_types=1);
function uploadErrorMessage(int $error): string
{
return match ($error) {
UPLOAD_ERR_OK => 'Upload received.',
UPLOAD_ERR_INI_SIZE, UPLOAD_ERR_FORM_SIZE => 'The file is too large.',
UPLOAD_ERR_PARTIAL => 'The file was only partly uploaded.',
UPLOAD_ERR_NO_FILE => 'No file was selected.',
default => 'The upload could not be processed.',
};
}
/**
* @param array{name: string, tmp_name: string, error: int, size: int} $file
*/
function describeUploadDecision(array $file, int $maxBytes): string
{
if ($file['error'] !== UPLOAD_ERR_OK) {
return uploadErrorMessage($file['error']);
}
if ($file['size'] > $maxBytes) {
return 'Reject ' . $file['name'] . ': larger than the feature limit.';
}
return 'Accept ' . $file['name'] . ': ready for validation and storage.';
}
$cases = [
['name' => 'avatar.jpg', 'tmp_name' => '/tmp/a', 'error' => UPLOAD_ERR_OK, 'size' => 82_000],
['name' => '', 'tmp_name' => '', 'error' => UPLOAD_ERR_NO_FILE, 'size' => 0],
['name' => 'video.mov', 'tmp_name' => '/tmp/b', 'error' => UPLOAD_ERR_OK, 'size' => 8_000_000],
];
foreach ($cases as $file) {
echo describeUploadDecision($file, 500_000) . PHP_EOL;
}
// Prints:
// Accept avatar.jpg: ready for validation and storage.
// No file was selected.
// Reject video.mov: larger than the feature limit.
In real upload handling, an accepted decision would be followed by MIME validation, filename generation, and move_uploaded_file().
Why This Works
The missing-file case proves the code respects the upload error code. The too-large case proves the feature can be stricter than PHP's global upload limits. The success case does not store the file yet; it only says the upload has passed the first mechanical checks.