PHP Programowanie obiektowe – początki. Walidacja formularza

W PHP programuję już ponad pół roku, jednak do tej pory tylko strukturalnie. Niestety w skutek tego wraz z rozrastaniem się mojego serwisu coraz częściej pojawiają się problemy. Dlatego postanowiłem przejść na programowanie obiektowe, a cały proces nauki i wyciąganych z niej wniosków spisać na tym blogu.

Wstęp

Już na samym początku chciał bym zaznaczyć że są to moje początki z programowaniem obiektowym, tak w PHP i w ogóle. Dlatego w żadnym przypadku nie należy w ciemno przyswajać tego co tutaj przeczytacie.

Artykuł ten jak i mam nadzieje cała ich seria powstaje między innymi po to abym mógł łatwiej przedstawić mój obiektowy kod bardziej doświadczonym w tej dziedzinie osobom.

Jestem jak najbardziej otwarty na konstruktywną krytykę, więc jeżeli widzisz że coś robię źle lub mógł bym zrobić lepiej, chętnie przeczytam o tym w komentarzu.

Aby nie wprowadzać w błąd osób „zielonych” które tutaj trafią, postaram się na bieżąco redagować ten post jeżeli sam bądź z czyjaś pomocą dojdę do wniosku że coś jest źle.

Tak aby każdy mógł zobaczyć jakie błędy przytrafiają się początkującym, jak ich uniknąć, i na co zwracać uwagę.

Uprzedzając pytania, wiedzę póki co czerpię głównie z najnowszego wydania książki PHP Obiekty, wzorce narzędzia. Dodam tylko że gdybym przeczytał tę książkę pół roku temu prawdopodobnie nic bym z niej nie zrozumiał więc raczej nie jest to pozycja dla osób całkiem niedoświadczonych.

Założenia

Zaczynam trochę nietypowo, bo od modułu resetowania hasła.

Jednak tym artykule nie będę opisywał całości gdyż pewnie okazało by się to zbyt rozwiązłe, a skupie się jedynie na odbieraniu danych z formularza i ich walidacji pod kątem dalszej pracy na nich.

Tak więc nasz komponent powinien wykonywać następujące czynności:

  1. Pobrać dane z Input’a.
  2. Rozpoznać czy użytkownik podał adres email czy login.
  3. Na podstawie rozpoznanej wartości skierować dane do walidacji odpowiedniej klasie która:
  4. sprawdzi czy formularz nie jest pusty.
  5. sprawdzi długość przekazanego ciągu..
  6. sprawdzi czy przekazano tylko akceptowane znaki.
  7. Jeżeli walidacja nie przebiegła pomyślnie doda błędy i skieruje skrypt do ponownego wyświetlenia formularza.
  8. Wyświetli błędy.

Nadmienię jeszcze że tworzona prze zemnie struktura może wydawać się zbyt rozbudowana jak na potrzeby tak niewielkiego skryptu.

Jednak piszę ją również pod kontem przyszłego wykorzystania w napisanych już skryptach strukturalnych, jak i tych które dopiero napiszę.

Dlatego starałem się aby klasy były w miarę możliwości elastyczne i zdatne do ponownego wykorzystania bez większych zmian w strukturach.

Pobieramy formularz

Oto interesujący nas formularz w formie jakiej widzi ją użytkownik:

A tak wygląda w wersji HTML. Jest tam co prawda kawałek kodu PHP ale tym zajmiemy się później.

<form class="reset-pass__form" action="reset_pass_script.php" method="post">
 <div class="reset-pass__div reset-padding">
  <span class="reset-pass__label" for="user">Podaj adres e-mail bądź login:</span>
  <div class="reset-pass__div-input">
   <input class="reset-pass__input" placeholder="e-mail lub login" name="identity"/>
  </div>
 </div>
 <div class="reset-pass__div reset-padding">
  <span class="reset-pass__span">Udowodnij że nie jesteś robotem:</span>
  <div class="reset-pass__recaptcha g-recaptcha" data-sitekey="6LdGKCIUAAAAAPkJ9mZG5OBWabq_OVfuyWzYIiWN"></div>
 </div>
 <div class="blad_user">
  <?php
   // jeżeli obiekt przechowujący błedy istnieje w sesji
   // wyświetl błędy walidacji
   if(isset($_SESSION['validError'])){
    $_SESSION['validError']->viewErrors();
    unset($_SESSION['validError']);
   }
  ?>
 </div>
 <input type="submit" class="btn btn--green reset-padding" value="Dalej"/>
