Валидация входных данных заслуженно является одним из важнейших правил во всей сфере IT. Если сузить сферу деятельности до разработки веб-сайтов, речь пойдет в основном про валидацию данных из форм.
Я не думаю, что многие разработчики любят проверять входные данные и делают это достаточно тщательно, поэтому в современных фреймворках, таких как 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' => 'Выберите поставщика' ] ,
];
}
Конечно можно все замыкания заменить на
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' => 'Выберите поставщика' ] ,
];
}
Этот пример казалось бы лучше предыдущего. Да, мы не захламляем Модель методами валидации, однако мы захламляем какую-либо из папок проекта
Само по себе «захламление» папок не столь критично на первый взгляд, но работать с ними неудобно… Эти классы имеют лишь 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 допустимо)
Лично мне кажется это решение более изящным, чем те, которые есть в документации:
- Мы не меняем движок -> не будет проблем с обновлением (ведь можно было просто добавить нужные методы в сам класс Model (но такого мы конечно никогда не делаем)
- Можно менять все именования ошибок и реализацию в одном файле
- Используя префикс trait для методов мы сразу даем понять разработчику, о чем идет речь
- Можно вообще пойти во все тяжкие и использовать методы rules() через трейт, тем самым — единственное, что нужно изменить в моделях — добавить use CustomTrait; и убрать базовый метод rules, а в самом трейте определять какие правила использовать
Послесловие
Разумеется я не навязываю свое мнение, и я более чем уверен, что могу ошибаться во многих моментах, поэтому мой первый опыт публикации на Хабре подскажет мне в любом случае, где я прав, а где нет, а комментарии помогут более подробно разобраться в причинах тех или иных последствий.
Автор: peresada