amcintosh/freshbooks

FreshBooks API 包装器

0.8.0 2024-04-21 19:50 UTC

This package is auto-updated.

Last update: 2024-09-21 20:43:49 UTC


README

Packagist Version Packagist PHP Version Support GitHub Workflow Status

一个 FreshBooks PHP SDK,允许您更轻松地利用 FreshBooks API。此库不是由 FreshBooks 直接维护的,并且欢迎 社区贡献

安装

通过 Composer 安装。

composer require amcintosh/freshbooks

需要一个 PSR-18 实现 客户端。如果您还没有兼容的客户端,您可以安装一个。

composer require amcintosh/freshbooks php-http/guzzle7-adapter

用法

查看完整的 文档 或查看一些 示例

此 SDK 使用 spryker/decimal-object 包。所有货币金额均表示为 Spryker\DecimalObject\Decimal,因此建议您参考 他们的文档

use Spryker\DecimalObject\Decimal;

$this->assertEquals(Decimal::create('41.94'), $invoice->amount->amount);

配置 API 客户端

您可以通过两种方式之一创建 API 客户端的实例

  • 通过提供您的应用程序的 OAuth2 clientIdclientSecret 并完成身份验证流程,完成后将返回一个访问令牌。
  • 或者如果您已经有了一个有效的访问令牌,您可以使用该令牌实例化客户端,但是没有应用程序 ID 和密钥,令牌刷新流程将无法正常工作。
use amcintosh\FreshBooks\FreshBooksClient;
use amcintosh\FreshBooks\FreshBooksClientConfig;

$conf = new FreshBooksClientConfig(
    clientSecret: 'your secret',
    redirectUri: 'https://some-redirect',
);

$freshBooksClient = new FreshBooksClient('your application id', $conf);

然后继续身份验证流程(见下文)。

或者

use amcintosh\FreshBooks\FreshBooksClient;
use amcintosh\FreshBooks\FreshBooksClientConfig;

$conf = new FreshBooksClientConfig(
    accessToken: 'a valid token',
);

$freshBooksClient = new FreshBooksClient('your application id', $conf);

身份验证流程

这是 OAuth2 身份验证流程的简要总结以及 FreshBooks API 客户端周围的相应方法。请参阅 FreshBooks API - 身份验证 文档。

首先,使用上述方法实例化您的客户端,包括 clientIdclientSecretredirectUri

为了获取访问令牌,用户必须首先授权您的应用程序。这可以通过将用户发送到 FreshBooks 授权页面来完成。一旦用户点击接受,他们将被重定向到您的 redirectUri,并带有访问授权码。可以通过调用 $freshBooksClient->getAuthRequestUri() 获取授权 URL。此方法还接受您希望用户为您的应用程序授权的权限列表。

$authUrl = $freshBooksClient->getAuthRequestUri(['user:profile:read', 'user:clients:read']);

一旦用户被重定向到您的 redirectUri 并且您已经获得了访问授权码,您可以用该代码交换有效的访问令牌。

$authResults = $freshBooksClient->getAccessToken($accessGrantCode);

此调用同时设置了您客户端 FreshBooksClientConfig 实例上的 accessTokenrefreshTokentokenExpiresAt 字段,并返回这些值。

echo $authResults->accessToken;  // Your token
echo $authResults->refreshToken; // Your refresh token
echo $authResults->createdAt;    // When the token was created (as a DateTime)
echo $authResults->expiresIn;    // How long the token is valid for (in seconds)
echo $authResults->getExpiresAt; // When the token expires (as a DateTime)

echo $freshBooksClient->getConfig()->accessToken;    // Your token
echo $freshBooksClient->getConfig()->refreshToken;   // Your refresh token
echo $freshBooksClient->getConfig()->tokenExpiresAt; // When the token expires (as a DateTime)

当令牌过期时,可以使用 FreshBooksClient 中的 refreshToken 值进行刷新

$authResults = $freshBooksClient->refreshAccessToken();
echo $authResults->accessToken;  // Your new token

或您可以直接传递刷新令牌

$authResults = $freshBooksClient->refreshAccessToken($storedRefreshToken);
echo $authResults->accessToken;  // Your new token

当前用户

FreshBooks 用户通过其电子邮件在整个产品中被唯一标识。一个用户可以在不同的业务中以不同的方式执行操作,而身份模型就是跟踪它的方法。每个唯一的用户都有一个身份,每个身份都有定义其权限的业务成员资格。

请参阅FreshBooks API - 业务、角色和身份FreshBooks API - 身份模型

可以通过以下方式访问当前用户:

$identity = $freshBooksClient->currentUser()
echo $identity.email // prints the current user's email

