amcintosh / freshbooks
FreshBooks API 包装器
Requires
- php: >=8.0 <8.4
- php-http/client-common: ^2.5
- php-http/discovery: ^1.14
- php-http/multipart-stream-builder: ^1.3
- psr/http-client: ^1.0
- psr/http-factory: ^1.0
- spatie/data-transfer-object: ^3.8
- spryker/decimal-object: ^1.0
Requires (Dev)
- mockery/mockery: 1.5
- php-http/guzzle7-adapter: 1.x-dev
- php-http/mock-client: 1.x-dev
- phpunit/phpunit: 9.6.x-dev
- quazardous/php-bump-version: 1.0.x-dev
- squizlabs/php_codesniffer: 4.0.x-dev
README
一个 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
clientId
和clientSecret
并完成身份验证流程,完成后将返回一个访问令牌。 - 或者如果您已经有了一个有效的访问令牌,您可以使用该令牌实例化客户端,但是没有应用程序 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 - 身份验证 文档。
首先,使用上述方法实例化您的客户端,包括 clientId
、clientSecret
和 redirectUri
。
为了获取访问令牌,用户必须首先授权您的应用程序。这可以通过将用户发送到 FreshBooks 授权页面来完成。一旦用户点击接受,他们将被重定向到您的 redirectUri
,并带有访问授权码。可以通过调用 $freshBooksClient->getAuthRequestUri()
获取授权 URL。此方法还接受您希望用户为您的应用程序授权的权限列表。
$authUrl = $freshBooksClient->getAuthRequestUri(['user:profile:read', 'user:clients:read']);
一旦用户被重定向到您的 redirectUri
并且您已经获得了访问授权码,您可以用该代码交换有效的访问令牌。
$authResults = $freshBooksClient->getAccessToken($accessGrantCode);
此调用同时设置了您客户端 FreshBooksClientConfig 实例上的 accessToken
、refreshToken
和 tokenExpiresAt
字段,并返回这些值。
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调用
客户端中的每个资源都提供了针对get
、list
、create
、update
和delete
操作的调用。请注意,某些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方法可用。例如,费用类别有list
和get
调用,但不能删除。如果您尝试调用不存在的方法,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
有可链式的方法page
和perPage
来设置值。
$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]);
可以使用以下方法构建过滤器:equals
、inList
、like
、between
、boolean
和datetime
,它们可以连在一起。
请参阅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");
然后可以将它传递给list
或get
调用
$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'
包含也可以传递给create
和update
调用,以便在更新资源的响应中包含数据
$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