Связи независимых модулей в Yii2

К предыдущей философской статье об организации более-менее независимой структуры модулей появился комментарий с интересной мыслью о том, возможно ли оставить каждый модуль независимым, если между ними всё-таки нужно организовать какое-либо взаимодействие:

Это он пока абсолютно самостоятельный. Но по мере появления новых модулей между ними будут образовываться зависимости. И с этим ничего вроде нельзя сделать. Максимум на границах модулей можно внедрить интерфейсы и зависеть от этих интерфейсов, а не делать вызовы к другому модулю напрямую. Тогда хотя бы будет возможность подменять зависимости разными реализациями. В противном же случае модули будут не разлей вода, монолит.

Да, ситуация весьма стандартная. Разберём подробнее. Предположим, что у нас имеются несколько модулей с моделями, которые мы написали сами или подключили чужие:

Первая же проблема, с которой сталкивается разработчик при внедрении в свой проект какого-нибудь чужого yii2-user и при связке со своим blog – это проблема организации связей User::getPosts и Post::getUser.

И сразу в голову приходит идея переопределить модели через Yii::$container или Yii::$classMap как самое прямое и лёгкое решение. Со сторонними расширениями это очень действенный способ, позволяющий экономить время, так как с чужим кодом особо ничего не поделаешь.

Другое дело – встречные проблемы, возникающие уже при написании своих модулей. И, вроде, хочется сделать «правильным» образом, но постоянно чего-то не хватает при попытке окинуть систему стратегическим взглядом. Первоначальная идея написания самодостаточного и независимого компонента не проживает даже нескольких первых дней, в итоге скатываясь к тактическим проблемам и возвращаясь к полному отказу от первоначальных задумок.

Довольно часто бывает, что хотелось бы сделать как лучше, но лодка любви к красивому коду разбилась о быт хардкода и зависимостей.

Но так ли страшны и нужны зависимости? И есть ли решение у этой извечной проблемы? Сейчас не будем рассматривать простейший вариант с переопределением моделей. Попробуем немного помечтать...

Ах уж этот реальный мир...

Продолжим с приведёнными выше моделями. Программисту в user нужен логин email и пароль, в blog нужно описание и соцсети автора, в магазине нужен профиль покупателя... Стандартная ситуация. Разработчик хотел сделать модули независимые (или прикрутить чужие), но пришёл к проблеме связей моделей.

В итоге, плюнув на всё, он добавляет все эти поля из всех пакетов в модель User, а в User добавляет связи на посты, заказы, корзину:

И сводную информацию помещает на страницу профиля...

В исходники чужих модулей мы не залезем, а свои захламлять жалко.

Тут и начинается адский мегатреш с подменой модели User через DI или Yii::$classMap для добавления связей hasOne и hasMany без затрагивания оригинальных моделей.

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

Как это решить? Нужно более гибкое управление зависимостями.

Но здесь всё более-менее понятно для понимания связей. Страдает только независимость и переносимость.

При такой ситуации можно не мучаться с управлением связями и наполовину или полностью отказаться от модульности. Для пишушего сайт программиста это вообще не проблема. Сложнее будет именно выкладывающему эти расширения в публичный доступ разработчику.

Любое архитектурное предположение – это практически всегда отход от разгильдяйства в сторону тотальной дисциплины. Оно намеренно привносит некое «неудобство» в систему с точки зрения её написания, внося строгие рамки и заставляя делать вещи только определённым способом.

Если изменить статус заказа в программе можно только одним способом, то все и будут так делать. Сложно, но никто не ошибётся. Можно написать всего один тест для одного метода. А если нет никакой дисциплины и в каждом контроллере разные программисты будут это делать по-своему, то ни о каком единстве и безотказности здесь не может быть и речи. Легко, но вечно надо за всеми всё проверять и исправлять.

Если таких неоднозначных мест в проекте станет слишком много, то это напомнит картину с мотком проводов, идущих откуда угодно куда угодно. Какой из вариантов на фотографии приятнее?

Думаю, что первый более интересен. А для авантюриста и второй сойдёт :)

Кто кому что обязан

