calcinai/xero-php

是Xero API的一个客户端实现,具有更简洁的OAuth接口和类似ORM的抽象。


README

Build Status Latest Stable Version Total Downloads

Xero API的一个客户端库,包含Guzzle和类似ORM的模型。

这个库最初是为传统的私有、公共和合作伙伴应用程序开发的,但现在基于OAuth 2作用域。

要求

  • PHP 5.6+

设置

使用composer

composer require calcinai/xero-php

从1.x/OAuth 1a迁移

现在所有应用程序只有一个流程,这与传统的公共应用程序最相似。所有应用程序现在都需要OAuth 2授权流程,并在运行时授权特定的组织,而不是在应用程序创建期间创建证书。

由于现在只有一种类型的应用程序,您现在可以使用访问令牌和tenantId创建一个通用的XeroPHP\Application。从现在开始,所有代码都应该保持不变。

用法

在可以发出资源请求之前,应用程序必须经过授权。授权流程将为您提供访问令牌和刷新令牌。访问令牌可以用来检索应用程序有权查询的租户(Xero组织)的列表。然后,结合所需的tenantId,您可以实例化一个XeroPHP\Application来查询特定组织的API。

对于需要长期访问组织的应用程序,需要将刷新流程内置以捕获和刷新已过期的访问令牌。

授权码流程

用法与The League的OAuth客户端相同,使用\Calcinai\OAuth2\Client\Provider\Xero作为提供者。

session_start();
 
$provider = new \Calcinai\OAuth2\Client\Provider\Xero([
    'clientId'          => '{xero-client-id}',
    'clientSecret'      => '{xero-client-secret}',
    'redirectUri'       => 'https://example.com/callback-url',
]);
 
if (!isset($_GET['code'])) {

    // If we don't have an authorization code then get one
    // Additional scopes may be required depending on your application
    // additional common scopes are:
    // Add/edit contacts: accounting.contacts
    // Add/edit attachments accounting.attachments
    // Refresh tokens for non-interactive re-authorisation: offline_access
    // See all Xero Scopes https://developer.xero.com/documentation/guides/oauth2/scopes/
    $authUrl = $provider->getAuthorizationUrl([
        'scope' => 'openid email profile accounting.transactions'
    ]);

    $_SESSION['oauth2state'] = $provider->getState();
    header('Location: ' . $authUrl);
    exit;

// Check given state against previously stored one to mitigate CSRF attack
} elseif (empty($_GET['state']) || ($_GET['state'] !== $_SESSION['oauth2state'])) {

    unset($_SESSION['oauth2state']);
    exit('Invalid state');

} else {

    // Try to get an access token (using the authorization code grant)
    $token = $provider->getAccessToken('authorization_code', [
        'code' => $_GET['code']
    ]);


    //If you added the openid/profile scopes you can access the authorizing user's identity.
    $identity = $provider->getResourceOwner($token);
    print_r($identity);

    //Get the tenants that this user is authorized to access
    $tenants = $provider->getTenants($token);
    print_r($tenants);
}

然后,您可以将令牌存储起来并使用它向api发出对所需租户的请求。

作用域

OAuth作用域,表示您的应用程序可以访问Xero组织的哪些部分。完整的范围列表可以在这里找到。

$authUrl = $provider->getAuthorizationUrl([
   'scope' => 'bankfeeds accounting.transactions'
]);

刷新令牌

// Requires scope offline_access
$newAccessToken = $provider->getAccessToken('refresh_token', [
    'refresh_token' => $existingAccessToken->getRefreshToken()
]);

客户端凭据流程(自定义连接)

您可以通过创建"自定义连接"来利用客户端凭据授权类型。一旦您有了客户端凭据,用法与The League的OAuth客户端相同。您可以在配置自定义连接时选择您的范围。

$provider = new \Calcinai\OAuth2\Client\Provider\Xero([
    'clientId'          => '{xero-client-id}',
    'clientSecret'      => '{xero-client-secret}',
]);
$token = $provider->getAccessToken('client_credentials');
$tenants = $provider->getTenants($token);

与API交互

一旦您有了有效的访问令牌和tenantId,您就可以实例化一个XeroPHP\Application。以下所有示例均引用XeroPHP\Models\Accounting命名空间中的模型。此外,还有PayrollAUPayrollUSFilesAssets的模型。

有关更复杂的使用和嵌套/相关对象的示例,请参阅示例

实例化应用程序

