24 мая 2011 г.

Zend, Yii, Symfony at validation example

Существует много разных фреймворков, ориентированных на web направленность. По большей части, вся суть web-сайтов состоит в общении с пользователем - причем в двух направлениях
  • Система отдает данные пользователю - например, новости.
  • Система получает данные пользователя - скажем, личные данные для магазина.

К сожалению, люди не идеальны, и не всегда указывают нужную информацию. Есть хороший рабочий принцип - не доверять данным от пользователя. Для такого "недоверия" принято использовать валидацию данных.

Итак, начнем. Опишем класс:
class Customer
{
    public $firstName;
    public $lastName;
    public $age;
    public $gender;
}

Допустим, что нужно обязательно запомнить lastName, $age должен быть больше 18 и gender нужно указать обязательно, и он может быть либо "male", либо "female". Как это сделать? Попробуем решение в лоб:
class SomeController
{
    public function actionBuy()
    {
        $customer = $this->getCustomer();
        if(
            !is_empty($customer->lastName)
            && !is_empty($customer->age) 
            && $customer->age > 18
            && !is_empty($customer->gender) 
            && in_array($customer->gender, array("male", "female"))
        )
        {
            echo "Correct!";
        }
        else
        {
            echo "Invalid!";
        }
    }
}

Отстойно! Что будет, если потребуется где-то еще провалидировать обьект? И как получить список ошибок? Сделаем для начала классический рефакторинг - вынесем этот ужасный огромный if() прямо в модель, которую валидируем:
class Customer
{
    ...
    public function validate()
    {
        $violations = array();
        if(is_empty($this->lastName))
            $violations[] = "Last name required";
        if(is_empty($this->age)
            $violations[] = "Age required";
        if(is_empty($this->gender))
            $violations[] = "Gender required";
        if(!in_array($this->gender, array("male", "female")))
            $violations[] = "Invalid gender";
    }
}

Теперь уже намного лучше. Как минимум, вся валидация скрыта от контроллера, и мы можем получить список ошибок, при желании даже сделать интернационализацию!
class SomeController
{
    public function actionBuy()
    {
        $customer = $this->getCustomer();
        $errors = $customer->validate();
        if(count($errors) == 0)
        {
            echo "Correct!";
        }
        else
        {
            echo "Invalid! list on errors:";
            foreach($errors as $error) echo _($error);
        }
    }
}

Но проблемы решены не все. Что делать, если логика валидации усложняется? Что делать, если паттерны проверок дублируются? А как же еще не раздувать код метода валидации?

Приходит понимание, что модель начинает содержать слишком много логики, да еще в некотором роде сервисной - мы же описали, какие поля нужны! Это можно назвать контрактом на использование кода. Для решения таких сложностей, нужно поступить очень просто.

Отказаться от валидации данных в модели. Это должен делать валидатор.

Отлично, вот оно решение. Но правильный программист - ленивый программист, следовательно попробуем поискать готовое решение.

1) Yii framework - очень быстрый, удобный фреймфорк со своим валидатором. Он бы нам помог, когда мы выносили валидацию в саму модель - в принципе так и сделано в этом фреймворке, модель хранит конфигурацию для валидации данных, сама запускает валидатор, сама заботится о получении списка ошибок и т.п. Все делает сама. К сожалению, модель является фабрикой, конфигом и конфигуратором для компонента валидации, в добавок еще и использует сама. Это работает, но как выяснили, далеко не во всех ситуациях.

2) Zend framework. Действительно, почему бы и нет? Лично я не выбрал его, так как для него приходится много допиливать в реальной работе - в пакете нет декларативного конфига, в котором можно явно указать, как валидировать тот или иной класс.

3) Symfony 2 validation component - вот то, что пришлось мне по вкусу! Это validation framework, c декларативным конфигом, с большой возможностью расширения существующего функционала, и кроме прочего, основан на спецификации jsr303

Перепишем немного код на использование symfory

class Customer
{
    public $firstName;
    public $lastName;
    public $age;
    public $gender;
}

class SomeController
{
    public function actionBuy()
    {
        $customer = $this->getCustomer();
        $errors = $this->getValidator()->validate($customer);
        if(count($errors) == 0)
        {
            echo "Correct!";
        }
        else
        {
            echo "Invalid! list on errors:";
            foreach($errors as $error) echo _($error->getMessage());
        }
    }
}

Для того, чтобы использовать этот компонент, нужен дистрибутив symfony 2, и в bootstrap файле вашего приложения подключить библиотеку и инициализировать ее при помощи конфига. Наш конфиг в формате yaml может выглядеть так:
Customer:
  properties:
    lastName:
      - NotBlank: ~
    age:
      - NotBlank: ~
      - Type: { type: "numeric" }
      - Min: { limit: 18 }
    gender:
      - NotBlank: ~
      - Choice: { choices: ["male", "female"]}

Что получили: модель предоставляет контракт на использование, но понятия не имеет, как себя использовать, если что не так - кидает ошибку. В архитектуре не заложено, как именно валидировать обьект, точнее - логика домена не содержит код, который ее прямо не касается, такой как валидация. Вся логика, связанная с обсуживанием модели, вынесена за ее пределы и может как угодно видоизменяться и расширяться - например, мы можем описать свой валидатор с хитрозаумными правилами, и использовать его где хочется.

Итог - низкая связанность кода, прозрачное изменение компонентов, масштабирование логики и отсутствие избыточного кода, логику которого перенесли в декларативную часть - в конфиг.

1 комментарий: