wyz/copernica-api

Copernica REST API PHP 工具

2.0 2020-11-05 16:43 UTC

This package is not auto-updated.

Last update: 2024-09-20 12:34:18 UTC


README

本项目包含

  • 一个用于 REST API 的客户端(它添加了一些功能,并改进了 Copernica 提供的下载的 CopernicaRestAPI 类的错误处理);
  • 用于处理(较大的/嵌入的)实体集的帮助类;
  • 一个框架,可以用来为使用此库的 PHP 代码编写自动化测试。

用法

使用 RestClient 类和下面提到的另外两个类;假设 CopernicaRestAPI 不存在。

RestClient 包含 get() / post() / put() 和 delete() 调用(就像标准的 CopernicaRestAPI 一样);它还包含两个额外的调用 getEntity() 和 getEntities(),它们执行一些额外的检查,并保证返回有效的 'entity' 或 'entity 列表'。('entity' 可能像配置文件/子配置文件/电子邮件/数据库这样的东西;很可能是有 ID 值的任何东西。)根据 API 端点,可以很明显地看出哪个三个 'get' 方法最好使用。

use CopernicaApi\Helper;
use CopernicaApi\RestClient;

$client = new RestClient(TOKEN);

// Create a new profile.
$id = $client->post("database/$db_id/profiles", ['fields' => ['email' => 'rm@wyz.biz']]);

// Update a single existing profile (or multiple, in the second example).
// put() often returns a location string for the updated entity, which often
// isn't very useful because it's the same as the first argument - e.g. in this
// case it always returns "profile/$id":
$client->put("profile/$id", ['fields' => ['email' => 'info@wyz.biz']]);
// ...but there are resources which can create new entities, e.g. the following
// call will update all profiles matching the company name and return true, but
// if zero profiles match then it will create a new profile and return its
// location:
$return = $client->put(
  "database/$db_id/profiles",
  // Profile data to update:
  ['fields' => ['email' => 'info@wyz.biz', 'company' => 'Wyz']],
  // Selection criteria for existing profiles:
  ['fields' => ['company==Wyz'], 'create' => true]);
if ($return !== true) {
    // (This is an odd way of having to extract the new ID, and requires you to
    // know how many parts the URL has - but it's compatible with the other
    // 'put' calls and allows the library to better handle any future behavior
    // changes.)
    list($unimportant__always_profile, $created_id) = explode('/', $return);
}

// Get non-entity data (i.e. data that has no 'id' property).
$stats = $client->get("publisher/emailing/$mailing_id/statistics");

// Get a single entity; throw an exception if it was removed earlier:
$profile = $client->getEntity("profile/$id");
// If we want to also have entity data returned (with all fields being empty
// strings) if the entity was 'removed' from Copernica, we can pass an argument
// to suppress that specific error. (There's practically no difference between
// the below and just calling get() instead.)
$profile = $client->getEntity("profile/$id", [], RestClient::GET_ENTITY_IS_REMOVED);

// Get a list of entities; this will return only the relevant 'data' part from
// the response. (If we want to have the full structure including start / count
// / etc, we can use get().) There's a limit to the returned number of entities.
$profiles = $client->getEntities("database/$db_id/profiles");
// Setting 'dataonly = true' on getEntities() calls for profiles or subprofiles
// can make the calls faster. It omits some property values from the individual
// (sub)profiles that are likely not needed anyway; see the method comments.
$profiles = $client->getEntities("database/$db_id/profiles", ['dataonly' => true]);
// The returned list has a zero-based index. If we want to access the profiles
// by ID, here's a quick helper method.
$profiles = Helper::rekeyEntities($profiles, 'ID');

// Delete a single entity; throw an exception if it was already removed/deleted
// earlier:
$profile = $client->delete("profile/$id");
// If we want to suppress the exception and just return true after re-deleting
// an entity:
$profile = $client->delete("profile/$id", RestClient::DELETE_RETURNS_ALREADY_REMOVED);
// To always suppress particular exceptions without having to pass it to
// every get() / delete() call, call e.g.:
$client->suppressApiCallErrors(RestClient::DELETE_RETURNS_ALREADY_REMOVED);
// There's a bunch of constants for suppressing other exceptions, but only the
// two mentioned here are likely to ever be needed.

