Walidacja formularzy

Elementy obowiązkowe

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

$form->addText('name', 'Imię:')
	->setRequired('Proszę podać imię');

Reguły

Reguły walidacji dodajemy do elementów metodą addRule(). Pierwszy parametr to reguła, drugi to tekst komunikatu o błędzie, a trzeci to argument reguły walidacji.

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

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

Nette dostarcza całą gamę predefiniowanych reguł, których nazwy są stałymi klasy Nette\Forms\Form. Dla wszystkich elementów możemy użyć tych reguł:

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ść jest równa parametrowi mixed
NotEqual wartość nie jest równa parametrowi mixed
IsIn wartość jest równa jednemu z elementów w tablicy array
IsNotIn wartość nie jest równa żadnemu z elementów w tablicy array
Valid czy element jest wypełniony poprawnie? (dla Podmínky)

Wejścia tekstowe

Dla elementów addText(), addPassword(), addTextArea(), addEmail(), addInteger(), addFloat() można użyć również niektórych z następujących reguł:

MinLength minimalna długość tekstu int
MaxLength maksymalna długość tekstu int
Length długość w zakresie lub dokładna długość para [int, int] lub int
Email prawidłowy adres e-mail
URL absolutny URL
Pattern pasuje do wyrażenia regularnego string
PatternInsensitive jak Pattern, ale niezależne od wielkości liter string
Integer wartość całkowita
Numeric alias dla Integer
Float liczba
Min minimalna wartość elementu numerycznego int|float
Max maksymalna wartość elementu numerycznego int|float
Range wartość w zakresie para [int|float, int|float]

