Форми, използвани самостоятелно

Nette Forms значително улесняват създаването и обработката на уеб форми. Можете да ги използвате във вашите приложения напълно самостоятелно, без останалата част от framework-а, което ще покажем в тази глава.

Но ако използвате 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 автоматично премахва левите и десните интервали. Опитайте. Това е нещо, което винаги трябва да правите с всеки едноредов input, но често се забравя. 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 кутиите и radio списъците проверява дали избраните елементи са били действително от предлаганите и дали не е имало подправяне. Вече споменахме, че при едноредовите текстови входове премахва знаците за край на ред, които нападателят е могъл да изпрати. При многоредовите входове пък нормализира знаците за край на ред. И така нататък.

Nette решава вместо вас рисковете за сигурността, за които много програмисти дори не подозират, че съществуват.

Споменатата CSRF атака се състои в това, че нападателят примамва жертвата на страница, която незабележимо в браузъра на жертвата изпълнява заявка към сървъра, на който жертвата е влязла, и сървърът смята, че заявката е била изпълнена от жертвата по нейна воля. Затова Nette предотвратява изпращането на POST форма от друг домейн. Ако по някаква причина искате да изключите защитата и да позволите изпращането на формата от друг домейн, използвайте:

$form->allowCrossOrigin(); // ВНИМАНИЕ! Изключва защитата!

Тази защита използва SameSite бисквитка с име _nss. Затова създавайте обекта на формата преди изпращането на първия изход, за да може бисквитката да бъде изпратена.

Защитата с помощта на SameSite бисквитка може да не е 100% надеждна, затова е препоръчително да включите и защита с помощта на токен:

$form->addProtection();

Препоръчваме да защитавате по този начин формите в административната част на сайта, които променят чувствителни данни в приложението. Framework-ът се защитава срещу CSRF атака чрез генериране и проверка на оторизационен токен, който се съхранява в сесията. Затова е необходимо преди показването на формата да има отворена сесия. В административната част на сайта обикновено сесията вече е стартирана поради влизането на потребителя. В противен случай стартирайте сесията с метода Nette\Http\Session::start().

Така, преминахме през бързо въведение във формите в Nette. Опитайте да разгледате още директорията examples в дистрибуцията, където ще намерите повече вдъхновение.

версия: 4.0