// Print name and role of each business the user is a member of
foreach ($identity.businessMemberships as $businessMembership) {
    echo $businessMembership->business.name
    echo $businessMembership->role; // eg. owner
}

发起API调用

客户端中的每个资源都提供了针对getlistcreateupdatedelete操作的调用。请注意,某些API资源的作用域限定在FreshBooks的accountId上,而其他资源的作用域限定在businessId上。一般来说,这些资源分为会计资源和项目/时间跟踪资源,但这并非绝对。

$client = $freshBooksClient->clients()->get($accountId, $clientId);
$project = $freshBooksClient->projects()->get($businessId, $projectId);

获取和列表

返回单个资源的API调用会返回一个包含返回数据的DataTransferObject,可以通过属性访问返回数据。

$client = $freshBooksClient->clients()->get($accountId, $clientId);

echo $client->organization; // 'FreshBooks'
$client->only('organization')->toArray(); // ['organization' => 'FreshBooks'];

visState数字对应各种状态。请参阅FreshBooks API - 活跃和已删除对象以获取详细信息。

use amcintosh\FreshBooks\Model\VisState;

echo $client->visState; // '0'
echo $client->visState == VisState::ACTIVE ? 'Is Active' : 'Not Active'; // 'Is Active'

返回资源列表的API调用会返回一个包含资源数组的DataTransferObject

$clients = $freshBooksClient->clients()->list($accountId);

echo $clients->clients[0]->organization; // 'FreshBooks'

foreach ($clients->clients as $client) {
    echo $client->organization;
}

创建、更新和删除

创建和更新API调用可以接受一个DataModel对象,或者一个资源数据的数组。成功的调用将返回一个与get调用相同的DataTransferObject对象。

注意:当使用数据数组时,需要指定与FreshBooks API中存在的字段相对应的字段。有些API字段在数据模型中被转换成了更直观的名称。例如fname = firstName,或bus_phone = businessPhone

创建

$clientData = new Client();
$clientData->organization = 'FreshBooks';
$clientData->firstName = 'Gordon';
$clientData->businessPhone = '416-444-4445';

$newClient = $freshBooksClient->clients()->create($accountId, model: $clientData);

echo $newClient->organization;  // 'FreshBooks'
echo $newClient->firstName;     // 'Gordon'
echo $newClient->businessPhone; // '416-444-4445'

$clientData = array(
    'organization' => 'FreshBooks',
    'fname' => 'Gordon',
    'bus_phone' => '416-444-4445'
);

$newClient = $freshBooksClient->clients()->create($accountId, data: $clientData);

echo $newClient->organization;  // 'FreshBooks'
echo $newClient->firstName;     // 'Gordon'
echo $newClient->businessPhone; // '416-444-4445'

更新

$clientData->organization = 'New Org';
$clientData->firstName = 'Gord';

$newClient = $freshBooksClient->clients()->update($accountId, $clientData->id, model: $clientData);

echo $newClient->organization; // 'New Org'
echo $newClient->firstName;    // 'Gord'

$clientData = array(
    'organization' => 'Really New Org',
    'fname' => 'Gord',
);

$newClient = $freshBooksClient->clients()->update($accountId, $clientId, data: $clientData);

echo $newClient->organization; // 'Really New Org'
echo $newClient->firstName;    // 'Gord'

删除

$client = $freshBooksClient->clients()->delete($accountId, $clientId);

echo $client->visState; // '1'
echo $client->visState == VisState::ACTIVE ? 'Is Active' : 'Not Active'; // 'Not Active'

错误处理

当FreshBooks API返回非2xx响应时,调用将被包装在FreshBooksException中。这个异常类包含错误消息、HTTP响应代码、如果存在则包含FreshBooks特定的错误编号以及HTTP响应体。

示例

use amcintosh\FreshBooks\Exception\FreshBooksException;

try {
    $client = $freshBooksClient->clients()->get($accountId, 134);
} catch (FreshBooksException $e) {
    echo $e->getMessage();     // 'Client not found'
    echo $e->getCode();        // 404
    echo $e->getErrorCode();   // 1012
    echo $e->getRawResponse(); // '{"response": {"errors": [{"errno": 1012,
                               // "field": "userid", "message": "Client not found.",
                               // "object": "client", "value": "134"}]}}'
}

TODO:并非所有资源都有完整的CRUD方法可用。例如,费用类别有listget调用,但不能删除。如果您尝试调用不存在的方法,SDK将引发FreshBooksNotImplementedError异常,但您可能不需要在开发之外考虑这一点。

分页、过滤和包含

list调用接受一个列表构建器对象,可用于分页、过滤以及将可选数据包含在响应中。请参阅FreshBooks API - 参数文档。

分页

分页结果包含在list响应中

