academe/xero-php

是Xero API的客户端实现,提供原始数据。

0.8.0 2017-12-10 21:18 UTC

This package is auto-updated.

Last update: 2024-09-23 05:53:41 UTC


README

Build Status Latest Stable Version Total Downloads Latest Unstable Version License

目录

XeroPHP API OAuth访问

用于处理Xero OAuth API的PHP库。

简介

此库解决了Xero API访问的以下部分

  • 协调OAuth层以提供安全访问。
  • 自动刷新过期的令牌(对于合作伙伴应用程序)。
  • 将响应解析为通用嵌套对象。

此包将这些功能留给其他包处理,尽管它也协调它们

  • 所有HTTP通信通过Guzzle 6
  • OAuth请求签名到Guzzle OAuth Subscriber
  • 建议通过OAuth 1.0 Client进行OAuth身份验证
  • 建议使用Xero Provider for The PHP League OAuth 1.0 Client作为OAuth 1.0 Client的Xero提供程序
  • 将OAuth令牌存储到您的应用程序中。提供了一个钩子,以便可以在存储中更新刷新的令牌。
  • 如何导航结果的知识留给了您的应用程序。然而,响应构建的通用嵌套数据对象有助于做到这一点。

此包与优秀的calcinai/xero-php包在以下基本方面有所不同

  • 它不参与与最终用户的OAuth授权过程。您需要自己处理。
  • 它没有固定的响应模型,而是使用通用的模型结构为资源和资源集合。
  • 此包支持UK Payroll v2.0 API。

哪个包最适合您,将取决于您的用例。每个都有优点和缺点。

此包需要通过授权获得的OAuth令牌和密钥来访问API,以及如果需要为合作伙伴应用程序自动刷新令牌,则还需要会话处理程序令牌。

此包不关心您在前面使用什么来获取这些令牌。上述推荐的两个包是可靠的、文档齐全的,并且专注于仅完成这项工作。

我主要关注为Xero合作伙伴应用程序使其正常工作,因为我需要一个健壮的库,该库作为一个计划的任务持续运行而不会丢失令牌和需要用户重新认证。一旦合作伙伴应用程序被授权,理论上它应该能够访问Xero账户10年,每30分钟刷新一次。实际上,令牌可能会丢失 - 即使Xero也有可能导致认证令牌丢失的停机时间。

待完成区域(TODO)

  • 截至目前,该软件包的开发主要集中在从Xero API读取。写入API应该得到支持,但目前尚未进行任何测试。
  • 更多文档和示例。
  • 错误处理更加一致。应用程序不应该需要到处查找才能确定错误是在配置、网络、远程应用程序、请求语法等方面。所有这些细节都应该直接提供给应用程序。

快速开始

use use Academe\XeroPHP;

// Most of the configuration goes into one place.

$myStorageObject = /* Your OAuth token storage object */

$clientProvider = new XeroPHP\ClientProvider([
    // Account credentials.
    'consumerKey'       => 'your-consumer-key',
    'consumerSecret'    => 'your-consumer-sectet',
    // Current token and secret from storage.
    'oauthToken'            => $myStorageObject->oauthToken,
    'oauthTokenSecret'      => $myStorageObject->oauthTokenSecret,
    // Curresnt session for refreshes also from storage.
    'oauthSessionHandle'    => $myStorageObject->oauthSessionHandle,
    // The local time the OAuth token is expected to expire.
    'oauthExpiresAt'        => $myStorageObject->oauthExpiresAt, // Carbon, Datetime or string
    // Running the Partner Application
    'oauth1Options' => [
        'signature_method' => \GuzzleHttp\Subscriber\Oauth\Oauth1::SIGNATURE_METHOD_RSA, // Default
        'private_key_file' => 'local/path/to/private.pem',
        'private_key_passphrase' => 'your-optional-passphrase', // Optional
    ],
    'clientOptions' => [
        // You will almost always want exceptions off, so Guzzle does not throw an exception
        // on every non-20x response.
        // false is the default if not supplied.
        'exceptions' => false,
        'headers' => [
            // We would like JSON back for most APIs, as it is structured nicely.
            // Exceptions include 'application/pdf' to download or upload files.
            // JSON is the default if not supplied.
            'Accept' => XeroPHP\ClientProvider::HEADER_ACCEPT_JSON,
        ],
    ],
    // When the token is automatically refreshed, then this callback will
    // be given the opportunity to put it into storage.
    'tokenRefreshCallback' => function($newClientProvider, $oldClientProvider) use ($myStorageObject) {
        // The new token and secret are available here:
        $oauthToken= $newClientProvider->oauthToken;
        $oauthTokenSecret = $newClientProvider->oauthTokenSecret;
        $oauthExpiresAt = $newClientProvider->oauthExpiresAt; // Carbon\Carbon

        // Now those new credentials need storing.
        $myStorageObject->storeTheNewTokenWhereever($oauth_token, $oauth_token_secret, $oauthExpiresAt);
    },
    // If you want to force a token refresh immediately, then set this option.
    //'forceTokenRefresh' => true,
]);

