Nette Documentation Preview

syntax
Формуляри за водещите
*********************

.[perex]
Nette Forms значително опростява създаването и обработката на уеб формуляри. В тази глава ще научите как да използвате формуляри в презентаторите.

Ако искате да ги използвате напълно самостоятелно, без останалата част от рамката, има ръководство за [самостоятелни форми |standalone].


Първа форма .[#toc-first-form]
==============================

Ще се опитаме да напишем прост формуляр за регистрация. Кодът ще изглежда по следния начин:

```php
use Nette\Application\UI\Form;

$form = new Form;
$form->addText('name', 'Имя:');
$form->addPassword('password', 'Пароль:');
$form->addSubmit('send', 'Зарегистрироваться');
$form->onSuccess[] = [$this, 'formSucceeded'];
```

и в браузъра резултатът трябва да изглежда така:

[* form-en.webp *]

Формулярът в презентатора е обект от клас `Nette\Application\UI\Form`, неговият предшественик `Nette\Forms\Form` е за офлайн употреба. Добавихме към него полета за име, парола и бутон за изпращане. И накрая, в реда с `$form->onSuccess` се казва, че след подаване и успешно валидиране трябва да се извика методът `$this->formSucceeded()`.

От гледна точка на водещия формулярът е общ компонент. Затова той се третира като компонент и се включва в презентатора чрез [метода factory |application:components#Factory-Methods]. Това ще изглежда по следния начин:

```php .{file:app/UI/Home/HomePresenter.php}
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', 'Password:');
		$form->addSubmit('send', 'Register');
		$form->onSuccess[] = [$this, 'formSucceeded'];
		return $form;
	}

	public function formSucceeded(Form $form, $data): void
	{
		// тук ще обработваме данните, изпратени от формата
		// $data->name съдържа името
		// $data->password съдържа парола
		$this->flashMessage('Регистрирахте се успешно.');
		$this->redirect('Home:');
	}
}
```

А визуализирането в шаблона се извършва с помощта на тага `{control}`:

```latte .{file:app/UI/Home/default.latte}
<h1>Регистрация</h1>

{control registrationForm}
```

И това е всичко :-) Имаме функционална и отлично [защитена |#Защита от уязвимостей] форма.

Сега сигурно си мислите, че това е станало твърде бързо, и се чудите как е възможно да се извика методът `formSucceeded()` и какви параметри получава. Разбира се, че сте прави, заслужава обяснение.

Нете измисли готин механизъм, който нарекохме [холивудски стил |application:components#Hollywood-Style]. Вместо постоянно да питате дали нещо се е случило ("дали формулярът е изпратен?", "дали е изпратен правилно?" или "дали е манипулиран?"), вие казвате на рамката "когато формулярът е изпратен правилно, извикайте този метод". Ако програмирате на JavaScript, този стил на програмиране ви е познат. Пишете функции, които се извикват при настъпване на определено [събитие |nette:glossary#Events]. И езикът им предава съответните аргументи.

Ето как е конструиран кодът на презентатора по-горе. Масивът `$form->onSuccess` е списък с обратни връзки на PHP, които Nette ще извика, когато формулярът е изпратен и правилно попълнен.
Като част от [жизнения цикъл на презентатора |application:presenters#Life-Cycle-of-Presenter], това се нарича сигнал, така че те се извикват след метода `action*` и преди метода `render*`.
И предава на всяко обратно извикване самата форма в първия параметър и изпратените данни като обект [ArrayHash |utils:arrays#ArrayHash] във втория. Можете да пропуснете първия параметър, ако нямате нужда от обекта на формата. Вторият параметър може да бъде още по-удобен, но за това ще стане дума [по-късно |#Mapping-to-Classes].

Обектът `$data` съдържа свойства `name` и `password` с данни, въведени от потребителя. Обикновено изпращаме данните директно за по-нататъшна обработка, която може да бъде например вмъкване в база данни. При обработката обаче може да възникне грешка, например потребителското име вече е заето. В този случай изпращаме грешката обратно към формата с `addError()` и я оставяме да се прерисува със съобщение за грешка:

```php
$form->addError('Извините, имя пользователя уже используется.');
```

В допълнение към `onSuccess`, има и `onSubmit`: обратните извиквания се извикват винаги след изпращането на формуляра, дори ако той не е попълнен правилно. И накрая, `onError`: обратните повиквания се извикват само ако подаването е невалидно. Те се извикват дори ако обезсилим формуляр в `onSuccess` или `onSubmit`, като използваме `addError()`.

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

Опитайте да добавите повече [контроли на формуляра |controls].


Достъп до контролите .[#toc-access-to-controls]
===============================================

Формулярът е компонент на презентатора, в нашия случай с име `registrationForm` (по името на фабричния метод `createComponentRegistrationForm`), така че навсякъде в презентатора можете да получите достъп до формуляра, като използвате

```php
$form = $this->getComponent('registrationForm');
//алтернативен синтаксис: $form = $this['registrationForm'];
```

Също така отделните контроли на формата са компоненти, така че можете да имате достъп до тях по същия начин:

```php
$input = $form->getComponent('name'); // или $input = $form['name'];
$button = $form->getComponent('send'); // или $button = $form['send'];
```

Контролите се изтриват с помощта на функцията за изтриване:

```php
unset($form['name']);
```


Правила за валидиране .[#toc-validation-rules]
==============================================

Думата *valid* е използвана няколко пъти, но формулярът все още няма правила за валидиране. Нека поправим това.

Името ще бъде задължително, затова ще го маркираме с метода `setRequired()`, чийто аргумент е текстът на съобщението за грешка, което ще се покаже, ако потребителят не успее да го попълни. Ако не е посочен аргумент, се използва съобщението за грешка по подразбиране.

```php
$form->addText('name', 'Имя:')
	->setRequired('Пожалуйста, введите имя.');
```

Ако се опитате да изпратите формуляра, без да сте го попълнили, ще се появи съобщение за грешка и браузърът или сървърът ще отхвърлят формуляра, докато не го попълните.

В същото време не можете да заблудите системата, като например въведете само интервали в полето за въвеждане. В никакъв случай. Nette автоматично подрязва левия и десния бял интервал. Опитайте, това е нещо, което винаги трябва да правите при въвеждането на всеки отделен ред, но често се забравя. Nette прави това автоматично. (Можете да се опитате да измамите формата и да изпратите многоредов низ като име. Дори и тук Nette няма да се заблуди и прекъсванията на редовете ще бъдат заменени с интервали).

Формулярът винаги се проверява от страна на сървъра, но се генерира и проверка на JavaScript, която се извършва бързо и потребителят веднага разбира за грешката, без да се налага да изпраща формуляра към сървъра. С това се занимава скриптът `netteForms.js`.
Вмъкнете го в шаблона за оформление:

```latte
<script src="https://unpkg.com/nette-forms@3/src/assets/netteForms.js"></script>
```

Ако разгледате изходния код на страницата с формуляра, можете да забележите, че Nette вмъква задължителни полета в елементи с клас CSS `required`. Опитайте се да добавите следния стил към шаблона и тагът "Име" ще бъде червен. Елегантно маркираме задължителните полета за потребителите:

```latte
<style>
.required label { color: maroon }
</style>
```

Допълнителни правила за валидиране ще бъдат добавени чрез метода `addRule()`. Първият параметър е правилото, вторият е текстът на съобщението за грешка, последван от незадължителен аргумент за правило за валидиране. Какво означава това?

Формулярът ще получи още един незадължителен елемент за въвеждане *възраст*, като условието е той да бъде число (`addInteger()`) и да е в определени граници (`$form::Range`). И тук ще използваме третия аргумент `addRule()`, самия диапазон:

```php
$form->addInteger('age', 'Возраст:')
	->addRule($form::Range, 'Вы должны быть старше 18 лет и иметь возраст до 120 лет.', [18, 120]);
```

.[tip]
Ако потребителят не попълни полето, правилата за валидиране няма да бъдат проверени, тъй като полето е незадължително.

Очевидно е, че тук има място за малко преработване. В съобщението за грешка и в третия параметър числата са изброени двойно, което не е идеално. Ако създаваме [многоезичен формуляр |rendering#translating] и съобщението, съдържащо числата, трябва да бъде преведено на няколко езика, това би затруднило промяната на стойностите. По тази причина можем да използваме заместващи символи `%d`:

```php
	->addRule($form::Range, 'Вы должны быть старше %d лет и иметь возраст до %d лет.', [18, 120]);
```

Нека се върнем към полето *парола*, да го направим *задължително* и да проверим минималната дължина на паролата (`$form::MinLength`), като отново използваме заместители в съобщението:

```php
$form->addPassword('password', 'Пароль:')
	->setRequired('Выберите пароль')
	->addRule($form::MinLength, 'Ваш пароль должен быть длиной не менее %d', 8);
```

Ще добавим поле към формуляра `passwordVerify`, в което потребителят въвежда отново паролата, за да я валидира. С помощта на правила за валидиране проверяваме дали двете пароли (`$form::Equal`) са еднакви. И даваме връзка към първата парола като аргумент, като използваме [квадратни скоби |#Access-to-Controls]:

```php
$form->addPassword('passwordVerify', 'Повторите пароль:')
	->setRequired('Введите пароль ещё раз, чтобы проверить опечатку')
	->addRule($form::Equal, 'Несоответствие пароля', $form['password'])
	->setOmitted();
```

С помощта на `setOmitted()`, маркираме елемент, чиято стойност всъщност не ни интересува и който съществува само за валидиране. Стойността му не се предава на `$data`.

Имаме напълно функционален формуляр с валидиране в PHP и JavaScript. Възможностите за валидиране в Nette са много по-широки, можете да създавате условия, да показвате и скривате части от страницата в зависимост от тях и т.н. Можете да научите повече за всичко това в главата [Проверка на формата |validation].


Стойности по подразбиране .[#toc-default-values]
================================================

Често задаваме стойности по подразбиране за контролите на формуляра:

```php
$form->addEmail('email', 'Имейл')
	->setDefaultValue($lastUsedEmail);
```

Често е полезно да зададете стойности по подразбиране за всички контроли едновременно. Например, когато формулярът се използва за редактиране на записи. Прочитаме запис от базата данни и го задаваме като стойност по подразбиране:

```php
//$row = ['name' => 'John', 'age' => '33', /* ... */];
$form->setDefaults($row);
```

Извикайте `setDefaults()` след дефиниране на контролите.


Форма за показване .[#toc-rendering-the-form]
=============================================

По подразбиране формулярът се показва като таблица. Отделните контроли следват основните насоки за достъпност на уеб страниците. Всички етикети се генерират като елементи `<label>` и са свързани с техните елементи, като щракването върху маркер ще премести курсора върху съответния елемент.

Можем да зададем всякакви атрибути на HTML за всеки елемент. Например, добавете заместител:

```php
$form->addInteger('age', 'Возраст:')
	->setHtmlAttribute('placeholder', 'Пожалуйста, заполните возраст');
```

Всъщност има много начини за визуализиране на формуляри, повече подробности в глава [Изобразяване" |rendering].


Съпоставяне с класове .[#toc-mapping-to-classes]
================================================

Нека се върнем към обработката на данни от формуляри. Методът `getValues()` връща подадените данни като обект `ArrayHash`. Тъй като това е общ клас, нещо като `stdClass`, при работа с него ще ни липсват някои удобни функции, като например допълване на кода за свойства в редактори или статичен анализ на кода. Това може да бъде решено чрез създаване на отделен клас за всяка форма, чиито свойства представляват отделните контроли. Например:

```php
class RegistrationFormData
{
	public string $name;
	public int $age;
	public string $password;
}
```

От PHP 8.0 можете да използвате този елегантен запис, който използва конструктор:

```php
class RegistrationFormData
{
	public function __construct(
		public string $name,
		public int $age,
		public string $password,
	) {
	}
}
```

Как казвате на Nette да връща данни като обекти от този клас? По-лесно е, отколкото си мислите. Всичко, което трябва да направите, е да посочите класа като параметър от тип `$data` в обработващото устройство:

```php
public function formSucceeded(Form $form, RegistrationFormData $data): void
{
	// $name - екземпляр RegistrationFormData
	$name = $data->name;
	// ...
}
```

Можете също така да посочите `array` като тип и тогава данните ще бъдат предадени като масив.

По същия начин можете да използвате метода `getValues()`, на който като параметър подаваме името на класа или обекта, който трябва да се хидратира:

```php
$data = $form->getValues(RegistrationFormData::class);
$name = $data->name;
```

Ако формулярите се състоят от структура на няколко нива, съставена от контейнери, създайте отделен клас за всеки от тях:

```php
$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` и предайте класа, който ще бъде съпоставен директно с контейнера:

```php
$person->setMappedType(PersonFormData::class);
```

Можете да генерирате предложение за класа данни на формуляра, като използвате метода `Nette\Forms\Blueprint::dataClass($form)`, който ще го отпечата на страницата на браузъра. След това можете просто да щракнете, за да изберете и копирате кода в проекта си. .{data-version:3.1.15}


Множество бутони за изпращане .[#toc-multiple-submit-buttons]
=============================================================

Ако формулярът съдържа повече от един бутон, обикновено трябва да се разграничи кой бутон е бил щракнат. Можем да създадем собствена функция за всеки бутон. Задайте го като обработващо устройство за [събитието |nette:glossary#Events] `onClick`:

```php
$form->addSubmit('save', 'Сохранить')
	->onClick[] = [$this, 'saveButtonPressed'];

$form->addSubmit('delete', 'Удалить')
	->onClick[] = [$this, 'deleteButtonPressed'];
```

Тези обслужващи програми се извикват също само ако формулярът е валиден, както в случая със събитието `onSuccess`. Разликата е, че първият параметър може да бъде обектът на бутона за изпращане, а не формата, в зависимост от типа, който сте задали:

```php
public function saveButtonPressed(Nette\Forms\Controls\Button $button, $data)
{
	$form = $button->getForm();
	// ...
}
```

Когато формулярът се изпрати с бутона <kbd>Enter</kbd>, той се третира по същия начин, както ако беше изпратен с първия бутон.


Събитието onAnchor .[#toc-event-onanchor]
=========================================

Когато създавате формуляр с фабричен метод (например `createComponentRegistrationForm`), той все още не знае дали е бил изпратен и с какви данни е бил изпратен. Но има случаи, в които е необходимо да знаем изпратените стойности - може би от тях зависи външният вид на формуляра или се използват за зависими списъци и т.н.

Затова можем да направим така, че кодът, създаващ формата, да се извиква, когато формата е прикачена, т.е. вече е свързана с презентатора и знае изпратените данни. Ще поставим този код в масива `$onAnchor`:

```php
$country = $form->addSelect('country', 'Country:', $this->model->getCountries());
$city = $form->addSelect('city', 'City:');

$form->onAnchor[] = function () use ($country, $city) {
	// тази функция ще бъде извикана, когато формата разпознае данните, с които е изпратена
	// така че можете да използвате метода getValue().
	$val = $country->getValue();
	$city->setItems($val ? $this->model->getCities($val) : []);
};
```


Защита от уязвимост .[#toc-vulnerability-protection]
====================================================

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

В допълнение към защитата на формулярите срещу атаки от известни уязвимости, като например [Cross-Site Scripting (XSS) |nette:glossary#Cross-Site-Scripting-XSS] и [Cross-Site Request Forgery (CSRF) |nette:glossary#Cross-Site-Request-Forgery-CSRF], тя изпълнява много малки задачи по сигурността, за които вече не е необходимо да мислите.

Например той филтрира всички контролни символи от входните данни и валидира кодирането UTF-8, така че данните от формуляра ви винаги ще бъдат чисти. За полетата за избор и радиосписъците се проверява дали избраните елементи са наистина от предлаганите елементи и дали няма фалшиви. Вече споменахме, че при въвеждане на текст от един ред тя премахва символите от края на реда, които атакуващият може да изпрати там. При многоредово въвеждане се нормализират символите в края на реда. И така нататък.

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

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

```php
$form->allowCrossOrigin(); // ПРЕДУПРЕЖДЕНИЕ! Деактивира защитата!
```

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

```php
$form->addProtection();
```

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


Използване на един формуляр в няколко презентатора .[#toc-using-one-form-in-multiple-presenters]
================================================================================================

Ако трябва да използвате един формуляр в няколко презентатора, препоръчваме ви да създадете фабрика за него, която след това да предадете на презентатора. Подходящо място за такъв клас е например директорията `app/Forms`.

Един фабричен клас може да изглежда по следния начин:

```php
use Nette\Application\UI\Form;

class SignInFormFactory
{
	public function create(): Form
	{
		$form = new Form;
		$form->addText('name', 'Имя:');
		$form->addSubmit('send', 'Войти');
		return $form;
	}
}
```

Изискваме от класа да създаде формуляр в метода на фабриката за компоненти в презентатора:

```php
public function __construct(
	private SignInFormFactory $formFactory,
) {
}

protected function createComponentSignInForm(): Form
{
	$form = $this->formFactory->create();
	//можем да променим формата, например етикета на бутона
	$form['login']->setCaption('Продолжить');
	$form->onSuccess[] = [$this, 'signInFormSubmitted']; // и добавяне на обработваща програма
	return $form;
}
```

Обработчикът на формуляри може да бъде получен и чрез метода factory:

```php
use Nette\Application\UI\Form;

class SignInFormFactory
{
	public function create(): Form
	{
		$form = new Form;
		$form->addText('name', 'Име:');
		$form->addSubmit('send', 'login');
		$form->onSuccess[] = function (Form $form, $data): void {
			// тук обработваме подадената форма
		};
		return $form;
	}
}
```

И така, запознахме се накратко с формите в Nette. Опитайте се да потърсите вдъхновение в каталога с [примери |https://github.com/nette/forms/tree/master/examples] в дистрибуцията.

Формуляри за водещите

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().

От гледна точка на водещия формулярът е общ компонент. Затова той се третира като компонент и се включва в презентатора чрез метода factory. Това ще изглежда по следния начин:

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', 'Password:');
		$form->addSubmit('send', 'Register');
		$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() и какви параметри получава. Разбира се, че сте прави, заслужава обяснение.

Нете измисли готин механизъм, който нарекохме холивудски стил. Вместо постоянно да питате дали нещо се е случило („дали формулярът е изпратен?“, „дали е изпратен правилно?“ или „дали е манипулиран?“), вие казвате на рамката „когато формулярът е изпратен правилно, извикайте този метод“. Ако програмирате на 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($form['name']);

Правила за валидиране

Думата valid е използвана няколко пъти, но формулярът все още няма правила за валидиране. Нека поправим това.

Името ще бъде задължително, затова ще го маркираме с метода setRequired(), чийто аргумент е текстът на съобщението за грешка, което ще се покаже, ако потребителят не успее да го попълни. Ако не е посочен аргумент, се използва съобщението за грешка по подразбиране.

$form->addText('name', 'Имя:')
	->setRequired('Пожалуйста, введите имя.');

Ако се опитате да изпратите формуляра, без да сте го попълнили, ще се появи съобщение за грешка и браузърът или сървърът ще отхвърлят формуляра, докато не го попълните.

В същото време не можете да заблудите системата, като например въведете само интервали в полето за въвеждане. В никакъв случай. Nette автоматично подрязва левия и десния бял интервал. Опитайте, това е нещо, което винаги трябва да правите при въвеждането на всеки отделен ред, но често се забравя. Nette прави това автоматично. (Можете да се опитате да измамите формата и да изпратите многоредов низ като име. Дори и тук Nette няма да се заблуди и прекъсванията на редовете ще бъдат заменени с интервали).

Формулярът винаги се проверява от страна на сървъра, но се генерира и проверка на JavaScript, която се извършва бързо и потребителят веднага разбира за грешката, без да се налага да изпраща формуляра към сървъра. С това се занимава скриптът netteForms.js. Вмъкнете го в шаблона за оформление:

<script src="https://unpkg.com/nette-forms@3/src/assets/netteForms.js"></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:

	->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;
}

От PHP 8.0 можете да използвате този елегантен запис, който използва конструктор:

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. Разликата е, че първият параметър може да бъде обектът на бутона за изпращане, а не формата, в зависимост от типа, който сте задали:

public function saveButtonPressed(Nette\Forms\Controls\Button $button, $data)
{
	$form = $button->getForm();
	// ...
}

Когато формулярът се изпрати с бутона Enter, той се третира по същия начин, както ако беше изпратен с първия бутон.

Събитието onAnchor

Когато създавате формуляр с фабричен метод (например createComponentRegistrationForm), той все още не знае дали е бил изпратен и с какви данни е бил изпратен. Но има случаи, в които е необходимо да знаем изпратените стойности – може би от тях зависи външният вид на формуляра или се използват за зависими списъци и т.н.

Затова можем да направим така, че кодът, създаващ формата, да се извиква, когато формата е прикачена, т.е. вече е свързана с презентатора и знае изпратените данни. Ще поставим този код в масива $onAnchor:

$country = $form->addSelect('country', 'Country:', $this->model->getCountries());
$city = $form->addSelect('city', 'City:');

$form->onAnchor[] = function () use ($country, $city) {
	// тази функция ще бъде извикана, когато формата разпознае данните, с които е изпратена
	// така че можете да използвате метода getValue().
	$val = $country->getValue();
	$city->setItems($val ? $this->model->getCities($val) : []);
};

Защита от уязвимост

Рамката на Nette полага големи усилия за гарантиране на сигурността и тъй като формулярите са най-често срещаната форма на въвеждане на данни от потребителя, формулярите на 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().

Използване на един формуляр в няколко презентатора

Ако трябва да използвате един формуляр в няколко презентатора, препоръчваме ви да създадете фабрика за него, която след това да предадете на презентатора. Подходящо място за такъв клас е например директорията 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;
}

Обработчикът на формуляри може да бъде получен и чрез метода factory:

use Nette\Application\UI\Form;

class SignInFormFactory
{
	public function create(): Form
	{
		$form = new Form;
		$form->addText('name', 'Име:');
		$form->addSubmit('send', 'login');
		$form->onSuccess[] = function (Form $form, $data): void {
			// тук обработваме подадената форма
		};
		return $form;
	}
}

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