• Najnowsze pytania
  • Bez odpowiedzi
  • Zadaj pytanie
  • Kategorie
  • Tagi
  • Zdobyte punkty
  • Ekipa ninja
  • IRC
  • FAQ
  • Regulamin
  • Książki warte uwagi

Silnik walidatora w PHP OOP

VPS Starter Arubacloud
0 głosów
262 wizyt
pytanie zadane 8 czerwca 2017 w PHP przez Benek Szeryf (90,690 p.)

W Internecie można znaleźć wiele poradników dotyczących programowania OOP, brakuje mi jednak dobrych przykładów. Zapewne gdyby przejrzeć GitHuba, to można by znaleźć dużo wartościowego materiału. Trzeba jednak się przebijać przez całą aplikację, by cokolwiek zrozumieć. Brakuje mi małych projekcików, które można złożyć w przyszłości w całość. Z tego względu postanowiłem się przetestować i napisać silnik walidatora wraz z przykładowymi walidatorami i kontrolerem, który pozwala przetestować silnik. Całość celowo umieszczam w jednym pliku, aby każdy mógł sobie szybko skopiować źródła, wkleić do pliku z rozszerzeniem index.php i uruchomić przeglądarkę z localhostem.

<?php

// ---------- Mandatory components for validation classes ----------

interface ValidationInterface
{
    public function __construct(array $parameters);
    public function isValidated($value);   // return TRUE or FALSE
    public function getMessage();
}

abstract class ValidationAbstractClass implements ValidationInterface
{
    protected $message;

    public function getMessage()
    {
        return $this->message;
    }
}

// ----------------------- Engine -----------------------

class ValidatonEngine
{
    private $fields;
    private $constraintsForFields = array();
    private $errorMessage;

    public function setLabels($fieldsToSet)
    {
        $this->fields = $fieldsToSet;
    }

    public function setConstraints($fieldName, $constraintsForField)
    {
        $this->isValidationAbstractClassInherited($constraintsForField);
        $this->constraintsForFields[$fieldName] = $constraintsForField;
    }

    private function isValidationAbstractClassInherited($constraintClasses)
    {
        foreach ($constraintClasses as $className) {
            if ($className instanceof ValidationAbstractClass) {
                continue;
            } else {
                throw new ValidationAbstractClassNotInheritedException(get_class($className) . " must inherit ValidationAbstractClass");
            }
        }
    }

    public function setFields($fieldsValuesPairs)
    {
        $setFieldStatus = TRUE;

            foreach ($fieldsValuesPairs as $field => $value) {
                if (!$this->setField($field, $value)) {
                    $setFieldStatus = FALSE;
                    break;
                }
            }

        return $setFieldStatus;
    }

    public function setField($fieldName, $valueOfField)
    {
        $validFieldNameStatus = TRUE;
        $validSetFieldStatus = FALSE;

            foreach ($this->fields as $fields) {
                if ($fields == $fieldName) {
                    if ($this->validate($fieldName, $valueOfField)) {
                        $validSetFieldStatus = TRUE;
                    }
                    $validFieldNameStatus = FALSE;
                    break;
                }
            }

            if ($validFieldNameStatus) {
                throw new NoFieldInValidatorException("Field " . $fieldName . " wasn't define");
            }

        return $validSetFieldStatus;
    }

    private function validate($fieldName, $valueOfField)
    {
        $validFieldStatus = TRUE;

        foreach ($this->constraintsForFields as $field => $constraints) {
            if ($fieldName == $field) {
                foreach ($constraints as $constraint) {
                    $validFieldStatus = $constraint->isValidated($valueOfField);
                    if (!$validFieldStatus) {
                        $this->errorMessage = $constraint->getMessage();
                        $validFieldStatus = FALSE;
                        break 2;
                    }
                }
            }
        }

        return $validFieldStatus;
    }

    public function getErrorMessage()
    {
        return $this->errorMessage;
    }
}

// ----------------------- Validators -----------------------

class NotEmptyValidator extends ValidationAbstractClass
{
    public function __construct(array $inputParameters)
    {
        foreach ($inputParameters as $key => $value) {
            if ($key == "notEmptyMessage") {
                $this->message = $value;
            } else {
                throw new IncorrectKeywordInValidatorException($key . " is not a keyword in the " . get_class($this) . " class");
            }
        }
    }

    public function isValidated($value)
    {
        return !empty($value);
    }
}

class LengthValidator extends ValidationAbstractClass
{
    private $minimumOfLength;
    private $maximumOfLength;

    public function __construct(array $inputParameters)
    {
        foreach ($inputParameters as $key => $value) {
            if ($key == "minLength") {
                $this->minimumOfLength = $value;
            } else if ($key == "maxLength") {
                $this->maximumOfLength = $value;
            } else if ($key == "lengthMessage") {
                $this->message = $value;
            } else {
                throw new IncorrectKeywordInValidatorException($key . " is not a keyword in the " . get_class($this) . " class");
            }
        }
    }

    public function isValidated($value)
    {
        $len = strlen($value);
        return ($len >= $this->minimumOfLength) && ($len <= $this->maximumOfLength) ? TRUE : FALSE;
    }
}

// ----------------------- Exceptions -----------------------

class IncorrectKeywordInValidatorException extends \Exception
{
}

class ValidationAbstractClassNotInheritedException extends \Exception
{
}

class NoFieldInValidatorException extends \Exception
{
}

// ----------------------- Controller -----------------------

$validator = new ValidatonEngine();

$validator->setLabels(array(
    'username',
    'id'
));

try {
    $validator->setConstraints('username', array(
        new NotEmptyValidator(array(
            'notEmptyMessage' => 'Field username must be filled out'
        )),
        new LengthValidator(array(
            'minLength' => 3,
            'maxLength' => 25,
            'lengthMessage' => "Field username must contain 3-25 letters"
        ))
    ));

    $validator->setConstraints('id', array(
        new NotEmptyValidator(array(
            'notEmptyMessage' => "You must type your ID"
        )),
        new LengthValidator(array(
            'minLength' => 10,
            'maxLength' => 10,
            'lengthMessage' => "Field ID must consist of 10 integers"
        ))
    ));
} catch (IncorrectKeywordInValidatorException $err) {
    echo $err->getMessage();
} catch (ValidationAbstractClassNotInheritedException $err) {
    echo $err->getMessage();
}

try {
    if (!$validator->setFields(array(
        'username' => 'John',   // not validated: 'Jo'
        'id' => '9876543210'    // not validated: '123'
    ))
    ) {
        echo $validator->getErrorMessage();
    }
} catch (NoFieldInValidatorException $err) {
    echo $err->getMessage();
}

Chętnie zbiorę krytyczne uwagi od bardziej doświadczonych kolegów, bo ja sam nie programuję w PHP na co dzień i pisałem go kilka godzin. Jeśli ktoś ma ochotę, to może sobie ten kod podpiąć do swoich projektów.

komentarz 8 czerwca 2017 przez efiku Szeryf (75,160 p.)
Ja na chwilę obecną nie mam jak przysiąść do tego, ale zerknij na Vaildator od Symfony ;)

2 odpowiedzi

+1 głos
odpowiedź 8 czerwca 2017 przez CzikaCarry Szeryf (75,340 p.)

Ogólnie spoko, ale kilka uwag:

  • Zamiast używania tablicy jako argumentu dla konstruktora walidatorów możesz stworzyć jakaś klasę np. parameters, stworzyć tam publiczne zmienne, ustawić wartości obiektu, i podać jako parametr do konstruktora walidatora. Potem poszczególny walidator sprawdzałby wartość zmiennych która go interesuje. Ma to taką zaletę, że większość IDE od razu podpowie jakie pola w obiekcie możemy zmienić, a zatem dla osoby używającej tego jest to wielkim udogodnieniem, bo nie musi się za bardzo zagłębiać w kod samego walidatora. Poza tym według mnie trochę bardziej elegancko to wygląda, ale to kwestia gustu.
  • Polecam używać mb_strlen* zamiar strlen, bo można tą funkcję łatwo oszukać. *w drugim parametrze musisz zdefiniować kodowanie, np UTF-8, aby nie dało się oszukać :)
+1 głos
odpowiedź 8 czerwca 2017 przez Ehlert Ekspert (212,630 p.)
edycja 9 czerwca 2017 przez Ehlert

Oj narobiłeś się trochę bez sensu. Nie chodzi mi o to, że to co napisałeś jest złe. Po prostu bez sensu jest wymyślać koło jeszcze raz.

Naucz się korzystać z Composera i masz dostęp do ogromnej ilości istniejących walidatorów począwszy od tych z frameworków kończąc na autorskich.

To co udostępnia Github, packagist itp to piękny przykład na to, że kod napisany raz może być wykorzystywany setki tysięcy razy. Warto o tym pamiętać.