创建XeroPHP实例(包括示例配置)

$xero = new \XeroPHP\Application($accessToken, $tenantId);

加载集合

$contacts = $xero->load(Contact::class)->execute();

foreach ($contacts as $contact) {
    print_r($contact);
}

带分页加载集合

加载单个页面的对象集合,并通过循环遍历它们(为什么?)

$contacts = $xero->load(Contact::class)->page(1)->execute();

foreach ($contacts as $contact) {
    print_r($contact);
}

使用WHERE过滤加载集合

搜索满足特定标准的对象

$xero->load(Invoice::class)
    ->where('Status', Invoice::INVOICE_STATUS_AUTHORISED)
    ->where('Type', Invoice::INVOICE_TYPE_ACCREC)
    ->where('Date', 'DateTime(2020,11,25)')
    ->execute();

$xero->load(Invoice::class)
    ->where('Date >= DateTime(2020,11,25)')
    ->where('Date < DateTime(2020,12,25)')
    ->execute();

加载特定资源

按GUID加载资源

$contact = $xero->loadByGUID(Contact::class, $guid);

创建新资源

使用设置器填充资源参数

$contact = new Contact($xero);

$contact->setName('Test Contact')
    ->setFirstName('Test')
    ->setLastName('Contact')
    ->setEmailAddress('test@example.com');

保存资源

// Requires scope accounting.contacts to add/edit contacts
$contact->save();

如果您已创建多个相同类型的对象,您可以通过传递一个数组到$xero->saveAll()来批量保存它们。

从v1.2.0+开始,在创建对象时可以直接注入Xero上下文,这会暴露->save()方法。这对于对象与其关系保持状态是必要的。

保存相关模型

如果您一次保存多个模型,默认情况下不会更新额外的模型属性。这意味着如果您正在保存带有新联系人的发票,联系人的ContactID不会更新。如果您想更新相关模型的属性,可以将布尔标志true传递给保存方法。

$invoice = $xero->loadByGUID(Invoice::class, '[GUID]');
$invoice->setContact($contact);
$xero->save($invoice, true);

附件

$attachments = $invoice->getAttachments();
foreach ($attachment as $attachment) {
    //Do something with them
    file_put_contents($attachment->getFileName(), $attachment->getContent());
}

//You can also upload attachemnts
// Requires scope accounting.attachments
$attachment = Attachment::createFromLocalFile('/path/to/image.jpg');
$invoice->addAttachment($attachment);

要设置附件上的IncludeOnline标志,将true作为->addAttachment()的第二个参数传递。

PDF文件

支持PDF导出的模型将继承一个->getPDF()方法,该方法返回PDF的原始内容。目前这仅限于发票和贷项通知。

单价精度

单价小数位精度unitdp参数)通过配置选项设置

$xero->setConfigOption('xero', 'unitdp', 3);

实践管理器

如果需要“practicemanager”范围,请使用以下语法查询模型

$clients = $xero->load(\XeroPHP\Models\PracticeManager\Client::class)
            ->setParameter('detailed', true)
            ->setParameter('modifiedsince', date('Y-m-d\TH:i:s', strtotime('- 1 week')))
            ->execute();

foreach ($clients as $client) {
    $name = $client->getName();
}

Webhooks

如果您正在接收来自Xero的webhooks,有一个Webhook类可以帮助处理请求和解析相关的事件列表。

// Configure the webhook signing key on the application
$application->setConfig(['webhook' => ['signing_key' => 'xyz123']]);
$webhook = new Webhook($application, $request->getContent());

/**
 * @return int
 */
$webhook->getFirstEventSequence();

/**
 * @return int
 */
$webhook->getLastEventSequence();

/**
 * @return \XeroPHP\Webhook\Event[]
 */
$webhook->getEvents();

查看:Webhooks文档

验证Webhooks

为确保webhooks来自Xero,您必须验证Xero提供的传入请求头。

if (! $webhook->validate($request->headers->get('x-xero-signature'))) {
    throw new Exception('This request did not come from Xero');
}

查看:签名文档

处理错误

您的Xero请求可能会导致错误,您可能需要处理。您可能会遇到如下错误

  • HTTP 400 Bad Request,例如发送无效数据,如格式错误的电子邮件地址。
  • HTTP 429 Too Many Requests,在短时间内快速调用API。
  • HTTP 503 Rate Limit Exceeded,在短时间内快速调用API。
  • HTTP 400 Bad Request,请求不存在的资源。