</form>

Użytkownik wypełnia formularz, klika przycisk „dalej”  i przekazuje sterowanie do skryptu zawartego w pliku reset_pass_script.php który wygląda następująco:

<?php
$DOCUMENT_ROOT=$_SERVER['DOCUMENT_ROOT'].'/../ini/';

require ($DOCUMENT_ROOT.'trait/String.php');
require ($DOCUMENT_ROOT.'class/Input.php');
require ($DOCUMENT_ROOT.'class/ValidInput.php');
require ($DOCUMENT_ROOT.'class/MyError.php');
require ($DOCUMENT_ROOT.'class/ResetPass.php');
require ($DOCUMENT_ROOT.'class/Login.php');
require ($DOCUMENT_ROOT.'class/Email.php');

session_start();

 $input = Input::CreateInput($_POST['identity'],"identity");
 $validError = new ValidError();
 $validInput = ValidInput::createValidInput($input,$validError);
 $validInput->valid();
 if($validError->checkErrors()){
  // dalsza część skryptu
  echo "walidacja przebiegła pomyslnie";
  }
  else{
  $_SESSION['validError'] = $validError;
  header("Location:reset_pass.php");
 }
?>

Na początku oczywiście ustalamy położenie katalogu z plikami klas względem katalogu głównego strony i ładujemy pliki z potrzebnymi nam klasami.

Dopiero po załadowaniu klas rozpoczynamy sesję, ponieważ gdybyśmy zrobili to w odwrotnej kolejności a później chcieli przekazać do niej jakiś obiekt, nie zadziała automatyczna serializacja i pojawi się błąd mówiący nam że próbujemy pracować na niekompletnym obiekcie.

W linij 14 odwołujemy się do statycznej metody Input::CreateInput() przekazując jej dwa parametry. Zawartość inputa oraz jego nazwę.  Metoda ta jest wytwórnią obiektów klas pochodnych abstrakcyjnej klasy Input.

W efekcie otrzymujemy obiekt przechowujący 2 wymienione wcześniej składowe, oraz posiadający 2  metody do ich wyświetlania. Owe składowe ustaliliśmy jako prywatne ponieważ nie chcemy abo można było nimi swobodnie manipulować.

Klasy pochodne na razie nie posiadają żadnych składowych ani metod ale tak jak już mówiłem jest to krok z myślą o przyszłości.

Klasa Input i jej pochodne:

<?php
 //klasa odbierająca zawartość formulaży
 abstract class Input
 {
  protected $value;// wartość pobranego inputa
  protected $name;// nazwa inputa
  // end component
 
 
  // fabryka obiektów pochodnych klasy Input
  // przyjmuje zawartość inputa, nazwa inputa obiekt ValidInput i obiekt ValidError
  // po podjęciu decyzji jakiej klasy obiekt należy utworzyć, zwraca utworzony obiekt
  static public function CreateInput(
   $value,
   string $name):Input
  {
   if(is_array($value)){
    return new ArrayInput($value,$name);
   }
   if(is_float($value)){
    return new FloatInput($value,$name);
   }
   if(is_integer($value)){
    return new IntegerInput($value,$name);
   }
   if(is_string($value)){
    return new StringInput($value,$name);
   }
  }// koniec metody CreateInput
 
 
  // konstruktor obiektów pochodnych klasy Input
  protected function __construct($value,$name)
  {
   $this->value = $value;
   $this->name = $name;
  }// end __construct()
 
 
   // metoda zwracająca value obiektu
   public function getValue()
   {
    return $this->value;
   }
 
 
   // metoda zwracająca name obiektu
   public function getName():string
   {
    return $this->name;
   }

 }// end class Input
 
 class ArrayInput extends Input{}
 
 class FloatInput extends Input{}

class IntegerInput extends Input{}

class StringInput extends Input{}
?>


Klasa obsługująca błędy

W linii 15 tworzymy obiekt klasy ValidError służącej jako pojemnik przechowujący i wyświetlający błędy walidacji. Jest to klasa pochodna abstrakcyjnej klasy MyError.

