Kontrola danych wejściowych w miarę możliwości powinna być poza ciałem akcji. Jakkolwiek nie zależy zapominać że to konkretna akcja determinuje nam warunki walidacji.
Warunki typu czy to liczba, czy to jest wymagane i tak dalej można wyodrębnić do klas, które załatwią nam walidację. Jak zwykle w takim przypadku zaczynamy od interfejsu:
<?php
interface Validation {
/**
* @param $ctx Kontekst z informacjami na temat walidacji
**/
public function validate(ValidationContext $ctx);
}
?>
Dobrze, teraz jak może wyglądać definicja kontekstu walidacji:
<?php
interface ValidationContext {
// tu mamy ważny błąd
public function addError($key, ValidationMessage $msg);
// a tu powiedzmy, ostrzeżenie
public function addNotice($key, ValidationMessage $msg);
public function addValidator(Validator $v);
// czy wszystko "ok"
public function isPassed();
// mapa wiadomości gdzie kluczem jest nazwa pola a wartością kolekcja wiadomości
public function getMessages();
// tutaj dane z requestu
public function getArgument($key);
}
?>
Dobra, dorzućmy do tego jeszcze wiadomość:
<?php
interface ValidationMessage {
// tutaj wiadomość, oczekujemy tylko tego - jak ona jest ustawiana
// to już kwestia wtórna
public function getMessage();
}
?>
Może wydawać się dziwne to, że robię interfejs do takiego banału, aczkolwiek mam jeden cel:
<?php
abstract class AbstracMessage {
// nie można się dobrać do wiadomości nawet z potomków
private $message;
public function __construct
($message, array $args = array()) { $this->message = $this->transform($this->getMessageString($message), $args);
}
protected abstract function getMessageString($message);
private function transform
($string, array $args) { // tu może być coś innego
}
// to nam wymusza odwołanie do konstruktora
public final function getMessage() {
return $this->message;
}
}
// ta wiadomość po prostu zwraca tekst, który był
class RawMessage extends AbstractMessage {
protected function getMessageString($message) {
return $message;
}
}
// a ta wiadomość tłumaczy ładnie komunikaty
class I18nMessage extends AbstractMessage {
private $resource;
public function __construct
(I18nValidationContext
$ctx, $message, array $args = array()) { $this->resource = $ctx->getI18Resource();
parent::__construct($message, $args);
}
protected function getMessageString($message) {
return $this->resource->getString($message);
}
}
// to powtórka z rozrywki - mamy interfejs i dorabiamy implementacje
interface MessageResource {
public function getString($key);
public function getStringArray($key);
}
// implementacja do zrobienia
interface I18nValidationContext extends ValidationContext {
public function getResource();
}
?>
Dobra, wiadomości wiadomościami, ale przecież nie jest to najistotniejsze, najbardziej przecież nas interesuje powiązanie tego wszystkiego - czyli jak akcja "się waliduje". Zasadniczo znowu byłbym za interfejsem:
<?php
interface Action {
public function execute();
}
interface SecureAction {
public function getCredentials();
}
interface ValidationRequiredAction {
public function validate(ValidationContext $ctx);
}
?>
Mając takie definicje front controller, który ma podniesiony ValidationContext może rozróżnić co zrobić - czyli najpierw sprawdza czy akcja wymaga autoryzacji - jeśli tak sprawdza uprawnienia, następnie weryfikuje czy dane, które przychodzą są ok.
Teraz rzecz zasadnicza - czyli co z konfiguracją. Jak widać same walidatory to pestka - ale jak zrobić to ładnie. No więc - może przykładowa implementacja:
<?php
class DefaultValidationContext implements ValidationContext {
private $errors = 0;
// tu mamy ważny błąd
public function addError($key, ValidationMessage $msg) {
$this->addMessage(new StackEntry('error', $key, $msg));
++$this->errors;
}
// a tu powiedzmy, ostrzeżenie
public function addNotice($key, ValidationMessage $msg) {
$this->addMessage(new StackEntry('notice', $key, $msg));
}
protected final function addMessage(StackEntry $msg) {
if (!isset($this->message[$msg->getKey()])) { $this->message[$msg->getKey()] = array(); }
$reference = &$this->message[$msg->getKey()];
// teraz w widoku/akcji mamy do dyspozycji info o "powadze" błędu
// oraz powiązaną z nim wiadomość
$reference[sizeof($reference)]['severity'] = $msg->getSeverity(); $reference[sizeof($reference)]['msg'] = $msg->getValidationMessage(); }
// czy wszystko "ok"
public function isPassed() {
$i = 0;
$validatorsCount = sizeof($this->validators); do {
// jeśli coś się tu stanie to zwiększy nam się rozmiar tablicy $errors
$this->validators[$i]->validate($this);
} while ($this->errors == 0 && ++$i < $validatorsCount);
// przeszliśmy wszystkie walidatory i nie ma błędów
return ++$i == $validatorsCount && $this->erros == 0;
}
// mapa wiadomości gdzie kluczem jest nazwa pola a wartością kolekcja wiadomości
public function getMessages() {
return $this->message;
}
public function addValidator(Validator $v) {
$this->validators[] = $v;
}
}
?>
No i na końcu akcja
<?php
class MyAction implements SecureAction, ValidationRequiredAction {
// dorzucamy co trzeba do kontekstu - resztę załatwia za nas FrontController
public function validate(ValidationContext $ctx) {
$ctx->addValidator(new NotEmptyValidator('user_id'));
$ctx->addValidator(new NumberFormatValidator('user_id'));
}
}
?>
Aa i jeszcze walidatory:
<?php
class NotEmptyValidator implements Validator {
private $field;
public function __construct($field) {
$this->field = $field;
}
public function validate(ValidationContext $ctx) {
if (empty($ctx->getArgument($field)) { $ctx->addError(new RawMessage
('Wartość {0} jest pusta', array($field))); }
}
}
?>
Kilka uwag odnośnie kodu, który jest wyżej .. przede wszystkim walidatory dodają wiadomości - równie dobrze mogą bronić się wyjątkami, aczkolwiek mamy wówczas problem ze sformułowaniem komunikatu dla użytkownika. Można zatem rozważyć dwie opcje -
wydelegować formatowanie wiadomości z AbstractMessage do ValidationContext - wówczas zmieniły by się nam sygnatury metod addError, addNotice. Druga opcja to
przesunąć formatowanie wiadomości do akcji.
Dlaczego dodałem coś takiego jak notice w walidacji? Są to informacje, które nie wywalają całego procesu, a które można wyświetlić przy ponownym wyświetlaniu błędu - powiedzmy ktoś nie podał wartości w wymaganym polu a w innym gdzie był powinien trafić float dostaliśmy int - dodajemy notice o konwersji.
Aaa i jeszcze walka z tworzeniem walidatorów:
<?php
class XmlValidationForm implements Action {
public final function validate(ValidationContext $ctx) {
$path = $this->getContextPath();
$binder new XmlValidatorBinder($path, $ctx);
}
public function getContextPath() {
return get_class($this) .'-validation.xml';
}
}
class MyAction extends XmlValidationForm implements SecureAction, ValidationRequiredAction {
// tu już nic nie musimy dodawać - na podstawie nazwy klasy zostanie wczytana
// konfiguracja walidatorów
}
?>
No i kolejna ciekawa wariacja nad którą można pomyśleć:
<?php
$orValidator = new OrValidationCondition($validator1, $validator2);
$ctx->addValidator($orValidator);
?>
Kod który przedstawiłem nie jest ani kompletny ani nadzwyczajnie spójny. Może stanowić podstawę do rozpoczęcia prac, aczkolwiek pozostaje jeszcze kilka problemów, które wymagają niezłej gimnastyki.