smith-carson / prosperworks-sdk
ProsperWorks CRM API的非官方(但相当不错的)SDK
Requires
- php: ^7.0
- doctrine/inflector: ^1.2
- guzzlehttp/guzzle: ^6.3
Requires (Dev)
- phpunit/phpunit: ^6.2
README
布局 |
---|
默认 |
介绍
在实施时,没有ProsperWorks的SDK,我们需要执行大量操作来从我们的旧CRM迁移数据并与其他子系统同步信息,因此我们设计了一些类来封装API操作。它使用Guzzle,但最终变成了一个独立的库。
该项目最初由 igorsantos07 编写,现在由 smith-carson 维护(网站)。
安装
1. 在您的项目中安装
您需要运行 PHP 7 (是的,自2015年12月以来它已经稳定,并包含许多有用的功能,现在就升级吧!)。
要使用Composer将其添加到您的项目中,请运行 composer require igorsantos07/prosperworks
。如果您的 min-requirements
是 "stable",则它将获取最稳定的版本;否则为 dev-master
。
2. 配置软件包
要使SDK运行,需要配置几件事情
API凭据
目前,ProsperWorks上没有 "API用户",因此您需要一个活跃用户来与API通信。
-
登录ProsperWorks,转到 设置 > 偏好 / API密钥。在那里,您将为登录用户生成一个新的API密钥。复制该密钥以及用户电子邮件。
-
创建一个引导文件,或在您的项目的配置部分包含以下代码:
\ProsperWorks\Config::set($email, $token)
。
Webhooks参数
[可选] 如果您打算使用Webhooks从ProsperWorks获取更新,您还需要在该方法中提供另外三个参数
- 一个 Webhooks密钥,它将用于防止对您的路由的不当调用。它应该是一个纯文本字符串。
- 一个 根URL。这可能是您用于系统的相同域名/路径,ProsperWorks将向其POST。有关Webhooks部分的更多信息。
- 一个 加密对象。它应该响应
encryptBase64()
和decryptBase64()
,两者都接收和返回字符串(它还可以实现\ProsperWorks\Interfaces\Crypt
以简化操作)。它将被用来发送加密密钥,并解密以确保您收到的调用来自ProsperWorks(或者至少,拥有加密密钥的人)。
缓存对象
[可选] 为了使某些部分更快,您还可以将第六个参数传递给缓存层。它是一个需要响应 get()
和 save()
,或者实现 \ProsperWorks\Interfaces\Cache
的对象。
它主要用于缓存(一个小时)API 的元数据,如自定义字段、活动、联系人类型等。这些信息很少改变,所以可以安全地缓存,使调用更快(否则,对于每个具有自定义字段的资源,我们还需要从自定义字段端点检索)。
3. 调试模式
在导入脚本和类似任务期间,查看网络流量并查看您是否正确执行了预期操作可能很有用。您可以通过调用 ProsperWorks\Config::debugLevel()
启用库的调试信息 echo
。
ProsperWorks\Config::DEBUG_BASIC
- 将触发一些消息,例如 "POST /people/search",这样您就知道哪些请求正在发送。它还会在达到速率限制时发出警告。
ProsperWorks\Config::DEBUG_COMPLETE
- 执行上述所有操作,并完整请求有效负载。
null
,false
,0
或ProsperWorks\Config::DEBUG_NONE
- 将停止打印消息。
这不需要与
Config::set()
一起完成;它可以在任何地方发生,并将从该部分开始更改行为。
技巧:"沙盒"帐户
经过一段时间,当我们第一次实现这个库时,我们与一位支持代表谈到了沙盒环境的缺失。他们建议我们创建一个试用帐户,并用它来代替付费帐户上的用户,并告诉支持人员正在使用它来测试 API 实现 - 因此,他们会将那个单独帐户的试用期延长到所需的时间。
API 通信
大多数操作都是通过 \ProsperWorks\CRM
抽象类完成的,以及从中生成的对象(您可以将它视为某种类型的工厂类)。例外的是 Webhooks,它有一个特殊的端点类,以便更容易实现。
技巧:ProsperWorks API 文档
您可能想阅读 REST API 文档,以了解组成此 SDK 的内部组件。
在设置好配置后,ProsperWorks API 调用通过简单的流畅 API 完成。大多数端点行为相同,特殊案例是帐户和大多数元数据端点。
在以下示例中,我们将假设类已导入当前命名空间。
常见端点
对 CRM
的单一、空静态调用将返回一个 Endpoint
对象(参见 保存实例),该对象允许您运行所有常见操作
<?php //runs GET /people/10 to retrieve a single record $people = CRM::person()->find(10); //runs GET /people multiple times (it's paged) until all entries are retrieved $people = CRM::person()->all(); //there's no such operation in some endpoints; all() runs an empty /search, instead //runs POST /people to generate a new record $newPerson = CRM::person()->create(['name' => 'xxx']); //runs PUT /people/25 to edit a given record $person = CRM::person()->edit(25, ['name' => 'xxx']); //runs DELETE /people/10 to destroy that record $bool = CRM::person()->delete(10); //runs POST /people/search with the given parameters until all entries are found (it's paged) $people = CRM::person()->search(['email' => 'test@example.com']);
所有成功调用都将返回一个 BareResource
对象,其中包含该端点的所有信息,或一个包含这些对象的列表。有关详细信息,请参阅 响应对象。
如果失败,将给出错误消息。(待办事项:选项抛出异常)
还有一些快捷方式,例如
<?php //plural calls do the same as the singular all() call $people = CRM::people(); //same as CRM::person()->all() $tasks = CRM::tasks(); //same as CRM::task()->all() $companies = CRM::companies(); //same as CRM::company()->all() //there's also two other types of magic calls $people = CRM::person(23); //same as CRM::person()->find(23) $people = CRM::person(['country' => 'US']); //same as CRM::person()->search(...)
特殊情况:受限制的端点
所有元数据资源(在文档中称为二级资源),以及Account
端点,仅具有读访问权限。尚未进行有效操作的验证(参见#7)。以下是那些只读端点的列表,可通过复数调用(例如CRM::activityTypes()
)访问,但除Account
外,它使用单数
- Account(您必须以单数形式调用的唯一一个)
- 活动类型
- 联系人类型
- 自定义字段
- 客户来源
- 损失原因
- 管道
- 管道阶段
元数据快捷方式
由于这些端点大多是列表,您也可以通过可缓存的CRM::fieldList()
方法访问这些数据,该方法以更组织化的方式返回信息
<?php $types = CRM::fieldList('contactType'); //singular! print_r($types); // gives an array of names, indexed by ID: // ( // [123] => Potential Customer // [124] => Current Customer // [125] => Uncategorized // [126] => Former Customer // ) echo CRM::fieldList('contactType', 524131); //search argument // prints "Potential Customer". That argument searches on both sides of the array $actTypes = CRM::fieldList('activityType', null, true); //asks for "detailed response" print_r($actTypes); // gives the entire resources, still indexed by ID // [166] => stdClass Object // ( // [id] => 166 // [category] => user // [name] => Social Media // [is_disabled] => // [count_as_interaction] => 1 // ) // // [...] // )
警告:那里的ID是样本;每个ProsperWorks客户的ID都不同。
还值得注意的是,一些字段是从API“转换”到特定对象的,例如时间戳、地址、自定义字段等,因此您可能永远不需要直接处理自定义字段端点。有关此信息,请参阅子资源和响应对象部分。
相关项目
有一个统一的API用于在两个资源之间创建链接。因此,每个资源对象都有自己的related
方法,用于操作这些链接。由于这是一个非常简单的API,您只能列出、创建和删除关系。请参阅相关项目文档以查看关系限制 - 一些资源只允许一个链接,并不是每个资源都与每个其他资源有联系。
<?php // you always have to feed the origin resource ID to related() // and then call the operation you want, like: $tasks = CRM::task()->related(10)->all(); //lists all $task_projects = CRM::task()->related(22)->projects(); //lists specific type $task_project = CRM::task()->related(22)->create(10, 'project'); //create one $task_project = CRM::task()->related(22)->delete(27, 'project'); //and remove
批量操作
您还可以运行批量操作,使用Guzzle的并发功能来加快并行调用。一些单用途方法有一个*Many对应方法,例如
createMany()
- 很简单;而不是有效载荷,您传递一个有效载荷列表
editMany()
- 在这种情况下,您必须按ID传递一个有效载荷列表。
delete()
- 是特殊的,因为它可以处理任意数量的ID。其响应将根据参数的数量而变化。
您可以使用数组、迭代器或生成器在这些操作上,它将确保同时运行多达10 (未来:可配置)个HTTP调用。
例如,让我们基于查询结果创建许多任务条目(这也具有低内存使用),然后删除这些条目
<?php //this call is using a simple generator $thousandsOfTasksQueryResult = [...]; $allTasks = CRM::task()->createMany(function() use ($thousandsOfTasksQueryResult) { foreach ($thousandsOfTasksQueryResult as $task) { yield [ 'name' => $task->name, 'due_date' => $task->dueDate->format('U'), 'status' => $task->completed? 'Completed' : 'Open' ]; } }); // as that's a batch operation, it seemed unsafe to throw harsh errors. // thus, success will give an object of data, while errors return a simple message $toDelete = []; foreach ($allTasks as $response) { if (is_object($response) { $toDelete[] = $response->id; } else { $logger->warning("Couldn't create Task: $response"); } } //here we use a plain list of arguments: you have to unpack the array CRM::task()->delete(...$toDelete); }
生成器在这些情况下特别有用,因为它将通过不在内存中存储大量有效载荷/请求来节省大量内存。
批量关系操作
与批量API调用类似,也可以运行一系列关系变更。为此,请使用relatedBatch()
的方法,通过以原始ID索引的ID + 类型(或Relation
辅助对象)的列表来实现。
<?php use ProsperWorks\SubResources\Relation; $relClientsQuery = [...]; CRM::task()->relatedBatch()->create(function() use ($relClientsQuery, $pwTaskId) { foreach ($relatedClientsQuery as $client) { // this would generate an array of Relation() objects, indexed by the same ID // causes no error; this won't become a real array (thus, with keys conflicts) yield new $pwTaskId => new Relation($client->id, 'company'); // the following would also work //yield new $pwTaskId => ['id' => $client->id, 'type' => 'company']; } });
我认为这些静态调用并不高效
确实,在非常小的范围内,它们可能并不高效。您始终可以使用半路对象来运行常见操作,例如在同一个端点上运行多个操作时。然而,静态调用可以在一次性调用中为您节省一些配置/实例;()
<?php $peopleRes = CRM::person(); $client = $peopleRes->find($clientId); $tags = array_merge($client->tags, 'new tag'); $peopleRes->edit($clientId, compact('tags'));
速率限制
SDK内置了速率限制器,当它注意到即将达到限制时,会进行一些sleep()
,以便在限制释放后不久进行新操作。当开启调试模式时,这会在CLI上发出一些通知。这对于批量操作特别有用。
响应对象
大多数(所有?)响应将是一个BareResource
对象,或者这些对象的列表。最大的优点是这个类“转换”功能:它通过利用具有更简单/可预测的结构或一些底层的数据验证/转换的对象,使有效载荷的一些部分更容易使用(即子资源)。
- 大多数日期字段(date_created,due_date,date_last_contacted,...)将从UNIX时间戳转换为
DateTime
对象。有一个实际上不起作用的设置可以禁用此功能,在BareResource中(请参阅#8) - 如果存在,将生成一个与
contact_type_id
相关的contact_type
字段。 - 自定义字段将生成两个条目
custom_fields_raw
,包含来自API的原始有效载荷custom_fields
,包含一个按字段名称索引的CustomField
对象列表
- 一些其他复杂结构也将成为子资源对象,例如
子资源
有几个依赖对象,不能直接用于API调用,但它们是主要资源的一部分。大多数情况下,这些是JSON有效载荷中的内部文档。它们用于响应(请参阅响应对象),但它们也被设计用来简化您的调用,“翻译”一些信息来回传递,并确保您始终遵循对子文档的要求。
特别地,实现TranslateResource
特质的子资源将允许读取一些受保护字段(列在$altFields
中)。简而言之,当您在PHP中将对象转换为数组时(这是我们获取最终JSON有效载荷所做的事情),它创建了一个包含所有公共字段的数组。因此,TranslateResource
能够在不将其暴露给API的情况下提供对一些“隐藏”属性的读取访问。下面是行为示例的列表
地址
第一个参数可以是完整的行(一个名为 street
的字符串,因为 ProsperWorks 就是这么做的),或者是一个包含两个地址行的数组(分别称为 address
和 suite
)。要在这两种格式之间切换,有一个“suite”分隔符(提示:如果 suite 部分已经有 suite 了,就不会重复)。其他参数都很标准,比如城市、邮政编码等等。
<?php $sherlock = new Address('221B Baker St. suite 2', 'London'); // same as new Address(['221B Baker St.', '2'], 'London'); // same as new Address(['221B Baker St.', 'suite 2'], 'London'); echo $sherlock->street; //'221B Baker St. suite 2' echo $sherlock->address; //'221B Baker St.' echo $sherlock->suite; //'2' $nemo = new Address('42 Wallaby Way', 'Sydney'); echo $nemo->street; //'42 Wallaby Way' echo $nemo->address; //'42 Wallaby Way' echo $nemo->suite; //null //and then, use at will: CRM::person()->create([ 'name' => 'P. Sherman', 'address' => $nemo ]);
关系
之前已经 展示过:它是一个简单的容器,用于存储 id
和 type
,没有其他功能。
自定义字段
这是最大的一个。它在裸自定义字段规范(id + value,对人类来说并不真正有意义)和实际可读信息之间进行翻译。原始字段 ID(相同字段的相同 ID,即使在不同类型的资源中也是如此)存储在 custom_field_definition_id
中,与 value
一起。总是有一个只读的 name
属性,包含字段的实际名称,如果是列表,则还有一个只读的字符串 valueName
,其中包含字段的可读值。
要创建自定义字段条目,您可以使用字段 ID 或字段名称作为第一个参数,以及值 ID 或字符串作为第二个参数。不过,当您使用字符串而不是 ID 时,请务必检查大小写!
以下示例显示了如何使用该类,以及它在 SDK 响应中的返回方式
<?php $person = CRM::person()->create([ 'name' => 'John Doe', 'custom_fields' => [ new CustomField('Alias', 'Johnny Doe') ] ]); print_r($person); // ProsperWorks\Resources\BareResource Object ( // [id] => 12340904 // [name] => John Doe // [custom_fields] => Array ( // [Alias] => ProsperWorks\SubResources\CustomField Object ( // [custom_field_definition_id] => 128903 // [value] => Johnny Doe // [name:protected] => Alias // [valueName:protected] => // [...] // ) // ) // [custom_fields_raw] => Array ( // [0] => stdClass Object ( // [custom_field_definition_id] => 128903 // [value] => Johnny Doe // ) // [1] => stdClass Object ( // [custom_field_definition_id] => 124953 // [value] => // ) // ) // [...] // )
"分类"子资源
所有这些类都继承自 Categorized
,因此它们有一个 category
属性和一些用作值的常量。如果在子类中一个常量被标记为“已弃用”,这意味着它实际上不与该对象一起使用。
它们的签名是相同的:值作为第一个参数,类别作为第二个参数。
电子邮件
最简单的子类:有一个 email
属性和 category
。
电话
第一个参数是字符串 number
。但是这里的技巧是您可以使用类似“123-4444 x123”的方式提供分机号码,它将被分成两个只读字段:simpleNumber
和 extension
。
URL
将有效的 url
作为第一个参数,如果存在匹配的社会 URL category
,则将自动填充。对于其他情况,您可以根据惯例手动提供类别作为第二个参数。
Webhooks
另一方面,如果您想从 ProsperWorks 获取更新,您必须设置 您自己的 端点,它们可以调用以获取任何更改。
提示:您可能想查看[Webhooks 指南];它仍在开发中——这就是为什么它仍然是一篇知识库文章。
可用事件
根据文档,您可以订阅三种类型的事件
Webhooks::EV_NEW
- 创建了一个新条目
Webhooks::EV_UPDATE
- 一个条目被更新
Webhooks::EV_DELETE
- 一个条目被删除
Webhooks::EV_ALL
- 捕获所有事件的常量,将一次订阅所有事件
以下内容适用于所有主要端点,列在Webhooks::ENDPOINTS
下
- 公司
- 线索
- 机会
- 个人
- 项目
- 任务
如何与Webhook交互
在配置Webhook时,您应该在项目的REPL或CLI工具中使用Webhooks
类上的几个方法,或者在自己的控制器中操作ProsperWorks调用
配置Webhook
首先,您必须使用应用程序环境的根路径实例化Webhooks
对象,否则它将使用由Config
配置的默认值(如果有)。在测试期间,这种就地配置能力将很有用。
然后,您可以使用一些方法与ProsperWorks Webhook API进行交互
list(int $id = null)
- 按ID索引返回Webhook详细信息列表。
create(string $endpoint, $event = self::ALL)
- 为指定的端点和事件匹配订阅一个新的Webhook,在`Config`中指定密钥。您应该为第一个参数使用CRM::RES_*常量之一。
delete(int ...$id)
- 从ProsperWorks池中删除一个或多个Webhook。
解释Webhook调用
每当在设置端点上发生设置的事件时,ProsperWorks的服务器将向一个地址发出HTTPS调用,该地址由以下内容组成:<root_path>/prosperworks_hooks/<endpoint>/<event>
。您的控制器应该以最适合您应用程序的方式解释这一点,但大部分信息也重复在负载中,因此您可以有一个单个的catch-all操作来处理它。
Webhook负载
负载简单明了。它包含受影响/生成的ID(通常只有一个,除非是批量操作),受影响端点和生成事件,Webhook订阅ID,时间戳和我们的密钥。
在执行任何其他操作之前,出于安全考虑,您应该验证密钥是否正确。为此,以数组格式调用带有负载的validateSecret()
。它将搜索密钥,解密并验证它。如果密钥不存在,则返回null;如果存在但无效,则返回false。
下一步通常是通过常规方式访问ProsperWorks API来收集有关创建/更新资源的详细信息,并相应地更新您的数据库,或删除需要删除的内容。
值得一提的是,根据他们当前的UI,用户所做的每个字段更改都会自动保存;这对他们的用户来说很好,但每个保存调用都会产生一个新的Webhook调用,这可能会使您的服务器不堪重负。
因此,一个很好的想法是有一个某种形式的队列系统来处理这些更改,甚至可能有一个debouncer可以减少对同一资源的重复更改 - 即在短时间内忽略具有相同端点、事件和ID的负载。
如何使用Webhook进行开发
这里一个重要的事实是,ProsperWorks要求所有webhook调用都必须加密,这意味着你必须为它们提供一个HTTPS地址。除此之外,你的web服务器必须公开地可供网络访问。这对于开发环境来说可能不是最简单易用的设置,对吧?
嗯,建议在开发过程中使用ngrok。你可以很方便地从命令行运行它,并让它打开一个稳定的HTTPS隧道,并给你它的URL;你将这个URL输入到Webhooks
对象中,并设置新的订阅,当你在ProsperWorks的UI上做出更改时,ProsperWorks可以调用这些订阅。
使用Ngrok检查器还可以检查HTTP流量;当你运行它时,它的URL也会显示出来。这可能很有用,因为你不需要编辑字段千百次来验证你的代码是否工作,因为它能够存储和重复收到的调用。很酷,不是吗?