Форми в презентерах
Nette Forms значно полегшують створення та обробку веб-форм. У цьому розділі ви дізнаєтеся, як використовувати форми всередині презентерів.
Якщо вас цікавить, як використовувати їх повністю окремо без решти фреймворку, для вас призначений посібник для самостійного використання.
Перша форма
Спробуємо написати просту форму реєстрації. Її код буде таким:
use Nette\Application\UI\Form;
$form = new Form;
$form->addText('name', 'Ім\'я:');
$form->addPassword('password', 'Пароль:');
$form->addSubmit('send', 'Зареєструватися');
$form->onSuccess[] = [$this, 'formSucceeded'];
і в браузері вона відобразиться так:

Форма в presenter'і є об'єктом класу Nette\Application\UI\Form
, її попередник
Nette\Forms\Form
призначений для самостійного використання. Ми додали
до неї так звані елементи ім'я, пароль та кнопку відправки. І, нарешті,
рядок з $form->onSuccess
говорить, що після відправки та успішної
валідації має бути викликаний метод $this->formSucceeded()
.
З точки зору presenter'а форма є звичайним компонентом. Тому з нею поводяться як з компонентом і включають її до presenter'а за допомогою фабричного методу. Це виглядатиме так:
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, цей стиль програмування вам добре знайомий. Ви пишете функції, які викликаються, коли настає певна подія. І мова передає їм відповідні аргументи.
Саме так побудований і вищезгаданий код presenter'а. Масив
$form->onSuccess
представляє список PHP callback'ів, які Nette викличе в
момент, коли форма буде відправлена і правильно заповнена (тобто є
валідною). У рамках життєвого циклу
presenter'а це так званий сигнал, тому вони викликаються після методу
action*
і перед методом render*
. І кожному callback'у передає як
перший параметр саму форму, а як другий — надіслані дані у вигляді
об'єкта ArrayHash. Перший параметр
можна пропустити, якщо об'єкт форми вам не потрібен. А другий параметр
може бути хитрішим, але про це пізніше.
Об'єкт $data
містить ключі name
та password
з даними, які
заповнив користувач. Зазвичай дані відразу відправляються на подальшу
обробку, що може бути, наприклад, вставкою в базу даних. Однак під час
обробки може виникнути помилка, наприклад, ім'я користувача вже
зайняте. У такому випадку ми передаємо помилку назад у форму за
допомогою addError()
і дозволяємо їй відобразитися знову, вже з
повідомленням про помилку.
$form->addError('Вибачте, це ім\'я користувача вже використовується.');
Крім onSuccess
, існує ще onSubmit
: callback'и викликаються завжди
після відправлення форми, навіть якщо вона заповнена неправильно. А
також onError
: callback'и викликаються тільки якщо відправлення не є
валідним. Вони викликаються навіть тоді, якщо в onSuccess
або
onSubmit
ми зробимо форму невалідною за допомогою addError()
.
Після обробки форми ми перенаправляємо на наступну сторінку. Це запобігає небажаному повторному надсиланню форми кнопкою оновити, назад або рухом в історії браузера.
Спробуйте додати й інші елементи форми.
Доступ до елементів
Форма є компонентом presenter'а, у нашому випадку названим
registrationForm
(за назвою фабричного методу createComponentRegistrationForm
),
тому будь-де в presenter'і ви можете отримати доступ до форми за
допомогою:
$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
) ми
створюємо форму, вона ще не знає, чи була вона надіслана, і з якими
даними. Але є випадки, коли нам потрібно знати надіслані значення,
наприклад, від них залежить подальший вигляд форми, або вони потрібні
для залежних селектбоксів тощо.
Тому частину коду, що створює форму, можна викликати лише в момент,
коли вона так звано “заякорена”, тобто вже пов'язана з presenter'ом і знає
свої надіслані дані. Такий код передаємо до масиву $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 в дистрибутиві, де знайдете більше натхнення.