В идеальном мире модули должны быть независимыми вообще. Но реально ли это? Посмотрим, как этого добиваются в реальном мире на примере стандартной рабочей ситуации обычной фирмы. Если это «дружная семья», где все равны и все говорят со всеми, то непонятно ничего:

строитель
строитель
уборщица
уборщица
уборщица
механик
водитель
водитель
бухгалтер
бухгалтер

Даже не знаешь, у кого что спросить и кому что сказать. Никакой структуры и дисциплины. Полный хаос на работе.

Так что при отсутствии иерархии четыре равноценных модуля ни о чём так и не договорятся, так как в более монолитной системе всё получается примерно так:

Непонятно, кто что кому должен и как это всё охватить. С наскока сделать это сложно. Веселее бывает, когда обнаруживаются взаимные связи чего-то с чем-то. Циклические связи – зло.

Когда же имеются ответственные (начальники) и когда каждое звено только даёт команды и ресурсы из казны подчинённым и когда подчинённые только что-то отвечают сразу (return), отчитываются позже о событиях вроде "я сделал" (Event) или сразу сообщают о проблемах (Exception), тогда никакого хаоса не возникает. Сразу видно, кто за что отвечает и как какой отдел работает:

директор
│
├── заместитель
│   ├── прораб
│   │   ├── строитель
│   │   └── строитель
│   └── старшая уборщица
│       ├── уборщица
│       └── уборщица
│
├── заместитель
│   ├── механик
│   ├── водитель
│   └── водитель
│
└── старший бухгалтер
    ├── бухгалтер
    └── бухгалтер

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

По аналогии с работниками и отделами, чтобы модули друг друга не съели, нужен парящий над ними как коршун некий дирижёр:

При этом отделы напрямую друг о друге могут и не знать. И любой отдел можно легко поменять. Либо вообще отдать на аутсорс какому-нибудь агентству:

директор
│
├── заместитель
│   ├── [ строительное агентство ]
│   └── [ клининговое агентство ]
│
├── заместитель
│   ├── механик
│   ├── водитель
│   └── водитель
│
└── старший бухгалтер
    ├── бухгалтер
    └── бухгалтер

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

В нашем коде не обязательно такой «начальник» должен быть «большим и главным». Если это вполне себе самостоятельные системы, то это может быть просто посредник-регулировщик, подписанный на события каждого блока и передающий им сообщения:

В любом случае, в такой системе модули ни от кого не зависят и могут быть спокойно заменены. Не понравился один модуль форума? Быстро поменяли на другой, переконвертировав базу данных, и поменяли нужный метод посредника.

Рассмотрим, как этого добиться на нашем примере.

Определимся, чего мы хотим

Мы уже говорили, что модуль, по-хорошему, должен быть самодостаточным. Сейчас же у нас с этим проблемы, так как мы не можем их использовать без модели User.

Сначала избавимся от хаотических сязей модели пользователя со всеми остальными.

Зачем нам в остальных местах понадобилась модель User? И кто такой этот User?

  • В user «Пользователь» – это пользователь сайта.
  • В forum «Пользователь» – это участник форума.
  • В blog «Пользователь» – это автор поста.
  • В shop «Пользователь» – это покупатель.

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

Лучший способ начала решения проблемы проектирования - назвать вещи своими именами и посмотреть, что из этого получилось.

В каждом контексте назовём всех своими именами: идентифицированного пользователя пользователем (User), участника – участником (Member), автора – автором (Author), а покупателя – покупателем (Buyer) в соответствующих модулях.

Вот что получилось:

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

Как теперь всё выводить в одном профиле? А надо ли всё выводить на одной странице? Вместо одной ссылки на профиль в меню просто выводим четыре ссылки:

Мой блог | Мой форум | Мои покупки | Профиль

А там уже в контроллерах можно получить нужную модель по идентификатору вроде Member::findOne(Yii::$app->user->id) и вывести информацию по ней.

