Формы в презентерах
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
предназначен для самостоятельного
использования. Мы добавили в нее так называемые элементы: имя, пароль и
кнопку отправки. И, наконец, строка с $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;
}
public function formSucceeded(Form $form, $data): void
{
// здесь мы обрабатываем данные, отправленные формой
// $data->name содержит имя
// $data->password содержит пароль
$this->flashMessage('Вы были успешно зарегистрированы.');
$this->redirect('Home:');
}
}
А в шаблоне форму отрисовываем тегом {control}
:
<h1>Регистрация</h1>
{control registrationForm}
И это, собственно, все :-) У нас есть рабочая и идеально защищенная форма.
А теперь вы, вероятно, думаете, что это было слишком быстро, и
размышляете, как возможно, что вызывается метод formSucceeded()
и какие
параметры он получает. Конечно, вы правы, это заслуживает объяснения.
Nette предлагает свежий механизм, который мы называем Hollywood style. Вместо того чтобы вам, как разработчику, постоянно спрашивать, произошло ли что-то («была ли форма отправлена?», «была ли она отправлена валидно?» и «не была ли она подделана?»), вы говорите фреймворку: «когда форма будет валидно заполнена, вызови этот метод» и оставляете дальнейшую работу ему. Если вы программируете на JavaScript, этот стиль программирования вам хорошо знаком. Вы пишете функции, которые вызываются, когда наступает определенное событие. И язык передает им соответствующие аргументы.
Именно так построен и вышеприведенный код презентера. Массив
$form->onSuccess
представляет собой список PHP callback-ов, которые Nette
вызовет в момент, когда форма отправлена и правильно заполнена (т. е.
валидна). В рамках жизненного цикла
презентера это так называемый сигнал, то есть они вызываются после
метода action*
и перед методом render*
. И каждому callback-у он
передает в качестве первого параметра саму форму, а в качестве
второго — отправленные данные в виде объекта ArrayHash. Первый параметр можно
опустить, если объект формы вам не нужен. А второй параметр может быть
хитрее, но об этом позже.
Объект $data
содержит ключи name
и password
с данными,
которые заполнил пользователь. Обычно данные сразу отправляются на
дальнейшую обработку, что может быть, например, вставка в базу данных.
Однако во время обработки может возникнуть ошибка, например, имя
пользователя уже занято. В таком случае мы передаем ошибку обратно в
форму с помощью addError()
и позволяем ей отрисоваться снова, уже с
сообщением об ошибке.
$form->addError('Извините, это имя пользователя уже используется.');
Кроме onSuccess
существует еще onSubmit
: callback-и вызываются
всегда после отправки формы, даже если она заполнена неправильно. А
также onError
: callback-и вызываются только если отправка невалидна.
Они вызываются даже тогда, когда в onSuccess
или onSubmit
мы
делаем форму невалидной с помощью addError()
.
После обработки формы мы перенаправляем на следующую страницу. Это предотвратит нежелательную повторную отправку формы кнопкой обновить, назад или перемещением в истории браузера.
Попробуйте добавить и другие элементы формы.
Доступ к элементам
Форма является компонентом презентера, в нашем случае названным
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()
, которым передадим валидатору требуемый
диапазон в виде пары [от, до]
:
$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', 'E-mail')
->setDefaultValue($lastUsedEmail);
Часто бывает полезно установить значения по умолчанию для всех элементов одновременно. Например, когда форма служит для редактирования записей. Мы читаем запись из базы данных и устанавливаем значения по умолчанию:
//$row = ['name' => 'John', 'age' => '33', /* ... */];
$form->setDefaults($row);
Вызывайте setDefaults()
после определения элементов.
Отрисовка формы
По умолчанию форма отрисовывается в виде таблицы. Отдельные элементы
соответствуют основному правилу доступности — все надписи записаны
как <label>
и связаны с соответствующим элементом формы. При
клике на надпись курсор автоматически появляется в поле формы.
Каждому элементу мы можем устанавливать любые HTML-атрибуты. Например, добавить placeholder:
$form->addInteger('age', 'Возраст:')
->setHtmlAttribute('placeholder', 'Пожалуйста, укажите возраст');
Способов отрисовки формы действительно много, поэтому этому посвящена отдельная глава об отрисовке.
Маппинг на классы
Вернемся к методу formSucceeded()
, который во втором параметре
$data
получает отправленные данные как объект 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
в методе-обработчике:
public function formSucceeded(Form $form, RegistrationFormData $data): void
{
// $data является экземпляром RegistrationFormData
$name = $data->name;
// ...
}
В качестве типа можно также указать array
, и тогда данные будут
переданы в виде массива.
Аналогичным образом можно использовать и функцию getValues()
,
которой мы передаем имя класса или объект для гидратации в качестве
параметра:
$data = $form->getValues(RegistrationFormData::class);
$name = $data->name;
Если формы образуют многоуровневую структуру, состоящую из контейнеров, создайте для каждого отдельный класс:
$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'];
Эти обработчики вызываются только в случае валидно заполненной
формы, так же как и в случае события onSuccess
. Разница в том, что в
качестве первого параметра вместо формы может передаваться кнопка
отправки, это зависит от типа, который вы укажете:
public 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(); // ВНИМАНИЕ! Отключает защиту!
Эта защита использует SameSite cookie с именем _nss
. Защита с помощью
SameSite cookie может быть не 100% надежной, поэтому рекомендуется включить еще
и защиту с помощью токена:
$form->addProtection();
Рекомендуем таким образом защищать формы в административной части
сайта, которые изменяют чувствительные данные в приложении. Фреймворк
защищается от CSRF-атаки путем генерации и проверки авторизационного
токена, который сохраняется в сессии. Поэтому перед отображением формы
необходимо иметь открытую сессию. В административной части сайта
сессия обычно уже запущена из-за входа пользователя. В противном
случае запустите сессию методом Nette\Http\Session::start()
.
Одна и та же форма в нескольких презентерах
Если вам нужно использовать одну и ту же форму в нескольких
презентерах, рекомендуем создать для нее фабрику, которую затем
передать в презентер. Подходящее место для такого класса — например,
каталог 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 в дистрибутиве, где вы найдете дополнительное вдохновение.