skip to content

In PHP, what shape does $_FILES take for <input type="file" name="docs[]" multiple>, and how do you loop over the files?

level: middleimportance: should knowfreq 38%

answer

  1. attribute first, index second
  2. $_FILES['docs']['name'][0]
  3. an empty input still sends one part
  4. max_file_uploads: extras vanish
  5. normalise into one array per file

basics

~10 s

PHP groups by attribute, not by file: $_FILES['docs']['name'][0], $_FILES['docs']['tmp_name'][0] and so on. Loop over the keys of $_FILES['docs']['error'] and read each attribute at that index, or normalise into one array per file first.

solid answer

~40 s

For a field named `docs[]`, PHP does not build a list of file records. It builds one array per **attribute**, each indexed by file: `$_FILES['docs']['name'][0]`, `$_FILES['docs']['type'][0]`, `['tmp_name'][0]`, `['error'][0]`, `['size'][0]` and, since PHP 8.1, `['full_path'][0]`. Nested names work the same way: `application[cv]` gives `$_FILES['application']['name']['cv']`. So iterate the indexes of one attribute - `foreach ($_FILES['docs']['error'] as $i => $error)` - and read the others at `$i`, or normalise into `[['name' => ..., 'tmp_name' => ..., ...], ...]` once. Check each file's error on its own. With nothing selected the browser still sends one empty part, so there is one entry with `UPLOAD_ERR_NO_FILE`. Files beyond `max_file_uploads` (default 20) are skipped with a warning and have no entry.

code

php · 27 lines
php
<?php
declare(strict_types=1);

/** @return list<array{name: string, type: string, tmp_name: string, error: int, size: int}> */
function normaliseFiles(array $field): array
{
    $files = [];
    foreach ($field['error'] ?? [] as $i => $error) {
        $files[] = [
            'name' => $field['name'][$i],
            'type' => $field['type'][$i],
            'tmp_name' => $field['tmp_name'][$i],
            'error' => $error,
            'size' => $field['size'][$i],
        ];
    }
    return $files;
}

$docs = $_FILES['docs'] ?? [];
$files = is_array($docs['error'] ?? null) ? normaliseFiles($docs) : [];
foreach ($files as $file) {
    if ($file['error'] !== UPLOAD_ERR_OK) {
        continue; // report per file
    }
    // check size and type, then move_uploaded_file($file['tmp_name'], ...)
}

go deeper

for a junior

Recall that multiple files arrive as $_FILES['docs']['name'][0], not $_FILES['docs'][0]['name'], and that each file has its own error code.

for a middle

Explain the attribute-first layout for docs[] and application[cv], and normalise it into per-file records before validating.

for a senior

Handle the edges: one empty entry when nothing is chosen, files silently skipped past max_file_uploads, and several files jointly exceeding post_max_size.

for a principal

Standardise on one upload abstraction at the application edge, so no handler reads the raw $_FILES layout and per-file checks cannot be skipped.

## The layout PHP builds For a single file field `cv`, `$_FILES['cv']` is one record with six keys. For an array field, PHP keeps the same six keys at the top and pushes the array index **down** into each of them: ``` $_FILES['docs'] = [ 'name' => [0 => 'cv.pdf', 1 => 'letter.pdf'], 'full_path' => [0 => 'cv.pdf', 1 => 'letter.pdf'], 'type' => [0 => 'application/pdf', 1 => 'application/pdf'], 'tmp_name' => [0 => '/tmp/phpA1b2C3', 1 => '/tmp/phpD4e5F6'], 'error' => [0 => 0, 1 => 0], 'size' => [0 => 184321, 1 => 50211], ]; ``` This is often called the **transposed** `$_FILES` layout: attribute first, file index second. It surprises most people, because every other part of PHP's request parsing would suggest `$_FILES['docs'][0]['name']`. The same rule applies to named keys and nesting: | Field name | Where the temp path lands | |---|---| | `cv` | `$_FILES['cv']['tmp_name']` | | `docs[]` (with `multiple` or repeated inputs) | `$_FILES['docs']['tmp_name'][0]`, `[1]`, ... | | `application[cv]` | `$_FILES['application']['tmp_name']['cv']` | | `application[cv]` and `application[letter]` | `...['tmp_name']['cv']` and `...['tmp_name']['letter']` | `full_path` exists since PHP 8.1, for folder uploads made with the `webkitdirectory` attribute; the other five keys are older. ## Iterating safely Two workable approaches: 1. **Iterate one attribute's keys** and read the rest at the same index. `error` is a good choice because every file part has one. 2. **Normalise once** into a list of per-file records, then pass those records to the rest of the code. This keeps the odd layout at the edge of the application, and it is exactly what request libraries that wrap uploads in objects do. Whichever you choose: - check `error` **per file**; one failed file does not fail the others; - confirm the field really is an array (`is_array($_FILES['docs']['error'] ?? null)`), since a client can send `docs` without brackets; - apply your checks and `move_uploaded_file()` to each file separately. ## Edge cases in multi-file uploads - **Nothing selected.** A `multiple` input with no files still sends one empty part, so `count($_FILES['docs']['error'])` is 1 and that entry is `UPLOAD_ERR_NO_FILE`. Do not treat "one entry" as "one file". - **Too many files.** `max_file_uploads` (default `20`) caps the files PHP writes per request. Beyond it PHP emits a warning ("Maximum number of allowable file uploads has been exceeded") and skips the remaining file parts entirely: they have **no** entry, not an error code. Empty file inputs do not use up the allowance. - **Body size.** Every file counts toward `post_max_size`; several files that each fit `upload_max_filesize` can still exceed the body limit together. - **Malformed names.** PHP skips a file part whose name has unbalanced brackets or text after a closing bracket. ## Worked example: a CV plus supporting documents A job-application page asks for one CV and up to five supporting documents: - `<input type="file" name="application[cv]">` - `<input type="file" name="supporting[]" multiple>` After a submission with a CV and two supporting files, the handler sees `$_FILES['application']['error']['cv']` as `0`, and `$_FILES['supporting']['error']` as `[0 => 0, 1 => 0]`. With a CV and no supporting files, `supporting` holds one `UPLOAD_ERR_NO_FILE` entry. With a CV that is too large and two supporting files, the CV's error is `UPLOAD_ERR_INI_SIZE` while both supporting files are fine, so the page can keep them and ask only for a smaller CV. Keeping the application's two fields separate like this makes "exactly one CV" and "at most five extras" easy to check independently. ## A reusable normaliser A short function that turns the transposed arrays into per-file records keeps handlers readable. It needs one level of indexing for `docs[]`; deeper nesting such as `docs[group][]` needs a recursive version.

  • Where is the temp path for a field named application[cv] in $_FILES?
    At `$_FILES['application']['tmp_name']['cv']`. The top-level key is the part of the name before the first bracket, the second level is the attribute, and the bracketed key comes after it. The same holds for `name`, `type`, `error`, `size` and `full_path`.
  • A user selects 25 files in one multiple input; what does the handler see with default settings?
    `max_file_uploads` defaults to 20, so PHP writes the first 20 files, emits a warning that the maximum number of file uploads was exceeded, and skips the rest. The 5 extra files have no `$_FILES` entry at all, so the handler must compare what arrived with what the form promised, or state the limit in the UI.

saying these in an interview costs you the question

  • $_FILES['docs'][0]['name'] holds the first file's name.
  • An empty multiple input produces an empty $_FILES['docs'] array.
  • Files beyond max_file_uploads appear with an error code.
  • One failed file sets the error for the whole docs field.
  • Each file only needs to fit post_max_size on its own.