In PHP, what does a form need to upload a file, what does $_FILES hold, and how do you keep the file?
answer
- enctype="multipart/form-data" and method post
- name, full_path, type, tmp_name, error, size
- temp file deleted at request end
- move_uploaded_file() returns false on refusal
- check error before tmp_name
basics
~10 sThe form must use method="post" and enctype="multipart/form-data". PHP writes each file to a temporary path and describes it in $_FILES; move_uploaded_file() moves it somewhere permanent, otherwise PHP deletes it when the request ends.
solid answer
~40 sA file input only sends file content when the form uses `method="post"` and `enctype="multipart/form-data"`; with the default encoding only the file name travels. PHP streams each file part into a temporary file (in `upload_tmp_dir`, or the system temp directory) and describes it in `$_FILES['cv']` with six keys: `name` (client file name, path stripped), `full_path` (as sent, since PHP 8.1), `type` (the client's claimed MIME type), `tmp_name`, `error` (an `UPLOAD_ERR_*` int) and `size` (bytes PHP actually received). Check `error === UPLOAD_ERR_OK` first, then call `move_uploaded_file($tmp, $target)`, which returns `false` unless `$tmp` is a file PHP received in this request. Anything not moved is deleted when the request ends.
code
php · 17 lines<?php
declare(strict_types=1);
// <form method="post" enctype="multipart/form-data">
// <input type="file" name="cv">
$cv = $_FILES['cv'] ?? null;
if (!is_array($cv) || $cv['error'] !== UPLOAD_ERR_OK) {
http_response_code(400);
exit('No CV was received.');
}
// Name chosen by the server, directory outside the web root
$target = '/srv/jobs/storage/cvs/' . bin2hex(random_bytes(16));
if (!move_uploaded_file($cv['tmp_name'], $target)) {
http_response_code(500);
exit('The CV could not be stored.');
}go deeper
Recall the multipart enctype, the six $_FILES keys, and that move_uploaded_file() keeps the file while everything else is deleted at request end.
Explain which $_FILES keys are client-supplied and which PHP produces, and what move_uploaded_file() checks that rename() does not.
Lay out the handler order - error code, size, real type, server-chosen name, storage outside the web root - and why each step exists.
Decide where uploaded files live, local disk or object storage, and how the upload path is isolated from code that serves or processes them.
## What the form must send An `<input type="file">` only transmits file **content** when its form uses both: - `method="post"` - a GET form has nowhere to put a body; - `enctype="multipart/form-data"` - the default `application/x-www-form-urlencoded` encoding sends only the file's name as an ordinary field. With multipart encoding, the body is split into **parts**, one per field, each with its own headers. A file part carries a `filename` and usually a `Content-Type` chosen by the browser. PHP's multipart parser writes the file bytes straight to disk as they arrive, so large files do not have to fit in memory. ## The `$_FILES` entry For `<input type="file" name="cv">`, PHP fills `$_FILES['cv']` with these keys: | Key | Content | Who decides it | |---|---|---| | `name` | the file name, with any directory part stripped | client | | `full_path` | the path as the browser sent it (PHP 8.1+, for folder uploads) | client | | `type` | the MIME type from the part's `Content-Type` header | client | | `tmp_name` | where PHP stored the bytes | PHP | | `error` | an `UPLOAD_ERR_*` integer, `0` on success | PHP | | `size` | the number of bytes PHP received | PHP | Only the last three come from PHP itself. `name`, `full_path` and `type` are whatever the client sent, and must never be trusted as a type check or used unchanged as a storage path. If the user submits the form without choosing a file, the entry still exists: `error` is `UPLOAD_ERR_NO_FILE` (4), `name` and `tmp_name` are empty strings and `size` is 0. So `isset($_FILES['cv'])` does not tell you a file arrived; the error code does. ## The temporary file and its lifetime PHP creates the temporary file in `upload_tmp_dir`, or the system temp directory when that directive is unset, and records it as an **uploaded file** for this request. At the end of the request PHP deletes every recorded upload that is still there. That is why nothing needs cleaning up after a rejected file, and why a file you want to keep must be moved during the request. ## Keeping the file: `move_uploaded_file()` `move_uploaded_file(string $from, string $to): bool` is the function built for this: 1. It returns `false` **without a warning** if `$from` is not a file PHP received in this request. That blocks tricks where a manipulated `tmp_name` points at `/etc/passwd`. 2. It checks `open_basedir` for the destination. 3. It renames the file, falling back to copy-and-delete across filesystems; after a rename it sets the mode to `0666` minus the process umask. 4. It overwrites an existing file at `$to`. 5. If the move itself fails, it emits a warning ("Unable to move ...") and returns `false`. `is_uploaded_file(string $filename): bool` performs the same "did PHP receive this?" test on its own, for code that reads the temp file in place (for example, to inspect it) before deciding to move it. Plain `rename()` or `copy()` skip that check, which is the reason not to use them on upload paths. ## Common mistakes in first upload handlers - **Forgetting the enctype.** The form posts, `$_FILES` is empty, and `$_POST['cv']` holds just a file name. The fix is in the HTML, not in PHP. - **Reading `tmp_name` before `error`.** On any error `tmp_name` is an empty string, so the next file function fails with a confusing message. - **Storing under the client's name.** Two applicants both upload `cv.pdf` and the second silently overwrites the first, because `move_uploaded_file()` replaces an existing target. - **Storing inside the web root.** Anything placed under the document root can be requested by URL, and depending on the server setup may even be executed. - **Trusting `type`.** It is the browser's guess, or an attacker's choice, never PHP's finding. ## A safe minimal handler for a CV upload - Read `$_FILES['cv'] ?? null` and require `error === UPLOAD_ERR_OK`. - Check `size` against your own maximum. - Check the real content type from the file's bytes, not from `type`. - Generate the stored name yourself (random bytes, a database ID), never from `name`. - Move it into a directory outside the web root and record the original name as data if you need to show it later. Each of those steps has its own depth; the point here is the order: error code first, then checks on `tmp_name`, then `move_uploaded_file()`.
- Why use move_uploaded_file() rather than rename() on $_FILES['cv']['tmp_name']?`move_uploaded_file()` first checks that the path is one PHP itself received as an upload in this request, and returns `false` if not. `rename()` moves any file the process can reach, so a handler bug or a tampered value could move a system or application file instead. It also checks `open_basedir` on the destination.
- What does $_FILES['cv'] contain when the user submits the form without choosing a file?The key still exists. `error` is `UPLOAD_ERR_NO_FILE` (4), `name` and `tmp_name` are empty strings and `size` is 0. That is why handlers test the error code rather than `isset($_FILES['cv'])` to decide whether a file arrived.
- What happens to an uploaded temp file the script never touches?PHP keeps a list of the temp files it created for the request and deletes any still present when the request ends. A rejected upload therefore needs no cleanup, and a file you want to keep must be moved with `move_uploaded_file()` before the script finishes.
An upload is a parcel left in a pickup locker at a depot: the depot records which lockers it filled today, will only hand over parcels from those lockers, and clears any unclaimed locker when it closes for the night.
saying these in an interview costs you the question
- A file input uploads content with the default form encoding.
- $_FILES['cv']['type'] is a MIME type PHP detected from the file's bytes.
- isset($_FILES['cv']) proves that a file was uploaded.
- The temporary upload file stays on disk until you delete it.
- rename() is as safe as move_uploaded_file() for temp files.