$clients = $freshBooksClient->clients()->list($accountId);

echo $clients->pages()->page    // 1
echo $clients->pages()->pages   // 1
echo $clients->pages()->perPage // 30
echo $clients->pages()->total   // 6

要执行分页调用,首先创建一个可以传递给list方法的PaginateBuilder

use amcintosh\FreshBooks\Builder\PaginateBuilder;

$paginator = new PaginateBuilder(2, 4);

$clients = $freshBooksClient->clients()->list($accountId, builders: [$paginator]);

echo $clients->pages()->page    // 2
echo $clients->pages()->pages   // 2
echo $clients->pages()->perPage // 4
echo $clients->pages()->total   // 6

PaginateBuilder有可链式的方法pageperPage来设置值。

$paginator = new PaginateBuilder(1, 3);
echo $paginator->page;    // 1
echo $paginator->perPage; // 3

$paginator->page(2)->perPage(4);
echo $paginator->page;    // 2
echo $paginator->perPage; // 4
过滤

要过滤由list方法调用返回的结果,构建一个FilterBuilder并将其传递给list方法的构建器列表。

use amcintosh\FreshBooks\Builder\FilterBuilder;

$filters = new FilterBuilder();
$filters->equals('userid', 123);

$clients = $freshBooksClient->clients()->list($accountId, builders: [$filters]);

可以使用以下方法构建过滤器:equalsinListlikebetweenbooleandatetime,它们可以连在一起。

请参阅FreshBooks API - 活跃和已删除对象以获取有关过滤活跃、存档和已删除资源的详细信息。

$filters = new FilterBuilder();
$filters->inList('clientids', [123, 456]);
// Creates `&search[clientids][]=123&search[clientids][]=456`

$filters = new FilterBuilder();
$filters->like('email_like', '@freshbooks.com');
// Creates `&search[email_like]=@freshbooks.com`

$filters = new FilterBuilder();
$filters->between('amount', 1, 10);
// Creates `&search[amount_min]=1&search[amount_max]=10`

$filters = new FilterBuilder();
$filters->between('amount', min=15); // For just minimum
// Creates `&search[amount_min]=15`

$filters = new FilterBuilder();
$filters->between('amount_min', 15); // Alternatively
// Creates `&search[amount_min]=15`

$filters = new FilterBuilder();
$filters->between("start_date", min: new DateTime('2020-10-17'))
// Creates `&search[start_date]=2020-10-17`

$filters = new FilterBuilder();
$filters->boolean('complete', false); // Boolean filters are mostly used on Project-like resources
// Creates `&complete=false`

$filters = new FilterBuilder();
$filters->equals('vis_state', VisState::ACTIVE)->between('updated', new DateTime('2020-10-17'), new DateTime('2020-11-21'));
// Chaining filters
// Creates `&search[vis_state]=0&search[updated_min]=2020-10-17&search[updated_max]=2020-11-21`
包含

要包含额外的关联、子资源或数据在响应中,可以构建一个IncludesBuilder

use amcintosh\FreshBooks\Builder\IncludesBuilder;

$includes = new IncludesBuilder();
$includes->include("outstanding_balance");

然后可以将它传递给listget调用

$clients = $freshBooksClient->clients()->list($accountId, builders: [$includes]);
echo $clients->clients[0]->outstanding_balance->amount; // '100.00'
echo $clients->clients[0]->outstanding_balance->code; // 'USD'

$client = $freshBooksClient->clients()->get($accountId, $clientId, $includes);
echo $client->outstanding_balance->amount; // '100.00'
echo $client->outstanding_balance->code; // 'USD'

包含也可以传递给createupdate调用,以便在更新资源的响应中包含数据

$clientData = array(
    'email' => 'john.doe@abcorp.com'
);

$newClient = $freshBooksClient->clients()->create($accountId, data: $clientData);

echo $client->outstanding_balance->amount; // null, new client has no balance
排序

要对列表的结果按支持的字段进行排序,可以使用SortBuilder(有关该资源的文档中说明了如何使用)。

use amcintosh\FreshBooks\Builder\SortBuilder;

$sort = new SortBuilder();
$sort->ascending("invoice_date");

$invoices = $freshBooksClient->invoices()->list($accountId, builders: [$sort]);

按发票日期升序排序,或

use amcintosh\FreshBooks\Builder\SortBuilder;

$sort = new SortBuilder();
$sort->descending("invoice_date");

$invoices = $freshBooksClient->invoices()->list($accountId, builders: [$sort]);

按降序排序。

开发

测试

运行所有测试

make test

运行特定测试

 ./vendor/bin/phpunit --filter testCreateValidationError

文档

您可以通过以下方式生成文档:

make generate-docs