Время работы PHP-кода CRM складывается из чтения данных, проверок и связанных действий при сохранении. Фабрика выбирает элементы, а операция проверяет и сохраняет их, обновляет связанные данные и запускает автоматизацию. Чтобы найти причину задержки, измеряйте эти участки отдельно. Чтобы исправить ошибку, сохраняйте результат операции и проверяйте фактическое состояние элемента перед повтором.
Диагностику выполняйте на тестовых данных. Пример записи меняет название существующей сделки. Роли фабрики и операции объясняет статья Схема работы CRM и основные объекты API, общий порядок создания, чтения и изменения — статья Работа с элементами CRM.
Измерить нагрузку
Сначала воспроизведите проблему для одного типа элемента, одного пользователя и фиксированного набора данных. Запишите фильтр, состав полей, размер порции и ожидаемый результат. Сравнение разных пользователей или направлений не покажет эффект изменения кода — выборки и проверки доступа могут различаться.
Измерить время и SQL-запросы
Метод Connection::startTracker(true) включает сбор SQL-запросов на выбранном соединении и очищает предыдущую статистику. Метод stopTracker() останавливает сбор. Объект Bitrix\Main\Diag\SqlTracker возвращает число запросов через getCounter() и их суммарное время в секундах через getTime(). Работа с отдельными запросами описана в статье Отладка запросов.
Запускайте замер в отдельном диагностическом запросе без другого активного SQL-трекера. Время участка измеряйте отдельно от времени SQL. Разница включает работу PHP и другие действия внутри участка, а не только работу CRM с базой данных.
Пример. Прочитайте до 100 сделок, доступных пользователю $userId, и соберите показатели выборки. Фабрика создается до начала замера.
Переменная $userId содержит положительный ID пользователя, от имени которого код читает и меняет данные. В фоновом задании передайте ID назначенного для этой задачи пользователя. Чтение списка и контекст записи должны использовать одного пользователя.
if (!\Bitrix\Main\Loader::includeModule('crm'))
{
throw new \RuntimeException('Не удалось подключить модуль CRM');
}
if ($userId <= 0)
{
throw new \RuntimeException('Укажите ID пользователя');
}
$factory = \Bitrix\Crm\Service\Container::getInstance()->getFactory(\CCrmOwnerType::Deal);
if ($factory === null)
{
throw new \RuntimeException('Фабрика сделок недоступна');
}
$connection = \Bitrix\Main\Application::getConnection();
$tracker = $connection->startTracker(true);
$startedAt = microtime(true);
try
{
$deals = $factory->getItemsFilteredByPermissions([
'select' => ['ID', 'TITLE', 'COMPANY_ID'],
'filter' => ['>ID' => 0],
'order' => ['ID' => 'ASC'],
'limit' => 100,
], $userId);
}
finally
{
$elapsedSeconds = microtime(true) - $startedAt;
$connection->stopTracker();
}
$metrics = [
'items' => count($deals),
'elapsedSeconds' => $elapsedSeconds,
'sqlQueries' => $tracker->getCounter(),
'sqlSeconds' => $tracker->getTime(),
];
Массив $metrics содержит показатели выборки.
-
items— число выбранных элементов, -
elapsedSeconds— длительность участка в секундах, -
sqlQueries— число SQL-запросов, -
sqlSeconds— суммарное время SQL-запросов в секундах.
Если выборка выбросит исключение, блок finally остановит трекер и передаст исключение выше по стеку вызовов. Код не сформирует массив $metrics.
Параметры выборки определяют состав и объем данных.
-
select— поля результата, -
filter— условия отбора, -
order— порядок элементов, -
limit— максимальное число элементов.
Поле COMPANY_ID хранит ID компании клиента и понадобится для пакетного чтения названий компаний.
Трекер учитывает только запросы выбранного соединения во время замера. Он не измеряет весь HTTP-запрос, другие соединения и будущую работу фоновых заданий. Метод getQueries() возвращает отдельные запросы для дальнейшего разбора. Их тексты могут содержать данные клиентов, поэтому храните подробную диагностику в закрытом журнале.
Сравнить результаты
Повторите одинаковую выборку несколько раз и отдельно сравните первый и последующие запуски. Кеши и уже загруженные данные влияют на измерение. Меняйте за один опыт один параметр — состав полей, размер порции или способ получения связанных записей.
|
|| Признак | Что проверить | Следующее действие ||
|| Число запросов растет вместе с числом элементов | Чтение компании, контактов или других данных внутри цикла | Собрать уникальные ID и загрузить связанные элементы порцией ||
|| Запросов немного, но SQL занимает большую часть времени | Фильтр, сортировку и состав выборки | Сузить выборку и сравнить длительность тех же запросов ||
|| Чтение быстрое, а launch() занимает много времени | Обработчики, действия операции и автоматизацию | Измерить запись отдельно от чтения и разобрать связанные действия ||
|| Время приемлемое, но процессу не хватает памяти | Размер выборки, накопленные элементы и журнал в памяти | Уменьшить порцию и сохранять результаты обработки вне массива всего задания ||
|#
Ни один признак сам по себе не доказывает причину. Например, число вызовов фабрики не равно числу SQL-запросов — фабрика может сначала выбрать ID, а затем загрузить данные элементов.
Оптимизировать чтение элементов
Основной источник лишней работы при чтении — данные, которые код загрузил, но не использовал. Ограничьте поля и размер выборки, затем проверьте обращения к связанным объектам внутри цикла.
Выбрать нужные поля и сохранить проверку прав
Метод Factory::getItemsFilteredByPermissions() добавляет ограничения по правам пользователя. Методы getItems() и getItem() сами не ограничивают чтение его правами. Замена метода ради меньшего времени меняет доступ к данным. Для пользовательского списка сохраняйте выборку с проверкой прав.
Если не передать select или передать пустой массив, фабрика использует ['*']. Такой выбор включает пользовательские поля и поддерживаемые типом связанные данные, в том числе контакты, товары и наблюдателей. Для списка названий задайте ['ID', 'TITLE'], для списка с компанией — ['ID', 'TITLE', 'COMPANY_ID'].
Узкий select предназначен для чтения. При запуске операции над существующим элементом метод launch() вызывает Item::fill() и догружает данные. Поэтому сокращение полей списка не означает, что запись выполнится без дополнительных запросов.
Фильтруйте элементы в параметре filter, а не после загрузки всей CRM в PHP. Для длинного обхода задайте order по ID, limit и условие >ID от последней обработанной записи. Подробный пример с верхней границей ID и продолжением после сбоя приведен в статье Работа с элементами CRM.
Загрузить связанные данные порцией
Если для каждой сделки отдельно запрашивать компанию, один список порождает множество повторных обращений. Соберите уникальные ID компаний из порции сделок, затем получите компании одной выборкой фабрики. Ограничения чтения компаний проверяйте отдельно — доступ к сделке не заменяет право прочитать клиента.
Пример. Получите названия компаний для массива $deals с загруженным полем COMPANY_ID. Чтобы подготовить массив, выберите сделки с замером нагрузки. Параметр filter с ключом @ID отбирает компании по набору ID, а select ограничивает результат полями ID и TITLE.
$companyIds = [];
foreach ($deals as $deal)
{
$companyId = (int)$deal->get('COMPANY_ID');
if ($companyId > 0)
{
$companyIds[$companyId] = $companyId;
}
}
$companyTitles = [];
if ($companyIds !== [])
{
$companyFactory = \Bitrix\Crm\Service\Container::getInstance()->getFactory(\CCrmOwnerType::Company);
if ($companyFactory === null)
{
throw new \RuntimeException('Фабрика компаний недоступна');
}
$companies = $companyFactory->getItemsFilteredByPermissions([
'select' => ['ID', 'TITLE'],
'filter' => ['@ID' => array_values($companyIds)],
], $userId);
foreach ($companies as $company)
{
$companyTitles[$company->getId()] = (string)$company->get('TITLE');
}
}
Массив $companyTitles сопоставляет ID компании и название. Размер списка ID ограничен исходной порцией сделок. Если компании нет в результате, не пытайтесь получить ее название методом без проверки прав — запись может отсутствовать или быть недоступна. При пустом списке ID код вообще не вызывает выборку компаний.
Пакетное чтение сокращает число обращений к фабрике, но не гарантирует один SQL-запрос. Измерьте весь участок, включая загрузку компаний, и сравните его с чтением внутри цикла. Для контактов и других видов привязок учитывайте модель связи из статьи Связи между элементами.
Обработать большой объем изменений
Массовое изменение состоит из отдельных операций над элементами. Размер порции ограничивает объем чтения и работы за один проход, но каждый launch() сохраняет свои проверки и связанные действия. Начните с небольшой порции на тестовой установке и подберите размер по времени и памяти.
Сохранить результат каждого элемента
Используйте порядок по ID и постоянный журнал задания. В журнале храните тип элемента, фильтр, ID пользователя, верхнюю границу выборки, последнюю обработанную позицию и результаты отдельных операций. Список ошибок нужен отдельно от позиции — продолжение после последнего ID само по себе не повторит пропущенные записи.
Верхняя граница ID исключает более поздние записи с большими ID, но не фиксирует значения полей и права на момент старта. Пользователь или другой процесс может изменить элемент во время обхода. Для параллельных заданий распределите элементы так, чтобы ваши обработчики не меняли один ID одновременно.
Перед записью сравните текущее и нужное значение. Если изменение не требуется, не запускайте операцию только ради отметки в журнале. После каждого элемента сохраняйте результат и контрольную точку задания. Если процесс остановился между записью в CRM и записью в журнал, при продолжении сначала перечитайте элемент.
Полный цикл обхода приведен в статье Работа с элементами CRM. Для обмена с внешней системой дополнительно нужны внешний ключ и правила повторной доставки, описанные в статье Синхронизация CRM с внешней системой.
Учесть стоимость сохранения
Метод launch() выполняет больше работы, чем запись основных полей. В зависимости от типа и настроек операция обрабатывает поля и связанные данные, обновляет права, индексы и счетчики, пишет историю и таймлайн, выполняет действия после сохранения и запускает автоматизацию.
Сначала измерьте чтение, затем отдельно запуск операции над теми же тестовыми данными. Если задержка связана с доработкой, проверьте запросы и внешние вызовы ее обработчика. Повторная запись того же элемента из обработчика может снова запустить обработку. Способы защиты от повторов описаны в статье События и расширение поведения CRM.
Сохраняйте элементы через операцию фабрики. Прямая запись через ORM не заменяет полный порядок действий CRM. Методы disableAllChecks(), disableAutomation() и disableBizProc() меняют поведение операции, поэтому не используйте их как общее средство ускорения. Аналогично отключение истории или действий после сохранения требует отдельного решения о том, какие результаты больше не нужны.
Найти причину ошибки
Отделяйте ошибку подготовки данных от ошибки операции и от неожиданного результата после сохранения. Для воспроизведения сохраните тип и ID элемента, ID пользователя, изменяемые поля и момент сбоя. Не записывайте в общий журнал полную карточку клиента.
Различить Result и исключение
Метод launch() возвращает Bitrix\Main\Result. Общая модель этого объекта описана в статье Результат работы и ошибки. Для диагностики используйте методы результата.
-
isSuccess()— возвращаетtrue, если результат не содержит ошибок. -
getErrorMessages()— возвращает массив сообщений об ошибках. -
getErrors()— возвращает массив объектовBitrix\Main\Error.
Объект Error предоставляет детали каждой ошибки.
-
getCode()— код ошибки, -
getMessage()— текст сообщения, -
getCustomData()— дополнительные данные.
Сохраняйте все ошибки результата. Если для ошибки есть константа, используйте ее при проверке кода ошибки. Текст сообщения зависит от языка. Произвольная ошибка может иметь код 0 и не содержать дополнительных данных.
Исключение при подготовке поля, чтении или внутри обработчика может прервать выполнение до возврата Result. Проверка isSuccess() такое исключение не перехватит. На границе задания записывайте исключение в журнал и передавайте его выше либо помечайте задание как неуспешное.
Пример. Переименуйте тестовую сделку с ID 123 и соберите диагностику в $diagnostics. Замените ID на существующую сделку, доступную пользователю $userId. Модуль crm уже подключен. Код перехватывает исключение, добавляет его данные и выбрасывает повторно.
$dealId = 123;
$diagnostics = [
'entityTypeId' => \CCrmOwnerType::Deal,
'itemId' => $dealId,
'userId' => $userId,
'success' => false,
'errors' => [],
];
try
{
$factory = \Bitrix\Crm\Service\Container::getInstance()->getFactory(\CCrmOwnerType::Deal);
if ($factory === null)
{
throw new \RuntimeException('Фабрика сделок недоступна');
}
$items = $factory->getItemsFilteredByPermissions([
'filter' => ['=ID' => $dealId],
], $userId);
$deal = $items[0] ?? null;
if ($deal === null)
{
throw new \RuntimeException('Сделка не найдена или недоступна');
}
$deal->set('TITLE', 'Проверка обработки сделки');
$context = (new \Bitrix\Crm\Service\Context())->setUserId($userId);
$result = $factory->getUpdateOperation($deal, $context)->launch();
$diagnostics['success'] = $result->isSuccess();
foreach ($result->getErrors() as $error)
{
$diagnostics['errors'][] = [
'code' => $error->getCode(),
'message' => $error->getMessage(),
];
}
}
catch (\Throwable $exception)
{
$diagnostics['exception'] = [
'class' => get_class($exception),
'message' => $exception->getMessage(),
];
throw $exception;
}
В массиве $diagnostics ключи entityTypeId, itemId и userId определяют контекст, success хранит статус результата, errors — список кодов code и сообщений message. При исключении ключ exception содержит его класс class и сообщение. Массив существует только в памяти — подключите постоянный журнал задания, если диагностика нужна после завершения процесса. Дополнительные данные ошибки читайте через getCustomData() только при необходимости.
Чтение с учетом прав не гарантирует права изменения. Операция отдельно проверяет запись от пользователя, которого передали в Context::setUserId().
Проверить тип, поля, стадию и доступ
Начните с признака ошибки и проверьте конкретный параметр. Один и тот же ID может существовать у элементов разных типов, поэтому для диагностики всегда нужна пара типа и ID.
|
|| Признак | Возможная причина и проверка | Исправление ||
|| getFactory() вернул null | Передан неверный ID типа или для него нет фабрики. Для смарт-процесса проверьте entityTypeId, а не ID записи настроек типа | Получить поддерживаемый тип. Порядок определения типа приведен в статье Смарт-процессы ||
|| Выборка вернула пустой массив | Элемент отсутствует, не соответствует фильтру или недоступен пользователю. Пользователь с ID 0 не получает элементы через getItemsFilteredByPermissions() | Проверить тип, ID, фильтр и пользователя. Не заменять выборку на чтение без прав ||
|| Ошибка неизвестного поля или неверного значения | Имя не входит в getFieldsCollection() выбранной фабрики либо значение не соответствует типу поля | Проверить описание поля и формат. Подготовка значений описана в статье Поля и данные клиента ||
|| Значение не изменилось без ошибки | Поле доступно только для чтения или неизменяемо после создания | Проверить Field::isValueCanBeChanged() и использовать API соответствующего вида данных ||
|| Операция сообщает об обязательном поле | Обязательность зависит от настроек, операции и стадии | После задания новых значений вызвать getRequiredFields() у операции и заполнить нужные поля ||
|| Код CRM_ITEM_UPDATE_ACCESS_DENIED | Пользователь контекста не может изменить элемент | Проверить права записи, направление и пользователя операции. Не отключать checkAccess ||
|| Стадия осталась прежней или CRM выбрала другую | Передан статус не из текущего направления. При изменении в прежнем направлении CRM может вернуть старую стадию, при создании или переносе — подобрать стадию | Выбрать статус из Factory::getStages($categoryId) и проверить итог. Порядок приведен в статье Направления и стадии ||
|| Стадия существует, но переход запрещен | Право изменить элемент не заменяет проверку конкретного перехода | Проверить право сменить стадию и разрешенные переходы пользователя ||
|| Код CRM_ITEM_HAS_RUNNING_WORKFLOWS при смене направления | Для элемента выполняются бизнес-процессы | Дождаться завершения или завершить процессы по правилам задачи, затем повторить перенос ||
|| Клиент или другая связь отсутствует в результате | Выбран другой вид привязки, связанная запись недоступна либо набор связей заменили целиком | Проверить пару типа и ID, права и актуальный набор. Модель привязок описана в статье Связи между элементами ||
|#
Коды CRM_ITEM_ADD_ACCESS_DENIED и CRM_ITEM_DELETE_ACCESS_DENIED обозначают отказ в создании и удалении. Константы этих кодов находятся в классе Bitrix\Crm\Service\Operation. Для иных ошибок сохраняйте исходное сообщение — не назначайте им код по похожему тексту.
Проверить состояние перед повтором
После ошибки Result, исключения или обрыва запроса сначала определите, какие изменения уже сохранились. Причины частичного выполнения и границы результата приведены в статье Схема работы CRM и основные объекты API. Выполните проверку по порядку.
-
Перечитайте элемент через новую выборку фабрики с учетом прав.
-
Сравните сохраненные поля и связи с ожидаемыми значениями.
-
Проверьте результаты обработчиков и внешних вызовов, если они участвовали в операции.
-
Повторите только незавершенное действие после устранения причины.
Не запускайте повторное создание, пока не проверили, появился ли элемент при первой попытке. Для повторяемого изменения формулируйте действие через нужное итоговое значение. Установка заданного названия допускает проверку перед повтором, а безусловное добавление комментария или отправка уведомления может дать дубль.
Если дальнейший шаг зависит от робота или бизнес-процесса, отдельно проверьте его результат даже при успешном isSuccess().
Проверить исправление
После изменения кода повторите исходный сценарий с тем же пользователем и сопоставимыми данными. Сначала проверьте результат, затем сравните нагрузку. Ускорение за счет пропущенной проверки или потерянной связи не решает задачу.
|
|| Сценарий | Что проверить в данных | Что сравнить ||
|| Чтение списка | Набор ID, выбранные поля и доступ пользователя | Время, число SQL-запросов и память при одинаковом размере порции ||
|| Чтение связанных данных | Названия доступных компаний, отсутствие лишних записей | Весь участок с загрузкой компаний, а не только исходную выборку ||
|| Запись | Результат операции, перечитанные поля, связи и необходимые связанные действия | Время чтения и launch() по отдельности ||
|| Отказ в правах или неверные данные | Ошибку в диагностике и отсутствие недопустимого результата | Поведение обычного пользователя, а не только администратора ||
|| Повтор после сбоя | Отсутствие дублей и сохранность уже выполненных изменений | Журнал, фактическое состояние и действие, выбранное для повтора ||
|| Продолжение массового задания | Успешные и неуспешные ID, сохраненную границу и позицию | Число обработанных элементов и суммарное время задания ||
|#
Сохраните исходные и новые измерения вместе с условиями запуска. Если изменились версия CRM, настройки автоматизации или права пользователя, повторите проверку — прежнее измерение уже не описывает эти условия.