这只是几个例子,您应该阅读官方文档以了解更多有关可能错误的信息。

速率限制异常

Xero返回的标头值指示在达到API限制之前剩余的调用次数。https://developer.xero.com/documentation/guides/oauth2/limits/

每次请求后都会更新应用程序,您可以使用Application::getAppRateLimits()方法跟踪剩余请求数量。它返回一个包含以下键和关联整数值的数组。

'last-api-call' // The int timestamp of the last request made to the Xero API
'app-min-limit-remaining' // The number of requests remaining for the application as a whole in the current minute. The normal limit is 10,000.
'tenant-day-limit-remaining' // The number of requests remaining for the individual tenant by the day, limit is 5,000.
'tenant-min-limit-remaining' // The number of requests remaining for the individual tenant by the minute, limit is 60.

可以使用这些值来决定是否对额外的请求进行节流或将它们发送到某个消息队列。例如

    // If you know the number of API calls that you intend to make. 
    $myExpectedApiCalls = 50;

    // Before executing a statement, you could check the the rate limits.
    $tenantDailyLimitRemaining = $xero->getTenantDayLimitRemining();

    // If the expected number of API calls is higher than the number remaining for the tenant then do something.
    if($myExpectedApiCalls > tenantDailyLimitRemaining){
       // Send the calls to a queue for processing at another time
       // Or throttle the calls to suit your needs.
    }

如果应用程序超过速率限制,Xero将返回HTTP 429 Too Many Requests响应。默认情况下,此响应被捕获并作为RateLimitException抛出。

您可以通过使用Guzzle RetryMiddleware提供更优雅的方法来处理HTTP 429响应。您需要替换在实例化Application时创建的传输客户端。例如

// use GuzzleHttp\Client;
// use GuzzleHttp\HandlerStack;
// use GuzzleHttp\Middleware;
// use GuzzleHttp\RetryMiddleware;
// use Psr\Http\Message\RequestInterface;
// use Psr\Http\Message\ResponseInterface;

public function yourApplicationCreationMethod($accessToken, $tenantId): Application {

   // By default the contructor creates a Guzzle Client without any handlers. Pass a third argument 'false' to skip the general client constructor.
   $xero = new Application($accessToken, $tenantId, false);

   // Create a new handler stack
   $stack = HandlerStack::create();

   // Create the MiddleWare callable, in this case with a maximum limit of 5 retries.
   $stack->push($this->getRetryMiddleware(5));

   // Create a new Guzzle Client
   $transport = new Client([
       'headers' => [
           'User-Agent' => sprintf(Application::USER_AGENT_STRING, Helpers::getPackageVersion()),
           'Authorization' => sprintf('Bearer %s', $accessToken),
           'Xero-tenant-id' => $tenantId,
       ],
       'handler' => $stack
   ]);

   // Replace the default Client from the application constructor with our new Client using the RetryMiddleware
   $xero->setTransport($transport);

   return $xero

}

/**
 * Customise the RetryMiddeware to suit your needs. Perhaps creating log messages, or making decisions about when to retry or not.
 */
protected function getRetryMiddleware(int $maxRetries): callable
{
    $decider = function (
        int $retries,
        RequestInterface $request,
        ResponseInterface $response = null
    ) use (
        $maxRetries
    ): bool {
        return
            $retries < $maxRetries
            && null !== $response
            && \XeroPHP\Remote\Response::STATUS_TOO_MANY_REQUESTS === $response->getStatusCode();
    };

    $delay = function (int $retries, ResponseInterface $response): int {
        if (!$response->hasHeader('Retry-After')) {
            return RetryMiddleware::exponentialDelay($retries);
        }

        $retryAfter = $response->getHeaderLine('Retry-After');

        if (!is_numeric($retryAfter)) {
            $retryAfter = (new \DateTime($retryAfter))->getTimestamp() - time();
        }

        return (int)$retryAfter * 1000;
    };

    return Middleware::retry($decider, $delay);
}

抛出异常

本库将解析Xero返回的响应,并在遇到以下错误之一时抛出异常。下表显示了响应代码和对应的抛出异常

见: 响应代码和错误文档

异常处理

要捕获和处理这些异常,您可以将请求包裹在try / catch块中,并根据需要处理每个异常。

try {
    $xero->save($invoice);
} catch (NotFoundException $exception) {
    // handle not found error
} catch (RateLimitExceededException $exception) {
    // handle rate limit error
}

见: 异常处理指南