[Go to site: main page, start]

Walidacja formularzy

Elementy obowiązkowe

Elementy oznaczamy jako obowiązkowe metodą setRequired(). Jej argumentem jest tekst komunikatu o błędzie, który wyświetli się, jeśli użytkownik elementu nie wypełni. Jeśli argumentu nie podamy, użyty zostanie domyślny komunikat o błędzie.

$form->addText('name', 'Imię:')
	->setRequired('Wypełnij swoje imię.');

Reguły

Reguły walidacyjne dodajemy elementom metodą addRule(). Pierwszym parametrem jest reguła, drugim komunikat o błędzie, a trzecim argument reguły walidacyjnej.

$form->addPassword('password', 'Hasło:')
	->addRule($form::MinLength, 'Hasło musi mieć co najmniej %d znaków', 8);

Reguły walidacyjne sprawdzane są tylko wtedy, gdy użytkownik element wypełnił.

Nette zawiera szereg predefiniowanych reguł, których nazwy są stałymi klasy Nette\Forms\Form. Te reguły możemy zastosować do wszystkich elementów:

stała opis typ argumentu
Required element obowiązkowy, alias dla setRequired()
Filled element obowiązkowy, alias dla setRequired()
Blank element nie może być wypełniony
Equal wartość musi być równa parametrowi mixed
NotEqual wartość nie może być równa parametrowi mixed
IsIn wartość musi być jedną z pozycji tablicy array
IsNotIn wartość nie może być żadną z pozycji tablicy array
Valid czy element jest poprawnie wypełniony? (tylko w addConditionOn())

Inputy tekstowe

Dla elementów addText(), addPassword(), addTextArea(), addEmail(), addInteger(), addFloat() można zastosować także niektóre z poniższych reguł:

MinLength minimalna długość tekstu int
MaxLength maksymalna długość tekstu int
Length długość w przedziale albo dokładna długość para [int, int] albo int
Email poprawny adres e-mail
URL absolutny URL
Pattern pasuje do wyrażenia regularnego string
PatternInsensitive jak Pattern, ale bez rozróżniania wielkości liter string
Integer wartość całkowita
Numeric nieujemna liczba całkowita (same cyfry)
Float liczba
Min minimalna wartość elementu liczbowego int|float
Max maksymalna wartość elementu liczbowego int|float
Range wartość w przedziale para [int|float, int|float]

