Многие, кто писал приложение на yii 1.x, (на самом деле как я понимаю все нижеописанное актуально и для yii 2.x) и если проект достаточно сложный,n в какой то момент приходил к ситуации, что модели становились толстые, что, количество сценариев в модели растет, методы beforeSave, beforeValidate, afterValidate становятся неуправляемые и все это превращается в нетестируемый, неуправляемый код. И тут появляется то самое чувство, что ты делаешь что-то не так.
Вариантов решения на самом деле наверное несколько, но я почему-то созрел и увидел только на основе Layered Architecture. Достаточно логично и легко применимый шаблон для приложения в описанной ситуации.
Итак вы в ситуации когда менять и добавлять логику сложно, но бизнес требует. Две важные установки, который приходится следовать:
- Новую логику добавляем используя новую предложенную архитектуру.
- Старую логику, перед изменением оцениваем объем работ по изменению в текущем виде, и если это достаточно затратно по времени, а может быть и нервная система будет страдать, то опять же переписываем уже
- измененную логику под новую архитектуру.
Как она теперь выглядит эта "новая архитектура" ?
- Ваша модель больше не используется для валидации пользовательского ввода. Вся старая валидация остается там, а вот для новой логики, больше не пишем валидацию в модели.
- Больше не добавляете ничего нового в метод
пример: Ваша бизнес логика требует ввод новых данных от пользователя и сохранение этих данных в базу. Пусть это будет просто ModerateComment. Фича, позволяющая вам модерировать комментарий для вашего блога. Все просто, старый метод потребовал бы от нас просто добавленеи нового сценария и вызова метода "save". У модели Comment. И может быть этот простой путь покажется вам проще нижеописанного, но вы понимаете, это просто пример который сильно упрощен и этим путем мы уже написали в своем приложении модель на сотню килобайт и даже такая простая задача требует от нас теперь очень много времени, потому что старую логику приходится перепроверять и не стараться не сломать.
Мы будем использовать теперь несколько новых для нас сущностей. Для примера новая логика будет названа "ModeratePost"
- ModerateModel - для валидации пользовательских данны
- ModerateService - сущность которая будет непосредственно писать и читать в/из базы данных.
Введение двух новых сущностей как раз и позволяет нам изолировать новую логику от старого приложения. В UML это будет выглядеть примерно так:
+---------------+ +-----------------+
| ModerateModel | | ModerateService |
| | | |
+---------------+ +-----------------+
| | | |
| | | |
| | | |
| | | |
| | | |
+---------------+ ++----------------+
Наша модель теперь ответственна только за валидацию пользовательских данных, а Service за действия связанные с базой данных.
Такой подход позволяет изолированно производить тестирование модели, не затрагивая базу данных. Ну а также тестировать код, который что-то пишет или читает из базы с помощью phpunit only или в паре с codeception. Знаете yii фикстуры ? phpunit тоже их умеет.
Ну теперь немного кода:
class ModerateModel extends Model
{
/**
* yii валидация, здесь можно описать правила.
* Для нашего простого примера это только 1 поле is_moderated.
* Пример для yii 1.x
*/
public function rules()
{
return ['is_moderated', 'in', 'range' => [1], 'allowEmpty' => false];
}
}
/**
* Класс для работы с базой данных.
* Не наследуется ни от каких классов фреймворка. Yii умеет только
* ActiveRecord для работы с базой, мы уже наелись проблем этого
* паттерна.
*/
class ModerateService
{
/**
* Ставит колонку is_moderated в таблице комментариев в 1.
*/
public function moderate(Comment $comment)
{
$command = Yii::app()->db->createCommand('UPDATE comments SET
is_moderated=1 WHERE id=:comment_id')->execute([':comment_id' => $comment->id]);
}
}
Теперь нам не достает только соответсвующего экшена в контроллере.
public actionModerate()
{
$comment = $this->loadModel($_GET['id']);
$service = new ModerateService;
$model = new ModerateModel;
$model->setAttributes($_POST['ModerateModel']);
if ($model->validate()) {
$service->moderate($comment);
}
}
Итак что нам дает такой подход: написание новой логики никак не использует старый код, в который уже не хочется лезть из за страха что-то сломать. Новая логика полностью изолирована и легко может быть протестирована. А это немаловажно: иметь тестируемый код. Отсюда вытекает такая хорошая вещь, как возможность целиком выпилить всю логику, ответственную за модерацию, просто удалив 2 файла и один экшен, без страха что-то сломать.
И еще одна особенность, наш ModerateService
не наследуется ни от
какого класса фреймворка, в будущем, когда выйдет новая версия
фреймворка, вам не потребуется переписывать этот класс. А придется
переписать только класс ModerateModel который используется для
валидации. А ведь со временем приложение обрастает логикой и чем
меньше вам придется переписать, чтобы сменить фреймворк тем
лучше. Фреймворко-независимое приложение это уже сейчас становится
важным. Но это тоже целый подход.
@cyhalothrin, я еще честно говоря не понял в чем профит использования отдельной модели для сохранения, а почитать и вникнуть в паттерн репозитроий не хватает времени.
Но если рассмотреть простой пример, например создать запись в блоге, то выходит следующее:
но так как данные уже все есть нам хватит 1 строчки $model->save()
выходит, что нужно создавать отдельную модель PostRepository::create() для строки $model->save() ?
С другой стороны, если есть связанные модели, которые тоже нужно сохранить, например для записи в блоге нужно сохранить список тегов. То в таком случае может быть PostRepository и будет оправдан, хотя я не совсем понимаю, как это можно красиво организовать.
Сейчас мои экшины выглядят так:
Тоесть на каждую сущность я держу свою модель:
Post - общая модель актив рекорд.
PostForm - (extend Post) - модель для формы.
PostSearch - (extend Post) - модель для поиска.
PostQuery - кастомные квери.
Тут все красиво разграничено и модели получаются не жирные. Самое интересное, что я заканчиваю средний проект на yii2 (пол года разработки) и вся моя логика можно сказать не выходила за пределы AR. Вроде все вышло более-менее красиво.
А вот как это оптимизировать еще лучше добавив PostRepository я не совсем понимаю.
*Плюс ко всему я наткнулся на еще одну проблему, возможно она тоже будет интересной. *
Суть такова, что есть очень много вариантов запросов например:
- Все статьи со статусом 1
- Все статьи со статусом 1 + джоин юзер
- Все статьи со статусом 1 + джоин юзер + профиль
- Все статьи со статусом 1 + джоин все теги + джин юзер + профиль + еще что-то
И таких вариантов может быть около 5 - 7 на одну сущность. Я старался всю выборку инкапсулирвоать в методы модели. Но выходит нужно делать 25 методов на все случаи жизни или делать 1 божественный метод с 25 условиями и параметрами.
И все это ради того, чтобы избежать прямых вызовов AR в виде или контроллере. Есть ли у кого мысли на этот счет?