大于 API 在一个响应中允许的极限的大实体集需要分批获取。BatchableRestClient 是一个客户端,包含一些有用的方法:getMoreEntities() 和 getMoreEntitiesOrdered()。根据所获取的数据集,可以使用这两个方法中的任何一个;getMoreEntities() 对所有类型的实体都适用,但有时可能会跳过实体,风险略高。请参阅方法注释以获取更多信息。(或者,可以先复制下面的示例,如果不起作用,将其替换为 getMoreEntities()。)

use CopernicaApi\BatchableRestClient;

$client = new BatchableRestClient(TOKEN);
$profiles = $client->getEntities("database/$db_id/profiles", ['orderby' => 'modified', 'fields' => ['modified>=2020-01-01'], 'dataonly' => true]);

// It is possible to pause execution and fetch the next batch in a separate
// PHP thread, with some extra work; check getState() for this.
while (!$client->allEntitiesFetched()) {
    $next_batch = $client->getMoreEntitiesOrdered([], ['fall_back_to_unordered' => true]);
    $profiles = array_merge($profiles, $next_batch);
}

一些 API 调用的响应包含实体内部的实体列表。这不是很常见。(这可能是像数据库和集合这样的 '结构' 实体的情况,而不是 '用户数据' 实体。)这些嵌入的实体被包裹在类似 API 列表响应的元数据集合中。虽然这个库没有实现与嵌入实体完美工作的结构,但它确实提供了一个方法来验证和 '展开' 这些元数据,这样调用者就不需要担心它。示例

$database = $client->getEntity("database/$db_id");
$collections = Helper::getEmbeddedEntities($database, 'collections');
$collections = Helper::rekeyEntities($collections, 'ID');
$collection_fields = Helper::getEmbeddedEntities($collections[$a_collection_id], 'fields');
// Note if we only need the collections of one database, or the fields of one
// collection, it is recommended to call the dedicated API endpoint instead.

错误处理

如果 RestClient 方法接收到无效或不认识的响应/无法从 API 获取任何响应,它们会抛出异常。调用者可以影响这种行为,并使类方法仅返回响应。注意事项

  • 可以通过设置某些类型的错误为 '抑制'(即不抛出异常)来全局影响行为,使用 suppressApiCallErrors()。相同的值也可以作为参数传递给单个调用。

  • 如果抑制的错误是由非 2XX HTTP 代码返回的,则 post() / put() / delete() 调用返回 HTTP 响应中返回的完整标题和正文,这样调用者就可以找出如何处理它。get() 调用只返回正文。

  • 某些响应是否 '无效',可能取决于特定的应用程序。例如,默认情况下,如果查询(例如 getEntity("profile/$id"))单个实体(例如,配置文件),它已被删除,则会抛出异常。

    • 但是,API 实际上返回了一个具有正确 ID 的实体,所有字段都为空,并有一个具有日期值的 'removed' 属性。如果应该返回空实体而不会抛出异常,请参阅上面的示例代码。
    • 如果我们尝试重新删除之前已删除的实体。也参见上面的示例代码。在这种情况下,delete() 的返回值可以被忽略,调用返回正常可以视为“成功”。
  • 一些API端点表现未知。RestClient默认对未知行为抛出异常;目的是在无法确定如何处理该值时,永远不会向调用者返回值。但这可能会引起“有效”响应的异常。可能的示例

    • 标准的CopernicaRestApi代码似乎暗示,并非所有POST请求的响应都包含一个“X-Created”头,该头包含新创建实体的ID。目前,这个头的缺失将导致异常(因此调用者可以确信默认返回的是实际ID)。如果您遇到这种情况不合适的情况:将 RestClient::POST_RETURNS_NO_ID 传递给 post() 的第三个参数,或者使用 suppressApiCallErrors() 设置它。(我们知道一些这样的调用,但它们是PUT调用的未记录的POST等效调用。我们真的应该使用文档化的PUT调用 - 因此我们不介意这些抛出异常。)

