Форми в презентерах
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 придумала класний механізм, який ми назвали Голлівудський стиль. Замість того, щоб постійно запитувати, чи відбулося щось (“чи була форма надіслана?”, “чи була вона надіслана правильно?”, або “чи не була вона підроблена?”), ви кажете фреймворку: “коли форма заповнена правильно, викличте цей метод”. Якщо ви програмуєте на JavaScript, ви знайомі з цим стилем програмування. Ви пишете функції, які викликаються при настанні певної події. І мова передає їм відповідні аргументи.
Ось як побудований наведений вище код презентера. Масив
$form->onSuccess
являє собою список зворотних викликів PHP, які Nette буде
викликати, коли форма буде відправлена і правильно заповнена. У рамках
життєвого циклу
презентера це так званий сигнал, тому вони викликаються після методу
action*
і перед методом render*
. І він передає кожному
зворотному виклику саму форму в першому параметрі та відправлені дані
у вигляді об'єкта ArrayHash у другому.
Ви можете опустити перший параметр, якщо вам не потрібен об'єкт форми.
Другий параметр може бути ще зручнішим, але про це пізніше.
Об'єкт $data
містить властивості name
і password
з
даними, введеними користувачем. Зазвичай ми надсилаємо дані
безпосередньо для подальшого опрацювання, яке може бути, наприклад,
вставкою в базу даних. Однак у процесі обробки може виникнути помилка,
наприклад, ім'я користувача вже зайнято. У цьому випадку ми передаємо
помилку назад у форму за допомогою addError()
і дозволяємо їй
перемальовуватися заново, з повідомленням про помилку:
$form->addError('Извините, имя пользователя уже используется.');
На додаток до onSuccess
, існує також onSubmit
: зворотні виклики
завжди викликаються після надсилання форми, навіть якщо вона
заповнена неправильно. І, нарешті, onError
: зворотні виклики
викликаються тільки в тому випадку, якщо відправка недійсна. Вони
викликаються, навіть якщо ми анулюємо форму в 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']);
Правила валідації
Слово valid було використано кілька разів, але форма ще не має правил валідації. Давайте виправимо це.
Ім'я буде обов'язковим, тому ми позначимо його методом 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()
.
Першим параметром є правило, другим – текст повідомлення про помилку,
далі може йти необов'язковий аргумент правила перевірки. Що це
означає?
Форма отримає ще один необов'язковий елемент введення 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;
}
Крім того, ви можете скористатися конструктором:
class RegistrationFormData
{
public function __construct(
public string $name,
public int $age,
public string $password,
) {
}
}
Властивості класу даних також можуть бути переліченими, і вони будуть автоматично відображені.
Як вказати Nette повертати дані у вигляді об'єктів цього класу? Легше,
ніж ви думаєте. Все, що вам потрібно зробити, це вказати клас як тип
параметра $data
в обробнику:
public function formSucceeded(Form $form, RegistrationFormData $data): void
{
// $name - екземпляр 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
. Різниця в тому, що першим
параметром може бути об'єкт кнопки submit, а не форма, залежно від типу,
який ви вкажете:
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) : []);
};
Vulnerability Protection
Nette Framework докладає великих зусиль для забезпечення безпеки, а оскільки форми є найпоширенішим видом користувацького введення, форми Nette настільки ж гарні, наскільки непроникні.
На додаток до захисту форм від атак відомих вразливостей, таких як Cross-Site Scripting (XSS) і Cross-Site Request Forgery (CSRF), він виконує безліч дрібних завдань із забезпечення безпеки, про які вам більше не потрібно думати.
Наприклад, він відфільтровує всі керуючі символи з даних, що вводяться, і перевіряє правильність кодування UTF-8, так що дані з форми завжди будуть чистими. Для полів вибору і радіосписків перевіряється, що обрані елементи дійсно були із запропонованих і не було підробки. Ми вже згадували, що для введення однорядкового тексту він видаляє символи кінця рядка, які зловмисник може туди відправити. Для багаторядкових вводів він нормалізує символи кінця рядка. І так далі.
Nette усуває за вас уразливості в системі безпеки, про існування яких більшість програмістів навіть не підозрюють.
Згадана CSRF-атака полягає в тому, що зловмисник заманює жертву відвідати сторінку, яка мовчки виконує запит у браузері жертви до сервера, на якому жертва на даний момент зареєстрована, і сервер вважає, що запит був зроблений жертвою за власним бажанням. Таким чином, Nette запобігає надсиланню форми через POST з іншого домену. Якщо з якоїсь причини ви хочете відключити захист і дозволити надсилання форми з іншого домену, використовуйте:
$form->allowCrossOrigin(); // УВАГА! Вимикає захист!
Цей захист використовує файл cookie SameSite з назвою _nss
. Захист за
допомогою файлів cookie SameSite може бути не на 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['login']->setCaption('Продовжити');
$form->onSuccess[] = [$this, 'signInFormSubmitted']; // і додати обробник
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 у дистрибутиві.