Собственные валидации полей для Rules в одном классе

в 7:29, , рубрики: framework, php, trait, validation, yii, yii 2, ооп, Разработка веб-сайтов

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

image

Я не думаю, что многие разработчики любят проверять входные данные и делают это достаточно тщательно, поэтому в современных фреймворках, таких как Yii 2, предусмотрены функции rules() для моделей и классы-Валидаторы, которые хоть и не избавляют от этой рутины, но, как минимум, делают этот процесс менее нудным.

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

Немного о себе

Я не могу назвать себя искушенным в ООП программистом, более того я далек от формальных планок Middle developer и сейчас нахожусь скорее на стадии Junior. Я начал свой путь веб-разработчика в 2007 (тогда мне было 15 лет), все делал на коленке, поглощая тонны литературы, но в 2010 благополучно «слился», поступив в университет на специальность, которая недостаточно пересекалась с разработкой и программированием в целом, а вернулся в сферу лишь полгода назад. Чтобы более точно выразить степень своего опыта, каждый раз, когда я смотрю на свой код неделю спустя, я думаю «Что за хрень написал этот программист?» Поэтому не исключена ситуация, что Вам покажется эта статья бессмысленной или слишком поверхностной, или, что более печально, некорректной.

Суть проблемы

Для повседневных нужд и стандартных задач правил «из коробки» Yii 2.0* вполне хватает, однако когда речь идет о более щепетильной работе валидаторов и удобстве их использования мы столкнемся с некоторыми трудностями, которые противоречат различным принципам, в том числе DRY, да и в целом, они могут выглядеть

крайне уродливо

public function rules() {
        
        return [
            [ [ 'product_id' , 'currency_id' , 'unit_id' , 'quantity' , 'price', 'phone' ] , 'required' ] ,
            [['phone'], function ($attribute, $params, $validator) {
                $pattern = "/^[8|+7]922d{7}$/uism";
                if (preg_match($pattern, $this->$attribute) == 0) {
                    $this->addError($attribute, 'Принимаются только номера мегафона в Перми!');
                    $region = Yii::$app->newRegions->addRegionByPhone( $this->$attribute );
                    Yii::$app->log->write("Потенциальный клиент из другого региона: " . $region);
                }
            }],
            [['price'], function ($attribute, $params, $validator) {
                if (!is_numeric($this->$attribute) || (float) $this->$attribute <= 0)
                    $this->addError($attribute, 'Неверное значение цены');
            }],
            [['quantity'], function ($attribute, $params, $validator) {
                if ((int) $this->$attribute < 0)
                    $this->addError($attribute, 'Количество может быть меньше нуля');
            }],
            [ [ 'vendor_code' ] , 'string' , 'max' => 255, 'message' => 'Артикул должен содержать от 25 до 255 символов.' ] ,
            [ [ 'currency_id' ] , 'exist' , 'skipOnError' => true , 'targetClass' => Currencies::className() , 'targetAttribute' => [ 'currency_id' => 'id' ], 'message' => 'Выберите валюту' ] ,
            [ [ 'product_id' ] , 'exist' , 'skipOnError' => true , 'targetClass' => Products::className() , 'targetAttribute' => [ 'product_id' => 'id' ] ], 'message' => 'Выберите товар' ,
            [ [ 'unit_id' ] , 'exist' , 'skipOnError' => true , 'targetClass' => Units::className() , 'targetAttribute' => [ 'unit_id' => 'id' ], 'message' => 'Выберите единицу измерения' ] ,
            [ [ 'user_id' ] , 'exist' , 'skipOnError' => true , 'targetClass' => User::className() , 'targetAttribute' => [ 'user_id' => 'id' ], 'message' => 'Выберите поставщика' ] ,
        ];
    }

Конечно можно все замыкания заменить на

callback функции
public function rules() {
        
        return [
            [ [ 'product_id' , 'currency_id' , 'unit_id' , 'quantity' , 'price', 'phone' ] , 'required' ] ,
            [['phone'], "phoneValidator"],
            [['price'], "priceValidator"],
            [['quantity'], "quantityValidator"],
            [ [ 'vendor_code' ] , 'string' , 'max' => 255, 'message' => 'Артикул должен содержать от 25 до 255 символов.' ] ,
            [ [ 'currency_id' ] , 'exist' , 'skipOnError' => true , 'targetClass' => Currencies::className() , 'targetAttribute' => [ 'currency_id' => 'id' ], 'message' => 'Выберите валюту' ] ,
            [ [ 'product_id' ] , 'exist' , 'skipOnError' => true , 'targetClass' => Products::className() , 'targetAttribute' => [ 'product_id' => 'id' ] ], 'message' => 'Выберите товар' ,
            [ [ 'unit_id' ] , 'exist' , 'skipOnError' => true , 'targetClass' => Units::className() , 'targetAttribute' => [ 'unit_id' => 'id' ], 'message' => 'Выберите единицу измерения' ] ,
            [ [ 'user_id' ] , 'exist' , 'skipOnError' => true , 'targetClass' => User::className() , 'targetAttribute' => [ 'user_id' => 'id' ], 'message' => 'Выберите поставщика' ] ,
        ];
    }

    function phoneValidator ($attribute, $params, $validator) {
                $pattern = "/^[8|+7]922d{7}$/uism";
                if (preg_match($pattern, $this->$attribute) == 0) {
                    $this->addError($attribute, 'Принимаются только номера мегафона в Перми!');
                    $region = Yii::$app->newRegions->addRegionByPhone( $this->$attribute );
                    Yii::$app->log->write("Потенциальный клиент из другого региона: " . $region);
                }
            }
     ...

