Pomoc - Szukaj - Użytkownicy - Kalendarz
Pełna wersja: Kontrola danych wejsciowych
Forum PHP.pl > Forum > PHP > Object-oriented programming
kicaj
Jak optymalnie i bardziej uniwersalnie sprawdzac dane w kontrolerach (data input)?
Pisanie kontrolerow na zasadzie:
  1. <?php
  2. class NameController extends Controllers
  3. {
  4. function indexAction( $aParams )
  5. {
  6.  if( isset( $aParams['id'] ) 
  7.  {
  8. if( is_numeric( $aParams['id'] ) // ...
  9.  }
  10.  else
  11.  {
  12.  echo 'nie podano id!';
  13.  
  14. //...i tak w kazdej metodzie, sprawdzane rozne warunki z danych wejsciowych
  15. ?>


Sprawdzanie tego w kazdym kontrolerze i w jego kazdej metodzie mija sie z celem, jak to rozwiazac inaczej?
Sedziwoj
Przez kontrole typów argumentów <lol> sorry nie ma tego w PHP dla typów podstawowych, bo mija się to z ideologią twórców ;]
Ja zawsze robiłem
  1. <?php
  2. if( !is_numeric( $intPar ){
  3.  throw new ParamException( 'Paramet musi być liczbą!' );
  4. }
  5. ?>

czy raczej is_int(), taka kontrola czy wszystko jest ok. Na wydajność nie patrzyłem, wolę aby działało poprawnie, a wydajność można zyskać optymalizując coś co ma większy kosz czasowy.
kicaj
Wiem, sa rozne rodzaje funkcji.
Problem polega na tym ze jest to powtarzanie kodu co jest nie zgodne z programowaniem OOP i zasada DRY

Czesto tych instrukcji if jest duzo, co lubie, bo dokladnie user dostaje komunikat czego brakuje/co zle zrobil itp.
splatch
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:
  1. <?php
  2. interface Validation {
  3. /**
  4. * @param $ctx Kontekst z informacjami na temat walidacji
  5. **/
  6. public function validate(ValidationContext $ctx);
  7. }
  8. ?>


Dobrze, teraz jak może wyglądać definicja kontekstu walidacji:
  1. <?php
  2. interface ValidationContext {
  3.  
  4. // tu mamy ważny błąd
  5. public function addError($key, ValidationMessage $msg);
  6.  
  7. // a tu powiedzmy, ostrzeżenie
  8. public function addNotice($key, ValidationMessage $msg);
  9.  
  10. public function addValidator(Validator $v);
  11.  
  12. // czy wszystko "ok"
  13. public function isPassed();
  14.  
  15. // mapa wiadomości gdzie kluczem jest nazwa pola a wartością kolekcja wiadomości
  16. public function getMessages();
  17.  
  18. // tutaj dane z requestu
  19. public function getArgument($key);
  20. }
  21. ?>


Dobra, dorzućmy do tego jeszcze wiadomość:
  1. <?php
  2. interface ValidationMessage {
  3.  
  4. // tutaj wiadomość, oczekujemy tylko tego - jak ona jest ustawiana
  5. // to już kwestia wtórna
  6. public function getMessage();
  7. }
  8. ?>


Może wydawać się dziwne to, że robię interfejs do takiego banału, aczkolwiek mam jeden cel:
  1. <?php
  2. abstract class AbstracMessage {
  3.  
  4. // nie można się dobrać do wiadomości nawet z potomków
  5. private $message;
  6.  
  7. public function __construct($message, array $args = array()) {
  8. $this->message = $this->transform($this->getMessageString($message), $args);
  9. }
  10.  
  11. protected abstract function getMessageString($message);
  12.  
  13. private function transform($string, array $args) {
  14.  // tu może być coś innego
  15.  return strtr($string, array_keys($args), array_values($args));
  16. }
  17.  
  18. // to nam wymusza odwołanie do konstruktora
  19. public final function getMessage() {
  20. return $this->message;
  21. }
  22. }
  23.  
  24. // ta wiadomość po prostu zwraca tekst, który był
  25. class RawMessage extends AbstractMessage {
  26. protected function getMessageString($message) {
  27. return $message;
  28. }
  29. }
  30.  
  31. // a ta wiadomość tłumaczy ładnie komunikaty
  32. class I18nMessage extends AbstractMessage {
  33.  
  34. private $resource;
  35.  
  36. public function __construct(I18nValidationContext $ctx, $message, array $args = array()) {
  37. $this->resource = $ctx->getI18Resource();
  38. parent::__construct($message, $args);
  39. }
  40.  
  41. protected function getMessageString($message) {
  42. return $this->resource->getString($message);
  43. }
  44. }
  45.  
  46. // to powtórka z rozrywki - mamy interfejs i dorabiamy implementacje
  47. interface MessageResource {
  48. public function getString($key);
  49. public function getStringArray($key);
  50. }
  51.  
  52. // implementacja do zrobienia
  53. interface I18nValidationContext extends ValidationContext {
  54. public function getResource();
  55. }
  56.  
  57. ?>


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:
  1. <?php
  2. interface Action {
  3. public function execute();
  4. }
  5.  
  6. interface SecureAction {
  7. public function getCredentials();
  8. }
  9.  
  10. interface ValidationRequiredAction {
  11. public function validate(ValidationContext $ctx);
  12. }
  13. ?>

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:
  1. <?php
  2.  
  3. class DefaultValidationContext implements ValidationContext {
  4.  
  5. private $errors = 0;
  6.  
  7. // tu mamy ważny błąd
  8. public function addError($key, ValidationMessage $msg) {
  9. $this->addMessage(new StackEntry('error', $key, $msg));
  10. ++$this->errors;
  11. }
  12.  
  13. // a tu powiedzmy, ostrzeżenie
  14. public function addNotice($key, ValidationMessage $msg) {
  15. $this->addMessage(new StackEntry('notice', $key, $msg));
  16. }
  17.  
  18. protected final function addMessage(StackEntry $msg) {
  19. if (!isset($this->message[$msg->getKey()])) {
  20. $this->message[$msg->getKey()] = array();
  21. }
  22. $reference = &$this->message[$msg->getKey()];
  23. // teraz w widoku/akcji mamy do dyspozycji info o "powadze" błędu
  24. // oraz powiązaną z nim wiadomość
  25. $reference[sizeof($reference)]['severity'] = $msg->getSeverity();
  26. $reference[sizeof($reference)]['msg'] = $msg->getValidationMessage();
  27. }
  28.  
  29. // czy wszystko "ok"
  30. public function isPassed() {
  31. $i = 0;
  32. $validatorsCount = sizeof($this->validators);
  33. do {
  34. // jeśli coś się tu stanie to zwiększy nam się rozmiar tablicy $errors
  35. $this->validators[$i]->validate($this);
  36. } while ($this->errors == 0 && ++$i < $validatorsCount);
  37.  
  38. // przeszliśmy wszystkie walidatory i nie ma błędów
  39. return ++$i == $validatorsCount && $this->erros == 0;
  40. }
  41.  
  42. // mapa wiadomości gdzie kluczem jest nazwa pola a wartością kolekcja wiadomości
  43. public function getMessages() {
  44. return $this->message;
  45. }
  46.  
  47. public function addValidator(Validator $v) {
  48. $this->validators[] = $v;
  49. }
  50.  
  51. }
  52.  
  53.  
  54. ?>



No i na końcu akcja
  1. <?php
  2. class MyAction implements SecureAction, ValidationRequiredAction {
  3.  
  4. // dorzucamy co trzeba do kontekstu - resztę załatwia za nas FrontController
  5. public function validate(ValidationContext $ctx) {
  6. $ctx->addValidator(new NotEmptyValidator('user_id'));
  7. $ctx->addValidator(new NumberFormatValidator('user_id'));
  8. }
  9.  
  10. }
  11. ?>


Aa i jeszcze walidatory:
  1. <?php
  2. class NotEmptyValidator implements Validator {
  3.  
  4. private $field;
  5.  
  6. public function __construct($field) {
  7. $this->field = $field;
  8. }
  9.  
  10. public function validate(ValidationContext $ctx) {
  11. if (empty($ctx->getArgument($field)) {
  12. $ctx->addError(new RawMessage('Wartość {0} jest pusta', array($field)));
  13. }
  14. }
  15. }
  16. ?>


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:
  1. <?php
  2. class XmlValidationForm implements Action {
  3.  
  4. public final function validate(ValidationContext $ctx) {
  5. $path = $this->getContextPath();
  6. $binder new XmlValidatorBinder($path, $ctx);
  7. }
  8.  
  9. public function getContextPath() {
  10. return get_class($this) .'-validation.xml';
  11. }
  12.  
  13. }
  14.  
  15. class MyAction extends XmlValidationForm implements SecureAction, ValidationRequiredAction {
  16.  
  17. // tu już nic nie musimy dodawać - na podstawie nazwy klasy zostanie wczytana
  18. // konfiguracja walidatorów
  19.  
  20. }
  21. ?>


No i kolejna ciekawa wariacja nad którą można pomyśleć:
  1. <?php
  2. $orValidator = new OrValidationCondition($validator1, $validator2);
  3. $ctx->addValidator($orValidator);
  4. ?>


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. smile.gif
Sedziwoj
No tak jak zwykle czegoś nie doczytałem ;]
Bo ja nie o tej kontroli pisałem, tylko już "wewnętrznej"...

@splatch Znów spore ilości kodu umieszczasz biggrin.gif
splatch
Cytat(Sedziwoj @ 5.12.2007, 00:04:56 ) *
@splatch Znów spore ilości kodu umieszczasz biggrin.gif

@Sedziwoj odpowiedzi na forum to obecnie mój jedyny sposób na utrzymanie kontaktu z PHP, stąd tak duże ilości kodu.

Myślę też, że takie przykłady z wykorzystaniem interfejsów, abstrakcji i tak dalej są jak najbardziej na miejscu i pomagają odwiedzającym bardziej niż mega rozwlekłe opisy i dywagacje. smile.gif Po prostu można zobaczyć "Object Oriented Programing in Action". winksmiley.jpg
Sedziwoj
Wież tylko że dywagacje i rozwlekłe opisy się rozumie czytając, ty jednak przy kodzie trzeba chwilę pomyśleć zanim się zrozumie założenia twórcy. Choć ciągle mi się przypomina "witaj świecie" w wzorcach projektowych, czyli aby gdzieś przypadkiem forma nie przerosła treść. Tak jak dziś od dłuższego czasu napisałem coś w pełni proceduralne, ale nie widzę sensu aby dla 3 linijek jednokrotnego wykorzystania (coś do obliczeń dla koleżanki) pisać więcej.
Ale zawsze patrzenie co inni tworzą jest przydatne, tego nie krytykuję smile.gif

Tak z grubsza przed snem przejrzałem, na pewno są nieścisłości w tym kodzie, ale ogólną koncepcję chyba załapałem. Po prostu rozdzielenie funkcjonalności i umożliwienie stosowania różnych wariantów.
A jeszcze co do łączenia walidatorów warunkami, to ja chyba był bym za zdefiniowaniem, które połączyć i jako trzeci argument jaki warunek pomiędzy nimi. Do tego wartości 'notice' (w StackEntry()) zastąpił bym stałą, podobnie ten trzeci argument, warunek złączenia, walidatorów też jako stałą w obiekcie (czy interfejsie, już tego nie będę rozstrzygał :])

No dobra mykam spać.

Edit takie tam przecinki, kropi i tak pewnie kiepskie jest to co napisałem
splatch
Tak, zasadniczo skłaniając się w stronę opisu.

Validator element zajmujący się walidacją, dostaje kontekst i z niego musi wyciągnąć to, czego potrzebuje i przeprowadzić walidację. Kwestia konfiguracji walidatora - albo przez akcesory albo przez konstruktory.

ValidationContext zawiera informacje na temat całego procesu. Ma również zgromadzone dane wejściowe i udostępnia je walidatorom.

FrontController element, który nie jest tu widoczny, a który zasadniczo odpowiada za odpalenie akcji i zweryfikowanie czy wszystko "ok", innymi słowy robi $action->validate($this->validationContext), $this->validationContext->isPassed() - gdy dostajemy false podejmuje odpowiednie działania - jakie to zależy już od implementacji.

ValidationRequiredAction typ, który sugeruje FrontControllerowi, że dana akcja chciałaby otrzymywać dane, które były zweryfikowane. Jest to tylko interfejs, pod spodem możemy w różny sposób dodawać do kontekstu swoje walidatory. Mogą być one odczytane z XML, mogą być wpisane z palca. Co kto woli.

I18nValidationContext kontekst dostarczający "tłumaczenia".

I18nMessage wiadomość, która zostanie przetłumaczona, przekazujemy do konstruktora klucz, na podstawie którego z kontekstu zostanie pobrana odpowiednia wiadomość.

OrValidationCondition walidator prezentujący prosty warunek logiczny, to lub to. Jesteśmy wolni od problemów związanych z tym, że przychodzi nam raz to raz to.
Cysiaczek
@splatch - w jaki sposób ładujesz (fizycznie) taką ilość interfejsów? Przeanalizowałem Twoje ostatnie posty i tak sobie pomyślałem, że starałem się unikać interfejsów na rzecz abstrakcji również z tego powodu, że autoloader w końcu by wymiękł ;p

Pozdrawiam.
Sedziwoj
@Cysiaczek
Nie przesadzaj dla autoloader'a to nie jest kłopot, gorzej tylko z nazewnictwem ;]
splatch
Cytat(Cysiaczek @ 5.12.2007, 10:11:05 ) *
@splatch - w jaki sposób ładujesz (fizycznie) taką ilość interfejsów? Przeanalizowałem Twoje ostatnie posty i tak sobie pomyślałem, że starałem się unikać interfejsów na rzecz abstrakcji również z tego powodu, że autoloader w końcu by wymiękł ;p


Kod piszę na poczekaniu, na potrzeby poszczególnych postów. Wydaje mi się, że interfejsy w tym przypadku nie są problematyczne, są małe i lekkie, część z nich zawiera tylko po parę metod.
Co do optymalizacji autoloadera - w Mojavi 3 metoda była prosta - stworzenie pliku z definicjami wszystkich wymaganych interfejsów etc. Wczytywany był 1 plik a nie kilka.
kicaj
Dobra dobra, bo troche odbiegacie od tematu...

To co splatch wklepal jest troche za bardzo przesadzaone, pytanie brzmialo jak z zasada DRY robic kontrole danych wejsciowych, tzn. jak nie powtarzac sie sprawdzajac dane z parametrow metody, takich jak id, liczba, string, itp
splatch
Cytat(kicaj @ 5.12.2007, 12:04:12 ) *
To co splatch wklepal jest troche za bardzo przesadzaone, pytanie brzmialo jak z zasada DRY robic kontrole danych wejsciowych, tzn. jak nie powtarzac sie sprawdzajac dane z parametrow metody, takich jak id, liczba, string, itp

Jeśli przykład kodu, który jest wyżej koliduje z DRY to proszę pokaż mi gdzie. Być może źle rozumiem to pojęcie i gdzieś popełniłem błąd. Ewentualnie powiedz co w moim przykładzie odbiega od Twoich oczekiwań.
kicaj
No stworzylem sobie cos na ten wzor, ale duzo duzo mniej kodu, tzn. z czasem bedzie to sie rozrastalo

Beda problemy, beda pytania smile.gif
To jest wersja lo-fi głównej zawartości. Aby zobaczyć pełną wersję z większą zawartością, obrazkami i formatowaniem proszę kliknij tutaj.
Invision Power Board © 2001-2025 Invision Power Services, Inc.