Skip to content

Instantly share code, notes, and snippets.

@RusAlex
Last active January 7, 2022 12:48
Show Gist options
  • Save RusAlex/0397b42e0f052ec84948 to your computer and use it in GitHub Desktop.
Save RusAlex/0397b42e0f052ec84948 to your computer and use it in GitHub Desktop.

Разделение приложения на слои

Многие, кто писал приложение на yii 1.x, (на самом деле как я понимаю все нижеописанное актуально и для yii 2.x) и если проект достаточно сложный,n в какой то момент приходил к ситуации, что модели становились толстые, что, количество сценариев в модели растет, методы beforeSave, beforeValidate, afterValidate становятся неуправляемые и все это превращается в нетестируемый, неуправляемый код. И тут появляется то самое чувство, что ты делаешь что-то не так.

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

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

  • Новую логику добавляем используя новую предложенную архитектуру.
  • Старую логику, перед изменением оцениваем объем работ по изменению в текущем виде, и если это достаточно затратно по времени, а может быть и нервная система будет страдать, то опять же переписываем уже
  • измененную логику под новую архитектуру.

Как она теперь выглядит эта "новая архитектура" ?

  1. Ваша модель больше не используется для валидации пользовательского ввода. Вся старая валидация остается там, а вот для новой логики, больше не пишем валидацию в модели.
  2. Больше не добавляете ничего нового в метод

пример: Ваша бизнес логика требует ввод новых данных от пользователя и сохранение этих данных в базу. Пусть это будет просто ModerateComment. Фича, позволяющая вам модерировать комментарий для вашего блога. Все просто, старый метод потребовал бы от нас просто добавленеи нового сценария и вызова метода "save". У модели Comment. И может быть этот простой путь покажется вам проще нижеописанного, но вы понимаете, это просто пример который сильно упрощен и этим путем мы уже написали в своем приложении модель на сотню килобайт и даже такая простая задача требует от нас теперь очень много времени, потому что старую логику приходится перепроверять и не стараться не сломать.

Мы будем использовать теперь несколько новых для нас сущностей. Для примера новая логика будет названа "ModeratePost"

  1. ModerateModel - для валидации пользовательских данны
  2. 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 который используется для валидации. А ведь со временем приложение обрастает логикой и чем меньше вам придется переписать, чтобы сменить фреймворк тем лучше. Фреймворко-независимое приложение это уже сейчас становится важным. Но это тоже целый подход.

@devkpv
Copy link

devkpv commented Nov 9, 2015

Смешались в кучу кони, люди.
Класс ModerateService можно написать примерно так:

class ModerateService
{
  private $command;

  public function __construct(Command $command) {
    $this->command = $command;
  }

  public function moderate(Comment $comment)
  {
    return $this->command
        ->update('comment', ['is_moderated' => 1], 'id = :id')
        ->execute([':id' => $comment->id]);
  }
}

В этом случае мы пользуемся уже QueryBuilder'ом. AR для меня пока и остался загадкой для разработки действительно сложных приложений. Приходится его откидывать, т.к. слишком большая область отвественности получается. Зато отлично подходит для реализации CRUD'a, прототипирования и реализации простых вещей. Главное во время останавливаться и понимать где стоит использовать AR, а где уже стоит выделить логику в некий сервисный слой.

Как было выше сказано, в контроллере стоит вызывать наш сервис через DI, предварительного его там зарегистрировав. Так же не стоит напрямую обращаться к глобальным переменным вроде $_GET, $_POST etc.

public actionModerate()
{
  $comment = $this->loadModel(Yii::$app->request->getQueryParam('id'));
  $model = new ModerateModel();
  $model->setAttributes(Yii::$app->request->post('id'));
  if ($model->validate()) {
    Yii::$container->get('ModerateService')->moderate($comment);
  }
}

@yiimar
Copy link

yiimar commented Dec 11, 2015

loadModel(), setAttributtes() в контроллере...
Не точнее ли вынести это в форму, (кстати это же советовал и vova07). Там же можно проводить и валидацию.
В контроллере будет только

public actionMy()  
{
   $model = new MyForm();
   return $this->render('my', ['model' => $model,]);
}

Формально, контроллеру (здесь и везде подразумевается действию контроллера) надо знать, только свое представление, и что ввод данных обслуживается формой MyForm. Метод init() в классе Model есть (точнее в предке- классе Object). Всю логику туда.
Ну и, конечно, если нужны дополнительные данные для вывода в представлении, то в контроллере добавятся (один или несколько)

    ...
    $mymodel = Mymodel::makeData();

Для не очень крупных проектов, имхо, это применимо.

Буду признателен, коллеги, за Ваше мнение. СПС.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment