catcodeio / amocrm
用于与 amoCRM API 交互的 SDK
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 请求失败时(不包括通过小部件操作),抛出特定的异常,以便可以了解具体出了什么问题:是经理的凭据已过期,还是账户中的支付已过期。
在此期间,所有 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}()
)。 - 在提供者中,有反映 API 方法完整行为的 list 方法,而其他所有方法都是它的封装。方法
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。 - 在Status类中存在常量来表示标准状态142和143。
- 为了表示实体类型,在所有地方都使用了来自帮助器的 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());
创建和删除Webhook
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()) {
// Вебхук не добавился
}
}
可以找到所有或仅自己的Webhook
$webhooks = $webhookProvider->list();
// Так мы увидем все вебхуки, в том числе и чужие
foreach ($webhooks as $webhook) {
print_r($webhook->getUrl());
}
// Можно найти только те, которые совпадают с определённым паттерном, чтобы, например, найти только наши
$webhooks = $webhookProvider->findByUrl('https://some.widget.catcode.io/');
可以通过id删除Webhook,但首先需要使用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,即使额外字段在实体中为空(来自amoCRM API的响应),可以通过调用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获取的,则枚举返回为数字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;
}