Но как теперь организовать синхронизацию всех этих моделей? Чтобы при регистрации добавлялись записи о пользователе в каждую таблицу? Простейший лайфхак - повесить все модели на одну таблицу user. Но это костыльно. К тому же, имена разных полей могут в них нечаянно совпасть (например, status). Да и в ActiveRecord нельзя имя таблицы поменять (если не брать её из параметров).

Можно насоздавать моделей внутри переопределённой SignupForm, но это опять явная связанность, от которой мы и хотим уйти.

Вместо этого вполне применúм подход с дирижёром, который возьмёт обязанности по управлению этим оркестром на себя. Прямо как в Википедии:

Посредник (Mediator) — поведенческий шаблон проектирования, обеспечивающий взаимодействие множества объектов, формируя при этом слабую связанность и избавляя объекты от необходимости явно ссылаться друг на друга.

и как описано в хорошей статье Сергея Теплякова, которая подвернулась как нельзя кстати, хоть и после написания этой статьи.

Как раз это нам и нужно: построить взаимодействие независимых компонентов без необходимости явного связывания их друг с другом. Значит просто поместим коршуна над нашими модулями:

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

Обратная связь

Залог хорошей иерархии – наличие двустороннего общения в виде прямых команд и обратных сообщений. Но модули у нас пока немы и ничего о себе никому не говорят. Исправим это. Дадим им голос в моменты регистрации:

namespace app\modules\user\events;
 
use yii\base\Event;
 
class UserSignedUpEvent extends Event
{
    public $user;
 
    public function __construct(User $user)
    {
        $this->user = $user;
    }
}

создания пользователя в админке:

class UserCreatedEvent extends Event
{
    public $user;
 
    public function __construct(User $user)
    {
        $this->user = $user;
    }
}

редактирования:

class UserUpdatedEvent extends Event
{
    public $user;
 
    public function __construct(User $user)
    {
        $this->user = $user;
    }
}

и удаления:

class UserDeletedEvent extends Event
{
    public $user;
 
    public function __construct(User $user)
    {
        $this->user = $user;
    }
}

В модуле добавим идентификаторы этих событий и методы для запуска:

namespace app\modules\user;
 
class Module
{
    const EVENT_USER_SIGNED_UP = 'userSignedUp';
    const EVENT_USER_CREATED = 'userCreated';
    const EVENT_USER_UPDATED = 'userUpdated';
    const EVENT_USER_DELETED = 'userDeleted';
 
    public function notifyThatUserSignedUp($user) {
        $this->trigger(Module::EVENT_USER_SIGNED_UP, new UserSignedUpEvent($user))
    }
 
    ...
}

Эти методы лучше бы были с модификатором package вместо public, а метод trigger был бы protected, чтобы никто снаружи их не вызывал.

Контроллер без самого модуля практически не имеет смысла. Хороший контроллер – это расходный материал, не содержащий в себе никакой логики. Да и несколько контроллеров одновременно не создашь. Так что контролер с модулем с точки зрения структуры можно считать одним уровнем и спокойно обращаться именно из контроллера к $this->module. Кощунством будет обращаться к нему из модели.

Теперь в каждом нужном действии контроллера уведомляем наш модуль о случившемся:

public function actionSignup()
{
    $model = new SignupForm();
 
    if ($model->load(Yii::$app->request->post())) {
        if ($user = $model->signup()) {
            Yii::$app->login($user);
            $this->module->notifyThatUserSignedUp($user);
            return $this->goHome();
        }
    }
 
    return $this->render('signup', [
        'model' => $model,
    ]);
}

События появились. Теперь создадим посредника, который будет синхронизировать все профили:

namespace app\modules;
 
class ModuleMediator
{
    public static function onUserSignedUp(UserSignedUpEvent $event)
    {
        $user$event->user;
 
        $member = new Member();
        $member->id = $user->id;
        $member->username = $user->username;
        $member->email = $user->email;
        $member->save();
 
        $author = new Author();
        $author->id = $user->id;
        $author->username = $user->username;
        $member->email = $user->email;
        $author->save();
        ...
    }
}

И подпишем его на события модуля:

'user' => [
    'class' => 'app\modules\user\Module',
    'on userSignedUp' => ['app\modules\ModuleMediator', 'onUserSignedUp'],
    ...
],