Метод rules будет выглядеть чище, но это все равно захламляет код модели дополнительными методами валидации. Для этого случая разработчики Yii 2.0* позволяют нам добавлять классы-Валидаторы,

тем самым мы можем убрать 'ненужные' методы валидации из самой Модели

public function rules() {
        
        return [
            [ [ 'product_id' , 'currency_id' , 'unit_id' , 'quantity' , 'price', 'phone' ] , 'required' ] ,
            [['phone'], PhoneValidator::className()],
            [['price'], PriceValidator::className()],
            [['quantity'], QuantityValidator::className()],
            [ [ 'vendor_code' ] , 'string' , 'max' => 255, 'message' => 'Артикул должен содержать от 25 до 255 символов.' ] ,
            [ [ 'currency_id' ] , 'exist' , 'skipOnError' => true , 'targetClass' => Currencies::className() , 'targetAttribute' => [ 'currency_id' => 'id' ], 'message' => 'Выберите валюту' ] ,
            [ [ 'product_id' ] , 'exist' , 'skipOnError' => true , 'targetClass' => Products::className() , 'targetAttribute' => [ 'product_id' => 'id' ] ], 'message' => 'Выберите товар' ,
            [ [ 'unit_id' ] , 'exist' , 'skipOnError' => true , 'targetClass' => Units::className() , 'targetAttribute' => [ 'unit_id' => 'id' ], 'message' => 'Выберите единицу измерения' ] ,
            [ [ 'user_id' ] , 'exist' , 'skipOnError' => true , 'targetClass' => User::className() , 'targetAttribute' => [ 'user_id' => 'id' ], 'message' => 'Выберите поставщика' ] ,
        ];
    }

Этот пример казалось бы лучше предыдущего. Да, мы не захламляем Модель методами валидации, однако мы захламляем какую-либо из папок проекта

дополнительными файлами

image

Само по себе «захламление» папок не столь критично на первый взгляд, но работать с ними неудобно… Эти классы имеют лишь 3 метода: validateValue, ClientValidateAttribute, getClientOptions, последние 2 можно адекватно использовать, только если вы собираетесь пользоваться лишь «коробочным» функционалом. Но ведь хотелось бы, чтобы у меня был удобный способ обновлятьподдерживать валидацию десятка моделей, не прыгая по десяткам (а может и сотням) файлов.

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

Какое-никакое, но все же решение

Более подробно я начал изучать ООП пример 2 месяца назад, когда примерно на середине книги Стива я понял, что ничерта не понимаю в ООП и нужно реабилитироваться, я стал изучать все, что попадется под руку. Казалось бы, я знаю много, но в то же время ничерта, тем не менее каждая следующая неделя открывала мне глаза на то, что я изучал в предыдущую.

По такому же принципу я познакомился с Трейтами. Когда-то я прочитал документацию на официальном сайте PHP. Вроде бы понял, о чем идет речь. Но, как оказалось, не понял, как, где и зачем их применять. Лишь, когда я столкнулся с проблемой «комфорта» над текущим проектом, я начал искать варианты решения и вспомнил о тех самых «классах, которые я непонимаю как использовать».

Само решение выглядит так

CustomValidator.php
namespace commontraits;
use Yii;
trait CustomValidator {

    public function traitPhone($attribute, $params, $validator ) {
        $pattern = "/^[8|+7]922d{7}$/uism";
        if (preg_match($pattern, $this->$attribute) == 0) {
            $this->addError($attribute, 'Принимаются только номера мегафона в Перми!');
            $region = Yii::$app->newRegions->addRegionByPhone( $this->$attribute );
            Yii::$app->log->write("Потенциальный клиент из другого региона: " . $region);
        }
    }
}

ProductOffers.php
namespace commonmodels;
use commontraitsCustomValidator;

class ProductOffers extends yiidbActiveRecord {
    use CustomValidator;
public function rules() {
        
        return [
            ....
            [['phone'], 'traitPhone'],
            ....
            ];            
    }

Иными словами, все методы собственной валидации находятся в одном единственном Trait'e, и в самих моделях мы используем именно эти методы. Чтобы избежать постоянного дублирования use CustomValidator; можно вызывать его сразу в родителе моделей yiidbActiveRecord (имхо такое внедрение в базовый код Yii допустимо)

Лично мне кажется это решение более изящным, чем те, которые есть в документации:

  1. Мы не меняем движок -> не будет проблем с обновлением (ведь можно было просто добавить нужные методы в сам класс Model (но такого мы конечно никогда не делаем)
  2. Можно менять все именования ошибок и реализацию в одном файле
  3. Используя префикс trait для методов мы сразу даем понять разработчику, о чем идет речь
  4. Можно вообще пойти во все тяжкие и использовать методы rules() через трейт, тем самым — единственное, что нужно изменить в моделях — добавить use CustomTrait; и убрать базовый метод rules, а в самом трейте определять какие правила использовать

Послесловие

Разумеется я не навязываю свое мнение, и я более чем уверен, что могу ошибаться во многих моментах, поэтому мой первый опыт публикации на Хабре подскажет мне в любом случае, где я прав, а где нет, а комментарии помогут более подробно разобраться в причинах тех или иных последствий.

Автор: peresada

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js