EDIT, Code Review:

  1. Key words piszemy lowercasem.
  2. ValidationAbstractClassNotInheritedException jest całkowicie zbędny. Od PHP w wersji 5.0.0 mamy typowanie argumentów, dzięki czemu można wywalić całą funkcję isValidationAbstractClassInherited
  3. Tworzenie instancji walidatorów na podstawie tablic jest kiepskim pomysłem. Musisz się martwić o poprawną zawartość tablicy i rzucać exceptionami. Lepiej po prostu dać mu do konstruktora 3 argumenty i już. Po co kombinować.
  4. Nazewnictwo: setFields z nazwy ma ustawiać wartości pól. Tymczasem uruchamia wszystkie walidatory.
  5. Jak na kod którego mógłbym użyć we własnym projekcie to brakuje dokumentacji, która mogłaby mnie uświadomić co czyni między innymi funkcja setFields
  6. Ogólnie nie podoba mi się idea fieldNamesów i dodawania w locie tych ograniczeń dla konkretnych typów. Gdybym korzystał z tego kodu i pisał jakiś system do rozliczeń podatkowych to plik w którym przychodzi formularz miałby z 2000 linijek.  
komentarz 8 czerwca 2017 przez Benek Szeryf (90,690 p.)
edycja 9 czerwca 2017 przez Benek

Oj narobiłeś się trochę bez sensu. Nie chodzi mi o to, że to co napisałeś jest złe. Po prostu bez sensu jest wymyślać koło jeszcze raz.

Nie mogę się z Tobą zgodzić, bo:

  • napisałem kod po to, by zrozumieć interfejsy i klasy abstrakcji, czyli w celach edukacyjnych
  • chciałbym prosić doświadczonych programistów o uwagi
  • umiem korzystać z Composera, nawet za jego pomocą udostępniam swój większy projekt
  • mam konto na Githubie i kilka większych repozytoriów. Doskonale wiem do czego służy ten serwis. Nie udostępniam tylko tego na tym forum, bo to wąska dziedzina z której tutaj nikt nie korzysta :)

Poza tym przedstawiłem kompletny produkt, to nie jest zapowiedź stworzenia super-hiper-mega serwisu, z którego i tak nic nie wyjdzie. Myślę, że nie ma się co czepiać. W końcu każdy kiedyś zaczynał od Hello World!

komentarz 9 czerwca 2017 przez Ehlert Ekspert (212,630 p.)

Napisałem co myślę po przejrzeniu kodu smiley

komentarz 9 czerwca 2017 przez Benek Szeryf (90,690 p.)
edycja 9 czerwca 2017 przez Benek

Super, bardzo dziękuję za CR :) Przeanalizowałem to, co napisałeś i pozwolę sobie odpowiedzieć.

Key words piszemy lowercasem.

Kiedyś widziałem taką wypowiedź na SO, że niektórzy używają wielkich liter, by podkreślić, że są to stałe. Widzę jednak teraz w PSR-2, że faktycznie te słowa powinny być pisane małymi literami.

ValidationAbstractClassNotInheritedException jest całkowicie zbędny. Od PHP w wersji 5.0.0 mamy typowanie argumentów, dzięki czemu można wywalić całą funkcję isValidationAbstractClassInherited.

Nie bardzo wiem jak rozwiązać ten problem. Mógłbyś napisać kawałek pseudokodu? Zadaniem tej funkcji jest wymuszenie, by kolejne klasy walidatorów dziedziczyły klasę abstrakcyjną i jednocześnie implementowały interfejs. Jeśli klasa walidatora tego nie zrobi, silnik walidatora zrzuci wyjątek.

Tworzenie instancji walidatorów na podstawie tablic jest kiepskim pomysłem. Musisz się martwić o poprawną zawartość tablicy i rzucać exceptionami. Lepiej po prostu dać mu do konstruktora 3 argumenty i już. Po co kombinować.

Nawet jeśli obiekt dowolnego walidatora przyjmuje 10 argumentów, to będzie to dobre rozwiązanie? Czy wtedy programista połapie się, na którym miejscu powinien stać jaki argument? W przypadku tablic asocjacyjnych ten problem znika. Wyjątki się pojawią tylko w przypadku błędu programisty, który nie sprawdził klasy walidatora. Lepiej zawczasu rzucić wyjątkiem niż głowić się, czy sekwencja 10 argumentów ma poprawną kolejność.

Nazewnictwo: setFields z nazwy ma ustawiać wartości pól. Tymczasem uruchamia wszystkie walidatory.