Reguły walidacyjne Integer i Float automatycznie konwertują wartość odpowiednio na liczbę całkowitą albo zmiennoprzecinkową. Ponadto reguła URL akceptuje także adres bez schematu (np. nette.org) i schemat uzupełnia (https://nette.org). Wyrażenie w Pattern i PatternInsensitive musi być poprawne dla całej wartości, czyli tak, jakby było otoczone znakami ^ i $.

Liczba pozycji

Dla elementów addMultiUpload(), addCheckboxList(), addMultiSelect() możesz użyć także poniższych reguł ograniczających liczbę wybranych pozycji albo wysłanych plików:

MinLength minimalna liczba int
MaxLength maksymalna liczba int
Length liczba w przedziale albo dokładna liczba para [int, int] albo int

Wysyłanie plików

Dla elementów addUpload(), addMultiUpload() można użyć także poniższych reguł:

MaxFileSize maksymalny rozmiar pliku w bajtach int
MimeType typ MIME, dozwolone wildcardy ('video/*') string|string[]
Image obrazek JPEG, PNG, GIF, WebP, AVIF
Pattern nazwa pliku pasuje do wyrażenia regularnego string
PatternInsensitive jak Pattern, ale bez rozróżniania wielkości liter string

MimeType i Image wymagają rozszerzenia PHP fileinfo. To, czy plik albo obrazek jest wymaganego typu, wykrywane jest na podstawie jego sygnatury, a integralność całego pliku nie jest sprawdzana. To, czy obrazek nie jest uszkodzony, możesz ustalić na przykład, próbując go wczytać.

Komunikaty o błędach

Wszystkie predefiniowane reguły oprócz Pattern i PatternInsensitive mają domyślny komunikat o błędzie, więc można go pominąć. Podając jednak i formułując wszystkie własne komunikaty dopasowane do swoich potrzeb, uczynisz formularz przyjaźniejszym dla użytkownika.

Domyślne komunikaty możesz zmienić w konfiguracji, modyfikując teksty w tablicy Nette\Forms\Validator::$messages, albo za pomocą translatora.

W tekście komunikatów o błędach można używać poniższych zastępników:

%d zastępowany kolejno argumentami reguły
%n$d zastępowany n-tym argumentem reguły
%label zastępowany etykietą elementu (bez dwukropka)
%name zastępowany nazwą elementu (np. name)
%value zastępowany wartością wpisaną przez użytkownika
$form->addText('name', 'Imię:')
	->setRequired('Wypełnij %label');

$form->addInteger('id', 'ID:')
	->addRule($form::Range, 'co najmniej %d i najwyżej %d', [5, 10]);

$form->addInteger('id', 'ID:')
	->addRule($form::Range, 'najwyżej %2$d i co najmniej %1$d', [5, 10]);

Warunki

Oprócz reguł można dodawać także warunki. Zapisuje się je podobnie jak reguły, ale zamiast addRule() używamy metody addCondition() i naturalnie nie podajemy komunikatu o błędzie (warunek tylko pyta):

$form->addPassword('password', 'Hasło:')
	// jeśli długość hasła nie jest większa niż 8
	->addCondition($form::MaxLength, 8)
		// to musi zawierać cyfrę
		->addRule($form::Pattern, 'Musi zawierać cyfrę', '.*[0-9].*');

Warunek można powiązać z innym elementem niż bieżący za pomocą addConditionOn(). Pierwszym parametrem jest odwołanie do elementu. W tym przykładzie e-mail będzie obowiązkowy tylko wtedy, gdy checkbox będzie zaznaczony (czyli jego wartość będzie true):

$form->addCheckbox('newsletters', 'Wysyłaj mi newslettery');

$form->addEmail('email', 'Email:')
	// jeśli checkbox jest zaznaczony
	->addConditionOn($form['newsletters'], $form::Equal, true)
		// to wymagaj e-maila
		->setRequired('Podaj swój adres e-mail');

Z warunków można tworzyć złożone struktury za pomocą elseCondition() i endCondition():

$form->addText(/* ... */)
	->addCondition(/* ... */) // jeśli pierwszy warunek jest spełniony
		->addConditionOn(/* ... */) // i spełniony jest też drugi warunek na innym elemencie
			->addRule(/* ... */) // wymagaj tej reguły
		->elseCondition() // jeśli drugi warunek nie jest spełniony
			->addRule(/* ... */) // wymagaj tych reguł
			->addRule(/* ... */)
		->endCondition() // wracamy do pierwszego warunku
		->addRule(/* ... */);

Pierwszym argumentem addCondition() może być też wartość logiczna. Przydaje się to, gdy decyzja jest znana już w trakcie budowania formularza, na przykład żeby zastosować regułę tylko w określonych okolicznościach:

$form->addText('nickname')
	->addCondition($isRequired) // wartość znana przy budowaniu formularza
		->setRequired();

W Nette bardzo łatwo zareagujesz na spełnienie albo niespełnienie warunku po stronie JavaScriptu metodą toggle(), patrz Dynamiczny JavaScript.

Odwołanie do innego elementu

Jako argument reguły albo warunku możesz przekazać także inny element formularza. Reguła użyje wtedy wartości wpisanej później przez użytkownika w przeglądarce. Można to wykorzystać na przykład do dynamicznego sprawdzenia, czy element password zawiera ten sam ciąg co element password_confirm:

$form->addPassword('password', 'Hasło');
$form->addPassword('password_confirm', 'Potwierdź hasło')
    ->addRule($form::Equal, 'Hasła nie są zgodne', $form['password']);

Własne reguły i warunki

Czasem napotykamy sytuacje, w których wbudowane reguły walidacyjne Nette nie wystarczają i potrzebujemy zwalidować dane użytkownika po swojemu. W Nette jest to bardzo proste!

Metodom addRule() albo addCondition() możesz przekazać jako pierwszy parametr dowolny callback. Callback przyjmuje jako pierwszy parametr sam element i zwraca wartość logiczną mówiącą, czy walidacja się powiodła. Przy dodawaniu reguły metodą addRule() można podać kolejne argumenty, które są potem przekazywane jako drugi parametr.

Własny zestaw walidatorów można więc utworzyć jako klasę ze statycznymi metodami:

class MyValidators
{
	// sprawdza, czy wartość jest podzielna przez argument
	public static function validateDivisibility(BaseControl $input, $arg): bool
	{
		return $input->getValue() % $arg === 0;
	}

	public static function validateEmailDomain(BaseControl $input, $domain)
	{
		// kolejne walidatory
	}
}

Użycie jest potem bardzo proste:

$form->addInteger('num')
	->addRule(
		[MyValidators::class, 'validateDivisibility'],
		'Wartość musi być wielokrotnością %d',
		8,
	);

Własne reguły walidacyjne można dodać także do JavaScriptu. Warunkiem jest, żeby reguła była metodą statyczną. Jej nazwa dla walidatora JavaScriptowego powstaje przez połączenie nazwy klasy bez odwrotnych ukośników \, podkreślenia _ i nazwy metody. Na przykład App\MyValidators::validateDivisibility zapisujemy jako AppMyValidators_validateDivisibility i dodajemy do obiektu Nette.validators:

Nette.validators['AppMyValidators_validateDivisibility'] = (elem, args, val) => {
	return val % args === 0;
};

Zdarzenie onValidate

Po wysłaniu formularza przeprowadzana jest walidacja sprawdzająca poszczególne reguły dodane metodą addRule(), a następnie wywoływane jest zdarzenie onValidate. Jego handler można wykorzystać do dodatkowej walidacji, typowo do sprawdzenia poprawnej kombinacji wartości w kilku elementach formularza.

Jeśli zostanie wykryty błąd, przekazujemy go do formularza metodą addError(). Można ją wywołać albo na konkretnym elemencie, albo bezpośrednio na formularzu.

protected function createComponentSignInForm(): Form
{
	$form = new Form;
	// ...
	$form->onValidate[] = $this->validateSignInForm(...);
	return $form;
}

private function validateSignInForm(Form $form, \stdClass $data): void
{
	if ($data->foo > 1 && $data->bar > 5) {
		$form->addError('Ta kombinacja nie jest możliwa.');
	}
}

Błędy przy przetwarzaniu

W wielu przypadkach o błędzie dowiadujemy się dopiero przy przetwarzaniu poprawnego formularza, na przykład gdy zapisujemy nowy wpis do bazy danych i natrafiamy na zduplikowany klucz. W takim przypadku znów przekazujemy błąd z powrotem do formularza metodą addError(). Można ją wywołać albo na konkretnym elemencie, albo bezpośrednio na formularzu:

try {
	$data = $form->getValues();
	$this->user->login($data->username, $data->password);
	$this->redirect('Home:');

} catch (Nette\Security\AuthenticationException $e) {
	if ($e->getCode() === Nette\Security\Authenticator::InvalidCredential) {
		$form->addError('Nieprawidłowe hasło.');
	}
}

Jeśli to możliwe, zalecamy dodanie błędu bezpośrednio do elementu formularza, bo przy użyciu domyślnego renderera wyświetli się on obok niego.

$form['date']->addError('Przepraszamy, ten termin jest już zajęty.');

addError() możesz wywołać wielokrotnie, żeby przekazać formularzowi albo elementowi kilka komunikatów o błędach. Odczytasz je metodą getErrors().

Uwaga: $form->getErrors() zwraca podsumowanie wszystkich komunikatów o błędach, także tych przekazanych bezpośrednio poszczególnym elementom, nie tylko tych przekazanych bezpośrednio formularzowi. Komunikaty o błędach przekazane tylko formularzowi odczytasz przez $form->getOwnErrors().

Modyfikacja wpisanych wartości

Metodą addFilter() możemy zmodyfikować wartość wpisaną przez użytkownika. W tym przykładzie będziemy tolerować i usuwać spacje w kodzie pocztowym:

$form->addText('zip', 'Kod pocztowy:')
	->addFilter(function ($value) {
		return str_replace(' ', '', $value); // usuwamy spacje z kodu pocztowego
	})
	->addRule($form::Pattern, 'Kod pocztowy nie ma pięciu cyfr', '\d{5}');

Filtr włącza się między reguły walidacyjne i warunki, a więc kolejność metod ma znaczenie, czyli filtr i reguła wywoływane są w tej samej kolejności, w jakiej podane są metody addFilter() i addRule().

Walidacja w JavaScripcie

Język formułowania warunków i reguł jest bardzo mocny. Wszystkie konstrukcje działają zarówno po stronie serwera, jak i po stronie klienta w JavaScripcie. Przenoszone są w atrybutach HTML data-nette-rules jako JSON. Samą walidacją zajmuje się skrypt, który przechwytuje zdarzenie submit formularza, przechodzi przez poszczególne elementy i przeprowadza odpowiednią walidację.

Tym skryptem jest netteForms.js i jest dostępny z kilku możliwych źródeł:

Skrypt możesz wstawić bezpośrednio na stronę HTML z CDN:

<script src="https://unpkg.com/nette-forms@3"></script>

Albo skopiować lokalnie do publicznego folderu projektu (np. z vendor/nette/forms/src/assets/netteForms.min.js):

<script src="/path/to/netteForms.min.js"></script>

Albo zainstalować przez npm:

npm install nette-forms

A następnie wczytać i uruchomić:

import netteForms from 'nette-forms';
netteForms.initOnLoad();

Alternatywnie możesz wczytać go bezpośrednio z folderu vendor:

import netteForms from '../path/to/vendor/nette/forms/src/assets/netteForms.js';
netteForms.initOnLoad();

Walidację po stronie klienta możesz całkowicie wyłączyć, dodając formularzowi atrybut novalidate. Skrypt netteForms.js pominie wtedy jego walidację przy wysyłaniu, więc walidacja odbędzie się tylko na serwerze:

$form->setHtmlAttribute('novalidate');

Dynamiczny JavaScript

Chcesz wyświetlić pola adresu tylko wtedy, gdy użytkownik wybierze wysyłkę towaru pocztą? Nie ma problemu. Kluczem jest para metod addCondition() i toggle():

$form->addCheckbox('send_it')
	->addCondition($form::Equal, true)
		->toggle('#address-container');

Ten kod mówi, że gdy warunek zostanie spełniony (czyli gdy checkbox będzie zaznaczony), element HTML #address-container będzie widoczny, i odwrotnie. Elementy formularza z adresem odbiorcy umieścimy więc w kontenerze o tym ID, a będą się ukrywać i pokazywać po kliknięciu w checkbox. Zajmuje się tym skrypt netteForms.js.

Jako argument metody toggle() można przekazać dowolny selektor. Ze względów historycznych ciąg, który zaczyna się literą, cyfrą albo podkreśleniem i zawiera tylko litery, cyfry, podkreślenia, myślniki, kropki i dwukropki, traktowany jest jako ID elementu, tak jakby poprzedzał go znak #. Drugi, opcjonalny parametr pozwala odwrócić zachowanie; gdybyśmy na przykład użyli toggle('#address-container', false), element wyświetlałby się tylko wtedy, gdyby checkbox nie był zaznaczony.

Domyślna implementacja JavaScriptowa zmienia właściwość hidden elementów. Zachowanie możemy jednak łatwo zmienić, na przykład dodając animację. Wystarczy nadpisać w JavaScripcie metodę Nette.toggle własnym rozwiązaniem:

Nette.toggle = (selector, visible, srcElement, event) => {
	document.querySelectorAll(selector).forEach((el) => {
		// ukryj albo pokaż 'el' zależnie od wartości 'visible'
	});
};

Wyłączenie walidacji

Czasem może się przydać wyłączenie walidacji. Jeśli naciśnięcie przycisku wysyłającego nie ma przeprowadzać walidacji (odpowiednie dla przycisków Anuluj albo Podgląd), wyłączymy ją metodą $submit->setValidationScope([]). Jeśli ma przeprowadzać walidację tylko częściową, możemy określić, które pola albo kontenery formularza mają być walidowane.

$form->addText('name')
	->setRequired();

$details = $form->addContainer('details');
$details->addInteger('age')
	->setRequired('age');
$details->addInteger('age2')
	->setRequired('age2');

$form->addSubmit('send1'); // waliduje cały formularz
$form->addSubmit('send2')
	->setValidationScope([]); // nie waliduje nic
$form->addSubmit('send3')
	->setValidationScope([$form['name']]); // waliduje tylko element 'name'
$form->addSubmit('send4')
	->setValidationScope([$form['details']['age']]); // waliduje tylko element 'age'
$form->addSubmit('send5')
	->setValidationScope([$form['details']]); // waliduje kontener 'details'

setValidationScope nie wpływa na Zdarzenie onValidate na formularzu, które będzie wywoływane zawsze. Zdarzenie onValidate na kontenerze zostanie wywołane tylko wtedy, gdy ten kontener jest oznaczony do walidacji częściowej.

Walidacja częściowa wpływa też na wartości zwracane przez getValues(): wynik zawiera tylko wartości elementów mieszczących się w zakresie walidacji. Wartości elementów spoza tego zakresu są pomijane.

wersja: 4.x