Nette Documentation Preview

syntax
Формы в презентерах
*******************

.[perex]
Nette Forms существенно упрощают создание и обработку веб-форм. В этой главе вы узнаете, как использовать формы внутри презентеров.

Если вас интересует их совершенно самостоятельное использование без остального фреймворка, есть руководство по [самостоятельному использованию|standalone].


Первая форма
============

Попробуем написать простую регистрационную форму. Её код будет таким:

```php
use Nette\Application\UI\Form;

$form = new Form;
$form->addText('name', 'Имя:');
$form->addPassword('password', 'Пароль:');
$form->addSubmit('send', 'Зарегистрироваться');
$form->onSuccess[] = $this->formSucceeded(...);
```

а в браузере она отобразится так:

[* form-en.webp *]

Форма в презентере - это объект класса `Nette\Application\UI\Form`; его предок `Nette\Forms\Form` предназначен для самостоятельного использования. Мы добавили элементы с именами name, password и кнопку отправки. Наконец, строка `$form->onSuccess` говорит, что после отправки и успешной проверки должен быть вызван метод `$this->formSucceeded()`.

С точки зрения презентера форма - обычный компонент. Поэтому с ней и обращаются как с компонентом и встраивают в презентер через [фабричный метод |application:components#Фабричные методы]. Выглядеть это будет так:

```php .{file:app/Presentation/Home/HomePresenter.php}
use Nette;
use Nette\Application\UI\Form;

class HomePresenter extends Nette\Application\UI\Presenter
{
	protected function createComponentRegistrationForm(): Form
	{
		$form = new Form;
		$form->addText('name', 'Имя:');
		$form->addPassword('password', 'Пароль:');
		$form->addSubmit('send', 'Зарегистрироваться');
		$form->onSuccess[] = $this->formSucceeded(...);
		return $form;
	}

	private function formSucceeded(Form $form, $data): void
	{
		// здесь мы обработаем данные, отправленные формой
		// $data->name содержит имя
		// $data->password содержит пароль
		$this->flashMessage('Вы успешно зарегистрировались.');
		$this->redirect('Home:');
	}
}
```

А в шаблоне форма отрисовывается тегом `{control}`:

```latte .{file:app/Presentation/Home/default.latte}
<h1>Регистрация</h1>

{control registrationForm}
```

И это, по сути, всё :-) У нас есть работающая и превосходно [защищённая |#Защита от уязвимостей] форма.

Сейчас вы, наверное, думаете, что это было слишком быстро, и гадаете, как так вышло, что метод `formSucceeded()` вызывается и какие параметры получает. Да, вы правы, это заслуживает объяснения.

Nette привносит освежающий механизм под названием [голливудский стиль |application:components#Голливудский стиль]. Вместо того чтобы вам как разработчику постоянно спрашивать, не случилось ли чего ("была ли форма отправлена?", "была ли она отправлена корректно?" и "не была ли она подделана?"), вы говорите фреймворку: "когда форма будет корректно заполнена, вызови этот метод", а дальнейшую работу оставляете ему. Если вы программируете на JavaScript, этот стиль программирования вам близко знаком. Вы пишете функции, которые вызываются, когда происходит определённое [событие |nette:glossary#События]. И язык передаёт им подходящие аргументы.

Именно так построен код презентера выше. Массив `$form->onSuccess` представляет собой список PHP-callback'ов, которые Nette вызывает в момент, когда форма отправлена и правильно заполнена (то есть корректна). В рамках [жизненного цикла презентера |application:presenters#Жизненный цикл презентера] это так называемый сигнал, поэтому вызываются они после метода `action*` и перед методом `render*`. И каждому callback'у она передаёт первым параметром саму форму, а вторым - отправленные данные в виде объекта [ArrayHash |utils:arrays#ArrayHash] (либо stdClass, либо собственного класса). Первый параметр можно опустить, если объект формы вам не нужен. Второй параметр может быть умнее, но об этом [позже |#Отображение в классы].

Объект `$data` содержит свойства `name` и `password` с данными, которые ввёл пользователь. Обычно мы отправляем данные прямо на дальнейшую обработку, которой может быть, например, вставка в базу данных. Однако при обработке может возникнуть ошибка, например имя пользователя уже занято. В таком случае мы передаём ошибку обратно в форму методом `addError()` и даём отрисовать её снова вместе с сообщением об ошибке.

```php
$form->addError('Извините, это имя пользователя уже занято.');
```

Кроме `onSuccess` есть ещё `onSubmit`: callback'и вызываются всегда, когда форма отправлена, даже если она заполнена неправильно. А ещё `onError`: callback'и вызываются, только если отправка некорректна. Они вызываются даже тогда, когда мы объявляем форму некорректной в `onSuccess` методом `addError()`.

После обработки формы мы перенаправляем на другую страницу. Это предотвращает нежелательную повторную отправку формы кнопками *обновить*, *назад* или переходом по истории браузера.

Если форма отправляется по AJAX, вы обычно вместо перенаправления перерисовываете [сниппет |application:ajax] с заново отрисованной формой.

Попробуйте добавить и другие [элементы формы|controls].


Доступ к элементам
==================

Форма - компонент презентера, в нашем случае с именем `registrationForm` (по имени фабричного метода `createComponentRegistrationForm`), так что где угодно в презентере вы можете получить форму так:

```php
$form = $this->getComponent('registrationForm');
// альтернативная запись: $form = $this['registrationForm'];
```

Отдельные элементы формы - тоже компоненты, поэтому обращаться к ним можно так же:

```php
$input = $form->getComponent('name'); // либо $input = $form['name'];
$button = $form->getComponent('send'); // либо $button = $form['send'];
```

Элементы удаляются через `unset`:

```php
unset($form['name']);
```


Правила проверки
================

Прозвучало слово *корректна*, но у формы пока нет никаких правил проверки. Исправим это.

Имя будет обязательным, поэтому пометим его методом `setRequired()`. Его аргумент - текст сообщения об ошибке, которое отобразится, если пользователь имя не заполнит. Если аргумент опустить, будет использовано стандартное сообщение об ошибке.

```php
$form->addText('name', 'Имя:')
	->setRequired('Введите, пожалуйста, имя.');
```

Попробуйте отправить форму, не заполнив имя, и вы увидите, что отобразится сообщение об ошибке, а браузер или сервер будут её отклонять, пока вы поле не заполните.

При этом систему не обмануть, введя в поле, например, только пробелы. Никак. Nette автоматически обрезает пробелы слева и справа. Попробуйте. Это то, что нужно всегда делать с каждым однострочным полем, но об этом часто забывают. Nette делает это автоматически. (Можете попробовать одурачить форму и отправить в качестве имени многострочную строку. И тут Nette не проведёшь, переводы строк будут заменены пробелами.)

Форма всегда проверяется на стороне сервера, но порождается и проверка на JavaScript, которая выполняется мгновенно, и пользователь узнаёт об ошибке сразу, без необходимости отправлять форму на сервер. За это отвечает скрипт `netteForms.js`. Подключите его в шаблон макета:

```latte
<script src="https://unpkg.com/nette-forms@3"></script>
```

Если вы посмотрите на исходный код страницы с формой, то заметите, что Nette оборачивает обязательные элементы в элементы с CSS-классом `required`. Попробуйте добавить в шаблон следующий стиль, и метка "Имя" станет красной. Так вы элегантно выделите для пользователей обязательные поля:

```latte
<style>
.required label { color: maroon }
</style>
```

Дальнейшие правила проверки мы добавляем методом `addRule()`. Первый параметр - правило, второй - снова текст сообщения об ошибке, а за ним может следовать аргумент правила проверки. Что это значит?

Расширим форму новым необязательным полем "возраст", которое должно быть целым числом (`addInteger()`), да ещё и в допустимом диапазоне (`$form::Range`). Здесь мы используем третий параметр метода `addRule()`, чтобы передать валидатору нужный диапазон парой `[min, max]`:

```php
$form->addInteger('age', 'Возраст:')
	->addRule($form::Range, 'Возраст должен быть от 18 до 120 лет.', [18, 120]);
```

.[tip]
Если пользователь поле не заполнит, правила проверки проверяться не будут, потому что элемент необязателен.

Здесь появляется место для небольшого рефакторинга. В сообщении об ошибке и в третьем параметре числа дублируются, а это неидеально. Если бы мы создавали [многоязычные формы |rendering#Перевод] и сообщение с числами переводилось бы на несколько языков, менять значения стало бы трудно. Поэтому можно использовать подстановки `%d`, и Nette значения подставит:

```php
	->addRule($form::Range, 'Возраст должен быть от %d до %d лет.', [18, 120]);
```

Вернёмся к элементу `password`, сделаем его тоже обязательным и заодно проверим минимальную длину пароля (`$form::MinLength`), снова с подстановкой в сообщении:

```php
$form->addPassword('password', 'Пароль:')
	->setRequired('Выберите пароль')
	->addRule($form::MinLength, 'Пароль должен быть длиной не менее %d символов.', 8);
```

Добавим в форму ещё одно поле `passwordVerify`, где пользователь введёт пароль повторно для подтверждения. С помощью правил проверки убедимся, что оба пароля одинаковы (`$form::Equal`). В качестве аргумента передадим ссылку на первый пароль через [квадратные скобки |#Доступ к элементам]:

```php
$form->addPassword('passwordVerify', 'Пароль ещё раз:')
	->setRequired('Введите пароль ещё раз для проверки опечатки')
	->addRule($form::Equal, 'Пароли не совпадают.', $form['password'])
	->setOmitted();
```

С помощью `setOmitted()` мы пометили элемент, значение которого нас на самом деле не интересует и который существует только ради проверки. Его значение в `$data` не передаётся.

Тем самым у нас есть полностью работающая форма с проверкой и в PHP, и в JavaScript. Возможности проверки в Nette намного шире: можно создавать условия, по ним показывать и скрывать части страницы и т. д. Обо всём вы узнаете в главе о [проверке форм|validation].


Значения по умолчанию
=====================

Значения по умолчанию для элементов формы мы задаём обычным образом:

```php
$form->addEmail('email', 'Email')
	->setDefaultValue($lastUsedEmail);
```

Часто бывает полезно задать значения по умолчанию сразу всем элементам. Например, когда форма используется для редактирования записей. Мы считываем запись из базы данных и задаём значения по умолчанию:

```php
// $row = ['name' => 'John', 'age' => '33', /* ... */];
$form->setDefaults($row);
```

Вызывайте `setDefaults()` после определения элементов.

У уже отправленной формы `setDefaults()` ничего не делает: он не перезапишет то, что заполнил пользователь, так что вызывать его в фабрике формы безусловно безопасно. Если вам нужно задать значения принудительно и после отправки, используйте вместо него `setValues()`.


Отрисовка формы
===============

По умолчанию форма отрисовывается как таблица. Отдельные элементы соблюдают основные правила веб-доступности: все метки записаны как элементы `<label>` и связаны с соответствующими элементами формы. Щелчок по метке автоматически ставит курсор в поле формы.

Каждому элементу мы можем задать произвольные HTML-атрибуты. Например, добавить placeholder:

```php
$form->addInteger('age', 'Возраст:')
	->setHtmlAttribute('placeholder', 'Заполните, пожалуйста, возраст');
```

Способов отрисовать форму действительно очень много, поэтому этому посвящена [отдельная глава об отрисовке|rendering].


Отображение в классы
====================

Вернёмся к методу `formSucceeded()`, который получает отправленные данные вторым параметром `$data` как объект `ArrayHash` (либо `stdClass`). Поскольку это универсальный класс, похожий на `stdClass`, при работе с ним нам не хватает определённых удобств, например автодополнения свойств в редакторах или статического анализа кода. Это можно решить, заведя для каждой формы отдельный класс, свойства которого представляют отдельные элементы. Например:

```php
class RegistrationFormData
{
	public string $name;
	public ?int $age;
	public string $password;
}
```

Как вариант, можно использовать конструктор:

```php
class RegistrationFormData
{
	public function __construct(
		public string $name,
		public ?int $age,
		public string $password,
	) {
	}
}
```

Свойства класса данных могут быть и перечислениями, и они будут отображены автоматически. .{data-version:3.2.4}

Как сказать Nette, чтобы она возвращала данные как объекты этого класса? Проще, чем вы думаете. Достаточно указать класс как тип параметра `$data` в методе-обработчике:

```php
public function formSucceeded(Form $form, RegistrationFormData $data): void
{
	// $data - экземпляр RegistrationFormData
	$name = $data->name;
	// ...
}
```

В качестве типа можно указать и `array`, тогда данные будут переданы массивом.

Точно так же можно использовать метод `getValues()`, передав ему параметром имя класса или объект для наполнения:

```php
$data = $form->getValues(RegistrationFormData::class);
$name = $data->name;
```

Если вам нужно прочитать значения до проверки формы, обычно внутри обработчика `onValidate`, используйте вместо него метод `getUntrustedValues()`. Он принимает те же параметры, что и `getValues()`, но возвращает отправленные значения без гарантии, что они прошли проверку.

Если формы имеют многоуровневую структуру из контейнеров, создайте для каждого отдельный класс:

```php
$form = new Form;
$person = $form->addContainer('person');
$person->addText('firstName');
/* ... */

class PersonFormData
{
	public string $firstName;
	public string $lastName;
}

class RegistrationFormData
{
	public PersonFormData $person;
	public ?int $age;
	public string $password;
}
```

Тогда отображение по типу свойства `$person` выведет, что контейнер нужно отобразить в класс `PersonFormData`. Если бы свойство содержало массив контейнеров, укажите тип `array`, а класс для отображения передайте прямо контейнеру:

```php
$person->setMappedType(PersonFormData::class);
```

Заготовку класса данных формы можно породить методом `Nette\Forms\Blueprint::dataClass($form)`, который выведет её на страницу в браузере. Дальше достаточно щелчком выделить код и скопировать его в проект. .{data-version:3.1.15}


Несколько кнопок отправки
=========================

Если у формы больше одной кнопки, нам обычно нужно различить, какая была нажата. Для каждой кнопки можно создать отдельную функцию-обработчик. Задайте её как обработчик [события |nette:glossary#События] `onClick`:

```php
$form->addSubmit('save', 'Сохранить')
	->onClick[] = $this->saveButtonPressed(...);

$form->addSubmit('delete', 'Удалить')
	->onClick[] = $this->deleteButtonPressed(...);
```

.{data-version:3.3.0}
Обработчик можно передать кнопке и напрямую третьим аргументом метода `addSubmit()`.

Эти обработчики вызываются, только если форма корректно заполнена (если для кнопки не отключена проверка), как и событие `onSuccess`. Разница в том, что первым параметром вместо формы может быть передан объект кнопки отправки, в зависимости от того, какой тип вы укажете:

```php
private function saveButtonPressed(Nette\Forms\Controls\Button $button, $data)
{
	$form = $button->getForm();
	// ...
}
```

Когда форма отправляется нажатием клавиши <kbd>Enter</kbd>, это считается отправкой первой кнопкой отправки.


Событие onAnchor
================

Когда вы строите форму в фабричном методе (вроде `createComponentRegistrationForm`), она ещё не знает, была ли отправлена и с какими данными. Однако бывают случаи, когда нам нужно знать отправленные значения: возможно, от них зависит внешний вид формы или они нужны для зависимых выпадающих списков и т. п.

Поэтому вы можете сделать так, чтобы код, строящий форму, вызывался только тогда, когда она "заякорена", то есть уже связана с презентером и знает свои отправленные данные. Поместите такой код в массив `$onAnchor`:

```php
$country = $form->addSelect('country', 'Страна:', $this->model->getCountries());
$city = $form->addSelect('city', 'Город:');

$form->onAnchor[] = function () use ($country, $city) {
	// эта функция будет вызвана, когда форма узнает, с какими данными её отправили,
	// так что можно использовать метод getValue()
	$val = $country->getValue();
	$city->setItems($val ? $this->model->getCities($val) : []);
};
```


Защита от уязвимостей
=====================

Nette Framework уделяет безопасности огромное внимание и поэтому тщательно следит за безопасностью форм. Делает она это совершенно прозрачно и не требует никакой ручной настройки.

Кроме защиты форм от таких атак, как [Cross-Site Scripting (XSS) |nette:glossary#Cross-Site Scripting (XSS)] и [Cross-Site Request Forgery (CSRF) |nette:glossary#Cross-Site Request Forgery (CSRF)], она выполняет множество мелких мер безопасности, о которых вам больше не нужно думать.

Например, она отфильтровывает из ввода все управляющие символы и проверяет корректность кодировки UTF-8, благодаря чему данные из формы всегда чисты. У выпадающих списков и радиосписков она проверяет, что выбранные пункты действительно были среди предложенных и что подделки не произошло. Мы уже упоминали, что у однострочных текстовых полей она заменяет пробелами символы конца строки, которые мог отправить злоумышленник. У многострочных полей она приводит символы конца строки к единому виду. И так далее.

Nette решает за вас риски безопасности, о существовании которых многие программисты даже не подозревают.

Упомянутая атака CSRF состоит в том, что злоумышленник заманивает жертву на страницу, которая незаметно выполняет в браузере жертвы запрос к серверу, где жертва авторизована. Сервер тогда считает, что запрос сделала жертва по своей воле. Поэтому Nette отклоняет POST-формы, отправленные с чужого источника; чужим считается даже другой поддомен того же сайта. Если вам нужно разрешить отправку с другого источника, отключите защиту так:

```php
$form->allowCrossOrigin(); // ВНИМАНИЕ! Полностью отключает защиту!
```

Правда, это отключает защиту для любого источника. Чтобы разрешить только определённые источники, отключите защиту и сами сверяйте заголовок `Origin` со своим списком разрешённых.

Защита опирается на браузерный заголовок `Sec-Fetch-Site` (Fetch Metadata), который браузер отправляет автоматически и который нельзя подделать даже при наличии XSS-уязвимости. Для старых браузеров без его поддержки действует запасная cookie SameSite, которую приложение на Nette устанавливает автоматически. Подробно это описано в статье [Браузер наконец решает CSRF |https://blog.nette.org/en/quarter-century-of-csrf].

.[note]
Прежняя защита с помощью авторизационного токена в сессии, включаемая через `$form->addProtection()`, больше не нужна и объявлена устаревшей начиная с версии 3.3.


Использование одной формы в нескольких презентерах
=================================================

Если вам нужно использовать одну и ту же форму в нескольких презентерах, мы рекомендуем создать для неё фабрику, которую вы затем внедрите в презентеры. Подходящее место для такого класса - например, каталог `app/Forms`.

Класс фабрики может выглядеть так:

```php
use Nette\Application\UI\Form;

class SignInFormFactory
{
	public function create(): Form
	{
		$form = new Form;
		$form->addText('name', 'Имя:');
		$form->addSubmit('send', 'Войти');
		return $form;
	}
}
```

Класс, порождающий форму, мы запрашиваем в фабричном методе компонента внутри презентера:

```php
public function __construct(
	private SignInFormFactory $formFactory,
) {
}

protected function createComponentSignInForm(): Form
{
	$form = $this->formFactory->create();
	// форму можно изменить, здесь мы, например, меняем подпись на кнопке
	$form['send']->setCaption('Продолжить');
	$form->onSuccess[] = $this->signInFormSuceeded(...); // и добавляем обработчик
	return $form;
}
```

Обработчик формы может предоставить и сама фабрика:

```php
use Nette\Application\UI\Form;

class SignInFormFactory
{
	public function create(): Form
	{
		$form = new Form;
		$form->addText('name', 'Имя:');
		$form->addSubmit('send', 'Войти');
		$form->onSuccess[] = function (Form $form, $data): void {
			// здесь мы обрабатываем отправленную форму
		};
		return $form;
	}
}
```

Итак, мы прошли беглое знакомство с формами в Nette. За дополнительным вдохновением загляните в каталог [examples |https://github.com/nette/forms/tree/master/examples] в дистрибутиве.

Формы в презентерах

Nette Forms существенно упрощают создание и обработку веб-форм. В этой главе вы узнаете, как использовать формы внутри презентеров.

Если вас интересует их совершенно самостоятельное использование без остального фреймворка, есть руководство по самостоятельному использованию.

Первая форма

Попробуем написать простую регистрационную форму. Её код будет таким:

use Nette\Application\UI\Form;

$form = new Form;
$form->addText('name', 'Имя:');
$form->addPassword('password', 'Пароль:');
$form->addSubmit('send', 'Зарегистрироваться');
$form->onSuccess[] = $this->formSucceeded(...);

а в браузере она отобразится так:

Форма в презентере – это объект класса Nette\Application\UI\Form; его предок Nette\Forms\Form предназначен для самостоятельного использования. Мы добавили элементы с именами name, password и кнопку отправки. Наконец, строка $form->onSuccess говорит, что после отправки и успешной проверки должен быть вызван метод $this->formSucceeded().

С точки зрения презентера форма – обычный компонент. Поэтому с ней и обращаются как с компонентом и встраивают в презентер через фабричный метод. Выглядеть это будет так:

use Nette;
use Nette\Application\UI\Form;

class HomePresenter extends Nette\Application\UI\Presenter
{
	protected function createComponentRegistrationForm(): Form
	{
		$form = new Form;
		$form->addText('name', 'Имя:');
		$form->addPassword('password', 'Пароль:');
		$form->addSubmit('send', 'Зарегистрироваться');
		$form->onSuccess[] = $this->formSucceeded(...);
		return $form;
	}

	private function formSucceeded(Form $form, $data): void
	{
		// здесь мы обработаем данные, отправленные формой
		// $data->name содержит имя
		// $data->password содержит пароль
		$this->flashMessage('Вы успешно зарегистрировались.');
		$this->redirect('Home:');
	}
}

А в шаблоне форма отрисовывается тегом {control}:

<h1>Регистрация</h1>

{control registrationForm}

И это, по сути, всё :-) У нас есть работающая и превосходно защищённая форма.

Сейчас вы, наверное, думаете, что это было слишком быстро, и гадаете, как так вышло, что метод formSucceeded() вызывается и какие параметры получает. Да, вы правы, это заслуживает объяснения.

Nette привносит освежающий механизм под названием голливудский стиль. Вместо того чтобы вам как разработчику постоянно спрашивать, не случилось ли чего („была ли форма отправлена?“, „была ли она отправлена корректно?“ и „не была ли она подделана?“), вы говорите фреймворку: „когда форма будет корректно заполнена, вызови этот метод“, а дальнейшую работу оставляете ему. Если вы программируете на JavaScript, этот стиль программирования вам близко знаком. Вы пишете функции, которые вызываются, когда происходит определённое событие. И язык передаёт им подходящие аргументы.

Именно так построен код презентера выше. Массив $form->onSuccess представляет собой список PHP-callback'ов, которые Nette вызывает в момент, когда форма отправлена и правильно заполнена (то есть корректна). В рамках жизненного цикла презентера это так называемый сигнал, поэтому вызываются они после метода action* и перед методом render*. И каждому callback'у она передаёт первым параметром саму форму, а вторым – отправленные данные в виде объекта ArrayHash (либо stdClass, либо собственного класса). Первый параметр можно опустить, если объект формы вам не нужен. Второй параметр может быть умнее, но об этом позже.

Объект $data содержит свойства name и password с данными, которые ввёл пользователь. Обычно мы отправляем данные прямо на дальнейшую обработку, которой может быть, например, вставка в базу данных. Однако при обработке может возникнуть ошибка, например имя пользователя уже занято. В таком случае мы передаём ошибку обратно в форму методом addError() и даём отрисовать её снова вместе с сообщением об ошибке.

$form->addError('Извините, это имя пользователя уже занято.');

Кроме onSuccess есть ещё onSubmit: callback'и вызываются всегда, когда форма отправлена, даже если она заполнена неправильно. А ещё onError: callback'и вызываются, только если отправка некорректна. Они вызываются даже тогда, когда мы объявляем форму некорректной в onSuccess методом addError().

После обработки формы мы перенаправляем на другую страницу. Это предотвращает нежелательную повторную отправку формы кнопками обновить, назад или переходом по истории браузера.

Если форма отправляется по AJAX, вы обычно вместо перенаправления перерисовываете сниппет с заново отрисованной формой.

Попробуйте добавить и другие элементы формы.

Доступ к элементам

Форма – компонент презентера, в нашем случае с именем registrationForm (по имени фабричного метода createComponentRegistrationForm), так что где угодно в презентере вы можете получить форму так:

$form = $this->getComponent('registrationForm');
// альтернативная запись: $form = $this['registrationForm'];

Отдельные элементы формы – тоже компоненты, поэтому обращаться к ним можно так же:

$input = $form->getComponent('name'); // либо $input = $form['name'];
$button = $form->getComponent('send'); // либо $button = $form['send'];

Элементы удаляются через unset:

unset($form['name']);

Правила проверки

Прозвучало слово корректна, но у формы пока нет никаких правил проверки. Исправим это.

Имя будет обязательным, поэтому пометим его методом setRequired(). Его аргумент – текст сообщения об ошибке, которое отобразится, если пользователь имя не заполнит. Если аргумент опустить, будет использовано стандартное сообщение об ошибке.

$form->addText('name', 'Имя:')
	->setRequired('Введите, пожалуйста, имя.');

Попробуйте отправить форму, не заполнив имя, и вы увидите, что отобразится сообщение об ошибке, а браузер или сервер будут её отклонять, пока вы поле не заполните.

При этом систему не обмануть, введя в поле, например, только пробелы. Никак. Nette автоматически обрезает пробелы слева и справа. Попробуйте. Это то, что нужно всегда делать с каждым однострочным полем, но об этом часто забывают. Nette делает это автоматически. (Можете попробовать одурачить форму и отправить в качестве имени многострочную строку. И тут Nette не проведёшь, переводы строк будут заменены пробелами.)

Форма всегда проверяется на стороне сервера, но порождается и проверка на JavaScript, которая выполняется мгновенно, и пользователь узнаёт об ошибке сразу, без необходимости отправлять форму на сервер. За это отвечает скрипт netteForms.js. Подключите его в шаблон макета:

<script src="https://unpkg.com/nette-forms@3"></script>

Если вы посмотрите на исходный код страницы с формой, то заметите, что Nette оборачивает обязательные элементы в элементы с CSS-классом required. Попробуйте добавить в шаблон следующий стиль, и метка „Имя“ станет красной. Так вы элегантно выделите для пользователей обязательные поля:

<style>
.required label { color: maroon }
</style>

Дальнейшие правила проверки мы добавляем методом addRule(). Первый параметр – правило, второй – снова текст сообщения об ошибке, а за ним может следовать аргумент правила проверки. Что это значит?

Расширим форму новым необязательным полем „возраст“, которое должно быть целым числом (addInteger()), да ещё и в допустимом диапазоне ($form::Range). Здесь мы используем третий параметр метода addRule(), чтобы передать валидатору нужный диапазон парой [min, max]:

$form->addInteger('age', 'Возраст:')
	->addRule($form::Range, 'Возраст должен быть от 18 до 120 лет.', [18, 120]);

Если пользователь поле не заполнит, правила проверки проверяться не будут, потому что элемент необязателен.

Здесь появляется место для небольшого рефакторинга. В сообщении об ошибке и в третьем параметре числа дублируются, а это неидеально. Если бы мы создавали многоязычные формы и сообщение с числами переводилось бы на несколько языков, менять значения стало бы трудно. Поэтому можно использовать подстановки %d, и Nette значения подставит:

	->addRule($form::Range, 'Возраст должен быть от %d до %d лет.', [18, 120]);

Вернёмся к элементу password, сделаем его тоже обязательным и заодно проверим минимальную длину пароля ($form::MinLength), снова с подстановкой в сообщении:

$form->addPassword('password', 'Пароль:')
	->setRequired('Выберите пароль')
	->addRule($form::MinLength, 'Пароль должен быть длиной не менее %d символов.', 8);

Добавим в форму ещё одно поле passwordVerify, где пользователь введёт пароль повторно для подтверждения. С помощью правил проверки убедимся, что оба пароля одинаковы ($form::Equal). В качестве аргумента передадим ссылку на первый пароль через квадратные скобки:

$form->addPassword('passwordVerify', 'Пароль ещё раз:')
	->setRequired('Введите пароль ещё раз для проверки опечатки')
	->addRule($form::Equal, 'Пароли не совпадают.', $form['password'])
	->setOmitted();

С помощью setOmitted() мы пометили элемент, значение которого нас на самом деле не интересует и который существует только ради проверки. Его значение в $data не передаётся.

Тем самым у нас есть полностью работающая форма с проверкой и в PHP, и в JavaScript. Возможности проверки в Nette намного шире: можно создавать условия, по ним показывать и скрывать части страницы и т. д. Обо всём вы узнаете в главе о проверке форм.

Значения по умолчанию

Значения по умолчанию для элементов формы мы задаём обычным образом:

$form->addEmail('email', 'Email')
	->setDefaultValue($lastUsedEmail);

Часто бывает полезно задать значения по умолчанию сразу всем элементам. Например, когда форма используется для редактирования записей. Мы считываем запись из базы данных и задаём значения по умолчанию:

// $row = ['name' => 'John', 'age' => '33', /* ... */];
$form->setDefaults($row);

Вызывайте setDefaults() после определения элементов.

У уже отправленной формы setDefaults() ничего не делает: он не перезапишет то, что заполнил пользователь, так что вызывать его в фабрике формы безусловно безопасно. Если вам нужно задать значения принудительно и после отправки, используйте вместо него setValues().

Отрисовка формы

По умолчанию форма отрисовывается как таблица. Отдельные элементы соблюдают основные правила веб-доступности: все метки записаны как элементы <label> и связаны с соответствующими элементами формы. Щелчок по метке автоматически ставит курсор в поле формы.

Каждому элементу мы можем задать произвольные HTML-атрибуты. Например, добавить placeholder:

$form->addInteger('age', 'Возраст:')
	->setHtmlAttribute('placeholder', 'Заполните, пожалуйста, возраст');

Способов отрисовать форму действительно очень много, поэтому этому посвящена отдельная глава об отрисовке.

Отображение в классы

Вернёмся к методу formSucceeded(), который получает отправленные данные вторым параметром $data как объект ArrayHash (либо stdClass). Поскольку это универсальный класс, похожий на stdClass, при работе с ним нам не хватает определённых удобств, например автодополнения свойств в редакторах или статического анализа кода. Это можно решить, заведя для каждой формы отдельный класс, свойства которого представляют отдельные элементы. Например:

class RegistrationFormData
{
	public string $name;
	public ?int $age;
	public string $password;
}

Как вариант, можно использовать конструктор:

class RegistrationFormData
{
	public function __construct(
		public string $name,
		public ?int $age,
		public string $password,
	) {
	}
}

Свойства класса данных могут быть и перечислениями, и они будут отображены автоматически.

Как сказать Nette, чтобы она возвращала данные как объекты этого класса? Проще, чем вы думаете. Достаточно указать класс как тип параметра $data в методе-обработчике:

public function formSucceeded(Form $form, RegistrationFormData $data): void
{
	// $data - экземпляр RegistrationFormData
	$name = $data->name;
	// ...
}

В качестве типа можно указать и array, тогда данные будут переданы массивом.

Точно так же можно использовать метод getValues(), передав ему параметром имя класса или объект для наполнения:

$data = $form->getValues(RegistrationFormData::class);
$name = $data->name;

Если вам нужно прочитать значения до проверки формы, обычно внутри обработчика onValidate, используйте вместо него метод getUntrustedValues(). Он принимает те же параметры, что и getValues(), но возвращает отправленные значения без гарантии, что они прошли проверку.

Если формы имеют многоуровневую структуру из контейнеров, создайте для каждого отдельный класс:

$form = new Form;
$person = $form->addContainer('person');
$person->addText('firstName');
/* ... */

class PersonFormData
{
	public string $firstName;
	public string $lastName;
}

class RegistrationFormData
{
	public PersonFormData $person;
	public ?int $age;
	public string $password;
}

Тогда отображение по типу свойства $person выведет, что контейнер нужно отобразить в класс PersonFormData. Если бы свойство содержало массив контейнеров, укажите тип array, а класс для отображения передайте прямо контейнеру:

$person->setMappedType(PersonFormData::class);

Заготовку класса данных формы можно породить методом Nette\Forms\Blueprint::dataClass($form), который выведет её на страницу в браузере. Дальше достаточно щелчком выделить код и скопировать его в проект.

Несколько кнопок отправки

Если у формы больше одной кнопки, нам обычно нужно различить, какая была нажата. Для каждой кнопки можно создать отдельную функцию-обработчик. Задайте её как обработчик события onClick:

$form->addSubmit('save', 'Сохранить')
	->onClick[] = $this->saveButtonPressed(...);

$form->addSubmit('delete', 'Удалить')
	->onClick[] = $this->deleteButtonPressed(...);

Обработчик можно передать кнопке и напрямую третьим аргументом метода addSubmit().

Эти обработчики вызываются, только если форма корректно заполнена (если для кнопки не отключена проверка), как и событие onSuccess. Разница в том, что первым параметром вместо формы может быть передан объект кнопки отправки, в зависимости от того, какой тип вы укажете:

private function saveButtonPressed(Nette\Forms\Controls\Button $button, $data)
{
	$form = $button->getForm();
	// ...
}

Когда форма отправляется нажатием клавиши Enter, это считается отправкой первой кнопкой отправки.

Событие onAnchor

Когда вы строите форму в фабричном методе (вроде createComponentRegistrationForm), она ещё не знает, была ли отправлена и с какими данными. Однако бывают случаи, когда нам нужно знать отправленные значения: возможно, от них зависит внешний вид формы или они нужны для зависимых выпадающих списков и т. п.

Поэтому вы можете сделать так, чтобы код, строящий форму, вызывался только тогда, когда она „заякорена“, то есть уже связана с презентером и знает свои отправленные данные. Поместите такой код в массив $onAnchor:

$country = $form->addSelect('country', 'Страна:', $this->model->getCountries());
$city = $form->addSelect('city', 'Город:');

$form->onAnchor[] = function () use ($country, $city) {
	// эта функция будет вызвана, когда форма узнает, с какими данными её отправили,
	// так что можно использовать метод getValue()
	$val = $country->getValue();
	$city->setItems($val ? $this->model->getCities($val) : []);
};

Защита от уязвимостей

Nette Framework уделяет безопасности огромное внимание и поэтому тщательно следит за безопасностью форм. Делает она это совершенно прозрачно и не требует никакой ручной настройки.

Кроме защиты форм от таких атак, как Cross-Site Scripting (XSS) и Cross-Site Request Forgery (CSRF), она выполняет множество мелких мер безопасности, о которых вам больше не нужно думать.

Например, она отфильтровывает из ввода все управляющие символы и проверяет корректность кодировки UTF-8, благодаря чему данные из формы всегда чисты. У выпадающих списков и радиосписков она проверяет, что выбранные пункты действительно были среди предложенных и что подделки не произошло. Мы уже упоминали, что у однострочных текстовых полей она заменяет пробелами символы конца строки, которые мог отправить злоумышленник. У многострочных полей она приводит символы конца строки к единому виду. И так далее.

Nette решает за вас риски безопасности, о существовании которых многие программисты даже не подозревают.

Упомянутая атака CSRF состоит в том, что злоумышленник заманивает жертву на страницу, которая незаметно выполняет в браузере жертвы запрос к серверу, где жертва авторизована. Сервер тогда считает, что запрос сделала жертва по своей воле. Поэтому Nette отклоняет POST-формы, отправленные с чужого источника; чужим считается даже другой поддомен того же сайта. Если вам нужно разрешить отправку с другого источника, отключите защиту так:

$form->allowCrossOrigin(); // ВНИМАНИЕ! Полностью отключает защиту!

Правда, это отключает защиту для любого источника. Чтобы разрешить только определённые источники, отключите защиту и сами сверяйте заголовок Origin со своим списком разрешённых.

Защита опирается на браузерный заголовок Sec-Fetch-Site (Fetch Metadata), который браузер отправляет автоматически и который нельзя подделать даже при наличии XSS-уязвимости. Для старых браузеров без его поддержки действует запасная cookie SameSite, которую приложение на Nette устанавливает автоматически. Подробно это описано в статье Браузер наконец решает CSRF.

Прежняя защита с помощью авторизационного токена в сессии, включаемая через $form->addProtection(), больше не нужна и объявлена устаревшей начиная с версии 3.3.

Использование одной формы в нескольких презентерах

Если вам нужно использовать одну и ту же форму в нескольких презентерах, мы рекомендуем создать для неё фабрику, которую вы затем внедрите в презентеры. Подходящее место для такого класса – например, каталог app/Forms.

Класс фабрики может выглядеть так:

use Nette\Application\UI\Form;

class SignInFormFactory
{
	public function create(): Form
	{
		$form = new Form;
		$form->addText('name', 'Имя:');
		$form->addSubmit('send', 'Войти');
		return $form;
	}
}

Класс, порождающий форму, мы запрашиваем в фабричном методе компонента внутри презентера:

public function __construct(
	private SignInFormFactory $formFactory,
) {
}

protected function createComponentSignInForm(): Form
{
	$form = $this->formFactory->create();
	// форму можно изменить, здесь мы, например, меняем подпись на кнопке
	$form['send']->setCaption('Продолжить');
	$form->onSuccess[] = $this->signInFormSuceeded(...); // и добавляем обработчик
	return $form;
}

Обработчик формы может предоставить и сама фабрика:

use Nette\Application\UI\Form;

class SignInFormFactory
{
	public function create(): Form
	{
		$form = new Form;
		$form->addText('name', 'Имя:');
		$form->addSubmit('send', 'Войти');
		$form->onSuccess[] = function (Form $form, $data): void {
			// здесь мы обрабатываем отправленную форму
		};
		return $form;
	}
}

Итак, мы прошли беглое знакомство с формами в Nette. За дополнительным вдохновением загляните в каталог examples в дистрибутиве.