// Get a plain Guzzle client, with appropriate settings.
// Can pass in an options array to override any of the options set up in
// the `$clientProvider`.

$refreshableClient = $clientProvider->getRefreshableClient();

现在我们有一个发送请求的客户端。这是一个可刷新的客户端,因此如果您使用Xero合作伙伴应用程序,它将在令牌过期时自动刷新令牌,并通过tokenRefreshCallback通知您的应用程序。

发送请求后,您可以检查是否刷新了令牌,以便执行您可能希望执行的操作。

if ($refreshableClient->tokenIsRefreshed()) {
    // Maybe save the token or other details, or just log the event.
}

如果刷新了令牌,则新令牌将通过tokenRefreshCallback被您的应用程序存储。

如果您想在令牌过期前显式刷新令牌,则可以这样做。

// Refresh the token and get a new provider back:

$clientProvider = $refreshableClient->refreshToken();

// Use the new $clientProvider if you want to create additional refreshable clients.
// Otherwise just keep using the current $refreshableClient.

// The `$refreshableClient` will now have a new Guzzzle client with a refreshed token.
// The new token details are retrieved from the provider, and can then be stored,
// assuming your callback has not already stored it. Store these three details:

$clientProvider->oauthToken;        // String
$clientProvider->oauthTokenSecret;  // String
$clientProvider->oauthExpiresAt;    // Carbon time

这样做可能更方便,但请注意,除非您设置了保护时间,否则可能会有错过过期时间的情况,请求将返回过期令牌错误。

您可能希望检查每次运行时到期时间是否接近,并显式续订令牌。此检查可以用来查看我们是否进入了预期到期时间之前的“保护窗口”。

// A guard window of five minutes (300 seconds).
// If we have entered the last 300 seconds of the token lifetime,
// then renew it immediately.

if ($refreshableClient->isExpired(60*5)) {
    $refreshableClient->refreshToken();
}

响应消息

消息实例化

使用响应数据实例化ResponseMessage类。可以使用Response对象或从响应中提取的数据来初始化ResponseMessage

// Get the first page of payruns.
// This assumes the payrun Endpoint was supplied as the default endpoint:
$response = $refreshableClient->get('payruns', ['query' => ['page' => 1]]);

// or if no default endpoint was given in the config:
$response = $refreshableClient->get(
    XeroPHP\Endpoint::createGbPayroll('payruns')->getUrl(),
    ['query' => ['page' => 1]]
);

// Assuming all is fine, parse the response to an array.
$bodyArray = XeroPHP\Helper::parseResponse($response);

// Instantiate the response data object.
$result = new XeroPHP\ResponseMessage($bodyArray);

// OR just use the PSR-7 response without the need to parse it first:
$result = new XeroPHP\ResponseMessage($response);

### Navigating the Response Message

// Now we can navigate the data.

// At the top level will be metadata.

echo $result->getMetadata()->id;
// 14c9fc04-f825-4163-a0cf-3c2bc31c989d

echo $result->getPagination()->pageSize;
// 100

var_dump($result->getPagination()->toArray());
// array(4) {
//   ["page"]=>
//   int(1)
//   ["pageSize"]=>
//   int(100)
//   ["pageCount"]=>
//   int(1)
//   ["itemCount"]=>
//   int(3)
// }

The results object provides access to structured data of resources fetched from the API.
It is a value object, and does not provide any ORM-like functionality (e.g. you can't
update it then `PUT` it back to Xero, at least not yet).

A `ResponseMessage` object may contain a resource, a collection or resources, or may be empty.
The following methods indicate what the response contains:

```php
if ($result->isCollection()) {
    $collection = $result->getCollection();
}

if ($result->isResource()) {
    $resource = $result->getResource();
}

if ($result->isEmpty()) {
    // No resources - check the metadata to find out why (TODO).
}

响应集合

// 在ResponseMessage中将是资源或资源集合。// 如果它包含单个资源,则仍然可以将其提取为集合,这将包含单个资源。

foreach($result->getCollection() as $payrun) { echo $payrun->id . " at " . $payrun->periodStartDate . "\n"; } // e4df31c9-07db-47d5-a415-6ee32d9048eb at 2017-09-25 00:00:00 // fbd6fc76-dbfc-459d-b230-80334d175048 at 2017-10-20 00:00:00 // 46200d03-67f2-4f5d-8852-cdad50cbe886 at 2017-10-25 00:00:00


There may be further collections of resources deeper in the data, such as
a list of addresses for a contact.

### Response Dates and Times

An attempt is made to convert all dates and times to a `Carbon` datetime.
Xero mixes quite a number of date formats across its APIs, so it is helpful to
get them all normallised.
Formats I've found so far:

* "/Date(1509454062181)/" - milliseconds since the Unix epoch, UTC.
* "/Date(1439813704613+0000)/" - milliseconds since the Unix epoch, with a timezone offset.
* "2017-10-20T16:04:50" - ISO UTC time, to the second.
* "2017-10-31T12:50:15.9920037" - ISO UTC timestamp with microseconds.
* "2017-09-25T00:00:00" - ISO UTC date only.

I'm sure there will be more. These fields are recognised solely through the suffix to
their name at present. Suffixes recognised are:

* UTC
* Date
* DateTime
* DateOfBirth (as a prefix)

### Pagination

There is no automatic pagination feature (automatically fetching subsequent pages) when
iterating over a paginated resource.
A decorator class could easily do this though, and that may make a nice addition to take
the logic of "fetching all the matching things" that span more than one page away from
the application (ideally the application would make a query, then loop over the resources
and each page would be lazy-loaded into the collection automatically when going off the
end of the page).

All other datatypes will be either a scalar the API supplied (string, float, int, boolean)
or another `ResponseData` object containing either a single `Resource` (e.g. "Invoice")
or a `ResourceCollection` (e.g. "CreditNotes").

### Resource Properties

Accessing properties of a resource object is case-insensitive.
This decision was made due to the mixed use of letter cases throughout the Xero APIs.

A resource will have properties. Each property may be another resource, a resource
collection, a date or time, or a scalar (string, integer, float).

Accessing a non-existant property will return an empty `Resource`.
Drilling deeper into an empty resource will give you further empty resources.

```php
$value = $result->foo->bar->where->amI;
var_dump($value->isEmpty());
// bool(true)

但请注意,当您遇到标量(例如字符串)时,您将得到返回的内容,而不是Resource对象。

API有时会返回一个null值,而不是简单地省略字段或资源。例如,在获取单个payrun时返回的pagination字段,或者在没有问题时返回的problem字段。在这种情况下,当您获取值时,您将得到一个空的Resource对象。

Guzzle异常

默认情况下,Guzzle客户端在接收到非20x HTTP响应时会抛出异常。当这种情况发生时,HTTP响应可以在异常中找到。

try {
    $response = $client->get('PayRuns', []);
} catch (\Exception $e) {
    $response = $e->getResponse();
    ...
}

处理非20x消息可能不方便,因此Guzzle可以告知不要抛出异常,使用exceptions选项。

$response = $client->get('PayRuns', ['exceptions' => false]);

注意:此包现在默认关闭Guzzle异常。如果需要,可以使用此参数将其重新打开。无论是否启用异常,令牌刷新都将正常工作。

此选项可以在每个请求上使用,或在ClientProvider实例化选项中设置为默认值。此包旨在不关心您采用哪种方法。然而,抛出异常通常是有意义的,因为即使是20x之外的响应也几乎总是包含应用程序需要记录或做出决策的信息。

捕获错误

存在许多错误来源,并且它们以许多不同的方式、不同的数据结构报告。本包的目标是尝试规范化它们,但在此期间,以下是已知的一些列表

  • OAuth错误
  • 请求构造错误,例如无效的UUID格式
  • 无效资源错误,例如缺少资源或错误的URL
  • 数据错误,例如尝试获取结果列表的最后一页之后的数据

错误详情可以找到的地方是

  • OAuth错误将以URL编码的参数形式返回到响应体中。可以使用OAuthParams类解析这些详情并提供一些解释。
  • 请求构建错误返回待定

API响应结构

每个响应将属于以下几种结构之一。以下列出的结构已经被识别出来,目标是自动识别并将所有内容归一化到单个资源或集合。

A: 单个元数据头;单个资源

资源位于单个节点中,通常以资源内容命名,但并不总是如此。例如,在GB Payroll v2.0 API中获取单个Payrun。

Response Format A

B: 单个元数据头;资源集合

资源位于单个节点的数组中,通常以资源内容命名,但并不总是如此。例如,在GB Payroll v2.0 API中获取多个Payrun。分页元数据在一个单独的对象中。

Response Format B

C: 单个元数据头;单个资源的集合

某些API将返回单个资源和数组中的多个资源,但没有分页元数据。区分请求单个资源或匹配资源集合中的单个资源是不可能的;没有查看资源内容的更多详细信息,响应看起来是相同的。例如,在Accounting v2.0 API中获取单个付款。

Response Format C

D: 单个元数据头;资源集合

这种结构在根节点中包含一些元数据,包括未封装到对象中的分页详情。例如,在Files v1.0 API中获取多个文件。

Response Format D

E: 资源数组

某些API将返回没有任何元数据的资源数组,没有分页详情,没有源详情。

Response Format E

F: 单个资源

类似于格式E,响应包含单个资源,未封装到字段或对象中,并且没有提供上下文的元数据。

Response Format F

用于传递错误消息和异常有多种不同的格式(至少四种结构)。这些格式将很快被记录,因为它们需要使用相同的规则来处理。

我怀疑至少有两个这样的结构可以合并为一个。

其他说明

我注意到偶尔会出现401错误,然后通过重试可以解决。使用Guzzle重试处理程序将是一个很好的选择,以避免在处理大量数据时出现不必要的错误。