任何时候您遇到需要绕过(例如通过调整这些常量)的异常,但您认为实际上类应该更好地处理这个问题:请随意提交一个错误报告。

临时网络错误

对于某些软件项目,了解哪些错误可以归类为临时的是很重要的。(例如,那些在遇到意外错误时终止进程,并且只在遇到特定“已知为临时”的错误时继续/重复其操作的项目。另一种可能有效但可能危险的方法是,仅将HTTP 400响应代码视为永久错误,并将其他所有错误视为临时错误 - 这可能导致进程卡在重试中。)

Copernica的服务基础设施相当稳定,但任何地方都可能发生故障和临时中断。这是一个观察到的错误的半实时文档

临时

  • Curl偶尔返回错误7“连接失败”。这种性质和频率(相对于其他错误相对较高)可能需要将其视为临时错误。这通常不是单个连接的故障,而是一段持续几分钟的服务问题。
  • Copernica偶尔返回HTTP响应代码503(服务不可用)和504(网关超时),以及标题为“负载均衡器错误”的HTML正文和提到“请求过多”的头。这些通常最多持续几分钟。

不确定

  • 曾经,我们看到了错误35“OpenSSL SSL_connect: SSL_ERROR_SYSCALL in connection to api.copernica.com:443”。虽然这是一个临时错误,但这不是唯一可能返回Curl错误35的SSL错误类型 - 因此我仍然犹豫将其视为“始终是临时的”,除非有更多的需求。
  • ~2020年6月,我们观察到Curl错误52“Empty reply from server”对于一个将返回大量结果的GET查询。我们没有足够的信息来了解这种错误是否始终是临时的。(也有可能这种特定的情况在同时已经被返回HTTP 504响应所取代。)
  • 曾经,我们看到了HTTP响应代码502,以及标题为“502服务器错误”的HTML正文。这显然是一个非常短暂的内部故障,但502可能不应该被视为仅持续很短时间的错误(除非这种情况开始更频繁地发生)。

一些更多细节

以下文本对大多数人来说可能不重要(他们只想使用REST API客户端类)。

此存储库的其他内容

tests/ 目录包含 TestApi,这是 Copernica API 的 '测试实现',即一个可以用作 CopernicaRestAPI 替代的类,并内部存储数据。tests/ 目录中的其他代码是 PHPUnit 测试和一些小的测试辅助类;一些测试是真正的单元测试,但大多数需要与 API 进行交互才能工作,因此它们使用 TestApi。

TestApi 应该能够让您为使用 REST API 的自己的流程编写测试。extra/ 目录包含我为自己的流程编写的示例测试/其他代码。

不要使用 CopernicaRestAPI

建议不要直接使用 CopernicaRestAPI。我在使用它,但将其保存在单独的文件中,出于多种重叠的原因。

  • 尽管 Copernica 的 API 服务器相当稳定,API 行为在 https://www.copernica.com/en/documentation/restv2/rest-requests 中有文档记载,但仍然存在未知因素。(参见上面的 "错误处理"。)因此,我担心在观察到更多实际行为之前,完全从标准代码中移除可能会打扰我的实时流程。
  • 这样,我们可以更容易地跟踪 Copernica 是否对其参考类进行了更改,并且可以轻松地将它们合并到这里。
  • 将解释响应的额外功能(例如 getEntity() / getEntities())与执行 HTTP 请求的代码分离,似乎是有意义的。

