Nette Forms

repository·master·Indexed 19 days ago

https://github.com/nette/forms

A PHP library for creating, validating, and processing web forms with built-in XSS and CSRF security. It supports server-side and client-side validation (via netteForms.js), multiple rendering modes, and advanced features like validation scopes, form toggles, and DTO mapping for value extraction. Requires PHP 8.3+.

Tokens
13.3K
Snippets
35
Records
69
Agent score
68%

What's inside Nette Forms

  1. How data flows from HTTP to Nette Forms controls

    master

    Nette Forms does not use a central distribution system for submitted data. Instead, each control pulls its own value lazily from a single flat array of HTTP data.

    When a form is submitted, Form::receiveHttpData() populates Form::$httpData. Each non-disabled control then calls loadHttpData(), which uses getHttpData() to extract its specific value from the flat array using its HTML name (e.g., name="container[control]").

    Data Sanitization Sanitization is determined by the data-type bit used when adding the control via Helpers::sanitize:

    • DataText: Normalizes newlines only.
    • DataLine: Collapses newlines to spaces and trims (use this for single-line inputs).
    • DataFile: Ensures the value is a valid FileUpload instance, otherwise returns null.
  2. How client-side rule export works

    master

    Rules are serialized for client-side validation via Helpers::exportRules() into the data-nette-rules attribute. However, not all rules can be exported:

    • Exportable: Rules where the validator is a string or a static callback.
    • Non-exportable (Plain Rules): Typically mutating filter closures. If a non-exportable rule is encountered, exporting stops immediately for that control (and any subsequent rules in a branch are not exported). This is because the client cannot reproduce server-side mutations.
    • Non-exportable (Conditions): Rules containing a branch are simply skipped during export.
    • Enums: Form::Enum has no direct JS counterpart; it is exported as Form::Equal against the enum's case values.
  3. How to use $emptyOptional for optional fields

    master

    When validating an optional field that is not required and not filled, you can use the $emptyOptional setting.

    When $emptyOptional is active, every non-branch rule (except Filled) is skipped for that control, allowing an empty field to pass validation. This setting propagates into rule branches, but a Blank branch will reset the state to false.

  4. How the Latte runtime handles form rendering

    master

    The Latte runtime (Bridges/FormsLatte/Runtime) manages form rendering using two parallel stacks:

    • $stack: Tracks the current form or container scope.
    • $detachedIds: Tracks the detached form ID per stack level.

    Key Lifecycle Behaviors

    • begin(): Pushes to the stack. For a Form, it fires render events and resets every control's rendered option. This reset is critical because renderFormEnd relies on the rendered flag to emit any remaining hidden fields before the closing </form> tag.
    • end(): Pops both stacks.
    • get(): Resolves an element from the current scope. If a detached ID is active, it automatically calls setHtmlAttribute('form', $id) on BaseControl elements to link them to the detached form.

    Important Invariant

    To avoid breaking detached or nested forms, only the {form} machinery is permitted to emit the <form> tag. Themes or custom renderers must not manually emit <form> tags.

  5. Manage DateTimeControl normalization

    master

    The DateTimeControl uses a normalization funnel to ensure consistent data types:

    • Input normalization: All inputs (setValue, rule arguments) are passed through normalizeValue(), converting strings, timestamps, or DateTimeInterface objects into DateTimeImmutable objects.
    • Type-based truncation:
      • Date types: Time is zeroed out.
      • Time types: Date is collapsed to 0001-01-01 and seconds are dropped (unless withSeconds is used).
    • Error handling: loadHttpData() swallows parsing errors and returns null instead of throwing.
    • Output shaping: Use setFormat() to determine what getValue() returns: a DateTimeImmutable object (default), a timestamp, or a specific format() string.
    • HTML attributes: getControl() automatically derives min and max HTML attributes from Min, Max, or Range rules, provided no non-exportable rules precede them.
  6. Handle ChoiceControl value semantics

    master

    When working with ChoiceControl or MultiChoiceControl (like SelectBox), be aware of asymmetric validation behavior:

    • Setting values: setValue() and setDefaults() will throw an exception if the value is not present in the $items list. To avoid this when loading data from a database before the full item list is populated, call checkDefaultValue(false) first.
    • Submitting values: loadHttpData() does not validate membership. Instead, getValue() performs lazy filtering: if a submitted key is unknown or belongs to a disabled item, getValue() returns null (or an empty array for multi-choice). Use getRawValue() to inspect the actual submitted data.
    • Disabled items: You can disable specific items using setDisabled(array). A selection of a disabled item is considered not filled by isFilled().
    • SelectBox prompts: SelectBox includes a prompt (empty string ''). For required select boxes, the prompt is rendered as hidden+disabled. The constructor automatically adds a condition and a Filled rule (with message SelectBox::Valid) to prevent the prompt from being treated as a valid selection.
    • Optgroups: setItems() supports nested arrays for optgroups, which are flattened for membership checks.
    // Example: Avoiding exception when setting defaults before items are fully loaded
    $select->checkDefaultValue(false);
    $select->setDefaults(['some_id' => 'value_from_db']);
  7. How submission detection works in Nette Forms

    master

    A form is considered submitted via Form::receiveHttpData() only if all the following conditions are met:

    1. HTTP Method Match: The request method matches the form's defined method.
    2. Same-Origin Check: For POST requests, the request must pass a same-origin check. You can bypass this using allowCrossOrigin(), though note that token protection via CsrfProtection is deprecated in favor of this method.
    3. Tracker Match: If the form is named, the _form_ tracker in the request must match the form's name. Unnamed forms rely solely on the method and data presence.

    Identifying the Submitting Button submittedBy is initially a boolean. It is narrowed to a specific SubmitButton instance when SubmitButton::loadHttpData() is called, allowing you to determine which button triggered the submission.

  8. Treat controls as `BaseControl` rather than `Control`

    master

    While the Control interface is minimal (defining only setValue, getValue, validate, getErrors, and isOmitted), the framework actually relies on the much richer BaseControl class.

    When building custom controls or extending the library, you should treat the BaseControl contract as the real requirement. Most framework components (Validators, Renderers, Latte, etc.) expect BaseControl methods like getHtmlName(), getControl(), getLabel(), and getForm() to be available.

  9. How the rule tree and validation logic works

    master

    Nette Forms uses a Rules object to manage validation. Rules are stored in an ordered list, but they are categorized into different slots and priorities:

    • Required Rules: Rules added via Filled (e.g., addRule(Form::Filled)) are stored in a separate $required slot.
    • Standard Rules: All other rules are stored in the $rules[] array.
    • Evaluation Order: Validation does not follow insertion order. It follows a fixed priority: Blank (Priority 0) $\rightarrow$ Required (Priority 1) $\rightarrow$ The rest (Priority 2).
    • Conditions:
      • addCondition turns a boolean argument into a :static rule.
      • addConditionOn creates a rule with a branch (a nested Rules object) and returns that branch for chaining.
      • elseCondition clones the last rule and flips it (e.g., Filled becomes Blank).

    Note on Negation: Using the negation operator (~) on non-branch rules is deprecated and will throw an error. Negation is only supported within conditions.

  10. Use UploadControl for file uploads

    master

    The UploadControl is specialized for handling file uploads via HTTP:

    • No defaults: setValue() is a no-op. You cannot set default values for file uploads.
    • Return values: getValue() returns FileUpload objects. If nothing was uploaded, it returns a dummy FileUpload(null) (or null if setNullable() is used).
    • Automatic rules: The constructor automatically adds several rules:
      • An isOk() check (wrapped in a static condition to ensure sibling rules like MaxFileSize can still be exported to the client).
      • MaxFileSize based on the PHP upload_max_filesize setting.
      • For multiple uploads, a MaxLength rule capped by max_file_uploads.
    • Requirement: The form must use the POST method and include the enctype="multipart/form-data" attribute. If these are missing, a monitor will throw an error.
  11. Understand the `fireEvents` execution order

    master

    When Form::fireEvents() is called, it follows a strict sequence. The chain stops immediately if a handler invalidates the form:

    1. Validation: Runs only if no errors exist yet.
    2. Button Events: $submittedBy->onClick or $submittedBy->onInvalidClick (if a SubmitButton was used).
    3. Success/Error: onSuccess (if valid) OR onError (if invalid).
    4. Submit: onSubmit always runs.

    Handler Parameters invokeHandlers uses reflection to pass arguments to your handlers. You can type-hint the first parameter to receive the $form, the button, or getValues($type). If a second parameter is present, it is automatically populated with the result of getValues().

  12. Using the `:submitted` validator

    master

    The :submitted validator compares the current element to the element assigned to elem.form['nette-submittedBy']:

    // Internal logic check
    elem.form['nette-submittedBy'] === elem

    Important: The Nette Forms package does not assign this property. To use :submitted rules, you must use an external integration (such as Naja) that sets this property on the button click. Without an external writer, the :submitted condition will always evaluate to false.