move_uploaded_file

Moves a file uploaded via HTTP POST to a permanent location, verifying it really was an upload.

Signature
move_uploaded_file(string $from, string $to): bool
  • Added in PHP 4.0.3

Parameters

ParameterTypeDescription
$fromstringThe temporary path of the uploaded file, i.e. $_FILES['field']['tmp_name'].
$tostringThe destination path. An existing file at this path is overwritten.

Return value

booltrue on success; false if from is not a valid upload or the move could not be performed, in which case nothing is moved.

Errors

Emits an E_WARNING only when from is a valid upload file that cannot be moved, for example when the destination cannot be written. When from is not a valid HTTP POST upload, no action occurs and the function returns false without emitting a warning.

move_uploaded_file() does two things in one call: it verifies that $from is a file PHP recorded as an upload for the current request, and then moves it to $to. If the verification fails, nothing moves and the function returns false. That built-in check is why you should always prefer it to rename() or copy() for uploads.

When to reach for it

Every form or API endpoint that accepts a file. PHP deletes the temporary file when the request ends, so if you do not move it before the script finishes, the upload is gone.

What trips people up

The to path is written verbatim, so never build it from $_FILES['f']['name']. That value comes from the client and can contain ../, a null byte, or a name like shell.php that turns your upload folder into a code-execution endpoint. Generate the filename server-side and pick the extension from a verified MIME type. Store outside the document root when you can, and if you cannot, disable PHP execution in that directory.

Two more practical points: the destination directory must already exist and be writable — create it with mkdir($dir, 0775, true) first — and you must check $_FILES['f']['error'] === UPLOAD_ERR_OK before doing anything, since a file that exceeded upload_max_filesize still produces an entry in $_FILES.

Example

Example · php
<?php
// Requires a real multipart/form-data POST.
$f = $_FILES['avatar'] ?? null;
if (!$f || $f['error'] !== UPLOAD_ERR_OK) {
    exit("Upload failed\n");
}

$ext  = ['image/jpeg' => 'jpg', 'image/png' => 'png'][ (new finfo(FILEINFO_MIME_TYPE))->file($f['tmp_name']) ] ?? null;
if ($ext === null) { exit("Unsupported type\n"); }

$dir = 'storage/avatars';
if (!is_dir($dir)) { mkdir($dir, 0775, true); }

$dest = $dir . '/' . bin2hex(random_bytes(16)) . '.' . $ext;
echo move_uploaded_file($f['tmp_name'], $dest) ? "Saved to $dest\n" : "Move failed\n";

When to use it

  • Store a profile avatar under a random server-generated name after checking its real image type.
  • Accept a CSV import, move it into a private queue directory, and hand the path to a background job.
  • Move an upload into a date-partitioned archive tree created on demand with mkdir().

Frequently asked questions

What is the difference between move_uploaded_file and rename?

move_uploaded_file() first verifies that the source really is an HTTP POST upload from the current request and refuses otherwise; rename() moves any path it is given. Always use move_uploaded_file() for uploads.

Why does move_uploaded_file return false?

The usual causes are a destination directory that does not exist or is not writable by the web server user, a request where the upload actually failed (check $_FILES['f']['error'] first), or a source path that is not the tmp_name of a real upload.

Can I use the original filename from the upload?

Only after sanitising it, and even then it is safer not to. The client controls that string entirely, so generate the stored name yourself and keep the original in your database for display purposes.

More examples

Report why the upload failed

An oversized file still appears in $_FILES with tmp_name set to an empty string, so skipping the error check produces a confusing "move failed" instead of "file too big". Mapping the codes turns that into a message a user can act on.

Example · php
<?php
$messages = [
    UPLOAD_ERR_INI_SIZE   => 'Larger than upload_max_filesize',
    UPLOAD_ERR_FORM_SIZE  => 'Larger than the form MAX_FILE_SIZE',
    UPLOAD_ERR_PARTIAL    => 'Only partially uploaded',
    UPLOAD_ERR_NO_FILE    => 'No file was sent',
    UPLOAD_ERR_NO_TMP_DIR => 'Server has no temp directory',
    UPLOAD_ERR_CANT_WRITE => 'Server could not write to disk',
];
$code = $_FILES['doc']['error'] ?? UPLOAD_ERR_NO_FILE;

if ($code !== UPLOAD_ERR_OK) { exit($messages[$code] ?? "Unknown error $code"); }
move_uploaded_file($_FILES['doc']['tmp_name'], 'private/doc.bin');

Multiple files with server-generated names

Only the extension is taken from the client name, and only after it is checked against an allowlist; the filename itself is random. That removes path traversal and .php smuggling in one step while still preserving a sensible extension.

Example · php
<?php
$dir = 'storage/uploads';
if (!is_dir($dir)) { mkdir($dir, 0775, true); }

$saved = [];
foreach ($_FILES['photos']['tmp_name'] ?? [] as $i => $tmp) {
    if ($_FILES['photos']['error'][$i] !== UPLOAD_ERR_OK) { continue; }
    $safeExt = strtolower(pathinfo($_FILES['photos']['name'][$i], PATHINFO_EXTENSION));
    if (!in_array($safeExt, ['jpg', 'png'], true)) { continue; }
    $dest = "$dir/" . bin2hex(random_bytes(8)) . ".$safeExt";
    if (move_uploaded_file($tmp, $dest)) { $saved[] = $dest; }
}
print_r($saved);

Discussion

  • Be the first to comment on this lesson.