Если логика работы у нас настолько примитивна, что всё создание сводится только к созданию модели, то можно обойтись и так. В идеале, у каждого модуля мог бы быть свой сервисный слой с публичными методами для каждой операции (вместо нахождения всего кода в контроллерах и формах). Тогда работа медиатора сводилась бы к вызову именно методов addMember форума или addAuthor блога:

class ModuleMediator
{
    public static function onUserSignedUp (UserSignedUpEvent $event)
    {
        $user$event->user;
        ...
        $forumService->addMember($user->id, $user->username, $user->email);
        $blogService->addAuthor($user->id, $user->username, $user->email);
        $shopService->addBuyer($user->id, $user->email);
    }
}

что избавило бы нас от копипасты всего кода создания из контроллеров.

Но при использовании сервисов надо быть осторожнее, чтобы не вылетали встречные события, которые могут «зациклить» нашего посредника.

Но такие хоть немного осмысленные события вроде EVENT_USER_SIGNED_UP в реальности – редкость. Их используют чаще в компонентах, а не в модулях. Поэтому для сторонних вещей можно навеситься на глобальные события классов моделей:

class ModuleMediator extends Object implements BootstrapInterface
{
    private static $iHateToTestGlobalSingletons;
 
    public function bootstrap($app)
    {
        if (self::$iHateToTestGlobalSingletons === null) {
            Event::on(User::className(), ActiveRecord::EVENT_AFTER_INSERT, [self::className(), 'onUserCreated']);
            Event::on(User::className(), ActiveRecord::EVENT_AFTER_UPDATE, [self::className(), 'onUserUpdated']);
            Event::on(User::className(), ActiveRecord::EVENT_AFTER_DELETE, [self::className(), 'onUserDeleted']);
            self::$iHateToTestGlobalSingletons = true;
        }
    }
 
    public static function onUserCreated(Event $event)
    {
        $user$event->sender;
        ...
    }
 
    ...
}

и зарегистрировать этот класс в автозагрузке:

return [
    'name' => 'App',
    'basePath' => dirname(__DIR__),
    'bootstrap' => [
        'log',
        'app\modules\ModuleMediator',
    ],
];

Костыль $iHateToTestGlobalSingletons введён не случайно, так как это чистая правда :)

Если используете глобальный Event::on, то постарайтесь переписать конфиг tests/codeception/config/unit.php для модульных тестов, чтобы он не подключал такой класс вообще:

$config = yii\helpers\ArrayHelper::merge(
    require(__DIR__ . '/../../../config/web.php'),
    require(__DIR__ . '/config.php'),
    [
 
    ]
);
 
$config['bootstrap'] = array_diff($config['bootstrap'], ['app\modules\ModuleMediator']);
 
return $config;

Иначе на каждое сохранение модели в интеграционных тестах (например, тесты форм регистрации и смены пароля) коварный Event::trigger будет «ворочить» все ваши модули.

Что получилось

В общем, сделали каждому модулю свою модель, добавили генерацию событий и навесили над ними своего регулировщика. Вот и всё.

С такой системой синхронизации у нас не будет проблем, даже если имя человека будет храниться в нескольких местах. Если изменим имя в профиле блога, то сразу оттуда всплывёт своё событие вроде AuthorChangedEvent и медиатор обновит данные во всех остальных модулях.

А что там у них внутри... теперь для нас это не важно:

Всё будет работать достаточно понятно и стабильно.

Плюсы (в основном, технические):

  • Модули остаются независимыми, так как всё внутри и без внешних связей.
  • Можно спокойно менять модули на другие аналоги.
  • Весь связующий код находится только в посреднике.
  • Всё можно переносить из проекта в проект.

Минусы (в основном, человеческий фактор):

  • Нет архитектурного единства в написании сторонних модулей.
  • Практически никто не использует события. Спасает глобальный Event::on.
  • Нет сервисного слоя в сторонних пакетах. Нужно копипастить код из контроллеров.

Ну и сильно напрягает разработчика-новичка.

