Przejdź do głównej treści

Walidator


Walidator

Walidacja danych jest nie odłączną częścią platformy Read_. Dane wprowadzone przez Dialogi (formularze), czy też za pomocą API wymagają weryfikacji poprawności. Dane muszą być sprawdzone przed zapisaniem ich w bazie lub przekazaniem do innej usługi.

Za pomocą biblioteki symfony/validatoropen in new window stworzyliśmy komponent \Ready\Component\System\Validator, dzięki któremu możemy w prosty sposób stworzyć dedykowaną klasę walidatora do obsługi dedykowanego obiektu.

Tworzenie klasy walidatora

Aby móc użyć tego mechanizmu, musimy najpierw utworzyć klasę, która jeśli operuje na obiektach typu Bean to rozszerza się ją o \Ready\Component\System\Validator\BeanValidator w przeciwnym wypadku o \Ready\Component\System\Validator\ArrayValidator.

Klasy te dostarczają już cały mechanizm walidacji. Po stronie developera pozostaje tylko skonfigurowanie reguł, które później będą sprawdzane.

Zacznijmy od przykładu, który w dalszej części tego artykułu omówimy.

use Ready\Component\System\Validator\BeanValidator;
use Ready\Component\System\Validator\Metadata\MetadataInterface;
use Ready\Component\System\Validator\Metadata\ArrayMetadata;
use LoggedUser;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\NotNull;
use Ready\Component\System\Validator\Constraint\ContainsAlphanumeric;
use Ready\Component\System\Validator\Constraint\SqlRecord;
use Symfony\Component\Validator\Constraints\Type;
use Ready\Component\System\Validator\Constraint\EmployeeExist;
use Ready\Component\System\Validator\Constraint\RecordFromUsefulDatasetsExist;
use Ready\Component\System\Validator\Constraint\RecordFromSearchEngineExist;
use Symfony\Component\Validator\Constraints\Date;
use Symfony\Component\Validator\Constraints\GreaterThan;
use Symfony\Component\Validator\Constraints\CssColor;
use Symfony\Component\Validator\Constraints\Regex;

class Validator extends BeanValidator {

    /**
     * @inheritDoc
     */
    public function configure(array $contextData): MetadataInterface {
        $primaryKeyValue = $contextData['projid'] ?? NULL;

        $conditions = 'is_del IS NOT TRUE AND ent_id = '.LoggedUser::getEntityID();
        if ($primaryKeyValue) {
            $conditions .= ' AND projid != '.(int)$primaryKeyValue;
        }
        /** @var string $start_ */
        $start_ = $contextData['start_'] ?? SYSDATE;

        $metadata = new ArrayMetadata();
        $metadata->addConstraintsToField('projnm', TRUE, FALSE, [
            new NotBlank(),
            new NotNull(),
            new ContainsAlphanumeric(),
            new SqlRecord([
                'tableName'            => 'projects',
                'conditionFieldName'   => 'projnm',
                'conditionFieldType'   => SqlRecord::FIELD_TYPE_STRING_REGEX,
                'additionalConditions' => $conditions,
                'recordExist' => FALSE,
                'message' => \Translator::translate('Projekt o takiej nazwie {{ value }} już istnieje.'),
            ]),
        ]);
        $metadata->addConstraintsToField('number', TRUE, FALSE, [
            new NotBlank(),
            new NotNull(),
            new Regex([
                'pattern' => '/^\d[\d-]*\d$/',
                'match' => TRUE,
                'message' => \Translator::translate('Numer projektu zawiera nie dozwolone znaki (0-9)')
                ]),
            new SqlRecord([
                'tableName'            => 'projects',
                'conditionFieldName'   => 'number',
                'conditionFieldType'   => SqlRecord::FIELD_TYPE_STRING_REGEX,
                'additionalConditions' => $conditions,
                'recordExist' => FALSE,
                'message' => \Translator::translate('Projekt o takim numerze {{ value }} już istnieje.'),
            ]),
        ]);
        $metadata->addConstraintsToField('coorid', FALSE, FALSE, [
            new NotNull(),
            new Type(['type' => 'int']),
            new EmployeeExist(),
        ]);
        $metadata->addConstraintsToField('cadfid', FALSE, FALSE, [
            new NotNull(),
            new Type(['type' => 'int']),
            new RecordFromUsefulDatasetsExist(['className' => \CalendarsDefDataSet::class])
        ]);
        $metadata->addConstraintsToField('is_fix', FALSE, FALSE, [
            new NotNull(),
            new Type(['type' => 'bool']),
        ]);
        $metadata->addConstraintsToField('dscrpt', FALSE, FALSE, [
            new ContainsAlphanumeric(),
        ]);
        $metadata->addConstraintsToField('contid', FALSE, FALSE, [
            new Type(['type' => 'int']),
            new RecordFromSearchEngineExist(['className' => \ContactSearchEngine::class, 'fieldName' => 'contid']),
        ]);
        $metadata->addConstraintsToField('prior_', FALSE, FALSE, [
            new Type(['type' => 'int']),
        ]);
        $metadata->addConstraintsToField('start_', TRUE, FALSE, [
            new Type(['type' => 'string']),
            new Date(),
        ]);
        $metadata->addConstraintsToField('end___', FALSE, FALSE, [
            new Type(['type' => 'string']),
            new Date(),
            new GreaterThan(['value' => $start_]),
        ]);

        $metadata->addConstraintsToField('colour', FALSE, FALSE, [
            new Type(['type' => 'string']),
            new CssColor(['formats' => [CssColor::HEX_LONG_WITH_ALPHA, CssColor::HEX_SHORT_WITH_ALPHA]]),
        ]);



        return $metadata;
    }
}

