Механизм для сценариев вида «объект слишком долго находится в некотором состоянии — напомнить ответственным»: документ завис в статусе «Новый», счёт оплачен, но поставка не оприходована, и т.п.
Правила описываются декларативно в config/params-local.php (ключ
notifyRules) — без написания кода. Прогоняет их по расписанию команда
yii notify/watch: каждый объект, попавший под условие правила, порождает
письмо ответственным в очередь уведомлений;
фактическую отправку делает yii notify/send. Механизм должен быть включён —
см. параметры notify.* в настройке.
'notifyRules' => [
//ключ массива - имя правила; оно входит в ключ дедупликации,
//поэтому должно быть стабильным (переименование = "новое" правило,
//письма по нему уйдут заново)
'contract-stale-new' => [
//класс отслеживаемой модели
'class' => \app\models\Contracts::class,
//условие выборки (см. "Условия" ниже)
'condition' => ['state_id' => 1],
//как долго объект не менялся, чтобы считаться "залежавшимся"
'age' => '1 day',
//тема письма; {name} и {id} подставляются из объекта
'subject' => 'Документ «{name}» завис в статусе «Новый»',
//тело письма; если не задано - тема + ссылки на объект
'body' => null,
//повторное напоминание не чаще, чем раз в этот интервал;
//не указано - письмо по объекту уйдёт один раз
'repeat' => '3 days',
],
],
Обязателен только class; всё остальное имеет разумные умолчания
(без condition — все объекты класса, без age — без ограничения по
давности, без subject — «<Название класса> «имя»: требует внимания»).
Для признаков, которых нет в БД (вычисляемых моделью), есть ещё ключ
filter — см. Вычисляемые признаки.
condition)Два варианта:
andWhere() как есть. Годится для простых случаев:
['state_id' => 1], ['state_id' => [1, 2]] (любой из), вложенные
операторы Yii (['>', 'total', 100000]);callable — функция, получающая ActiveQuery и возвращающая его же;
внутри доступен весь арсенал Yii (join, подзапросы, произвольный SQL).
Пример — статус по имени вместо «магического» id (id статусов в разных
инсталляциях различаются):
'condition' => fn($query) => $query
->joinWith('state')
->andWhere(['contracts_states.name' => 'Новый']),
Выборка каждого правила выполняется целиком (all()), поэтому условие должно
отсекать разумное количество объектов — правило «все документы вообще»
на большой базе будет тяжёлым.
filter)Часть признаков в ARMS не хранится в БД, а вычисляется моделью (например,
состояние поставки документа — сопоставление заявленных количеств с реально
привязанным оборудованием/материалами/лицензиями). В SQL-condition такой
признак не выразить — для этого есть filter: PHP-функция, которая
применяется к каждому объекту после SQL-выборки и решает, попадает ли он
под правило (callable($model): bool).
Рабочее разделение труда: condition дёшево сужает выборку на стороне БД,
filter доводит точную проверку на стороне PHP. Готовый пример — «документ
оплачен, но поставка не оприходована; напоминать еженедельно»:
'contract-undelivered' => [
'class' => \app\models\Contracts::class,
//SQL-часть: только документы, по которым вообще заявлена поставка
'condition' => ['or',
['>', 'techs_delivery', 0],
['>', 'materials_delivery', 0],
['>', 'lics_delivery', 0],
],
//PHP-часть: вычисляемое состояние "оплачен, но недопоставка"
'filter' => fn($doc) => $doc->deliveryState === \app\models\Contracts::DELIVERY_INCOMPLETE,
'age' => '1 week',
'subject' => 'Документ «{name}» оплачен, но поставка не оприходована',
//в теле перечисляем, чего именно не хватает, и даём ссылки на документ
'body' => function ($doc) {
$undelivered = array_map(
fn($line) => \yii\helpers\Html::encode($line),
$doc->undeliveredDescription
);
return '<p>Документ <b>«' . \yii\helpers\Html::encode($doc->name) . '»</b> оплачен, '
. 'но по нему оприходовано не всё:</p>'
. '<ul><li>' . implode('</li><li>', $undelivered) . '</li></ul>'
. '<p>Если поставка уже пришла — привяжите полученное к документу, '
. 'и напоминания прекратятся.</p>'
. \app\components\Notifier::modelLinksFooter($doc);
},
'repeat' => '1 week',
],
Как только поставка закрывается (всё привязано), объект перестаёт проходить
filter — напоминания прекращаются сами.
Учтите: filter выполняется на каждом объекте SQL-выборки и может дёргать
связи (счётчики поставки — это запросы к привязкам), поэтому condition
рядом с ним — не украшение, а способ не гонять PHP-проверку по всей таблице.
age) — важный нюансage сравнивается с updated_at объекта — временем последнего изменения,
а не временем входа в текущее состояние. То есть правило читается как
«подходит под условие И не менялся дольше N»: любое редактирование объекта
(даже правка комментария) сбрасывает отсчёт. Для типового сценария «завис —
никто не трогает» это ровно то, что нужно; отсчитывать именно время
пребывания в статусе механизм не умеет.
Объекты с пустым updated_at (никогда не менялись после импорта) считаются
залежавшимися и под правило попадают.
Форматы интервалов (age, repeat) — любые, понятные strtotime:
'30 minutes', '2 hours', '1 day', '2 weeks'.
Получатели определяются самим объектом — методом getNotifyRecipients()
(интерфейс NotifyRecipientsInterface). Сейчас его реализуют:
Уволенные и сотрудники без e-mail отбрасываются автоматически. Чтобы сторожить другую сущность, её модель должна реализовать этот интерфейс (одна связь + одна строка кода) — это уже доработка, а не конфигурация.
extraRecipients)Если письма правила должен получать кто-то сверх автосписка ответственных
(типовой случай — руководитель хочет копии всех подобных напоминаний),
добавьте в правило ключ extraRecipients:
'extraRecipients' => ['ivanov-ii'], //логин...
'extraRecipients' => ['boss@example.com'], //...или e-mail...
'extraRecipients' => [42], //...или id сотрудника; можно вперемешку
'extraRecipients' => fn($doc) => [...], //callable($model): Users[] - если состав зависит от объекта
Дополнительные получатели добавляются к ответственным, дубли схлопываются,
фильтр «уволен/без почты» и интервал repeat действуют на них так же
(повтор отсчитывается каждому получателю отдельно). Адресат с опечаткой
не роняет прогон — он пропускается с записью в warning-лог приложения.
subject, body){name} (имя объекта) и {id};function($model): string, если нужно собрать текст из
произвольных полей объекта;body не задан — письмо соберётся из темы и стандартного футера со
ссылками «Открыть» / «История изменений». Чтобы ссылки работали из
консольной команды, в params должен быть задан web.hostInfo (см.
настройку).Каждая пара «правило + объект» имеет свой ключ события
(watch:<имя-правила>:<id>), и по нему механизм гарантирует:
notify/watch не плодят
дубли — просто освежают содержимое письма в очереди;repeat (или навсегда,
если repeat не задан). Память об отправленном хранится в самой
очереди уведомлений — поэтому её чистка
отложенная (notify/cleanup удаляет отправленное старше 90 дней), и
поэтому ручное удаление отправленной записи означает «можно напомнить
снова».Повтор отсчитывается на каждого получателя отдельно: сотрудник, добавленный к документу позже, получит своё письмо, даже если остальным оно уже уходило.
age (например '1 minute') и без repeat.php yii notify/watch — команда напишет, сколько
писем поставлено в очередь.php yii notify/send (или дождитесь cron); при включённом
mailer.useFileTransport письма падают в runtime/mail — удобно для
проверки без реального SMTP.age/repeat.Правило с ошибкой (несуществующий класс, класс без ответственных) не
останавливает прогон — notify/watch сообщает о нём и продолжает со
следующими правилами.