Chodziło o ustawienie pól formularza. Rozmyślałem o tej nazwie. Na pewno lepiej to brzmi niż fillOutFields, bo to użytkownik wypełnia formularze, a nie silnik walidatora. Zwróć uwagę, że z poziomu kontrolera to jest ustawianie pól: wpisanie par klucz-wartość (za wartość może być podstawiona zmienna) do metody setFields(). Mamy więc ustawienie pól i jednoczesne sprawdzenie, czy wstawiane wartości są poprawne (metoda uruchamia walidatory). Mogę oczywiście dla zmiennej $validator  w kontrolerze uruchamiać metodę validate(), ale wtedy:

  • metoda validate() musi być publiczna (łamanie enkapsulacji)
  • w kontrolerze dochodzi kolejna linia uruchamiająca metodę validate()

Jak na kod którego mógłbym użyć we własnym projekcie to brakuje dokumentacji, która mogłaby mnie uświadomić co czyni między innymi funkcja setFields

To prawda. Zwłaszcza, że było najpierw samo setField(), w celu ustawienia jednego tylko pola formularza. Potem się połapałem, że warto zrobić ogólniejszą metodę setFields() i przed wysłaniem kodu na forum zapomniałem ustawić setField() jako prywatną.

Ogólnie nie podoba mi się idea fieldNamesów i dodawania w locie tych ograniczeń dla konkretnych typów. Gdybym korzystał z tego kodu i pisał jakiś system do rozliczeń podatkowych to plik w którym przychodzi formularz miałby z 2000 linijek.  

Cieszę się, że poruszyłeś ten wątek, bo po kodzie może od razu tego nie widać. To jest właśnie uniwersalność walidatora. Każde pole formularza może być walidowane na różne sposoby. Na przykład pole id może mieć dodany kolejny walidator OnlyNumbersValidator(), ale pole username już go nie będzie mieć (w końcu to pole może zawierać litery). Dodawanie tych ograniczeń w locie jest konieczne. Jak sobie wyobrażasz rozpoznanie, które pole jak ma być walidowane? Możesz to zrobić tylko poprzez dodanie odpowiednich obiektów walidujących przed użyciem formularza. Podsumowując, zadaniem programisty, który podpina walidator do swojego projektu, jest ustawienie odpowiednich ograniczeń dla wybranych pól formularza (długość argumentu, brak pustego pola, występowanie tylko liczb całkowitych, występowanie liczb większych od 18 (pełnoletniość) itd.), a także sprecyzowanie każdego ograniczenia (na przykład ktoś może chcieć zdefiniować pełnoletniość przy osiągnięciu dopiero 21, a nie 18 lat).

Podobne pytania

+1 głos
1 odpowiedź 177 wizyt
pytanie zadane 11 lutego 2022 w PHP przez ZnaQu Nowicjusz (130 p.)
0 głosów
0 odpowiedzi 96 wizyt
pytanie zadane 26 kwietnia 2020 w PHP przez creend Gaduła (4,700 p.)
0 głosów
1 odpowiedź 271 wizyt
pytanie zadane 16 grudnia 2019 w PHP przez Piotr Jarema Użytkownik (970 p.)

92,417 zapytań

141,222 odpowiedzi

318,984 komentarzy

61,831 pasjonatów

Motyw:

Akcja Pajacyk

Pajacyk od wielu lat dożywia dzieci. Pomóż klikając w zielony brzuszek na stronie. Dziękujemy! ♡

Oto polecana książka warta uwagi.
Pełną listę książek znajdziesz tutaj.

Akademia Sekuraka

Akademia Sekuraka 2024 zapewnia dostęp do minimum 15 szkoleń online z bezpieczeństwa IT oraz dostęp także do materiałów z edycji Sekurak Academy z roku 2023!

Przy zakupie możecie skorzystać z kodu: pasja-akademia - użyjcie go w koszyku, a uzyskacie rabat -30% na bilety w wersji "Standard"! Więcej informacji na temat akademii 2024 znajdziecie tutaj. Dziękujemy ekipie Sekuraka za taką fajną zniżkę dla wszystkich Pasjonatów!

Akademia Sekuraka

Niedawno wystartował dodruk tej świetnej, rozchwytywanej książki (około 940 stron). Mamy dla Was kod: pasja (wpiszcie go w koszyku), dzięki któremu otrzymujemy 10% zniżki - dziękujemy zaprzyjaźnionej ekipie Sekuraka za taki bonus dla Pasjonatów! Książka to pierwszy tom z serii o ITsec, który łagodnie wprowadzi w świat bezpieczeństwa IT każdą osobę - warto, polecamy!

...