Po swojej matce dziedziczy 2 składowe $errorNumber oraz $errorMessage, czyli tablice przechowujące numery błędów walidacji i opisy tych błędów. Dziedziczy także 3 metody: addError(), checkError() i vievError() służące kolejno dodawaniu błędów, sprawdzaniu wystąpienia błędu i wyświetlania wykrytych błędów.

Sama przechowuje jeszcze stałą MESSAGE z opisami błędów, a także rozbudowuje metodę addError(), która oprócz dodania numeru błędu do $errorNumber dodaje jeszcze opis błędu do $errorMessage opcjonalnie rozbudowując go o nazwę formularza w którym wystąpił i opis pożądanych wartości.

W przyszłości z pewnością potrzebna będzie też klasa obsługująca błędy bazodanowe, która również została zainicjowana jako DatabaseError.

Klasa MyError i jej pochodne:

<?php
// abstrakcyjna klasa bazowa klas z błędami
abstract class MyError
{
 protected $errorNumber = [];// tablica z numerami dodanych błędów
 protected $errorMessage = [];// tablica z opisami błędów
 // end component
 
 
 
 public function addError(int $errorNumber)
 {
  $this->errorNumber[] = $errorNumber;
 }// end addError()
 
 
 // metoda sprawdzająca liczbę powstałych błędów
 // jeżeli błędy zostały wykryte zwróci false w przeciwnym wypadku true
 public function checkErrors() 
 {
  $error = count($this->errorNumber);
  if($error > 0){
  return false;
  }
  return true;
 }// end checkError()
 
 
 // metoda wyświetlająca opis dodanych błędów
 public function viewErrors()
 {
  if(!$this->checkErrors()){
   foreach ($this->errorMessage as $message){
    echo "<p>".$message."</p>";
   }
  }
 }// end vievErrors()
 
}// end class MyError


// klasa przechowująca i wyświetlająca błędy walidacji
class ValidError extends MyError
{
 // tablica z opisami błędów
 const MESSAGE = [
  null,
  "Formularz nie może być pusty!",
  /*$name*/"powinien zawierać"/*$content*/,
  /*$name*/"zawiera niedozwolone znaki!",
 ];
 // end component
 
 
 // metoda dodająca nowy błąd
 // przyjmuje nr.błędu, opcjonalnie nazwę inputaa, opcjonalnie dodatkową wiadomość
 // dodaje nr błędu do tablicy z błędami i opis do tablicy z opisami
 public function addError(
  int $errorNumber,
  string $name=null,
  string $content=null
 )
 {
  parent::addError($errorNumber);
  if($name == null && $content == null){
   $this->errorMessage[] = ValidError::MESSAGE[$errorNumber];
  }
  else if ($name != null && $content == null){
   $this->errorMessage[] = $name.' '.ValidError::MESSAGE[$errorNumber];
  }
  else{
   $this->errorMessage[] = $name.' '.ValidError::MESSAGE[$errorNumber].' '.$content;
  }
 }// end addError()
 
}// end class ValidError


// klasa przechowująca i wyświetlająca błędy bazy danych
class DatabaseError extends MyError{}

?>

Abstrakcyjna klasa walidująca i rozpoznanie przesłanej wartości.

W linii 16 za pomocą statycznej metody ValidInput::createValidInput tworzymy obiekt jednej z pochodnych klas abstrakcyjnej klasy ValidInput przekazując wcześniej utworzony obiekt $input z danymi z formularza a także $validError, obiekt obsługujący błędy.

Metoda ta na podstawie nazwy formularza pobranej z obiektu $input tworzy i zwraca nowy obiekt $validInput klasy odpowiadającej za walidację konkretnego typu formularza. (np. ValidEmil lub ValidLogin)

Klasa ValidInput:

<?php
// abstrakcyjna klasa bazowa dla klas walidujących
abstract class ValidInput
{
 protected $value;// wartość obiektu przekazanego do walidacji
 protected $name;// nazwa obiektu przekazanego do walidacji
 public $error;// obiekt klasy ValidError przechowujący błędy walidacji
 
 
 
