Форми, що використовуються окремо

Nette Forms значно полегшують створення та обробку веб-форм. Ви можете використовувати їх у своїх програмах абсолютно окремо від решти фреймворку, що ми покажемо в цьому розділі.

Однак, якщо ви використовуєте Nette Application та презентери, для вас призначений посібник для використання в презентерах.

Перша форма

Спробуємо написати просту реєстраційну форму. Її код буде таким (повний код):

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]);

Якщо користувач не заповнить поле, правила валідації не перевірятимуться, оскільки елемент є необов'язковим.

Тут виникає простір для невеликого рефакторингу. У повідомленні про помилку та в третьому параметрі числа вказані дубльовано, що не ідеально. Якби ми створювали багатомовні форми і повідомлення, що містить числа, було б перекладено кількома мовами, це ускладнило б можливу зміну значень. З цієї причини можна використовувати плейсхолдери %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', 'Будь ласка, заповніть вік');

Способів відображення форми є справді багато, тому цьому присвячено окремий розділ про відображення.

Мапінг на класи

Повернемося до обробки даних форми. Метод 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 у дистрибутиві, де ви знайдете більше натхнення.

версія: 4.0