Скрипты и выражения
Где встречаются скрипты
| Место | Движок | Доступные переменные |
|---|---|---|
Automation-хуки сущности (before_create, after_update…) | SafeScriptRunner::execute() | $data, $old, $user, $api |
BPMN scriptTask | BpmnEngine → eval() | $api, $payload, $entityId |
| IncomingAction (трансформация сообщений) | SafeScriptRunner::executeIncomingAction() | $incoming, $data, $api |
BPMN exclusiveGateway условие | Symfony ExpressionLanguage | переменные из process_variables |
Automation-хуки сущности
Определяются в конфигурации таблицы (поле automations). Запускаются через SafeScriptRunner::execute().
Триггеры
| Триггер | Когда | $old | Прерывает операцию при исключении |
|---|---|---|---|
before_create | До записи в БД | null | Да |
after_create | После записи | null | Нет (только лог) |
before_update | До UPDATE | данные до изменения | Да |
after_update | После UPDATE | данные до изменения | Нет (только лог) |
before_delete | До DELETE | текущие данные | Да |
after_delete | После DELETE | данные записи | Нет (только лог) |
Системные поля недоступны
Поля created_by, updated_by, created_at, updated_at удаляются из $data до передачи в хук. Обращаться к ним нельзя.
Переменные контекста
$data // array — текущие/новые данные; в before_* изменения на этом массиве попадают в БД
$old // array|null — данные до изменения (только в before/after_update и before/after_delete)
$user // object — текущий пользователь: $user->id, $user->name, $user->email
$api // PublicApiHelper — работа с БД, уведомления, утилитыРеальные примеры из пресетов
Автозаполнение поля при создании:
// before_create — it_requests
if (empty($data['requested_by'])) {
$data['requested_by'] = $user->id;
}Уведомление при смене исполнителя:
// after_update — it_requests
if (isset($data['assignee']) && ($old['assignee'] ?? null) != $data['assignee']) {
$api->sendPushNotification(
'Новая заявка',
'Вам назначена заявка: ' . $data['name'],
'/e/it_requests/' . $data['id'],
[$data['assignee']]
);
}Уведомление при смене статуса (с маппингом текста):
// after_update — it_requests
if (isset($data['status']) && ($old['status'] ?? null) != $data['status']
&& !empty($data['requested_by'])) {
$statusLabels = [
'assigned' => 'Назначен исполнитель',
'in_work' => 'Взята в работу',
'waiting' => 'Ожидает ответа от вас',
'resolved' => 'Решена — пожалуйста, подтвердите',
'rejected' => 'Отклонена',
'closed' => 'Закрыта',
];
if (isset($statusLabels[$data['status']])) {
$api->sendPushNotification(
'Заявка: ' . $data['name'],
$statusLabels[$data['status']],
'/e/it_requests/' . $data['id'],
[$data['requested_by']]
);
}
}Расчёт дедлайна по SLA категории:
// before_update — it_requests
if (isset($data['status']) && $data['status'] === 'assigned'
&& empty($data['due_date']) && !empty($data['category'])) {
$cat = $api->get('it_categories', $data['category']);
if ($cat && !empty($cat['sla_hours'])) {
$data['due_date'] = date('Y-m-d H:i:s', time() + $cat['sla_hours'] * 3600);
}
}Обновление связанной записи при выигрыше сделки:
// after_update — crm_deals
if (isset($data['stage']) && $data['stage'] === 'won' && !empty($data['client'])) {
$client = $api->get('crm_clients', $data['client']);
if ($client && in_array($client['status'] ?? '', ['lead', 'prospect'])) {
$api->update('crm_clients', $data['client'], ['status' => 'active']);
}
}Валидация дат (бросает исключение — прерывает операцию):
// before_create — hr_vacations
if (!empty($data['start_date']) && !empty($data['end_date'])) {
if (strtotime($data['start_date']) > strtotime($data['end_date'])) {
throw new \Exception('Дата начала не может быть позже даты окончания.');
}
}SafeScriptRunner — ограничения и детали
Чёрный список
Следующие функции и классы запрещены (проверяются через token_get_all(), регистронезависимо):
exec, passthru, system, shell_exec, popen, proc_open, pcntl_exec,
eval, assert, create_function,
include, require,
file_get_contents, file_put_contents, unlink, rmdir, mkdir,
phpinfo,
Database, DB, Schema, Artisan, Storage,
envТакже запрещены backtick-команды `ls`.
Как выполняется код
- Стрипаются PHP-теги (
<?php,?>). - Проверяется чёрный список — выбрасывается исключение при нарушении.
- Код оборачивается в замыкание с инъекцией контекстных переменных через
extract(). - Выполняется через
eval(). - Возвращается изменённый
$data.
PHP-теги
Можно писать скрипты как с <?php, так и без — runner их удаляет.
Различия контекстов
execute() — automation-хуки:
// Всегда доступны:
$data // модифицируемый массив данных записи
$old // данные до изменения (или null)
$user // текущий пользователь
$api // PublicApiHelperexecuteIncomingAction() — IncomingAction:
$incoming // данные входящего сообщения
$data // формируемые данные для записи
$api // PublicApiHelper
// Чёрный список НЕ применяется!IncomingAction скрипты
В скриптах IncomingAction чёрный список не проверяется. Ограничивайте доступ к настройке IncomingAction через права.
$api — PublicApiHelper: полный справочник
Чтение данных
$api->get(string $tableName, $id): ?array
Получить запись по ID.
$record = $api->get('it_requests', 42);
// ['id' => 42, 'name' => 'VPN не работает', 'status' => 'new', ...]
// null — если запись не найдена или $id пустойРаботает как со стандартными таблицами (users, sys_companies, sys_org_units, sys_roles), так и с динамическими. Для динамических декодирует JSON из колонки data и добавляет id, created_at, updated_at, created_by, updated_by.
$api->find(string $tableName, string $field, $value): ?array
Найти одну запись по значению поля.
$user = $api->find('users', 'email', 'ivan@example.com');
$category = $api->find('it_categories', 'name', 'Сеть');Для динамических таблиц использует PostgreSQL-синтаксис data->>'field' = ?.
$api->getRelated(int $relationId, string $tablePosition, $instanceId): array
Получить связанные записи (M2M).
// tablePosition: 'l_table' или 'r_table' — позиция текущей таблицы в связи
$relatedTasks = $api->getRelated($relationId, 'l_table', $projectId);$api->getRelatedByTable(string $sourceTable, string $targetTable, $instanceId, ?string $relationName = null): array
Удобная обёртка — не нужно знать ID связи:
$contacts = $api->getRelatedByTable('crm_clients', 'crm_contacts', $clientId);$api->findRelationInfo(string $sourceTable, string $targetTable, ?string $relationName = null): ?array
Найти метаданные связи. Возвращает ['relation_id', 'position', 'relation'] или null.
$api->getEntityRelations(string $tableName): array
Все связи, в которых участвует таблица.
Изменение данных
$api->update(string $tableName, int $id, array $data): bool
Обновить запись.
$api->update('it_requests', $entityId, [
'status' => 'resolved',
'resolution' => 'Обновлён драйвер'
]);Только стандартные таблицы
update() работает только со стандартными таблицами (users, sys_companies и т.д.). Для динамических сущностей (кастомные таблицы) используйте прямой вызов через BPMN serviceTask или update_field_* свойство, либо создавайте отдельный API-запрос. В контексте BPMN scriptTask — $api->update() на динамической таблице вернёт false.
Обновление динамических записей из scriptTask: движок делает это через прямой SQL-запрос внутри executeNode() при обработке serviceTask. В scriptTask для обновления используйте свойство update_field_* вместо $api->update().
$api->addRelated(int $relationId, string $tablePosition, $instanceId, array $relatedIds): bool
Привязать записи к связи.
$api->addRelated($relationId, 'l_table', $projectId, [5, 7, 12]);Проверяет дубликаты перед вставкой. Возвращает false при отсутствии связи/таблицы или если ничего не вставлено.
$api->addRelatedByTable(string $sourceTable, string $targetTable, $instanceId, array $relatedIds, ?string $relationName = null): bool
Удобная обёртка с именами таблиц:
$api->addRelatedByTable('crm_clients', 'crm_contacts', $clientId, [8, 9]);$api->unlinkRelated(int $relationId, string $tablePosition, $instanceId, $targetId): bool
Удалить одну запись из связи.
$api->unlinkRelatedByTable(string $sourceTable, string $targetTable, $instanceId, $targetId, ?string $relationName = null): bool
Обёртка с именами таблиц.
Уведомления
$api->sendPushNotification(string $title, string $message, string $route, array $userIds, string $category = 'system'): void
Внутреннее уведомление (колокольчик 🔔).
$api->sendPushNotification(
'Заявка решена',
'Пожалуйста, подтвердите решение.',
'/e/it_requests/' . $entityId,
[$record['requested_by']],
'system' // 'system' или 'message'
);category = 'message' — попадает в ленту сообщений, а не системных оповещений.
$api->sendTelegram(string $message, array $userIds, int|string|null $channel = null): void
Telegram-сообщение пользователям.
// бот по умолчанию
$api->sendTelegram('Новая заявка требует рассмотрения', [3, 5]);
// конкретный бот при нескольких TG-каналах — по названию или id канала
$api->sendTelegram('VIP-заявка', [3], 'Support Bot');
$api->sendTelegram('VIP-заявка', [3], 7);Отправляется только пользователям, у которых заполнено telegram_chat_id и включено notify_via_telegram. Уходит через исходящий Telegram-канал (раздел «Каналы» → «Исходящие») с ретраями и историей доставки: без третьего аргумента — канал по умолчанию, иначе указанный по названию (строка) или id (число). Если запрошенный канал не найден/неактивен — сообщение не отправляется (ошибка в лог), чтобы не уйти через чужого бота. Fallback на legacy-токен настроек срабатывает только когда default-канала нет вовсе.
$api->replyTelegram(string $chatId, string $text, ?int $channel = null): void
Прямой ответ в конкретный чат (не требует поиска пользователя по ID).
$api->replyTelegram($incoming['from_id'], 'Ваша заявка принята в обработку.');В скриптах входящих действий ответ по умолчанию уходит через того же бота, который принял сообщение (канал берётся из $incoming), — важно при нескольких ботах ТГ. Явно переопределить бота можно третьим аргументом — id входящего telegram-канала. Таймаут: 5 секунд.
$api->sendEmail(string $templateCode, $to, array $params = [], int|string|null $channel = null): bool
Email по шаблону.
$api->sendEmail('ticket_resolved', 'user@example.com', [
'ticket_name' => $record['name'],
'resolution' => $record['resolution'],
]);
// через конкретный SMTP-канал (по названию или id) при нескольких почтовых каналах
$api->sendEmail('invoice_ready', $record['email'], $params, 'Billing');$to — строка/user id или массив того и другого. Шаблон ищется по code в таблице sys_notification_templates. Плейсхолдеры заменяются значениями $params. Четвёртый аргумент $channel выбирает конкретный SMTP-канал (по умолчанию — default smtp-канал тенанта). Возвращает false при ошибке (также логируется).
$api->sendToChannel(int|string $channel, array $envelope): bool
Низкоуровневая отправка в любой исходящий канал — webhook, kafka, amqp, sms, а также telegram/smtp напрямую. Нужен, когда получатель — не пользователь системы (внешний вебхук, произвольный chat_id/телефон) или требуется payload-канал.
// в канал по id
$api->sendToChannel(7, [
'recipients' => ['79991234567'], // адреса; для Telegram — chat_id
'body' => 'Заявка #' . $data['id'] . ' в работе',
]);
// в канал по умолчанию заданного типа
$api->sendToChannel('webhook', [
'payload' => ['event' => 'ticket.updated', 'id' => $data['id']],
]);$channel — id канала или его тип для канала по умолчанию. $envelope: recipients (адреса или ['address' => ..., 'user_id' => ...]), subject, body, payload. В отличие от sendTelegram/sendEmail, получателей резолвит не система — chat_id/адреса передаются как есть.
Для типовых каналов удобнее специализированные методы ниже — у каждого свой естественный набор аргументов.
$api->sendSms(string|array $phones, string $text, int|string|null $channel = null): bool
SMS на один номер или массив номеров (fanout — каждому своя доставка). $channel — конкретный SMS-канал (id/название) при нескольких провайдерах, null → канал по умолчанию типа sms.
$api->sendSms($record['phone'], "Код подтверждения: 1234");
$api->sendSms(['79990000001', '79990000002'], 'Массовая рассылка', 'SMSC-резерв');$api->sendWebhook(array $payload, int|string|null $channel = null): bool
POST JSON во внешний HTTP-вебхук (HMAC-подпись, auth и URL настроены на канале). null → канал по умолчанию типа webhook.
$api->sendWebhook([
'event' => 'ticket.created',
'id' => $data['id'],
'title' => $data['name'],
]);$api->publish(array $payload, int|string $channel, ?string $key = null): bool
Публикация payload в брокер сообщений (Kafka / AMQP) или любой payload-канал. $channel обязателен — id канала или его тип (единого «default» среди брокеров нет). $key — ключ партиции Kafka (по умолчанию — id привязанной записи, что даёт стабильный порядок событий записи).
$api->publish(['id' => $data['id'], 'status' => $data['status']], 'kafka');
$api->publish($payload, 12, key: $data['order_no']); // конкретный канал + свой ключ| Тип канала | Метод | Ключевые аргументы |
|---|---|---|
| telegram | sendTelegram | текст, user id, бот |
| smtp | sendEmail | шаблон, адреса, канал |
| sms | sendSms | номера, текст, канал |
| webhook | sendWebhook | payload, канал |
| kafka / amqp | publish | payload, канал, ключ |
| любой | sendToChannel | канал, конверт |
Состояние Telegram-бота (для пошаговых диалогов)
$api->setBotState(string $chatId, string $state): void // TTL: 3600 сек
$api->getBotState(string $chatId): ?string
$api->clearBotState(string $chatId): voidПример пошагового диалога в IncomingAction-скрипте:
$state = $api->getBotState($incoming['from_id']);
if ($state === 'waiting_description') {
$data['details'] = $incoming['body'];
$data['name'] = $api->getBotState($incoming['from_id'] . '_name');
$api->clearBotState($incoming['from_id']);
$api->clearBotState($incoming['from_id'] . '_name');
// $data готов — создаём запись
} elseif ($state === 'waiting_name') {
$api->setBotState($incoming['from_id'] . '_name', $incoming['body']);
$api->setBotState($incoming['from_id'], 'waiting_description');
$api->replyTelegram($incoming['from_id'], 'Опишите проблему подробнее:');
return; // не создаём запись
} else {
$api->setBotState($incoming['from_id'], 'waiting_name');
$api->replyTelegram($incoming['from_id'], 'Введите тему обращения:');
return;
}Утилиты
$api->now(): Carbon // Carbon::now()
$api->parseDate(string $date): Carbon // Carbon::parse($date)
$api->log($message): void // Пишет в laravel.log с префиксом [Automation Log]
// Массивы/объекты — JSON_UNESCAPED_UNICODEУсловия в BPMN Gateway (ExpressionLanguage)
Symfony ExpressionLanguage. Переменные — все ключи из process_variables.
Синтаксис
${переменная оператор значение}Поддерживаются ${} и #{}.
Операторы
| Тип | Синтаксис |
|---|---|
| Равенство | == != |
| Сравнение | > < >= <= |
| Логика | and or not (или && || !) |
| Строки | status == 'approved' |
| null-проверка | assignee == null |
Примеры
${approved}
${approved == true}
${status == 'won'}
${amount > 100000}
${priority == 'critical' or priority == 'high'}
${approved and not rejected}
${assignee != null}
${stage == 'negotiation' and amount >= 500000}Булевы переменные
${approved} и ${approved == true} эквивалентны, если approved — это true или false.
Ошибки в условиях
При ошибке разбора условия evaluateCondition() возвращает false и логирует исключение. Процесс не падает, но ветка не выбирается — убедитесь в корректности атрибута default на gateway.