nette/forms 3.3 has been released
- David Grudl
- founder | 8322
nette/forms 3.3.0, which brings modernized CSRF protection, a cleaner Latte integration, and several backward-compatibility changes.
Requires PHP 8.3 or newer.
Security: new CSRF protection
Forms now defend against CSRF by checking the Sec-Fetch-Site
header (strict same-origin) instead of the previous SameSite cookie. These
headers are added by the browser itself and cannot be spoofed even with an XSS
vulnerability, so the protection needs no token and no server-side state. The
article CSRF
Protection in Nette: No More Tokens Thanks to Sec-Fetch Headers describes it
in detail.
What this means in practice:
- Token-based protection (
$form->addProtection()) is marked as deprecated. It still works, so there's no need to rewrite anything in a hurry, but the default protection fully replaces it. - The check is now strictly same-origin. The previous mechanism (SameSite cookie) considered submissions between subdomains of the same site to be safe; the new check requires an exact origin match (scheme + host + port).
If you legitimately accept the form from a different origin
(a foreign site or another subdomain), you can disable the built-in check with
$form->allowCrossOrigin(). But beware: this disables the CSRF
protection completely and the form will accept a request from
anywhere, so do it deliberately. If you want to allow only specific origins,
disable the check and verify the origin yourself against your own list using the
Origin header:
$form->allowCrossOrigin(); // disables the built-in check
$allowed = ['https://app.example.com', 'https://admin.example.com'];
$form->onValidate[] = function (Form $form) use ($httpRequest, $allowed) {
if (
!$httpRequest->isFrom(Nette\Http\FetchSite::SameOrigin)
&& !in_array((string) $httpRequest->getOrigin(), $allowed, strict: true)
) {
$form->addError('Invalid request origin.');
}
};
You get $httpRequest in a presenter via
$this->getHttpRequest(). getOrigin() returns
null if the header is missing, so an unknown origin is
rejected.
Latte: unifying {formContext} and
{formContainer}
Until now there were two separate tags for working with a form without
rendering <form>: {formContext} set up the
context of an entire form (taken from the presenter, when the
<form> itself is rendered elsewhere), whereas
{formContainer} stepped into a nested container of an already-open
form. The difference was subtle and confusing for many people; it wasn't obvious
which one to reach for.
Now a single construct {form scope name}
encompasses both:
- the
scopekeyword tells{form}not to emit the<form>HTML tag, only to push the form/container onto the stack (so{input},{label},{inputError}bind to it), - if a form is already active, the name is resolved
relatively to it (the
{formContainer}behavior), otherwise from the top level (the{formContext}behavior).
A single notation thus covers both former cases. The original
{formContext} and {formContainer} remain functional,
but will eventually be deprecated in favor of {form scope}.
Latte: {form detached name}
It lets you create nested forms! HTML forbids nested
<form> tags, and the browser discards the inner
<form> and “glues” its inputs to the outer form. The
detached keyword solves this: it emits an empty
<form></form> up front and links every control to it
via the HTML form="..." attribute. The controls are thus submitted
to the correct form regardless of where they sit in the DOM.
If you have two nested forms, render the outer one (the one
that surrounds the other) as {form detached}. Its empty
<form> tag is closed right at the start, so the inner form is
no longer nested and both work correctly. Alternatively, render both as
detached.
Latte: requirements and internal changes
- A missing comma before
{form}arguments is now deprecated – write{form name, attr=val}instead of{form name attr=val}. The reason is precisely the ability to write keywords likescope/detachedafter the name. - Support for Latte 2 and Latte 3.0 has been removed – Latte 3.1.4+ is now required.
- The internal
Runtimewas rewritten from a static to a regular (non-static) class. This is an internal change that affects you only if you calledRuntime's static methods directly from your own code.
New: callback in addSubmit()
You can now pass a handler to the button right when creating it, without a
separate ->onClick[] =:
$form->addSubmit('send', 'Submit', function (SubmitButton $button, $values) {
// ...
});
Backward-compatibility changes
setValues() / setDefaults() – preparing
to narrow the accepted types. The supported input is now
iterable|stdClass (array, Traversable,
stdClass). Functionally nothing changes yet, but if you pass a
different object type – typically an entity or DTO, which used to be cast to
an array via (array) (which produces “mangled” keys for private
and protected properties) – you'll get a deprecation notice. In that case,
convert the data to an array yourself.
Other behavior changes:
RadioList/CheckboxList:getControlPart()andgetLabelPart()throw an exception for a non-existent key (previously they produced a nonsensical result instead).- Negative validation rules (
~Rule) now throw an exception (previously just a deprecation notice).
Removed (previously deprecated):
DataClassGenerator, LatteRenderer,
getValues(true) (use getValues('array')), the
$default parameter in getOption() (use the
?? operator).
Newly deprecated (still works, only emits a notice) –
some magic properties. Instead of $control->htmlName,
caption, omitted, filled,
options, selectedItem(s), submittedBy and
$form->action, method, renderer, use
the corresponding methods (getHtmlName(),
getCaption(), isOmitted(), isFilled(),
getOptions(), getSelectedItem(),
isSubmittedBy(), getAction(),
getMethod(), getRenderer()).