Формы отдельно от фреймворка

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

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

Первая форма

Прежде чем начать, установите пакет с помощью Composer:

composer require nette/forms

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

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 используется в презентерах). Мы добавили в неё элементы с именами “name”, “password” и кнопку отправки.

Теперь оживим форму. Запросив $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']);

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

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

Имя будет обязательным, поэтому пометим его методом 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', 'Заполните, пожалуйста, возраст');

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

Отрисовка с помощью Latte

Если у вас под рукой шаблонизатор Latte, вы можете поручить отрисовку формы ему и получить полный контроль над итоговым HTML. Вы создаёте движок, регистрируете расширение форм и передаёте форму в шаблон переменной:

$latte = new Latte\Engine;
$latte->addExtension(new Nette\Bridges\FormsLatte\FormsExtension);

$latte->render('form.latte', ['form' => $form]);

В шаблоне вы затем работаете с формой через переменную $form и теги вроде {input}, {label} или n:name. Полный пример вместе с шаблоном найдёте в каталоге examples (файлы latte.php и latte/). Отдельные теги описаны в главе об отрисовке.

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

Вернёмся к обработке данных формы. Метод getValues() возвращал отправленные данные как объект ArrayHash. Поскольку это универсальный класс, похожий на 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 = $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);

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

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

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

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

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

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

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

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

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

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-уязвимости. Старые браузеры, которые эти заголовки не отправляют, проверку не пройдут. Подробно это описано в статье Браузер наконец решает CSRF.

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

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

версия: 4.x