Формы отдельно от фреймворка
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 в дистрибутиве.