А что, если в нашем любимом стороннем модуле нет событий или ещё чего-то, нам очень нужного? Это вообще не беда. Просто форкаем репозиторий, дописываем и отправляем Pull Request. И, если автор адекватный, через несколько дней радуемся новой дополненной версии с нужным нам кодом.

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

А что делать, если всё-таки нужно вывести всю информацию о пользователе на одной странице? Тогда, например, делаем фасад DashboardController::actionIndex и на этой странице выводим виджеты из папок widgets всех этих модулей или самого приложения. Или (если очень нужно, а модуль чужой) через темизацию переопределяем представление profile/index.php и выводим все виджеты туда.

Пример из практики

На этом, вроде, было всё. Но пока готовил черновик, появился ещё один комментарий:

Есть такая ситуация... Имеем три модуля:

  1. User
  2. Post
  3. Comments

Второй модуль зависит от первого, третий от первого и второго. Как в таком случаи организовать структуру модулей? Я вижу лишь в объедении в 1 модуль всех трех, но боюсь что этот один модуль будет расширяться дальше, т.к. можно добавить блог, магазин и еще что-нибудь.

Да, действительно, подход удачно применим только с по-настоящему полноценными модулями вроде Blog и Shop. С зависимостью Post от User мы уже разобрались. Но что если нам нужно подключить к проекту изначально несамостоятельный пакет вроде Comments, который содержит в себе модель Comment, виджет и контроллеры. И которому нужны связи с пользователем и с постами блога или товарами магазина?

Первым делом, можно сделать Comments полноценным, добавив модели Material и Author:

Material (id, item_id, type, name, url)
Author   (id, user_id, name)
Comment  (id, material_id, author_id)

или обойтись без Author:

Material (id, item_id, type, name, url)
Comment  (id, material_id, user_id, name)

Здесь user_id заполняется из Yii::$app->user->id (будет пустым для гостей), а остальная информация о комментируемом материале или залогиненном авторе передаётся прямо в виджет:

<?= Comments::widget([
    'item_id' => $model->id,
    'item_type' => 'Blog',
    'item_name' => $model->title,
    'item_url' => ['/blog/post/view', 'id' => $model->id],
    'user_name' => $blogUser ? $blogUser->name : '',
]) ?>

И ещё нужно отобразить число комментариев у каждой статьи. Для этого вместо любимого классического варианта со связями:

Комментариев: <?= $model->getComments()->count() ?>

можно воспользоваться ещё одним виджетом:

Комментариев: <?= CommentsCount::widget(['item_id' => $model->id, 'item_type' => 'Blog']) ?>

В любом случае, для получения количества будет выполняться отдельный запрос SELECT COUNT(*) к базе. Но в виджете он, например, может кешироваться. Так что с точки зрения производительности это никак наш проект не ухудшит.

При этом медиатор будет удалять Material при удалении поста в блоге. А задачу автоудаления комментариев пользователя можно безболезненно переложить на базу данных, на уровне приложения написав миграцию, проставляющую привязку к таблице user:

$this->addForeignKey('fk-comment_author-user', '{{%comment_author}}', 'user_id', '{{%user}}', 'id', 'CASCADE', 'RESTRICT');

С материалами, увы, такой трюк не пройдёт из-за отношений с разными таблицами в зависимости от значения поля type.

В итоге, такой модуль Comments будет абсолютно всеядным, так как он сам никакие наши модели не использует. Всё его взаимодействие с нашим кодом производится только через виджет. Извлечение заголовка и составление адреса статьи остаётся в представлении блога, и виджету передаётся уже готовый результат.

А дальше он пусть сам через hidden-поля напрямую или как-нибудь ещё отправляет всё это из формы в свой контроллер.

При этом вместо строки с текущем адресом поста material_url можно даже оставить значение массивом (и сериализовать при передаче и при записи в базу данных), чтобы потом в бэкенде в таблице комментариев вызывать Url::to($material->url). Это сделает ссылки на посты рабочими даже при изменении настроек UrlManager.

Мораль