尽管这种方法有缺点

  • 引入一些 '以防万一' 的常量来抑制可能的未来未知情况下的异常,这些常量与 CopernicaRestAPI 结构紧密相关...显得有些尴尬。
  • 我们希望不会遗漏任何异常情况(这意味着我们对任何可能异常的情况抛出异常,并使用这些常量来抑制它们)已经导致这两个类之间产生了紧密耦合。
  • CopernicaRestAPI 和 RestClient 之间的责任划分不理想/理想情况下应进行重写。以下是一些迹象
    • 一些 '从 API 到客户端类的通信' 是通过异常完成的。这导致了大量的抑制异常的常量(如果代码设置不同,这些常量将更多、更不合理),以及异常消息必须包含完整的响应体,以便 RestClient 能够访问它。
    • 目前,RestClient 无法获取 GET 请求响应的头部信息,因为它们不在异常消息中。

很可能,这段代码的下一步发展将是咬紧牙关,去掉 CopernicaRestAPI / 使其只成为一个非常薄的层(仅用于测试中模拟 API 调用)。尽管如此,所有这些都对 '99.99% 的情况' 没有影响,这种情况下应直接使用 RestClient,不需要使用这些不合理的常量。如果当前代码能适用于我们遇到的所有实际应用,可能需要几年时间才会进行下一次重写。

额外的分支

  • 'copernica' 包含未修改的下载的 copernica_rest_api.php。
  • 'copernica-changed' 包含对它的补丁(除了在 'master' 中添加的命名空间之外)
    • 一个额外的公共属性,允许在返回任何非 2xx HTTP 响应时抛出异常。(这使 RestClient 能够执行更严格的检查...甚至意味着我们需要额外的工作来捕获 HTTP 303 的异常,这对于 PUT 请求总是返回。它还使我们能够提取和返回 PUT 请求的位置头信息,这对于创建新实体的情况很重要。)
    • 正确处理数组参数,如 'fields'。

实际上... 第一个补丁似乎已经不再有任何额外的价值。如果您想知道

  • https://www.copernica.com/en/documentation/restv2/rest-fields-parameter 记录了 https://api.copernica.com/v2/database/$id/profiles?fields[]=land%3D%3Dnetherlands&fields[]=age%3E16 的字段参数示例。
  • 用 Copernica 自身类来做这是不可能的:传递带有参数的数组会导致 https://api.copernica.com/v2/database/$id/profiles?fields%5B0%5D=land%3D%3Dnetherlands&fields%5B1%5D=age%3E16
  • 我确信在 2019 年 9 月左右,查询后者的 URL 并不会返回正确数据,这就是为什么我修复了代码以生成前者。
  • 截至发布此代码时,后者 URL 运行正常。所以要么是我做了愚蠢的事情,要么是 Copernica 在 2019 年 9 月之后修补了 API 端点,使其能够处理未编码的 [] 字符。无论如何... 补丁似乎已经没有实际价值了,但我还是会保留它。

兼容性

该库与 PHP7/8 兼容,可能也与 PHP5 兼容。

尽管 PHP5 已经超过了生命周期的尽头,但我仍然试图保持兼容性,因为我不认为使用 PHP7-only 构造有真正的好处,因为我还习惯于它/因为谁知道公司内部仍在运行什么旧代码。但我不会拒绝 PHP7-only 的添加。并且由于测试本身包含 PHP7-only 构造,所以 PHP5 的测试覆盖率已经降低,所以如果我意外地破坏了 PHP5 兼容性,我也不会注意到。

项目名称

该包被称为 "copernica-api" 而不是例如 "copernica-rest-api",以留出添加代码以使用较旧的 SOAP API 的可能性。(我有一些使用 SOAP 的代码,但关于 v2 REST API 的最新工作表明这可能不再需要 - 所以我没有对其进行打磨。)

类似的项目

原则上,我不是创建新项目(如这个 PHP 库)而是与现有项目合作的粉丝。我四处看了看,发现了一些 - 所以冒着吹毛求疵的风险,我觉得有必要为这个选择进行辩护。也许这将是其他比较项目的指南。

如果情况改变,并且合并此项目到另一个项目中有价值,我对此持开放态度。

找到的项目(不包括那些仅仅取 Copernica 的 SOAP 类并提供 composer.json 的项目)

