A PHP admin form with 60 rows of 20 fields saves only the first rows and shows no error; how do you diagnose and fix it?
answer
- count the name=value pairs
- max_input_vars defaults to 1000
- "Input variables exceeded" E_WARNING
- INI_PERDIR: ini_set() cannot help
- sentinel field at the end of the form
basics
~20 s60 rows of 20 fields is 1,200 pairs, above max_input_vars (default 1000). PHP keeps the pairs up to the limit, drops the rest, and logs an E_WARNING. Raise it per directory or pool, or send fewer fields.
solid answer
~40 sThe symptom fits `max_input_vars`, which defaults to `1000` and counts every `name=value` pair (each `[]` element counts), separately for `$_GET`, `$_POST` and `$_COOKIE`. At 60 x 20 = 1,200 pairs, PHP stops registering at the limit and emits an `E_WARNING`, "Input variables exceeded 1000. To increase the limit change max_input_vars in php.ini.", during request startup, before any script line runs. With `display_errors` off that line exists only in the error log, so check it first. Whatever sits at the end of the form is lost: the last rows, and any hidden field placed last. Fixes: raise the directive where `INI_PERDIR` settings live (`php.ini`, `.user.ini`, `.htaccess`, or an FPM pool's `php_admin_value`) since `ini_set()` cannot change it; or submit only changed rows, paginate, or post JSON. A hidden sentinel field at the very end detects truncation.
code
ini · 5 lines; FPM pool file for the admin site (INI_PERDIR, so not settable with ini_set)
php_admin_value[max_input_vars] = 3000
; or a .user.ini in the admin directory
; max_input_vars = 3000go deeper
Recall that PHP limits how many form fields it accepts, 1000 by default, and that extra fields are silently dropped.
Explain how pairs are counted, that the limit is per superglobal, and why the warning fires before the script runs and only reaches the log.
Diagnose from the log and a sentinel field, raise the limit only where it is needed, and guard save-all handlers against deleting truncated rows.
Decide whether bulk-edit screens should post changed rows or JSON instead of raising limits, balancing DoS exposure against form design.
## The limit behind the symptom `max_input_vars` caps how many input variables PHP registers for one request. Its default is **1000**, set in `main/main.c` and shown commented out in both shipped `php.ini` files. The limit exists to blunt denial-of-service attacks that send thousands of keys designed to collide in PHP's hash tables. Three details decide whether a form trips it: - it counts **every `name=value` pair**, not top-level keys: `rows[0][name]` and `rows[0][ward]` are two variables, and a list of 300 `ids[]` values is 300; - it applies to `$_GET`, `$_POST` and `$_COOKIE` **separately**, each with its own budget; - it applies to both `application/x-www-form-urlencoded` and `multipart/form-data` bodies (for multipart, a second directive, `max_multipart_body_parts`, defaults to `-1`, meaning `max_input_vars` plus `max_file_uploads`). An admin grid of 60 rows x 20 fields sends 1,200 pairs, so about 200 are lost. ## What PHP actually does The body is parsed during **request startup**, before the first line of the script. When the count passes the limit, PHP: 1. emits one `E_WARNING`: "Input variables exceeded 1000. To increase the limit change max_input_vars in php.ini."; 2. keeps the pairs registered up to the limit and **drops the rest** of the body's variables; 3. runs the script normally, with a partial `$_POST`. Nothing throws, and the handler sees a well-formed array. Because the warning fires before your code, it lands in the error log (or on screen if `display_errors` is on) and is easily missed. Pairs are dropped from the end, in body order, which follows document order: - the last rows of a grid never arrive, which looks like "only the first rows save"; - a hidden field at the bottom of the form, such as a token or an action flag, goes missing, producing confusing downstream errors; - a save-all handler that deletes rows absent from the submission may **delete real data**. ## Diagnosing it - Search the PHP error log for "Input variables exceeded". - Compare the number of values received with the number of fields the form rendered; a total stuck at the limit is a strong sign. - Add a **sentinel**: a hidden field as the very last element of the form. If it is missing from `$_POST`, the body was truncated, and the handler should refuse to save. ## Working the numbers Size the problem before you pick a fix. For the admin grid: - 60 rows x 20 fields = 1,200 row variables, plus a few hidden and button fields; - the default budget of 1000 leaves roughly 200 variables, about the last ten rows, missing; - if each row also carries a `selected[]` checkbox, every ticked box adds one more pair; - `$_COOKIE` and `$_GET` do not eat into this budget, because each superglobal counts on its own. A form that is close to the limit today will cross it the next time someone adds a column, so measure the largest realistic submission, not the typical one. ## Fixing it Raising the limit is legitimate, but know where it can be set. The directive is **`INI_PERDIR`**: it may be set in `php.ini`, `.htaccess`, `httpd.conf` or a `.user.ini` file, or in an FPM pool with `php_value` / `php_admin_value`. `ini_set()` cannot change it, and would be too late anyway, because the body was parsed before the script started. | Option | Trade-off | |---|---| | Raise `max_input_vars` for that app or directory | Quick; widens the DoS surface a little, so raise it to a measured number, not an arbitrarily huge one | | Submit only changed rows | Needs a little client-side tracking; fewer pairs and less work on save | | Paginate or split the form | Simplest to reason about; more round trips for the user | | Send the grid as a JSON body | Not counted by `max_input_vars`, since PHP does not parse JSON into `$_POST`; needs a script on the client and your own decoding | A related directive, `max_input_nesting_level` (default 64), caps bracket depth. A variable nested deeper is dropped; per the source, its warning is only issued when `display_errors` is off, to avoid leaking details on screen. Whatever you choose, keep the sentinel check: a future form will grow past whatever limit you set.
- Why can't the handler fix this with ini_set('max_input_vars', '5000')?Two reasons. The directive is `INI_PERDIR`, so `ini_set()` cannot change it at runtime. And even if it could, the request body is parsed during request startup, before the script's first line, so the variables are already dropped by the time any code runs. The value must be set in `php.ini`, `.user.ini`, `.htaccess` or the FPM pool.
- Why can a max_input_vars overflow cause data loss rather than just a failed save?PHP gives the script a partial but well-formed `$_POST` and only logs a warning. A "save all rows" handler that deletes records missing from the submission reads the dropped rows as deleted by the user and removes them. A sentinel field at the end, checked before any write, turns that silent loss into a refused request.
saying these in an interview costs you the question
- max_input_vars counts only top-level keys, not nested array entries.
- Exceeding max_input_vars makes PHP reject the whole request with an error.
- ini_set() at the top of the handler can raise max_input_vars.
- The limit is shared across $_GET, $_POST and $_COOKIE combined.
- A multipart/form-data form is exempt from max_input_vars.