skip to content

In PHP, what happens to form field names with dots or spaces, and to names like rows[][name] and rows[][email]?

level: middleimportance: nice to knowfreq 28%

answer

  1. dot is not legal in a variable name
  2. only the part before the first [
  3. each [] appends a fresh element
  4. explicit indexes keep a row together
  5. text after ] is dropped

basics

~20 s

PHP turns dots and spaces in the top-level part of a field name into underscores, so user.email arrives as $_POST['user_email']. Every [] appends a new element, so rows[][name] and rows[][email] land in two different rows; use explicit indexes.

solid answer

~40 s

PHP rewrites the **top-level** part of an incoming name: leading spaces are dropped, and every `.` or space before the first `[` becomes `_`, so `user.email` and `first name` arrive as `user_email` and `first_name`, and `user.email` would collide with a real `user_email` field. Keys **inside** brackets are left alone, so `user[e.mail]` keeps its dot. An unclosed bracket is not an array: `a[b` becomes `a_b`, and characters after a closing bracket are discarded, so `foo[bar]baz` is `foo[bar]`. The subtle one is repeating rows: each `[]` appends a **new** element, so `rows[][name]` and `rows[][email]` produce `[0 => ['name' => ...], 1 => ['email' => ...]]`. Give each row an explicit index, `rows[0][name]` and `rows[0][email]`.

code

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

// parse_str() uses the same name rules as $_POST
parse_str('[email protected]&first name=Ana', $flat);
var_dump(array_keys($flat));   // ['user_email', 'first_name']

parse_str('rows[][name]=Ana&rows[][email][email protected]', $bad);
var_dump(count($bad['rows'])); // int(2) - name and email split apart

parse_str('rows[0][name]=Ana&rows[0][email][email protected]', $good);
var_dump($good['rows'][0]);    // ['name' => 'Ana', 'email' => '[email protected]']

go deeper

for a junior

Remember that dots and spaces in a field name become underscores, and that nested data needs bracket names rather than dotted ones.

for a middle

Explain why each [] appends a new element per pair, and show explicit row indexes as the fix for repeating fields.

for a senior

Recognise these rewrites when a front end's naming scheme silently collides or splits rows, and set a naming convention the whole team follows.

for a principal

Weigh a JSON request body for complex nested forms against bracket-named fields, considering the naming rules and the input limits each path carries.

## Why PHP rewrites names at all PHP's input parser was designed when request variables could become global variables, and a dot or a space cannot appear in a PHP variable name. That feature, `register_globals`, was removed in PHP 5.4, but the name rewriting stayed and still applies to `$_GET`, `$_POST` and `$_COOKIE`. The parser works on the raw name in this order: 1. **Leading spaces** are stripped. 2. Scanning left to right, every **`.` or space** is replaced by `_` - until the first `[`. 3. From the first `[`, the name is read as array syntax: each `[...]` segment is one level of nesting. 4. After a closing `]`, only another `[` continues the array; anything else is ignored. ## Top-level rewriting | Field name sent | Key in `$_POST` | |---|---| | `user.email` | `user_email` | | `first name` | `first_name` | | `sub.x` (from `<input type="image" name="sub">`) | `sub_x` | | `user[e.mail]` | `user` => `['e.mail' => ...]` (inner key untouched) | | `a[b` (no closing bracket) | `a_b` | | `foo[bar]baz` | `foo` => `['bar' => ...]` (`baz` ignored) | Two consequences matter in real code: - **Collisions.** A form with both `user.email` and `user_email` produces one key; the later pair overwrites the earlier one without warning. - **JavaScript-style names.** Front-end code that names fields `address.city` expecting nested data gets the flat key `address_city`. Nested data needs bracket names, `address[city]`. ## The repeating-rows trap Empty brackets mean "append at the next integer index", exactly like `$rows[] = ...` in code. The parser applies that meaning **per pair**. So a table of repeating rows whose body (one line, wrapped here) reads ``` rows[][name]=Ana&rows[][email][email protected] rows[][name]=Ben&rows[][email][email protected] ``` produces four elements, not two: `rows[0]['name']`, `rows[1]['email']`, `rows[2]['name']`, `rows[3]['email']`. Every field ends up in its own row, and the handler cannot tell which email belongs to which name. The fix is to give each row an **explicit index**, so every field of the row writes into the same element: - `rows[0][name]`, `rows[0][email]`, `rows[1][name]`, `rows[1][email]`; - the index can be a database ID (`rows[42][name]`) when editing existing records; - rows added in the browser need a counter or a temporary key (`rows[new1][name]`). A trailing `[]` on the **last** segment is fine: `answers[q1][]` appends each ticked value to the list under `q1`, which is what a checkbox group wants. ## Integer and string keys Inside brackets, a key that is a canonical decimal integer becomes an **integer** key (`rows[7]` gives `7`), while `rows[07]` keeps the string key `"07"`. This follows the same key-casting rule as array literals. Nesting depth is capped by the `max_input_nesting_level` directive (default 64); deeper names are dropped. ## Debugging a mis-shaped submission When a handler receives an unexpected structure, work through it in order: 1. Look at the **raw names** the browser sent, in the developer tools' network panel, not at the HTML you meant to render: a template may have produced `rows[][name]` where you expected an index. 2. Check the **top-level part** of each name for dots and spaces, which will have become underscores. 3. Check each `[]` that is **not the last segment**; each one starts a new element per pair. 4. Check for **two fields sharing one name** after rewriting, where the later one silently wins. 5. Reproduce with `parse_str()` on a copied body, which gives the same result as `$_POST` without a browser in the loop. ## Practical guidance - Name fields with letters, digits, underscores and brackets only. - Use explicit indexes for any structure with more than one field per row. - Dump `$_POST` (or run the body through `parse_str()`, which applies the same rules) when a structure arrives in an unexpected shape.

  • Does PHP rewrite dots inside the brackets, as in user[e.mail]?
    No. Rewriting stops at the first `[`. Everything before it is the top-level name and has dots and spaces turned into underscores; bracketed keys are stored as sent, so `$_POST['user']['e.mail']` exists. The exception is a name whose bracket never closes, which is treated as a plain name and rewritten throughout.
  • How would you name the fields of a survey table where residents can add rows in the browser?
    Give every field of a row the same explicit key, for example `residents[n1][name]` and `residents[n1][ward]`, with the browser generating a fresh key per added row. Existing rows can use their database IDs. The handler then loops over `$_POST['residents']` and gets complete rows, never fields split across elements.

saying these in an interview costs you the question

  • PHP keeps field names exactly as sent, dots included.
  • rows[][name] and rows[][email] on the same line form one row.
  • PHP rewrites dots inside bracketed keys too.
  • A name like address.city becomes a nested address array.
  • An unclosed bracket such as a[b still creates an array.