Можно либо заморачиваться на модульности, либо о ней особо не думать. Можно либо использовать чужие решения, либо придумать свои. Вопрос «какой вариант выбрать» так и остаётся висеть перед каждым разработчиком. Он сам должен решить, есть ли у него причина ввязываться в это всё дело, нужно ли ему следование каким-либо сложным принципам, не навредит ли это скорости разработки и будут ли в проекте другие программисты.

При большом количестве взаимосвязей любая система может превратиться в неповоротливого монстра, так что стоит быть аккуратнее. Если не планируется распространение модулей удобнее не фанатеть от «правильности» и вписывать зависимости явно. А в своём домашнем проекте работать без модулей намного проще и приятнее.

Другие статьи

В прошлой части мы написали удобный гибридный AuthManager, который можем захотеть использовать в других проектах. Да и в обратную связь поступало много вопросов, как подготовить какой-нибудь компонент для совместного использования в разных приложениях. Так что попробуем сделать полноценное переносимое расширение, которое выгрузим на GitHub и опубликуем для подключения через Composer.

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

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

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

Комментарии

 

maxyc

Спасибо. Статья очень хорошая получилась.
Есть небольшие ошибки в тексте, но читать это не мешает, например ордер и карт получают не пользователя, а баер ид.

в целом вопрос очень сложный, долго размышлял на эту тему.

по тексту все же хотелось бы сказать. Я так понимаю мы все равно не можем избавиться от привязки, например, к фреймворку, как в том же ларавеле?

Ответить

 

maxyc

http://www.youtube.com/watch?v=AxZLJA84_74 вот хороший текст по DI и модульности

судя по видосу у нас остается все же проблема в зависимости от центрального объекта. фреймворк, Yii user. В повествовании так же попросил бы сделать акцент на DI, на том, как передавать данные объектам. Чтоб меньше связанности было

Ответить

 

Дмитрий Елисеев

Про buyer_id исправил. Спасибо. А так да, в Yii изначально всё наследуется от классов фреймворка.

Ответить

 

Александр Макаров

Хрупкий получается контракт на событиях. Я бы делал явный:

1. Каждый модуль предоставляет необходимые ему интерфейсы.
2. В приложении эти интерфейсы реализуются.

Ответить

 

Evgeniy

Можно пример? Я просто изначально к интерфейсу больше склонялся, но не знаю как в данной задачи правильнее реализовать и использовать.

Ответить

 

Александр Макаров

Напишу как-нибудь пост на тему у себя...

Ответить

 

Сергей

Отличная статья, спасибо!

Дмитрий, обычно после Ваших статей, я привношу в свой стиль разработки что-нибудь новенькое :)
Так было с событиями и поведениями. Глядишь и отсюда что-нибудь перекочует в мой код ;)

Ответить

 

Дмитрий Елисеев

Ну не знаю. Слишком спорные и философские у меня статьи :)

Ответить

 

MihailDev

Статья прекрасная! Спасибо!
тоже много думал по этому поводу и склоняюсь больше к варианту Александра

Но всё же почему вы склоняетесь именно к такому подходу,
дабы добиться максимально независимых модулей?
по принципу из коробки установил и всё работает?

встаёт вопрос с обязательными полями и как их реализовывать
и в большинстве случаев от узера требуется имя и йди и из-за этого реализовывать под каждый таблицу для хранения имени или как?

Ответить

 

Дмитрий Елисеев

> Но всё же почему вы склоняетесь именно к такому подходу, дабы добиться максимально независимых модулей?

Да, результат долгих размышлений на тему, возможно ли это вообще.

> встаёт вопрос с обязательными полями и как их реализовывать

Тоже думал. Выбрал оптимальный вариант двухшаговой регистрации: для залогиненного посетителя в beforeAction модуля ставить if (empty($member->name)) и форсированно редиректить на forum/profile/settings.

А так да, основная идея – делать ствои модули с собственными отдельныи таблицами author, member и buyer (и синхронизировать наблюдателем при изменении) вместо запихивания этого всего в одну user.

Ответить

 

араыовра

Дублирование данных пользователя в разных моделях как-то тоже не хорошо.

Ответить

 

maxyc

