Формы, используемые автономно

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

Однако если вы используете приложение Nette и презентеры, для вас есть руководство: Формы в презентерах.

Первая форма

Мы постараемся написать простую регистрационную форму. Его код будет выглядеть следующим образом (полный код):

use Nette\Forms\Form;

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

И давайте сделаем рендеринг:

$form->render();

и результат должен выглядеть следующим образом:

Форма является объектом класса Nette\Forms\Form (класс Nette\Application\UI\Form используется в презентерах). Мы добавили в него имя элемента управления, пароль и кнопку отправки.

Теперь мы оживим форму. Спросив $form->isSuccess(), мы узнаем, была ли форма отправлена и правильно ли она была заполнена. Если да, то мы сбросим данные. После определения формы добавим:

if ($form->isSuccess()) {
	echo 'Форма была заполнена и отправлена правильно';
	$data = $form->getValues();
	// $data->name содержит имя
	// $data->password содержит пароль
	var_dump($data);
}

Метод getValues() возвращает отправленные данные в виде объекта ArrayHash. Мы покажем, как это изменить позже. Переменная $data содержит ключи name и password с данными, введёнными пользователем.

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

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

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

По умолчанию форма отправляется методом POST на ту же страницу. И то, и другое можно изменить:

$form->setAction('/submit.php');
$form->setMethod('GET');

И это всё :-) У нас есть функциональная и идеально защищенная форма.

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

Доступ к элементам управления

Форма и её отдельные элементы управления называются компонентами. Они создают дерево компонентов, корнем которого является форма. Доступ к отдельным элементам управления можно получить следующим образом:

$input = $form->getComponent('name');
// альтернативный синтаксис: $input = $form['name'];

$button = $form->getComponent('send');
// альтернативный синтаксис: $button = $form['send'];

Элементы управления удаляются с помощью функции unset:

unset($form['name']);

Правила валидации

Слово valid было использовано несколько раз, но форма ещё не имеет правил валидации. Давайте исправим это.

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

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

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

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

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

<script src="https://nette.github.io/resources/js/3/netteForms.min.js"></script>

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

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

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

Форма получит ещё один необязательный элемент ввода age с условием, что он должен быть числом (addInteger()) и находиться в определенных границах ($form::Range). И здесь мы будем использовать третий аргумент addRule(), сам диапазон:

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

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

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

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

Вернемся к полю пароль, сделаем его обязательным и проверим минимальную длину пароля ($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', 'Имейл')
	->setDefaultValue($lastUsedEmail);

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

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

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

Отображение формы

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

Мы можем установить любые атрибуты HTML для каждого элемента. Например, добавьте заполнитель:

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

На самом деле существует множество способов визуализации формы, подробнее в главе Рендеринг.

Сопоставление с классами

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

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

Начиная с PHP 8.0, вы можете использовать эту элегантную нотацию, которая использует конструктор:

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

Как сказать Nette, чтобы он возвращал нам данные в виде объектов этого класса? Легче, чем вы думаете. Всё, что вам нужно сделать, это указать имя класса или объекта для гидратации в качестве параметра:

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

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

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

$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);

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

Если форма содержит более одной кнопки, нам обычно нужно различать, какая из них была нажата. Метод isSubmittedBy() кнопки возвращает нам эту информацию:

$form->addSubmit('save', 'Сохранить');
$form->addSubmit('delete', 'Удалить');

if ($form->isSuccess()) {
	if ($form['save']->isSubmittedBy()) {
		// ...
	}

	if ($form['delete']->isSubmittedBy()) {
		// ...
	}
}

Не опускайте $form->isSuccess() для проверки достоверности данных.

Когда форма отправляется с помощью кнопки Enter, она обрабатывается так же, как если бы она была отправлена с помощью первой кнопки.

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

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

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

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

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

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

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

Эта защита использует файл куки SameSite с именем _nss. Поэтому создайте форму перед первым выводом, чтобы можно было отправить куки.

Защита куки-файлов SameSite может быть не на 100% надежной, поэтому хорошей идеей будет включить защиту с помощью токена:

$form->addProtection();

Настоятельно рекомендуется применять эту защиту к формам в административной части вашего приложения, которые изменяют конфиденциальные данные. Фреймворк защищает от атаки CSRF, генерируя и проверяя токен аутентификации, который хранится в сессии (аргументом является сообщение об ошибке, показываемое, если срок действия токена истек). Поэтому перед отображением формы необходимо, чтобы сессия была запущена. В административной части сайта сессия, как правило, уже началась, поскольку пользователь вошел в систему. В противном случае, запустите сессию с помощью метода Nette\Http\Session::start().

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