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.