Reguły walidacji Integer, Numeric i Float od razu konwertują wartość na integer lub float. Ponadto reguła URL akceptuje również adres bez schematu (np. nette.org) i dodaje schemat (https://nette.org). Wyrażenie w Pattern i PatternIcase musi pasować do całej wartości, tzn. tak jakby było otoczone znakami ^ i $.

Liczba elementów

Dla elementów addMultiUpload(), addCheckboxList(), addMultiSelect() można użyć również następujących reguł do ograniczenia liczby wybranych elementów lub przesłanych plików:

MinLength minimalna liczba int
MaxLength maksymalna liczba int
Length liczba w zakresie lub dokładna liczba para [int, int] lub int

Przesyłanie plików

Dla elementów addUpload(), addMultiUpload() można użyć również następujących reguł:

MaxFileSize maksymalny rozmiar pliku w bajtach int
MimeType Typ MIME, dozwolone symbole wieloznaczne ('video/*') string|string[]
Image obraz JPEG, PNG, GIF, WebP, AVIF
Pattern nazwa pliku pasuje do wyrażenia regularnego string
PatternInsensitive jak Pattern, ale niezależne od wielkości liter string

MimeType i Image wymagają rozszerzenia PHP fileinfo. To, czy plik lub obraz jest wymaganego typu, wykrywają na podstawie jego sygnatury i nie weryfikują integralności całego pliku. Czy obraz nie jest uszkodzony, można sprawdzić na przykład próbując go załadować.

Komunikaty o błędach

Wszystkie predefiniowane reguły z wyjątkiem Pattern i PatternInsensitive mają domyślny komunikat o błędzie, więc można go pominąć. Jednak podanie i sformułowanie wszystkich komunikatów na miarę sprawi, że formularz będzie bardziej przyjazny dla użytkownika.

Zmienić domyślne komunikaty można w konfiguracji, edytując teksty w tablicy Nette\Forms\Validator::$messages lub używając translatora.

W tekście komunikatów o błędach można używać następujących symboli zastępczych:

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

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

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

Warunki

Oprócz reguł można dodawać również warunki. Zapisuje się je podobnie jak reguły, tylko zamiast addRule() używamy metody addCondition() i oczywiście nie podajemy żadnego komunikatu o błędzie (warunek tylko pyta):

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

Warunek można powiązać również z innym elementem niż aktualny za pomocą addConditionOn(). Jako pierwszy parametr podajemy referencję do elementu. W tym przykładzie e-mail będzie obowiązkowy tylko wtedy, gdy zaznaczy się checkbox (jego wartość będzie true):

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

$form->addEmail('email', 'E-mail:')
	// jeśli checkbox jest zaznaczony
	->addConditionOn($form['newsletters'], $form::Equal, true)
		// to wymagaj e-maila
		->setRequired('Podaj 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 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(/* ... */);

W Nette można bardzo łatwo reagować na spełnienie lub niespełnienie warunku również po stronie JavaScriptu za pomocą metody toggle(), zobacz Dynamiczny JavaScript.

Odwołanie do innego elementu

Jako argument reguły lub warunku można przekazać również inny element formularza. Reguła następnie użyje wartości wprowadzonej później przez użytkownika w przeglądarce. W ten sposób można np. dynamicznie walidować, że element password zawiera ten sam ciąg znaków co element password_confirm:

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

Własne reguły i warunki

Czasami dochodzimy do sytuacji, gdy wbudowane reguły walidacji w Nette nam nie wystarczają i potrzebujemy zweryfikować dane od użytkownika po swojemu. W Nette jest to bardzo proste!

Metodom addRule() lub addCondition() można jako pierwszy parametr przekazać dowolny callback. Przyjmuje on jako pierwszy parametr sam element i zwraca wartość boolean określającą, czy walidacja przebiegła pomyślnie. Przy dodawaniu reguły za pomocą addRule() można podać również dodatkowe argumenty, które są następnie przekazywane jako drugi parametr.

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

class MyValidators
{
	// testuje, 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)
	{
		// inne walidatory
	}
}

Użycie jest następnie bardzo proste:

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

Własne reguły walidacji można dodawać również do JavaScriptu. Warunkiem jest, aby reguła była metodą statyczną. Jej nazwa dla walidatora JavaScriptowego powstaje przez połączenie nazwy klasy bez ukośników wstecznych \, podkreślenia _ i nazwy metody. Np. App\MyValidators::validateDivisibility zapiszemy jako AppMyValidators_validateDivisibility i dodamy do obiektu Nette.validators:

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

Zdarzenie onValidate

Po wysłaniu formularza przeprowadzana jest walidacja, podczas której sprawdzane są poszczególne reguły dodane za pomocą addRule(), a następnie wywoływane jest zdarzenie onValidate. Jego handler można wykorzystać do dodatkowej walidacji, typowo sprawdzenia poprawnej kombinacji wartości w wielu 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;
}

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

Błędy podczas przetwarzania

W wielu przypadkach o błędzie dowiadujemy się dopiero w momencie, gdy przetwarzamy poprawny formularz, na przykład zapisujemy nową pozycję do bazy danych i napotykamy na duplikat kluczy. W takim przypadku błąd ponownie przekazujemy 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 dołączenie błędu bezpośrednio do elementu formularza, ponieważ zostanie on wyświetlony obok niego przy użyciu domyślnego renderera.

$form['date']->addError('Przepraszamy, ale ta data jest już zajęta.');

Możesz wywoływać addError() wielokrotnie i w ten sposób przekazać formularzowi lub elementowi więcej komunikatów o błędach. Uzyskasz je za pomocą getErrors().

Uwaga, $form->getErrors() zwraca podsumowanie wszystkich komunikatów o błędach, również tych, które zostały przekazane bezpośrednio poszczególnym elementom, a nie tylko bezpośrednio formularzowi. Komunikaty o błędach przekazane tylko formularzowi uzyskasz przez $form->getOwnErrors().

Modyfikacja danych wejściowych

Za pomocą metody addFilter() możemy zmodyfikować wartość wprowadzoną 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 formatu pięciu cyfr', '\d{5}');

Filtr jest włączany między reguły walidacji i warunki, a zatem zależy od kolejności metod, tzn. filtr i reguła są wywoływane w takiej kolejności, jak kolejność metod addFilter() i addRule().

Walidacja JavaScript

Język do formułowania warunków i reguł jest bardzo potężny. Wszystkie konstrukcje działają zarówno po stronie serwera, jak i po stronie JavaScriptu. Są one przekazywane w atrybutach HTML data-nette-rules jako JSON. Samą walidację przeprowadza następnie skrypt, który przechwytuje zdarzenie formularza submit, przechodzi przez poszczególne elementy i wykonuje odpowiednią walidację.

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

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

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

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

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

Lub zainstalować przez npm:

npm install nette-forms

A następnie załadować i uruchomić:

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

Alternatywnie można go załadować bezpośrednio z folderu vendor:

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

Dynamiczny JavaScript

Chcesz wyświetlić pola do wprowadzenia adresu tylko wtedy, gdy użytkownik wybierze wysyłkę towaru pocztą? Żaden problem. Kluczem jest para metod addCondition() & toggle():

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

Ten kod mówi, że gdy warunek jest spełniony, czyli gdy checkbox jest zaznaczony, widoczny będzie element HTML #address-container. I odwrotnie. Elementy formularza z adresem odbiorcy umieścimy więc w kontenerze o tym ID, a po kliknięciu na checkbox ukryją się lub pokażą. Zapewnia to skrypt netteForms.js.

Jako argument metody toggle() można przekazać dowolny selektor. Z historycznych powodów alfanumeryczny ciąg znaków bez dodatkowych znaków specjalnych jest traktowany jako ID elementu, czyli tak samo, jakby poprzedzał go znak #. Drugi, opcjonalny parametr pozwala odwrócić zachowanie, tzn. gdybyśmy użyli toggle('#address-container', false), element zostałby wyświetlony tylko wtedy, gdy checkbox nie byłby zaznaczony.

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

Nette.toggle = (selector, visible, srcElement, event) => {
	document.querySelectorAll(selector).forEach((el) => {
		// ukrywamy lub pokazujemy 'el' zgodnie z wartością 'visible'
	});
};

Wyłączenie walidacji

Czasami 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 lub Podgląd), wyłączymy ją metodą $submit->setValidationScope([]). Jeśli ma przeprowadzać tylko częściową walidację, możemy określić, które pola lub kontenery formularza mają być walidowane.

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

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

$form->addSubmit('send1'); // Waliduje cały formularz
$form->addSubmit('send2')
	->setValidationScope([]); // Nie waliduje wcale
$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 formularza, które zostanie wywołane zawsze. Zdarzenie onValidate kontenera zostanie wywołane tylko wtedy, gdy ten kontener jest oznaczony do częściowej walidacji.

wersja: 4.0