 // fabryka obiektów pochodnych klasy ValidInput
 // przyjmuje obiekt klasy Input, obiekt klasy ValidError
 // zwraca obiekt klasy pochodnej ValidInput
 public static function createValidInput(
  Input $input,
  ValidError $error
 ):ValidInput
 {
  $className = "Valid".$input->getName();
  return new $className($input->getValue(),$input->getName(),$error);
 }
 
 
 // konstruktor obiektów klasy Input
 // przyjmuje wartosć walidowanego inputa, nazwę, obiekt klasy ValidError
 // ustala składowe input i error
 protected function __construct(
  $value,
  string $name,
  ValidError $error
 ){
  $this->value = $value;
  $this->name = $name;
  $this->error = $error;
 }
 
 
 // metoda psrawdzająca przesłanie puteego formulaża
 // jeżeli formularz jest pusty lub wypełniony spacjami dodaje błąd i zwraca false
 // jeżeli formularz posiada zawartość zwraca true
 protected function checkBlank()
 {
  if(preg_match('/^[ ]+$/D',$this->value) || $this->value == ""){
   $this->error->addError(1);
   return false;
  }
  else return true;
 }// end checkBlank()


 abstract public function valid();// interfejs metod walidujących
 
}// end class Valid
?>

Jednak tym razem nie wiemy czy użytkownik wypełni formularz adresem email czy loginem a sam input nosi nazwę „Identity„(tożsamość), więc zanim przekażemy dane do walidacji musimy zdecydować co chcemy walidować.

Być może właśnie tutaj jest najsłabszy punkt mojej struktury ale nie zdołałem wymyślić nic lepszego więc rozwiązałem to w ten sposób:

Tworzymy nowy obiekt klasy ValidIdentity (dziedziczący po abstrakcyjnej klasie StringValid która jedynie dodaje cechy typowe dla stringów i dziedziczy po ValidInput).

Przy wywołaniu metody valid() na podstawie obecności (lub nie) w $this->value znaku „@” utworzy on nowy obiekt klasy Input z parametrami $this->value jako wartość, i Login bądź Email jako nazwa.

Następnie przekaże go do wywołania nowego tym razem już odpowiedniego obiektu klasy ValidEmail bądź ValidLogin i wywoła jego metodę valid() w między czasie usuwając niepotrzebny już obiekt $imput.

Klasa ValidIdentity:

<?php
// klasa wspólna dla loginu i adresu email
// powstaje w momencie kiedy formularz przyjmuje zarówno adres email jak i login
class ValidIdentity extends StringValid
{
 // metoda steryjąca dalszą walidacją
 // jeżeli formularz zawiera znak "@" przekazuje walidacje do obiektu klasy ValidEmail
 // jeżeli nie przekazuje walidację do obiektu ValidLogin
 public function valid(){
  if(strstr($this->value,"@")){
   $inp = Input::CreateInput($this->value,"Email");
  }
  else{
   $inp = Input::CreateInput($this->value,"Login");
  }
  $new = ValidInput::createValidInput($inp,$this->error);
  unset($inp);
  $new->valid();
 }
}
?>

Walidacja danych

I w ostatnim już kroku w linii 17 wykonujemy metodę valid() na obiekcie $validInput.

Interfejs do tej metody zawarliśmy już w abstrakcyjnej klasie ValidInput jednak każda klasa pochodna posiadać będzie jej indywidualną implementację gdyż praktycznie każdy input będziemy walidowali inaczej.

Wymagania odnośnie walidacji zaszyte są na stałe w poszczególnych klasach ponieważ np. adres email walidowany jest zarówno tutaj, jak i przy rejestracji, dodawaniu ogłoszenia czy edycji profilu, a zmieniając owe wymagania nie chcemy przecież zmieniać ich w kilkunastu miejscach prawda?

Klasy pochodne ValidInput i pośrednicząca między nimi abstrakcyjna klasa StringValid służąca jedynie do załączenia metod typowych dla stringów:

// abstrakcyjna klasa pośrednicząca, importująca cechy typowe dla stringów
// dziedziczy po ValidInput
abstract class StringValid extends ValidInput
{
 use StringType; 
}// end class StringValid

