Использование форм отдельно
Nette Forms значительно упрощают создание и обработку веб-форм. Вы можете использовать их в своих приложениях совершенно отдельно от остальной части фреймворка, что мы и покажем в этой главе.
Однако, если вы используете Nette Application и презентеры, для вас предназначено руководство по использованию в презентерах.
Первая форма
Попробуем написать простую регистрационную форму. Ее код будет следующим (“[весь код” |https://gist.github.com/dg/57878c1a413ae8ef0c1d83f02c43ef3f):
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']);
Правила валидации
Здесь прозвучало слово валидный, но у формы пока нет никаких правил валидации. Давайте это исправим.
Имя будет обязательным, поэтому мы пометим его методом
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()
, которым передадим валидатору требуемый
диапазон в виде пары [от, до]
:
$form->addInteger('age', 'Возраст:')
->addRule($form::Range, 'Возраст должен быть от 18 до 120', [18, 120]);
Если пользователь не заполнит поле, правила валидации проверяться не будут, так как элемент необязательный.
Здесь возникает возможность для небольшого рефакторинга. В
сообщении об ошибке и в третьем параметре числа указаны дублировано,
что не идеально. Если бы мы создавали многоязычные формы и сообщение,
содержащее числа, было бы переведено на несколько языков, это
усложнило бы возможное изменение значений. По этой причине можно
использовать заполнители (placeholders) %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', 'E-mail')
->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;
}
Альтернативно можно использовать конструктор:
class RegistrationFormData
{
public function __construct(
public string $name,
public int $age,
public string $password,
) {
}
}
Свойства класса данных также могут быть перечислениями (enum), и они будут автоматически сопоставлены.
Как сказать 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, так что данные из формы всегда будут чистыми. У select box и radio list он проверяет, что выбранные элементы действительно были из предложенных и не произошло подделки. Мы уже упоминали, что у однострочных текстовых полей ввода он удаляет символы конца строки, которые мог отправить злоумышленник. У многострочных полей ввода он нормализует символы конца строки. И так далее.
Nette решает за вас риски безопасности, о существовании которых многие программисты даже не подозревают.
Упомянутая CSRF-атака заключается в том, что злоумышленник заманивает жертву на страницу, которая незаметно в браузере жертвы выполняет запрос на сервер, на котором жертва авторизована, и сервер полагает, что запрос был выполнен жертвой по ее собственной воле. Поэтому Nette предотвращает отправку POST-формы с другого домена. Если по какой-то причине вы хотите отключить защиту и разрешить отправку формы с другого домена, используйте:
$form->allowCrossOrigin(); // ВНИМАНИЕ! Отключает защиту!
Эта защита использует SameSite cookie с именем _nss
. Поэтому создавайте
объект формы еще до отправки первого вывода, чтобы можно было
отправить cookie.
Защита с помощью SameSite cookie может быть не 100% надежной, поэтому рекомендуется включить еще защиту с помощью токена:
$form->addProtection();
Рекомендуем таким образом защищать формы в административной части
сайта, которые изменяют чувствительные данные в приложении. Фреймворк
защищается от CSRF-атаки путем генерации и проверки авторизационного
токена, который сохраняется в сессии. Поэтому необходимо, чтобы перед
отображением формы была открыта сессия. В административной части
сайта сессия обычно уже запущена из-за входа пользователя. В противном
случае запустите сессию методом Nette\Http\Session::start()
.
Итак, мы рассмотрели быстрое введение в формы в Nette. Попробуйте еще заглянуть в каталог examples в дистрибутиве, где вы найдете дополнительное вдохновение.