Форми, що використовуються автономно
Nette Forms значно спрощує створення та обробку веб-форм. Ви можете використовувати їх у своїх додатках абсолютно самостійно, без решти фреймворку, що ми й продемонструємо в цьому розділі.
Однак якщо ви використовуєте додаток Nette і презентери, для вас є посібник: Форми в презентерах.
Перша форма
Ми постараємося написати просту реєстраційну форму. Його код матиме такий вигляд (повний код):
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']);
Правила валідації
Слово 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 = $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 докладає великих зусиль для забезпечення безпеки, а оскільки форми є найпоширенішим видом користувацького введення, форми Nette настільки ж гарні, наскільки непроникні.
На додаток до захисту форм від атак відомих вразливостей, таких як Cross-Site Scripting (XSS) і Cross-Site Request Forgery (CSRF), він виконує безліч дрібних завдань із забезпечення безпеки, про які вам більше не потрібно думати.
Наприклад, він відфільтровує всі керуючі символи з даних, що вводяться, і перевіряє правильність кодування UTF-8, так що дані з форми завжди будуть чистими. Для полів вибору і радіосписків перевіряється, що обрані елементи дійсно були із запропонованих і не було підробки. Ми вже згадували, що для введення однорядкового тексту він видаляє символи кінця рядка, які зловмисник може туди відправити. Для багаторядкових вводів він нормалізує символи кінця рядка. І так далі.
Nette усуває за вас уразливості в системі безпеки, про існування яких більшість програмістів навіть не підозрюють.
Згадана CSRF-атака полягає в тому, що зловмисник заманює жертву відвідати сторінку, яка мовчки виконує запит у браузері жертви до сервера, на якому жертва на даний момент зареєстрована, і сервер вважає, що запит був зроблений жертвою за власним бажанням. Таким чином, Nette запобігає відправленню форми через POST з іншого домену. Якщо з якоїсь причини ви хочете відключити захист і дозволити надсилання форми з іншого домену, використовуйте:
$form->allowCrossOrigin(); // УВАГА! Вимикає захист!
Цей захист використовує файл куки SameSite з ім'ям _nss
. Тому
створіть форму перед першим виведенням, щоб можна було
надіслати куки.
Захист кукі-файлів SameSite може бути не на 100% надійним, тому гарною ідеєю буде ввімкнути захист за допомогою токена:
$form->addProtection();
Настійно рекомендується застосовувати цей захист до форм в
адміністративній частині вашого застосунку, які змінюють
конфіденційні дані. Фреймворк захищає від атаки CSRF, генеруючи та
перевіряючи токен автентифікації, що зберігається в сесії (аргументом
є повідомлення про помилку, яке показується, якщо термін дії токена
закінчився). Тому перед відображенням форми необхідно, щоб сесія була
запущена. В адміністративній частині сайту сесія, як правило, вже
почалася, оскільки користувач увійшов у систему. В іншому випадку,
запустіть сесію за допомогою методу Nette\Http\Session::start()
.
Отже, ми коротко познайомилися з формами в Nette. Спробуйте пошукати натхнення в каталозі examples у дистрибутиві.