Zadaniem developera jest zaimplementowanie metody configure

public function configure(array $contextData): MetadataInterface;

Metoda ta jako argument przyjmuje argument $contextData, który zawiera w sobie dane do zwalidowania. Na tym etapie możemy te dane wykorzystać, aby zwalidować pola od siebie zależne. Na przykład, aby data początkowa była wcześniejsza niż data końcowa.

$metadata->addConstraintsToField('end___', FALSE, FALSE, [
    new Type(['type' => 'string']),
    new Date(),
    new GreaterThan(['value' => $start_]),
]);

Zadaniem metody configure jest zwrócenie obiektu implementującego interfejs \Ready\Component\System\Validator\Metadata\MetadataInterface, który zawiera w sobie zestaw reguł dla poszczególnych pól. W tym celu posłużymy się klasą \Ready\Component\System\Validator\Metadata\ArrayMetadata, która ten interfejs implementuje oraz dostarcza nam następujące metody:

  • addConstraintsToField(string $fieldName, bool $createObjectMandatory, bool $modifyObjectMandatory, array $constraints): void - metoda ta pozwala nam dodać reguły do pola (tworzenie i modyfikacja obiektu). Do metody możemy przekazać następujące argumenty:

    • string $fieldName - Nazwa pola, do którego chcemy dodać reguły.
    • bool $createObjectMandatory - Czy pole wymagane jest podczas tworzenia obiektu
    • bool $modifyObjectMandatory - Czy pole wymagane jest podczas modyfikacji obiektu
    • array $constraints - Lista reguł, które mają zostać sprawdzone podczas walidacji obiektu
  • addFieldConstraintsForCreateObject(string $fieldName, bool $mandatory, array $constraints): void - metoda ta pozwala nam dodać reguły do pola (tylko tworzenie obiektu). Do metody możemy przekazać następujące argumenty:

    • string $fieldName - Nazwa pola, do którego chcemy dodać reguły.
    • bool $mandatory - Czy pole jest wymagane
    • array $constraints - Lista reguł, które mają zostać sprawdzone podczas walidacji obiektu
  • addFieldConstraintsForModifyObject(string $fieldName, bool $mandatory, array $constraints): void - metoda ta pozwala nam dodać reguły do pola (tylko modyfikacji obiektu). Do metody możemy przekazać następujące argumenty:

    • string $fieldName - Nazwa pola, do którego chcemy dodać reguły.
    • bool $mandatory - Czy pole jest wymagane
    • array $constraints - Lista reguł, które mają zostać sprawdzone podczas walidacji obiektu
  • createFieldAsNestedArray(string $fieldName, bool $createObjectMandatory = false, bool $modifyObjectMandatory = false): NestedArray - metoda ta pozwala dodać prostą tablicę podrzędną, która zwraca obiekt NestedArray. Obiekt ten pozwala na dodawanie do siebie pól oraz kolejnych tablic (wszystkie metody opisane w tym rozdziale). Po dodaniu wszystkich pól na koniec pracy z tym obiektem trzeba wywołać metodę addField(): void

  • createFieldAsCollectionArray(string $fieldName, bool $createObjectMandatory = false, bool $modifyObjectMandatory = false): NestedArray - metoda ta pozwala dodać kolekcję tablic podrzędnych, która zwraca obiekt ArrayCollection. Obiekt ten pozwala na dodawanie do siebie pól oraz kolejnych tablic (wszystkie metody opisane w tym rozdziale). Po dodaniu wszystkich pól na koniec pracy z tym obiektem trzeba wywołać metodę addField(int $minimalAmount = 1): void

