|
| 1 | +# Container tree & HTTP data flow |
| 2 | + |
| 3 | +`Form extends Container extends Nette\ComponentModel\Container`. The tree and the |
| 4 | +way submitted data reaches each control are one emergent model. |
| 5 | + |
| 6 | +## Data is pulled, per control, from one flat array |
| 7 | + |
| 8 | +There is **no central distribution** of submitted data. Each control pulls its own |
| 9 | +value, lazily, driven by the component monitor: |
| 10 | + |
| 11 | +- `BaseControl`'s constructor registers `monitor(Form::class, …)`; when a |
| 12 | + **non-disabled** control is attached to an **anchored, submitted** form it calls |
| 13 | + `loadHttpData()` (`BaseControl::loadHttpData` → |
| 14 | + `setValue(getHttpData(Form::DataText))`). `loadHttpData` is the template-method |
| 15 | + hook overridden by `SubmitButton`, `CsrfProtection`, etc. |
| 16 | +- `BaseControl::getHttpData()` asks `Form::getHttpData($type, $htmlName)`, where |
| 17 | + `$htmlName` is the full bracketed path (`getHtmlName()` → |
| 18 | + `Helpers::generateHtmlName(lookupPath(Form::class))`); an explicitly set `name` |
| 19 | + attribute overrides the generated one, and setting it on a submitted form |
| 20 | + re-triggers `loadHttpData()`. |
| 21 | +- `Form::getHttpData()` **lazily** fills `Form::$httpData` **once**, from |
| 22 | + `receiveHttpData()`, and sets `$submittedBy = is_array($data)`. This is the only |
| 23 | + place `$httpData` is populated. |
| 24 | +- `Helpers::extractHttpData()` walks that flat array by the path: it strips `]`, |
| 25 | + turns `.`→`_`, splits on `[`, then `Arrays::get`s down the keys. A trailing `[]` |
| 26 | + triggers per-element sanitization; `DataKeys` preserves keys, otherwise |
| 27 | + `array_values` renumbers. |
| 28 | + |
| 29 | +**Sanitization is by data-type bit** (`Helpers::sanitize`): `DataText` normalizes |
| 30 | +newlines only; `DataLine` collapses newlines to spaces and trims (single-line |
| 31 | +inputs); `DataFile` passes only a real `FileUpload` (else `null`). An agent adding a |
| 32 | +control picks the bit that matches; picking `DataText` for a single-line field |
| 33 | +leaks newlines. |
| 34 | + |
| 35 | +## Submission detection lives in `receiveHttpData` |
| 36 | + |
| 37 | +`Form::receiveHttpData()` returns `null` (not submitted) unless **all** hold: |
| 38 | + |
| 39 | +1. the HTTP method matches the form's method; |
| 40 | +2. for POST, the request passes the **same-origin** check |
| 41 | + (`!crossOrigin && $request->isFrom(FetchSite::SameOrigin)`) — the actual |
| 42 | + Sec-Fetch-Site / cookie logic lives in **nette/http**, not here; Forms only calls |
| 43 | + `isFrom`. `allowCrossOrigin()` disables this (and token protection via |
| 44 | + `CsrfProtection`/`addProtection()` is deprecated in favor of it); |
| 45 | +3. the **`_form_` tracker** (present only for a **named** form) equals the form's |
| 46 | + name. An unnamed form has no tracker, so detection rests on method + data alone; |
| 47 | + for GET an **empty query string** already means "not submitted". |
| 48 | + |
| 49 | +`submittedBy` starts as the bool `true` and is **narrowed to a `SubmitButton` |
| 50 | +instance** by `SubmitButton::loadHttpData()` when that button is filled — that is |
| 51 | +how "which button submitted" is known. |
| 52 | + |
| 53 | +## `fireEvents` order |
| 54 | + |
| 55 | +`Form::fireEvents()` runs a fixed sequence: return if not submitted; validate only |
| 56 | +if there are no errors yet; then `$submittedBy->onClick`/`onInvalidClick` (for a |
| 57 | +`SubmitButton`), then `onSuccess` (if valid), then `onError` (if invalid), then |
| 58 | +always `onSubmit`; a warning fires if nothing was handled. `invokeHandlers` |
| 59 | +inspects each handler's first parameter type by reflection to pass `$form` / the |
| 60 | +button / `getValues($type)` (a second parameter, if present, always gets |
| 61 | +`getValues`), and **stops the chain the moment a handler invalidates the form**. |
| 62 | + |
| 63 | +`Form::validate()` (override) pulls the validation scope from the clicked |
| 64 | +`SubmitterControl`, runs `validateMaxPostSize()` (a form-level error when |
| 65 | +`CONTENT_LENGTH` exceeds `post_max_size` — reusing the `MaxFileSize` message), |
| 66 | +then delegates to `Container::validate($controls)`. |
| 67 | + |
| 68 | +## Reading values back out of the tree |
| 69 | + |
| 70 | +- **`getValues()` = `getUntrustedValues()` + guards.** It **throws** if called |
| 71 | + during validation (`validated === null`), warns if the form is invalid, applies |
| 72 | + the validation-scope narrowing, then delegates. |
| 73 | +- **`getUntrustedValues()`** walks the component tree: non-omitted `Control`s |
| 74 | + contribute `getValue()` (with enum coercion against the target property type), |
| 75 | + nested `Container`s recurse. The return shape is `ArrayHash` by default, or |
| 76 | + `$mappedType` (`setMappedType`), or a class you pass — a **DTO class** is built by |
| 77 | + reflection (constructor with required params, else property assignment). |
| 78 | +- **`isOmitted()`** controls exclusion (`setOmitted`, or a disabled control with |
| 79 | + `omitted === null`); the tracker, buttons, and CSRF field are omitted. |
| 80 | +- **`setDefaults()` on a submitted form only fills *disabled* controls** |
| 81 | + (`onlyDisabled: form->isSubmitted()`), which is why setting defaults after submit |
| 82 | + appears to "do nothing" for normal fields. |
| 83 | + |
| 84 | +## Validation scope |
| 85 | + |
| 86 | +A `SubmitButton::setValidationScope(iterable)` accepts `Container`/`Control` |
| 87 | +targets or component-name strings (resolved via `$form->getComponent()`); anything |
| 88 | +else throws. `Form::validate()` passes them down; `Container::validate($controls)` |
| 89 | +validates only that subset (`[]` validates nothing). The same scope also narrows |
| 90 | +`getValues` (a container is included when any of its ancestors is in scope), and is |
| 91 | +exported to the client as `data-nette-validation-scope` (plus `formnovalidate` on |
| 92 | +the button). |
| 93 | + |
| 94 | +## The `Control` contract is deliberately minimal — and not honored |
| 95 | + |
| 96 | +`Control` declares only **five** methods: `setValue`, `getValue`, `validate`, |
| 97 | +`getErrors`, `isOmitted`. In practice the framework requires far more of every |
| 98 | +control (`getHtmlName`, `getControl`, `getLabel`, `getForm`, `getOption`, |
| 99 | +`isFilled`, …), so it is written against `BaseControl` everywhere. The current state |
| 100 | +papers over the gap with **`instanceof BaseControl` guards** (a handful of sites: |
| 101 | +`Validator` for `%label`, the renderer's `translate`, `Form`, `Blueprint`, the Latte |
| 102 | +runtime) and a **`method.notFound` ignore block in `phpstan.neon`** that enumerates |
| 103 | +the "missing" interface methods. Treat "a control is a `BaseControl`" as the real, |
| 104 | +if unstated, contract. |
0 commit comments