Нормализацию никто не отменял. Если вы стремаетесь имя пользователя хранить в 5 местах, вынесите имена в отдельную таблицу, а в 5 местах храните только ид ссылки

Ответить

 

Василий

Хорошо бы в начале указать, что этот подход не является достаточным, а одни из. Иначе новички в данной теме просто "потеряются". На событиях далеко не уедешь, например, типичная ситуация, когда необходимо получить `ID` какого-то пользователя из стороннего модуля. Здесь увы, но события бессильны. Все что нам нужно: реализовать интерфейс, который реализует какие-то методы для выдергивания списка пользователей для компонента (если речь о Yii/Yii2), который будет оперировать ими.

Ответить

 

Василий

Этот пример для ситуации, когда уже из списка текущий пользователей нужно выбрать какого-то. Например, для таблицы "управляющие складов" указать ID пользователя и ID склада.

Ответить

 

Email

Эти методы лучше бы были с модификатором package вместо public, а метод trigger был бы protected, чтобы никто снаружи их не вызывал. Наверное вместо package будет более потяnно private

Ответить

 

Дмитрий Елисеев

Не совсем, так как его нужно вызывать из контроллера.

Ответить

 

Денис Волков

Отличная статья. Но насколько оправдано хранение одних и тех же данных в нескольких местах? Мне кажется правильнее было бы хранить только id пользователя. При достаточных размерах проекта - бегать по разным модулям и менять одно и тоже несколько раз - не очень то хочется, наоброт стараешься избавиться от дублей.

И в Yii 1.x - я так понимаю можно использовать CComponent::attachEventHandler или без Yii2 такое не решить?

Ответить

 

Дмитрий Елисеев

Смотря сколько у Вас будут одних и тех же данных в форуме и в магазине. В форуме нужны ID, email, ник, подпись, аватарка и статус. В магазине нужны ФИО, email, телефон и адрес.

Про Yii 1 не помню.

Ответить

 

Mike Artemiev

У Вас на клавиатуре буква "о" залипает.
перепределением, бычной)

Ответить

 

Дмитрий Елисеев

Спасибо! Исправил.

Ответить

 

Akulenok

Я хочу при смене username на сайте, сделать изменение username и в других модулях, т.е. Админ меняет у юзера имя и надо сразу поменять имя и во всех комментах, на форуме и тд...
Подскажите как это реализовать? События?

Ответить

 

Дмитрий Елисеев

Если комменты и сообщения привязаны просто к user_id и выводятся с $comment->user->email, то везде само поменяется. А если модули именно независимые и со своими моделями, то события.

Ответить

 

Akulenok

Да не зависимые, везде прописываются username без привязки к user_id.
Вопрос в том мне подойдет этот вариант из этой статьи или есть вариант более проще, да и не работает у меня почему-то, ругается на

PHP Recoverable Error – yii\base\ErrorException

Argument 1 passed to app\modules\user\events\UserChangeName::__construct() must be an instance of app\modules\user\events\User, string given, called in \www\modules\user\Module.php on line 15 and defined

я не совсем понял что надо передать в контроллере

$model = User::findOne($id);
$this->module->notifyThatUserChangeName($model)

?

Ответить

 

Akulenok

__construct(User $user
User это что?

Ответить

 

Akulenok

А..понял, надо было use app\modules\user\events\UserChangeNameEvent; прописать, жаль в статье про это не указано

Ответить

 

Akulenok

То есть грубо говоря, я в ModuleMediator делаю такой код

<?php
namespace app\modules\User;

use app\modules\user\events\UserChangeNameEvent;
use app\modules\comments\models\Comments;

class ModuleMediator
{
    public static function onUserChangeName(UserChangeNameEvent $event)
    {
        $user =  $event->user;
        Comments::updateAll(['username' => $user->username], 'user_id = ' . $user->id);
    }
}
 

А все остальное как у Вас в коде, так правильно?

Ответить

 

Сергей Ильичев

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

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

Ответить

Оставить комментарий

Войти | Завести аккаунт


(никто не увидит)



Можно использовать теги <p> <ul> <li> <b> <i> <a> <pre>