最后提交于 2018 年 10 月。使用 Copernica 示例类中的 Curl 代码,略有改动。该类明确表示必须为每个单独的 API 调用创建新方法 - 而目前只创建了三个。这不能被认为是“处于活跃开发中”。

最后更新于 2019 年 9 月,就方法而言似乎几乎完整。(不完整;它缺少 EmailingStatistics 和 EmailingTemplates,我正在使用它们。)

这已经用 Guzzle 替换了原始代码,我对此表示赞赏。根据代码和 README 文件,这是由一个人编写的

  • 喜欢链式方法调用;
  • 不喜欢需要检查返回数组的实际内容(例如数据库字段)的事实;他想要为每个实体及其属性创建专用方法和类。

我理解对于“嵌入实体”(如响应中的集合内的字段)的最后一种情绪,它们包含多层元数据,调用代码不需要处理。然而,嵌入实体是一个例外,而不是常态。

虽然我 不反对 在有实际用途的地方使用值对象... 我倾向于在理由不明确的情况下处理返回的数组数据。通常包装返回的数据只会使其更难以理解。这可能是一种个人偏好。

很不幸,我无法从代码或README中的示例中推导出其实际用途。README中只记录了一个嵌入式实体的示例,我认为这并不能很好地代表日常API的使用。

我认为,将数组数据包装到单独的PHP类中,与在处理远程API时进行所有必要的检查相比,具有更大的实用价值。这样做可以让调用者1) 不必处理这些;2) 可以绝对确信客户端类返回的数据是有效的。(为了第二个原因,我更喜欢非常严格,并对任何意外的数据进行抛出异常。)

因此:对我来说,将我在各种get*()命令中进行的严格检查集成到这个项目的各个类中,将非常重要。可能的好处是,我各种get*()调用的差异将消失,因为每个API端点只映射到其中一个。

  • 但是,我无法从阅读代码中得知这种集成的成功程度。(代码设置对我来说过于不透明,无法立即理解。)
  • 我也不确定将getEntitiesNextBatch()功能移植过来有多困难。

目前,我宁愿花些时间编写这份README,并完善现有的代码,将其作为一个独立项目发布,而不是将我需要的检查集成到copernica-api-php项目中,并在添加EmailingTemplates/EmailingStatistics后重新测试我的实际项目。

再次强调,一旦我看到将所有数据包装到大量类中而不是处理简单数组数据的用途……我将很高兴重新考虑将我的代码添加到copernica-api-php项目中,并废弃这个项目。

贡献

如果你提交的PR很小:请明确告诉我你是否不希望我rebase它/希望保持你自己的仓库不变。(如果一个项目进展缓慢,PR的更改不大,当无法快速前进时,我倾向于想'合并 --rebase' PR,以保持git历史简单明了。)

鼓励为你的更改添加测试,但可能不是必需的;我们将根据具体情况来决定。(单元测试覆盖率尚未完成。)

作者

  • 科珀尼卡团队
  • 罗德里克·缪特 - Wyz

致谢

  • 部分赞助商为Yellowgrape,电子商务专家。(原始代码是由他们作为封闭项目委托的;代码的润色/编写文档/重新发布以及大部分测试代码是在我自己的无偿时间里完成的。)

部分赞助的结果是一个用于同步过程的组件,可以基于导入的“项目”更新配置文件及其关联的子配置文件——以可靠、高效和可配置的方式。如果您需要这样的组件,请随时与我联系,并说明您为什么需要它。(它尚未开源,因为它花费了大量的付费和免费开发时间。extra/中的测试和辅助代码可以提供一些关于其代码健壮性的见解。)

许可

本库根据GPL,版本2或更高版本许可——除了CopernicaRestAPI类(我可以合法地重新发布,也可以从科珀尼卡网站下载)。

(许可证可能看起来有些奇怪,但这是为了保持与Drupal一起分发代码的可能性,这可能会成为配置文件导入组件的潜在问题。如果这种前景发生变化,我可能会以GPLv3或MIT许可证重新许可较新版本的代码。)