catcodeio / amocrm
SDK для работы с API amoCRM
Requires
- php: ^7.4
- ext-curl: *
- ext-json: *
- amocrm/oauth2-amocrm: ^2.0
- lcobucci/jwt: ^4.1
- phpquery/phpquery: ^0.0.2
- psr/log: ~1.0
Requires (Dev)
- phpunit/phpunit: ^7
README
Большая либа для всесторонней работы с amoCRM: API и различные хаки.
Тестирование
Для прогона тестов надо подтянуть зависиомсти и из вендеров можно запусктаь тесты:
$ composer install
$ ./vendor/bin/phpunit
Можно запускать тесты для конкретного файла или даже теста-метода:
./vendor/bin/phpunit --filter FieldProviderResponseTest
./vendor/bin/phpunit --filter FieldProviderResponseTest::testSaveOne
Фабрика
Есть фабрика \Amocrm\AmocrmFactory для создания объектов для работы с amoCRM и виджетами. При этом есть сразу с проверкой реквизитов (методы get) или без (методы create):
Получение объекта для работы с API:
$amocrmApi = \Amocrm\AmocrmFactory::createApi($domain, $email, $token);
// Или
try {
$amocrmApi = \Amocrm\AmocrmFactory::getApi($domain, $email, $token);
} catch (\Amocrm\Exception\AmocrmApiException $e) {
// ...
}
Получение объекта для работы с загрузкой виджетов черех хак:
$amocrmWidget = \Amocrm\AmocrmFactory::createWidget($domain, $email, $token);
// Или
try {
$amocrmWidget = \Amocrm\AmocrmFactory::getWidget($domain, $email, $token);
} catch (\Amocrm\Exception\AmocrmApiException $e) {
// ...
}
Получение объекта для недокументированной работы с аккаунтом amoCRM:
$amocrmAccount = \Amocrm\AmocrmFactory::createAccount($domain, $email, $token);
// Или
try {
$amocrmAccount = \Amocrm\AmocrmFactory::getAccount($domain, $email, $token);
} catch (\Amocrm\Exception\AmocrmApiException $e) {
// ...
}
Получение объекта для работы с API с проверкой прав доступа админа:
try {
$amocrmApi = \Amocrm\AmocrmFactory::getApiWithAdmin($domain, $email, $token);
} catch (\Amocrm\Exception\ManagerPermissionException $e) {
// ...
}
Получение доступов
$amocrmApi = \Amocrm\AmocrmFactory::createApi($domain);
# После получения токена он автоматически запоминается в библиотеке, потому явно указывать его не нужно после.
$accessToken = $amocrmApi->auth()->getAccessToken($code);
$amocrmApi->account()->getId();
$amocrmApi = \Amocrm\AmocrmFactory::createApi($domain, $accessToken);
$amocrmApi->account()->getId();
Исключения
В зависимости от кода возвращаемой ошибки amoCRM при неуспешном запросе в API (не при работе хаком виджетов) выбрасывается определённое исключение, чтобы можно было понять что конкретно пошло не так: виноваты просроченные реквизиты менеджера или просроченная оплата в аккаунте.
При этом все исключения при работе с API могут быть перехвачены одним родительским \Amocrm\Exception\AmocrmApiException.
Например:
use Amocrm\AmocrmFactory;
use Amocrm\Exception\AccountUnavailableException;
use Amocrm\Exception\ManagerPermissionException;
use Amocrm\Exception\AmocrmApiException;
try {
$amocrmApi = AmocrmFactory::getApi($domain, $email, $token);
} catch (ManagerPermissionException $e) {
// Реквизиты менеджера неверны.
} catch (AccountUnavailableException $e) {
// Аккаунт заблокирован.
} catch (AmocrmApiException $e) {
// Остальные ошибки.
}
Amocrm API
Большая либа для работы с API отходит от работы с массивами, предоставляя в замен коллекции и модель данных, где доступ к данным происходит с помощью геттеров, а установка данных через сеттеры. При это коллекции имеют многочисленные способы фильтрации списка по полям сущностей.
Некоторые нюансы:
При добавление элементов в различные списки надо быть аккуратным, чтобы не перетереть уже существующие элементы. Например, добавляя новый тег в сделку надо в начале получить данные сущности с уже существующими тегам, чтобы добавить к ним, а потом уже всех вместе отправить на обновление. Тоже самое и со связанными сущностями ()контакты - сделка) и некоторыми доп. полями.
При получении коллекции доп. полей у сущности, а потом выборке какого-то поля, оно запишется в массив modified даже если по факту после получения доп. поля не присваивали новые значения. Этот массив уйдёт в неизменённом виде в API. В этом ничего страшного т.к. данные не поменялись, просто лишний трафик. Может быть потом надо решить эту проблему, чтобы в конечный массив неизменённые данные не попадали.
Именование методов имеет некоторый смысл:
- В коллекциях есть методы filter (
filter()
,filterByField()
иfilterOneByField()
) и их короткое гибкое дополнение get (get()
,getBy{FieldName}()
,getOneBy{FieldName}()
). - В провайдерах есть как павило метод list, который отражает полностью поведение метода из API, а все остальные это обёртки над ним. Метод
get()
для получения данных тольк по id, а другие методы из группы find для поиска по каким-либо параметрам. Также в есть методыsave()
иsaveOne()
, их отличие в том, что первый вернёт коллекцию, а второй непосредственно модель или null (если не сохранилось). - У моделей есть методы из группы get и set для каждого поля (
get{FieldName}()
,set{FieldName}()
). Если поле является массивом, то скорее всего для него есть класс-обёртка, который и будет возвращён геттером. Каждый класс-обёртка является такой же моделью и работает по тому же принципу. - Каждый объект модели или их коллекция имеет метод
isModified()
, который подскажет изменялась ли модель за время её жизни. Например, при получении сделки перед её сохранением можно проверить.
- В коллекциях есть методы filter (
Для работы с некоторыми константыми значения введены соответствующие константы:
- Для обозначения типа сущности везде используются константы из хелпера EntityType. Например,
EntityType::LEAD
, а не 2, или leads, или lead. - Для обозначения стандартных типов задач используются константы из класса TaskType. Например,
TaskType::FOLLOW_UP
, а не 1 или FOLLOW_UP. - Для обозначения типа примечания используются константы из класса NoteType. Например,
NoteType::COMMON
, а не 4. - Для обозначения типа доп. поля при его создании используются константы из класса Field. Например,
Field::Text
, а не 1. - Для использование стандартных статусов 142 и 143 есть константы в классе Status.
- Для обозначения типа сущности везде используются константы из хелпера EntityType. Например,
Для обращения к определённым доп. полям сущности или для генерации нового доп. поля (значений для него) есть метод-синоним
getCF()
у моделей и у основного объекта. Послу получения объекта доп. поля, надо проверить не явялесят ли он null, после этого уже определять его тип и работать с ним. Про верка на null нужна т.к. у сущности может быть не заполнено поле.
Примеры
Настройка клиента на работу
use Amocrm\AmocrmFactory;
$amocrmApi = AmocrmFactory::createApi('catcode.amocrm.ru', 'support@catcode.io', 'ba86fc1b0efab077d8100b1ccc005bc1');
$amocrmApi
->client()
// Таймаут для запроса.
->setTimeout(30)
// Прокси для curl.
->setProxy('66.96.200.39:80')
// Локаль для ответов от amoCRM, дефотная en.
->setLocale('ru')
// Логер имплементирующий Psr\Log\LoggerInterface.
->setLogger($logger);
// Искуственная задержка для каждого запроса, в данном случае каждый будет исполняться минимум 1 секунду.
->setUsleep(1000000);
;
Создание и удаление доп. поля в аккаунте
use Amocrm\Api\Helper\ElementType;
use Amocrm\Api\CustomField\Field;
$fieldProvider = $amocrmApi->field();
$fieldModel = $fieldProvider->create();
$fieldModel
->setName('Название')
->setType(Field::TEXT)
->setElementType(ElementType::LEAD);
$field = $fieldProvider->saveOne($fieldModel);
print_r($field->getId());
Непосредственно удаление:
$cf = $amocrmApi->account()->getCustomFields()->findOneByName('Название');
if (!$cf) {
return;
}
$fieldProvider = $amocrmApi->field();
$field = $fieldProvider->deleteOne(
$fieldProvider->createFromCustomField($cf)
);
print_r($field->getId());
// Или можно самому сгенерировать объект для удаления поля если знаешь ID.
// Указание name, type и element_type требуется для генерации origin, если
// последний известен, то можно просто его вставить setOrigin().
$field = $fieldProvider->deleteOne(
$fieldProvider
->create()
->setId(1234)
->setName('Название')
->setType(Field::TEXT)
->setElementType(ElementType::LEAD)
// Или если каким-то образом сами генерировали origin
//->setOrigin('asdf')
);
print_r($field->getId());
Создание и удаление вебхуков
use Amocrm\Api\Model\Webhook;
$webhookProvider = $amocrmApi->webhook();
$webhookModel = $webhookProvider->create();
$webhookModel
->setUrl('https://some.widget.catcode.io/webhook/handler')
->setEvents([Webhook::UPDATE_LEAD, Webhook::ADD_COMPANY]);
$webhookModel = $webhookProvider->subscribe($webhookModel);
print_r($webhookModel->getResult());
// Внимание! Желательно проверять результат добавления вебхука в getResult() т.к. если URL недоступен, то
// вебхук не будет добавлен и getResult() вернёт false. Это хотя бы можно в лог отписать или принять ещё какие-то меры.
// Добавляем ещё вебхук
$webhookProvider->subscribe(
$webhookProvider->create()
->setUrl('https://some.widget.catcode.io/webhook/another_handler')
->setEvents([Webhook::ADD_LEAD])
);
// Или можно сразу несколько
$webhooks = $webhookProvider->subscribe([
$webhookProvider->create()
->setUrl('https://some.widget.catcode.io/webhook/add_company')
->setEvents([Webhook::ADD_COMPANY]),
$webhookProvider->create()
->setUrl('https://some.widget.catcode.io/webhook/update_company')
->setEvents([Webhook::UPDATE_COMPANY]),
]);
foreach ($webhooks as $webhook) {
if (!$webhook->getResult()) {
// Вебхук не добавился
}
}
Можно найти все или только свои вебхуки:
$webhooks = $webhookProvider->list();
// Так мы увидем все вебхуки, в том числе и чужие
foreach ($webhooks as $webhook) {
print_r($webhook->getUrl());
}
// Можно найти только те, которые совпадают с определённым паттерном, чтобы, например, найти только наши
$webhooks = $webhookProvider->findByUrl('https://some.widget.catcode.io/');
Удаление вебхуков можно производить по id, но их нужно в начале узнать с помощью list()
, проще удалить по паттерну, как выше делался поиск:
$result = $provider->unsubscribeByUrl('https://some.widget.catcode.io/')
if (!$result) {
// Не удалились, а точнее скорее всего их и не нашли т.к. это происходит в два запроса: в начале ищим вебхуки, а потом удаляем.
}
Создание задачи
use Amocrm\Api\Model\Account\TaskType;
$taskProvider = $amocrmApi->task();
$task = $taskProvider->saveOne(
$taskProvider
->create()
->setText('текст задачи')
// Создаём для сделки. Для каждой из сущностей свой сеттер.
->setLeadId(3543)
// Создаём задачу одного из дефолтных типов, но можно передать и ID кастомного.
->setTaskType(TaskType::MEETING)
->setCompleteTill(new DateTime('now'))
);
print_r($task->getId());
Создание примечаний
use Amocrm\Api\Model\Account\NoteType;
$noteProvider = $amocrmApi->note();
$note = $noteProvider->save(
$noteProvider
->create()
->setText('текст примечания')
// Создаём для сделки. Для каждой из сущностей свой сеттер.
->setLeadId(3543)
// Создаём обычное примечание, но можно и любое другое, некоторые требуют особый формат поля text.
->setNoteType(NoteType::COMMON)
);
print_r($note->getId());
Получение различных данных аккаунта
$account = $amocrmApi->account();
$account->getUsers()->getByGroupId($groupId);
$account->getUsers()->getOneByEmail($email);
$account->getUsers()->getOneById($id);
$account->getUser($id);
$account->getPipelines()->getStatusesActive();
$account->getPipeline($pipelineId)->getStatus($statusId)->getName();
$account->getId();
$account->getCurrency();
$account->getTimezone();
use Amocrm\Api\Model\Account\Pipeline;
// Получаем ID активных статусов со всех воронок кроме 1234
$statusIds = $account
->getPipelines()
->filter(function (Pipeline $pipeline) {
return $pipeline->getId() !== 1234;
})
->getStatusesActive()
->getIds();
// Запросив один раз данные аккаунта больше запросов по API производиться не будет.
// Для получения данных аккаунта каждый раз актуальных надо передавать true.
$account = $amocrmApi->account(true);
Получение различных сущностей
Демонстрация получения зависимых друг от друга сущностей по цеполчке сделка -> контакты -> компании:
use Amocrm\Api\Model\Company;
$contactProvider = $amocrmApi->contact();
$leadProvider = $amocrmApi->lead();
$companyProvider = $amocrmApi->company();
// Найдём одну сделку по ID.
$lead = $leadProvider->getOne(4123);
if ($lead) {
var_dump('Найдена сделка', $lead->getId());
}
// Найдём все контакты этой стедки
$contacts = $contactProvider->findByLeads($lead->getId());
if ($contacts->count()) {
// Найдём все компании, которые есть среди этих контактов.
$companies = $companyProvider->findByContacts($contacts->getIds());
// Отфильтруем только те, где нужный ответственный и заполнено какое-то поле.
$companies = $companies->filter(function (Company $company) {
return $company->getResponsibleUserId() == 1234
&& $company->getCF(6543456);
&& $company->getCF(6543456)->text()->get();
});
if ($companies->count()) {
var_dump('Найдена компания', $companies->first()->getName());
}
}
Работа со статусами при поиске сделок:
$leadProvider = $amocrmApi->lead();
// Найдём IDs активных статусов нужной воронки.
$statusIds = $account
->getPipeline(1234)
->getStatusesActive()
->getIds();
// Можем найти все сделки на этих статусах.
$leads = $leadProvider->find(null, $statusIds);
// Можем найти сделки на этих статусах из определённого пула сделок.
$leads = $leadProvider
->get([1234123, 5454565, 7674545])
->filterByStatusIds($statusIds);
// Можем найти сделки на определённых статусах связанные с определёнными контактами.
$leads = $leadProvider
->findByContacts([123423, 435454, 7654343])
->filterByStatusIds($statusIds);
foreach ($leads as $lead) {
var_dump('Найдена сделка', $lead->getName());
}
Есть также много других методов, названия которых говорят сами за себя:
$leadProvider->findByContacts(432443);
$leadProvider->findOneByContacts(432443);
$contactProvider->findByLeads(12343);
$contactProvider->findOneByLeads(12343);
$contactProvider->findByPhone(79009090900);
$contactProvider->findOneByPhone('+7 (900) 90-90-900');
$contactProvider->findByEmail('asdf@asdf.ff');
$contactProvider->findOneByEmail('asdf@asdf.ff');
$companyProvider->findByContacts(666565);
$companyProvider->findOneByContacts(666565);
$companyProvider->findByLeads(12343);
$companyProvider->findOneByLeads(12343);
$companyProvider->findByPhone(79009090900);
$companyProvider->findOneByPhone('+7 (900) 90-90-900');
$companyProvider->findByEmail('asdf@asdf.ff');
$companyProvider->findOneByEmail('asdf@asdf.ff');
Создание и обновление сущностей
Находим контакта, создаём в нём задачу и два примечния в связанной сделке:
use Amocrm\Api\Model\Account\TaskType;
use Amocrm\Api\Model\Account\NoteType;
$contactProvider = $amocrmApi->contact();
$leadProvider = $amocrmApi->lead();
$taskProvider = $amocrmApi->task();
$noteProvider = $amocrmApi->note();
$contact = $contactProvider->getOne(1343443);
if ($contact) {
$task = $taskProvider->saveOne(
$taskProvider
->create()
->setText('Перезвонить клиенту, поступила заявка с сайта')
->setTaskType(TaskType::FOLLOW_UP)
->setContactId($contact->getId())
->setUserId($contact->getResponsibleUserId())
->setCompleteTill((new \DateTime('now', new \DateTimeZone('Europe/Moscow')))->setTime(23, 59, 59))
);
var_dump('Создали задачу', $task->getId());
$lead = $leadProvider->findOneByContacts($contact->getId());
if ($lead) {
$note1 = $noteProvider
->create()
->setText('Комментарий к заказу')
->setLeadId($lead->getId())
->setNoteType(NoteType::COMMON);
$notе2 = $noteProvider
->create()
->setText('Пожелание клеинта')
->setLeadId($lead->getId())
->setNoteType(NoteType::COMMON);
$notes = $noteProvider->save([$note1, $note2]);
if ($notes->count()) {
var_dump('Создали примечания', implode(', ', $notes->getIds()));
}
}
}
Сокращённая версия для интеграции с сайтом: ищем контакт ищем сделку, если нет – создаём.
use Amocrm\Api\Model\Account\TaskType;
$contactProvider = $amocrmApi->contact();
$leadProvider = $amocrmApi->lead();
$taskProvider = $amocrmApi->task();
$responsibleUserId = null;
$contact = null;
$contact = $amocrmApi->findOneByPhone($phone);
if ($contact) {
$responsibleUserId = $contact->getResponsibleUserId();
$lead = $leadProvider->findOneByContacts($contact->getId());
if ($lead) {
$taskProvider->saveOne(
$taskProvider
->create()
->setText('Перезвонить клиенту, поступила заявка с сайта')
->setTaskType(TaskType::CALL)
->setContactId($contact->getId())
->setUserId($responsibleUserId)
->setCompleteTill((new \DateTime('now', new \DateTimeZone('Europe/Moscow')))->setTime(23, 59, 59))
);
}
}
$lead = $leadProvider->saveOne(
$leadProvider
->create()
->setName($leadName)
->setStatusId($leadStatus)
->setUserId($responsibleUserId)
->addTag('Заявка')
->setCF($amocrmApi->cf(1234)->text()->set($utmType))
->setCF($amocrmApi->cf(4444)->text()->set($utmSource))
->setCF($amocrmApi->cf(5434)->text()->set($googleId))
);
if (!$contact) {
$name = $name!= '' ? $name : 'Новый контакт ' . $phone;
$contact = $contactProvider->saveOne(
$contactProvider
->create()
->setName($name)
->setUserId($responsibleUserId)
->addLeadId($lead->getId())
// Генерируем значения для доп. полей.
->setCF($amocrmApi->cf(3333)->phone()->add($phone))
->setCF($amocrmApi->cf(6677)->email()->add($email))
);
} else {
// Тут добавляем новые телефон и мыло к уже имеющимся или создаём новые поля.
$cfPhone = $contact->getCF(3333);
$cfEmail = $contact->getCF(6677);
$contactProvider->saveOne(
$contact
->addLeadId($lead->getId())
->setCF($contact->getCF(3333)->phone()->add($phone))
->setCF($contact->getCF(6677)->email()->add($email))
);
}
Работа со значениями в доп. поле
Если требуется удалить содержимое любого поля в сущности в amoCRM, то при заполнении сущности надо это указать с помощью метода clearCF()
. Метод принимает как просто ID доп. поля, так и базовый объект CustomField (возаращается при получени полей из объекта сущности), так и конкретный тип поля (имплементация TypeInterface):
use Amocrm\Api\Helper\ElementType;
// Так можно отчистить одно конкретное поле.
$amocrmApi->lead()->saveOne(
$amocrmApi->lead()->create()
->setId(7014720)
->clearCF(12312312);
);
// Так можно отчистить все поля в сущности, получив ID доп. полей из аккаунта.
$lead = $amocrmApi->lead()->create()->setId(7014720);
$amocrmApi->account()->getCustomFields()
->getByElementType(ElementType::LEAD)->map(function ($item) use ($lead, $amocrmApi) {
$lead->clearCF($item->getId());
// Также можно передать внутри CustomField или TypeInterface, но зачастую это не нужно.
// $lead->clearCF($amocrmApi->cf($item->getId()));
// $lead->clearCF($amocrmApi->cf($item->getId())->text());
});
$amocrmApi->lead()->saveOne($lead);
Если трубется обновить или дополнить значение в доп. поле, то надо использовать метод setCF()
у сущности. Внутрь нужно передать конкретный тип-обеъкт доп. поля (имплементация TypeInterface). Для получения данных есть метод getCF()
, который возвращает CustomField в любом случае, даже если доп. поле в сущности пустое (из API amoCRM не пришло), а пустое оно или нет можно проверить вызовом isEmpty()
. Последнее значит, что если запросить у сущности точно несуществующее поле, то CustomField вернётся, в неёго можно передать значение и даже отправить сущность на обновление, но ничего не обновится. Примеры:
// Получим сущность с доп. полями.
$lead = $amocrmApi->lead()->get(52345);
$cfSelect = $lead->getCF(55554);
// Поле пустое, в сущности ничего не выделено.
if ($cfSelect->isEmpty()) {
$cfSelect->select()->set('Опция');
}
$lead->setCF($cfSelect);
// Есть более локаничный вариант.
$lead->setCF($lead->getCF(55554)->select()->set('Опция'));
// Также будет работать и, например, с телефонами контакта, чтобы не проверять что уже есть там.
$contact = $amocrmApi->contact()->get(453523);
$contact->setCF($contact->getCF(774564)->phone()->add('67676564563'));
Если требуется получить подтип конкретного телефона или мыла (и др.), то нужно учесть нюансы. Если сущность получена из amoCRM, то enum там возвращается как числовой ID. Получается в начале надо узнать к какому типу, например, телефона относиться ID и уже потом пытаться получить этот телефон:
$enumId = $amocrmApi->account()->getCustomField(1111)->getEnumId('MOB');
$phone = $lead->getCustomField(1111)->getByEnum($enumId);
Работа с покупателями
Функционал работы с покупателями немного кривой т.к. похоже amoCRM разрабатывало "как получилось" API покупателей, а от того и наша обёртка не очень прямая.
Для работы с покупателями может потребоваться сразу несколько разных провайдеров:
// Провайдер для поиска (https://developers.amocrm.com/rest_api/customers/list.php), создания, удаления и обновления покупателей (https://developers.amocrm.com/rest_api/customers/set.php).
$amocrmApi->customer();
// Провайдер для поиска(https://developers.amocrm.com/rest_api/transactions/list.php), создания, удаления (https://developers.amocrm.com/rest_api/transactions/set.php) и обновления (https://developers.amocrm.com/rest_api/transactions/comment.php) транзакций.
$amocrmApi->transaction();
// Провайдер для одновременного создания покупателей, транзакций и контактов (но не компаний) и связывания их. Также можно удалять транзакции и покупателей и обновлять покупателей. Соответствует, методу element/sync (https://developers.amocrm.com/rest_api/elements/sync.php).
$amocrmApi->element();
// Провайдер для связывания уже существующих покупателей, контактов и компаний (https://developers.amocrm.com/rest_api/links/set.php) и для поиска уже существующих связей (https://developers.amocrm.com/rest_api/links/list.php).
$amocrmApi->link();
Далее буду приводить примеры, как можно работать с покупателями через эти провайдеры.
Простые действия без сложного связывания делать просто:
// Создать покупателя. При создании надо понимать, что в зависимости от типа покупателя
// поле next_date может быть обязательным (Периодические покупки) или вообще ненужным (Динамическая сегментация).
// При этом цена и название всегда необходимы.
$amocrmApi->customer()->addOne(
$amocrmApi->customer()->create()
->setName('Новый покупатель')
->setNextPrice(100)
->setNextDate(new DateTime())
->setCF($amocrmApi->cf(342601)->text()->set(444))
->addTag('Проверочный тег')
);
// Обновить покупателя.
$amocrmApi->customer()->updateOne(
$amocrmApi->customer()->create()
->setId(1234)
->setName('Новое название покупателя')
);
// Получить существующего покупателя.
// По ID.
$amocrmApi->customer()->getOne(115183);
// По дате создания.
$amocrmApi->customer()->find('create', (new DateTime())->modify('-1 day'), (new DateTime())->modify('+1 day')));
// По дате модификации.
$amocrmApi->customer()->find('modify', (new DateTime())->modify('-1 day'), (new DateTime())->modify('+1 day')));
// По дате следующей покупки.
$amocrmApi->customer()->find(null, null, null, (new DateTime())->modify('-1 day'), (new DateTime())->modify('+1 day'));
// По значению доп. поля.
$amocrmApi->customer()->find(null, null, null, null, null, [$amocrmApi->cf(342601)->text()->set(444)]);
// По значению доп. поля типа дата (тоже задаётся диапазон).
$amocrmApi->customer()->find(null, null, null, null, null, null, ['from' => $amocrmApi->cf(342601)->date()->set(new DateTime())]);
// По ответственному пользователю.
$amocrmApi->customer()->find(null, null, null, null, null, null, null, 2291701);
// По текущим задачам.
$amocrmApi->customer()->find(null, null, null, null, null, null, null, null, ['today']);
// Создать транзацию. Обязательно customer_id и price. Соответственно, покупатель уже должен существовать.
$amocrmApi->transaction()->addOne(
$amocrmApi->transaction()->create()
->setPrice(12344444)
->setCustomerId(114555)
);
// Найти транзакции определённого покупателя.
$amocrmApi->transaction()->find(115183);
// Получить данные определённой транзакции.
$amocrmApi->transaction()->getOne(5234234);
// Также можно сразу по много покупателей добавлять, обновлять и удалять.
$amocrmApi->customer()->save([
'add' => [
$amocrmApi->customer()->create()
->setName('Новый покупатель 1')
->setNextPrice(100)
->setNextDate(new DateTime()),
$amocrmApi->customer()->create()
->setName('Новый покупатель 2')
->setNextPrice(200)
->setNextDate(new DateTime()),
],
'update' => [
$amocrmApi->customer()->create()
->setId(1234)
->setName('Новое название покупателя')
],
'delete' => [1234, 6543, 534534],
])
// Также можно сразу по много транзакций добавлять, обновлять и удалять. При этом внутри обновление происходит через
// другой метод нежели добавление и удаление т.к. обновлять в транзакции можно только комментарий (и ничего более)!
$amocrmApi->transaction()->save([
'add' => [
$amocrmApi->transaction()->create()
->setCustomerId(1234)
->setPrice(100)
->setDate(new DateTime()),
$amocrmApi->transaction()->create()
->setCustomerId(1234)
->setPrice(400),
],
'update' => [
$amocrmApi->transaction()->create()
->setId(41234)
->setComment('Новый комментарий')
],
'delete' => [11234, 16543, 1534534],
])
Получается с помощью провайдеров customer и transaction можно работать с соответсвующими сущностями, связывать транзакции с покупателями (во время создания), но никак нельзя покупателей связывать с контактами и компаниями.
Допустим нужно добавить полный набор за раз: компанию, контакт, покупателя и две транзакции (одна была давно, другая проводится в данный момент):
// Посольку с помощью провайдера element нельзя добавлять компаниий, а только контакты, то в любом случае компанию нужно добавить заранее.
$company = $amocrmApi->company()->addOne(
$amocrmApi->company()->create()
->setName('Название компании')
);
// Далее добавляем разом всё остальное, перелинковывая их друг с другом. Обратите внимание, что перелинковка происходит с ещё несозданными сущносями с помощью request_id.
$result = $amocrmApi->element()->save(
[
'add' => [
$amocrmApi->customer()->create()
->setNextPrice(12344444)
->setNextDate(new DateTime())
->setName('Проверка в работе 1')
->setRequestId(1),
]
],
[
'add' => [
$amocrmApi->transaction()->create()
->setPrice(333)
->setDate((new DateTime())->modify('-10 days'))
->setComment('Старая траназакция к покупателю')
->setCustomerRequestId(1),
$amocrmApi->transaction()->create()
->setPrice(333)
->setComment('Новая траназакция к покупателю')
->setCustomerRequestId(1),
]
],
[
'link' => [
$amocrmApi->link()->create()
->setFrom(ElementType::CUSTOMER)
->setFromRequestId(1)
->setTo(ElementType::CONTACT)
->setToRequestId(1),
// Тут связываем ещё не созданную сущность и уже существующую!
$amocrmApi->link()->create()
->setFrom(ElementType::CUSTOMER)
->setFromRequestId(1)
->setTo(ElementType::COMPANY)
->setToId($company->getId()),
]
],
[
'add' => [
$amocrmApi->contact()->create()
->setName('Контакт Проверка в работе 1')
->setRequestId(1),
]
]
);
// В ответ возвращается большой массив, где под соответствующими ключами сущности.
foreach ($result['customers']['add'] as $customer) {
echo sprintf("%s – %s\n", $customer->getName(), $customer->getId());
}
// Можно получить перебором список сущностей.
foreach ($result['transactions']['add'] as $transaction) {
echo sprintf("%s – %s\n", $transaction->getComment(), $transaction->getId());
}
// Можно получить только первый из списка сущностей.
$contact = $result['contacts']['add']->first();
echo sprintf("%s – %s\n", $contact->getName(), $contact->getId());
// Ссылки скорее всего не понадобятся, но они также возвращаются с уже нормальными id сущностями.
print_r($result['links']['link'][0]);
Скорее всего все за раз создавтаь сущности не понадобится, потому в методе save()
контакты добавляются последним параметром. Скорее всего контакт и компания уже будут существовать и надо будет добавить покупателя с транзакцией (или только покупателя) и связать их с контактами и компанией:
$amocrmApi->element()->save(
[
'add' => [
$amocrmApi->customer()->create()
->setNextPrice(12344444)
->setNextDate(new DateTime())
->setName('Проверка в работе 1')
->setRequestId(1),
]
],
[
'add' => [
$amocrmApi->transaction()->create()
->setPrice(333)
->setCustomerRequestId(1),
]
],
[
'link' => [
$amocrmApi->link()->create()
->setFrom(ElementType::CUSTOMER)
->setFromRequestId(1)
->setTo(ElementType::CONTACT)
->setToId(222222),
// Тут связываем ещё не созданную сущность и уже существующую!
$amocrmApi->link()->create()
->setFrom(ElementType::CUSTOMER)
->setFromRequestId(1)
->setTo(ElementType::COMPANY)
->setToId(111111),
]
],
);
// Либо добавить только покупателя и связать с сущносями.
$result = $amocrmApi->element()->save(
[
'add' => [
$amocrmApi->customer()->create()
->setNextPrice(12344444)
->setNextDate(new DateTime())
->setName('Проверка в работе 1')
->setRequestId(1),
]
],
null,
[
'link' => [
$amocrmApi->link()->create()
->setFrom(ElementType::CUSTOMER)
->setFromRequestId(1)
->setTo(ElementType::CONTACT)
->setToId(222222),
// Тут связываем ещё не созданную сущность и уже существующую!
$amocrmApi->link()->create()
->setFrom(ElementType::CUSTOMER)
->setFromRequestId(1)
->setTo(ElementType::COMPANY)
->setToId(111111),
]
],
);
// Потом позже будут добавляться транзакции.
$customer = $result['customers']['add']->first();
$amocrmApi->transaction()->addOne(
$amocrmApi->transaction()->create()
->setPrice(12344444)
->setCustomerId($customer->getId())
);
// Или если не требуется связывание с контактом или компаний, то можно сделать как двумя запросами (добавить покупателя, а потом транзакцию), так и одним:
$amocrmApi->element()->save(
[
'add' => [
$amocrmApi->customer()->create()
->setNextPrice(12344444)
->setNextDate(new DateTime())
->setName('Проверка в работе 1')
->setRequestId(1),
]
],
[
'add' => [
$amocrmApi->transaction()->create()
->setPrice(333)
->setCustomerRequestId(1),
]
],
);
Также есть странная возможность обновлять и удалять сущности с помощью этого метода. С трудом могу представить когда это понадобиться, но скорее всего будет выглядить как-то так:
$amocrmApi->element()->save(
[
'add' => [
$amocrmApi->customer()->create()
->setNextPrice(12344444)
->setNextDate(new DateTime())
->setName('Проверка в работе 1')
->setRequestId(1),
],
'udapte' => [
$amocrmApi->customer()->create()
->setId(23423423)
->setNextPrice(333),
],
'delete' => [44454],
],
// Транзакции можно только добавлять и удалять, но не обновлять.
[
'add' => [
$amocrmApi->transaction()->create()
->setPrice(333)
->setCustomerRequestId(1),
$amocrmApi->transaction()->create()
->setPrice(111)
->setCustomerId(23423423),
],
'delete' => [4234234, 65645645, 7567674],
],
[
'link' => [
// Связываем с существующим контактом.
$amocrmApi->link()->create()
->setFrom(ElementType::CUSTOMER)
->setFromRequestId(1)
->setTo(ElementType::CONTACT)
->setToId(1523423),
]
],
// Контакты можно только добавлять или обновлять.
[
'udapte' => [
$amocrmApi->contact()->create()
->setId(1523423)
->setName('Новое имя контакта'),
]
]
);
Также можно потребоваться связывать все существующие уже сущности. Допустим покупатель, контакт и компания уже существуют, но ещё не связаны друг с другом. Нужно добавить как компанию и контакт в покупателя, так и контакт в компанию:
$amocrmApi->link()->link([
$amocrmApi->link()->create()
->setFrom(ElementType::CUSTOMER)
->setFromId(115183)
->setTo(ElementType::CONTACT)
->setToId(16963517),
$amocrmApi->link()->create()
->setFrom(ElementType::CUSTOMER)
->setFromId(115183)
->setTo(ElementType::COMPANY)
->setToId(1234123),
$amocrmApi->link()->create()
->setFrom(ElementType::CONTACT)
->setFromId(16963517)
->setTo(ElementType::COMPANY)
->setToId(1234123),
]);
Помимо этого иногда может потребоваться узнать какие компании и контакты связанны с покупателем:
// Получить список ID связанных контактов с покупателем.
$amocrmApi->link()->findFromCustomerToContact(115183)->getToIds();
// Получить список ID связанных компаний с покупателем.
$amocrmApi->link()->findFromCustomerToCompany(115183)->getToIds();
// Получить список ID покупателей контакта.
$amocrmApi->link()->findFromContactToCustomer(543545)->getToIds();
// Получить список ID покупателей компании.
$amocrmApi->link()->findFromCompanyToCustomer(563345)->getToIds();
// Помимо этого можно узнать через этот метод и другие связи.
$amocrmApi->link()->findFromLeadToCompany(2134)->getToIds();
$amocrmApi->link()->findFromLeadToContact(2134)->getToIds();
$amocrmApi->link()->findFromContactToLead(2134)->getToIds();
$amocrmApi->link()->findFromContactToCompany(2134)->getToIds();
$amocrmApi->link()->findFromCompanyToLead(2134)->getToIds();
// и т.д.
Хак загрузки виджитов
С помощью незакрытой дыры в amoCRM можно заливать пользовательские виджеты удалённо. Ниже приведён довольно сложный пример с использованием некоторых компонентов Symfony, которые при желании могут быть заменены стандартными аналогами самого PHP (пример не факт что рабочий).
Изначально предположим, что у нас есть метод, который формирует и выдаёт архив с готовым виджетом, где manifest.json заполнен нужными данными:
/**
* Возвращает контент с архивом подготовленного для загрузки виджета, либо
* false в случае неудачи. Работает через создание временных файлов для
* архива, но всё за собой подтирает.
*
* @param string $path
* @param string $code
* @param string $secretKey
*
* @return string|false
*/
private function getWidgetArchive($path, $code, $secretKey)
{
$archive = file_get_contents($path);
$tempDir = sys_get_temp_dir() . '/' . uniqid(mt_rand());
$tempWidget = $tempDir . '/widget.zip';
$filesystem = new Filesystem();
try {
$filesystem->mkdir($tempDir);
} catch (IOExceptionInterface $e) {
$this->logger->error(
'При создании временной директории "' . $e->getPath() . '" произошла ошибка',
['exception' => $e]
);
return false;
}
$zipArchive = new ZipArchive();
$archiveContent = false;
try {
// Открываем архив проекта с Gitlab.
if (!$zipArchive->open($tempWidget)) {
throw new Exception('При открытии архива произошла ошибка');
}
if (!$zipArchive->extractTo($tempDir)) {
throw new Exception('При разархивировании произошла ошибка');
}
$zipArchive->close();
// Открываем новый архив поверх проекта с Gitlab, чтобы туда уже виджет записать.
if (!$zipArchive->open($tempWidget, ZipArchive::OVERWRITE)) {
throw new Exception('При открытии с перезаписью архива произошла ошибка');
}
$finder = new Finder();
// Архив распакуется в директорию вида <projectname>-<branch>-<commit>,
// а потому будем искать по маске со звёздочкой.
$finder->files()->in($tempDir . '/' . $projectName . '*/' . $path);
foreach ($finder as $file) {
$filePath = $file->getRealPath();
$filename = $file->getRelativePathname();
// Если это манифест виджета, то его надо немного изменить и записать.
if ($filename == 'manifest.json') {
$manifest = json_decode(file_get_contents($filePath), true);
$manifest['widget']['code'] = $code;
$manifest['widget']['secret_key'] = $secretKey;
$zipArchive->addFromString($filename, json_encode($manifest));
} else {
$zipArchive->addFile($filePath, $filename);
}
}
$zipArchive->close();
$archiveContent = file_get_contents($tempWidget);
} catch (Exception $e) {
$this->logger->warning($e->getMessage());
} finally {
try {
$filesystem->remove($tempDir);
} catch (IOExceptionInterface $e) {
$this->logger->error(
'При удалении временной директории "' . $e->getPath() . '" произошла ошибка',
['exception' => $e]
);
}
return $archiveContent;
}
}
Для загрузки необходимы только реквизиты доступа менеджера с правами админа:
use Amocrm\AmocrmFactory;
use Amocrm\Exception\WidgetException;
use Amocrm\Exception\AmocrmApiException;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Filesystem\Exception\IOExceptionInterface;
use Symfony\Component\Finder\Finder;
$code = 'widget_name';
try {
$amocrmWidget = AmocrmFactory::getWidget($domain, $email, $token);
} catch (AmocrmApiException $e) {
// ...
return false;
}
// Создаём виджет в аккаунте.
try {
$widgetData = $amocrmWidget->create($code);
} catch (WidgetException $e) {
// ...
return false;
}
// В полученной структуре в $widgetData следующие параметры:
// [id] => name_widget
// [code] => name_widget
// [secret_key] => c02fcc8b1e2a64fcae4e711ae421121e8a1546e11d9cc3e90666b2d4d2795eef
// [version] => 1.0.0
// [installs] => 0
$secretKey = $widgetData['secret_key'];
// Нужен sleep т.к. в amoCRM предыдущий шаг аснхронно происходит и медленно.
usleep(500000);
// Теперь загружаем архив с готовым Виджетом.
$widgetArchive = $this->getWidgetArchive('/tmp/widget.zip', $code, $secretKey);
try {
if ($widgetArchive === false) {
throw new Exception('Архив с подготовленным виджетом получить не удалось');
}
$response = $amocrmWidget->upload($code, $secretKey, $widgetArchive);
$this->logger->notice(
'Ответ amoCRM при загрузке виджета',
[$response]
);
} catch (Exception $e) {
// ...
return false;
}
Для обновления виджета уже необхождимо знать его код и секретный ключ помимо реквизитов доступа админа:
use Amocrm\AmocrmFactory;
use Amocrm\Exception\WidgetException;
use Amocrm\Exception\AmocrmApiException;
$code = 'widget_name';
$secretKey = 'secret_key';
try {
$amocrmWidget = AmocrmFactory::getWidget($domain, $email, $token);
} catch (AmocrmApiException $e) {
// ...
return false;
}
// Теперь загружаем архив с готовым Виджетом.
$widgetArchive = $this->getWidgetArchive('/tmp/widget.zip', $code, $secretKey);
try {
if ($widgetArchive === false) {
throw new Exception('Архив с подготовленным виджетом получить не удалось');
}
$response = $amocrmWidget->upload($code, $secretKey, $widgetArchive);
$this->logger->notice(
'Ответ amoCRM при обновлении виджета',
[$response]
);
} catch (WidgetException $e) {
// ...
return false;
}
Для удаления виджета всё также:
use Amocrm\AmocrmFactory;
use Amocrm\Exception\WidgetException;
use Amocrm\Exception\AmocrmApiException;
$code = 'widget_name';
$secretKey = 'secret_key';
try {
$amocrmWidget = AmocrmFactory::getWidget($domain, $email, $token);
} catch (AmocrmApiException $e) {
// ...
return false;
}
try {
$amocrmWidget->remove($code, $secretKey);
} catch (WidgetException $e) {
// ...
return false;
}