Lista dostępnych reguł

Dostarczone przez bibliotekę symfony/validator

Biblioteki symfony dostarcza następujące reguły:

Podstawowe ograniczenia

Ograniczenia ciągów tekstowych

Ograniczenia porównań

Ograniczenia liczbowe

Ograniczenia daty/czasu

Ograniczenia wyboru

Ograniczenia plikowe

Ograniczenia finansowe i inne ograniczenia liczbowe

Inne ograniczenia

Dostarczone przez platformę Ready_

Tworzenie własnych reguł

Aby stworzyć swoją własną regułę, musimy utworzyć we wspólnym katalogu dwie klasy ContainsAlphanumeric oraz ContainsAlphanumericValidator. Całość w przestrzeni nazw prezentowałą by się następująco:

- \ReadyApp\MyOwnApp\Model\Validator\Constraint\ContainsAlphanumeric
- \ReadyApp\MyOwnApp\Model\Validator\Constraint\ContainsAlphanumericValidator

Pierwsza klasa jest definicją reguły, która dziedziczy po \Symfony\Component\Validator\Constraint, natomiast druga to walidator tej reguły, który z koleji dziedziczy po \Symfony\Component\Validator\ConstraintValidator.

Definicja klasy reguły

Zacznijmy od przykładu

use Symfony\Component\Validator\Constraint;

class ContainsAlphanumeric extends Constraint {

    public string $message;
    public bool   $allowPolishLetters = TRUE;
    public bool   $allowWhitespace    = TRUE;

    /**
     * @inheritDoc
     */
    public function __construct($options = NULL, array $groups = NULL, $payload = NULL) {
        $this->message = \Translator::translate('Ciąg znaków "{{ string }}" zawiera nie dozwolone znaki. Możesz tylko używać znaków alfanumerycznych.');

        parent::__construct($options, $groups, $payload);
    }
}

Wszystkie opcje, jakie chcemy udostępnić do konfiguracji reguły, muszą być publicznymi właściwościami klasy.

Definicja klasy reguły walidatora

Zacznijmy od przykładu.

use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Exception\UnexpectedValueException;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;

class ContainsAlphanumericValidator extends ConstraintValidator {

    public function validate($value, Constraint $constraint): void
    {
        if (!$constraint instanceof ContainsAlphanumeric) {
            throw new UnexpectedTypeException($constraint, ContainsAlphanumeric::class);
        }

        // custom constraints should ignore null and empty values to allow
        // other constraints (NotBlank, NotNull, etc.) to take care of that
        if (null === $value || '' === $value) {
            return;
        }

        if (!is_string($value)) {
            // throw this exception if your validator cannot handle the passed type so that it can be marked as invalid
            throw new UnexpectedValueException($value, 'string');
        }

        $pattern = '/^[a-zA-Z0-9';
        if ($constraint->allowWhitespace) {
            $pattern .= '\s';
        }

        if ($constraint->allowPolishLetters) {
            $pattern .= 'ąćęłńóśżź';
        }

        $pattern .= ']+$/';

        if (preg_match($pattern, $value)) {
            return;
        }

        $violation = $this->context->buildViolation($constraint->message);
        $violation->setParameter('{{ string }}', $value);
        $violation->addViolation();
    }
}

Podsumowanie

W ten oto sposób w bardzo szybki sposób napisaliśmy własną regułę, która będzie nam sprawdzać, czy w ciągu znaków znajdują się znaki alfanumeryczne.

Należy również zauważyć, że rzucamy wyjątki tylko w sytuacji, kiedy jest ewidentny błąd konfiguracji.