// klasa walidująca adres email odebrany z formulaża
// dziedziczy po ValidInput i StringValid
class ValidEmail extends StringValid
{
 // metoda wykonująca wszystkie poszczególne metody walidujące
 public function valid()
 {
  if($this->checkBlank()){
   $this->checkLength();
   $this->checkCharacters();
  }
 }// end valid()
 
 
 // metoda badająca długość adresu email
 // jeżeli adres jest zbyt krótki bądź długi dodaje błąd
 private function checkLength()
 {
  if($this->getLength() < 8 || $this->getLength() > 50){
   $this->error->addError(2,$this->name," od 8 do 50 znaków!");
  }
 }// end chceckLength()
 
 
 // metoda sprawdzająca poprawność znaków w adresie email
 // jeżeli znajdzie nieakceptowalne znaki dodaje błąd
 private function checkCharacters()
 {
  $pattern='/^[a-zA-Z0-9\.\-_]+\@[a-zA-Z0-9\.\-_]+\.[a-z]{2,4}$/D';
  if(!preg_match($pattern,$this->value)){
   $this->error->addError(3,$this->name);
  } 
 }// end chceckLength()
 
}// end class ValidEmail

//
class ValidLogin extends StringValid
{
 // metoda walidująca Login
 // wywołuje funkcje sprawdzające poszczególne wymagania
 public function valid()
 {
  if($this->checkBlank()){
   $this->checkLength();
   $this->checkCharacters();
  }
 }// end valid()
 
 
 // metoda sprawdzająca czy Login posiada odpowiednią długość
 // jeżeli jest zbyt krótki bądź długi, dodaje błąd
 private function checkLength()
 {
  if($this->getLength() < 3 || $this->getLength() > 20){
   $this->error->addError(2,$this->name," od 3 do 20 znaków!");
  }
 }// end chceckLength()
 
 
 // metoda sprawdzająca poprawność znaków w Loginie
 // jeżeli wykryje nieprawidłowe znaki dodaje błąd
 private function checkCharacters()
 {
  $pattern='/^[a-zA-Z0-9\_\-]+$/D';
  if(!preg_match($pattern,$this->value)){
   $this->error->addError(3,$this->name);
  } 
 }// end chceckLength()
 
}// end class ValidLogin

Metoda valid() w każdej klasie wykonuje wszystkie przekazane jej prywatne metody walidacyjne i na tym kończy się jej działanie.

Każda z metod walidacyjnych w przypadku wykrycia nieprawidłowości dodaje błąd do obiektu $error za pomocą $this->error->addError() ale nie przerywa działania skryptu.

Wykonuje dalszą walidację i ewentualnie dodaje kolejne błędy ponieważ jeżeli użytkownik poda login np.”as$” za krótki i w dodatku zawierający niedozwolony znak, to chcemy aby został poinformowany o tym od razu a nie po kolejnym wykonaniu skryptu.

Dlatego też nie skorzystałem z wbudowanej w PHP funkcji przechwytywania wyjątków ponieważ po wyłapaniu pierwszego wyjątku skrypt automatycznie się przerywa i kieruje sterowanie do klauzuli catch więc trzeba było by dodawać walidację także do klauzuli finally.

Sprawdzenie i wyświetlenie błędów

Na koniec (linia18) pozostało tylko sprawdzenie błędów za pomocą metody $validError->checkErrors() i kontynuowanie wykonania skryptu bądź dodanie obiektu z błędami do sesji i ponowne wyświetlenie strony z formularzem jeżeli owe błędy wystąpiły.

A do wyświetlania błędów służy właśnie ten kawałek kodu PHP o którym wspomniałem na początku. Sprawdzamy czy istnieje zmienna sesyjna z obiektem błędów, jeśli tak to wyświetlamy je za pomocą metody $_SESSION[‚validError’]->viewErrors()

Tak więc oto jest mój pierwszy kawałek kodu obiektowego. Pierwszy z którego jestem w miarę zadowolony i mogę go zaprezentować.

W następnej kolejności trzeba będzie jeszcze sprawdzić te dane w bazie danych, zapisać kod resetujący, wysłać maila z linkiem a później to zweryfikować i ustawić nowe hasło.

Tak że myślę że na tym można zakończyć i tak już nieco przydługi jak na tego mnie artykuł.

Mam nadzieję że opisałem wszystko w miarę jasno a jeżeli coś skopałem lub mógł bym zrobić lepiej to liczę na info w komentarzu. Dzięki!

 

13 komentarzy

Dodaj komentarz

Twój adres email nie zostanie opublikowany.