Files
laravel-quickbooks/src/Client.php
T
Joel Male 32266f6dd5 fix: revert JSON serialization workaround, upgrade SDK to 6.3.1 instead
v2.0.1's useJson() workaround was wrong: the JSON request serializer
silently drops nested single-object fields (e.g. CustomerRef, which
QuickBooks requires), and the JSON response deserializer never returns
a typed entity, so $result->Id is always null downstream. That breaks
every dsCall() site, worse than the bug it was meant to fix.

The actual root cause was that quickbooks/v3-php-sdk 5.4.7 predates
QuickBooks Online's new invoice AllowOnlinePayPalPayment field, so its
XML deserializer throws on responses that include it. SDK 6.3.1
declares the field, so upgrading resolves the original error while
keeping the fully-supported XML path (and closes out the SDK bump
Dependabot already had queued in #3).

Verified: deserializing a response containing AllowOnlinePayPalPayment
throws RuntimeException on 5.4.7 and returns a correctly typed
IPPInvoice (with Id populated) on 6.3.1.
2026-07-27 07:15:59 +03:00

260 lines
7.0 KiB
PHP

<?php
namespace Wearepixel\QuickBooks;
use Exception;
use QuickBooksOnline\API\DataService\DataService;
use QuickBooksOnline\API\Exception\SdkException;
use QuickBooksOnline\API\Exception\ServiceException;
use QuickBooksOnline\API\ReportService\ReportService;
/**
* Class Client
*/
class Client
{
/**
* The configs to set up a DataService
*
* @var array
*/
protected $configs;
/**
* The DataService instance
*
* @var DataService
*/
protected $data_service;
/**
* The ReportService instance
*
* @var ReportService
*/
protected $report_service;
/**
* The Token instance
*
* @var Token
*/
protected $token;
/**
* Client constructor.
*/
public function __construct(array $configs, Token $token)
{
$this->configs = $configs;
$this->setToken($token);
}
/**
* Build URI to request authorization
*
* @throws SdkException
* @throws ServiceException
*/
public function authorizationUri(): string
{
return $this->getDataService()
->getOAuth2LoginHelper()
->getAuthorizationCodeURL();
}
/**
* Configure the logging per config/quickbooks.php
*/
public function configureLogging(): DataService
{
// In case any of the keys are not in the configs, just disable logging
try {
if (
$this->configs['logging']['enabled'] &&
dir($this->configs['logging']['location'])
) {
$this->data_service->setLogLocation($this->configs['logging']['location']);
return $this->data_service->enableLog();
}
} catch (Exception $e) {
// TODO: Figure out what to do with this exception
}
return $this->data_service->disableLog();
}
/**
* Delete the token
*
* @throws Exception
*/
public function deleteToken(): self
{
$this->setToken($this->token->remove());
return $this;
}
/**
* Convert code to an access token
*
* Upon the user allowing access to their account, there is a code sent to
* over that needs to be converted to an OAuth token.
*
* @throws SdkException
* @throws ServiceException
*/
public function exchangeCodeForToken(string $code, int $realm_id): self
{
$oauth_token = $this->getDataService()
->getOAuth2LoginHelper()
->exchangeAuthorizationCodeForToken($code, $realm_id);
$this->getDataService()->updateOAuth2Token($oauth_token);
$this->token->parseOauthToken($oauth_token)->save();
return $this;
}
/**
* Getter for the DataService
*
* Makes sure that it is setup & ready to be used.
*
* @throws SdkException
* @throws ServiceException
*/
public function getDataService(): DataService
{
if (! $this->hasValidAccessToken() || ! isset($this->data_service)) {
$this->data_service = $this->makeDataService();
$this->configureLogging();
}
return $this->data_service;
}
/**
* Getter for the ReportService
*
* Makes sure that it is setup & ready to be used.
*
* @throws SdkException
* @throws ServiceException
*/
public function getReportService(): ReportService
{
if (! $this->hasValidAccessToken() || ! isset($this->report_service)) {
$this->report_service = new ReportService($this->getDataService()->getServiceContext());
}
return $this->report_service;
}
/**
* Check to see if the token has a valid access token
*/
public function hasValidAccessToken(): bool
{
return $this->token->hasValidAccessToken;
}
/**
* Check to see if the token has a valid refresh token
*/
public function hasValidRefreshToken(): bool
{
return $this->token->hasValidRefreshToken;
}
/**
* Factory to make DataService
*
* There are 3 use cases for making a DataService....
*
* 1) Have valid access token, so ready to be used
* 2) Have valid refresh token, so renew access token & then use
* 3) No existing token, so need to link account
*
* @throws SdkException
* @throws ServiceException
*/
protected function makeDataService(): DataService
{
// Associative array to use to filter out only the needed config keys when using existing token
$existing_keys = [
'auth_mode' => null,
'baseUrl' => null,
'ClientID' => null,
'ClientSecret' => null,
];
// Have good access & refresh, so allow app to run
if ($this->hasValidAccessToken()) {
// Pull in the configs from the token into needed keys from the configs
return DataService::Configure(
array_merge(array_intersect_key($this->parseDataConfigs(), $existing_keys), [
'accessTokenKey' => $this->token->access_token,
'QBORealmID' => $this->token->realm_id,
'refreshTokenKey' => $this->token->refresh_token,
]),
);
}
// Have refresh, so update access & allow app to run
if ($this->hasValidRefreshToken()) {
// Pull in the configs from the token into needed keys from the configs
$data_service = DataService::Configure(
array_merge(array_intersect_key($this->parseDataConfigs(), $existing_keys), [
'QBORealmID' => $this->token->realm_id,
'refreshTokenKey' => $this->token->refresh_token,
]),
);
$oauth_token = $data_service->getOAuth2LoginHelper()->refreshToken();
$data_service->updateOAuth2Token($oauth_token);
$this->token->parseOauthToken($oauth_token)->save();
return $data_service;
}
// Create new...
return DataService::Configure($this->parseDataConfigs());
}
/**
* QuickBooks is not consistent on their naming of variables, so map them
*/
protected function parseDataConfigs(): array
{
return [
'auth_mode' => $this->configs['data_service']['auth_mode'],
'baseUrl' => $this->configs['data_service']['base_url'],
'ClientID' => $this->configs['data_service']['client_id'],
'ClientSecret' => $this->configs['data_service']['client_secret'],
'RedirectURI' => route('quickbooks.token'),
'scope' => $this->configs['data_service']['scope'],
];
}
/**
* Allow setting a token to switch "user"
*/
public function setToken(Token $token): self
{
$this->token = $token;
// The DataService is tied to a specific token, so remove it when using a new one
unset($this->data_service);
return $this;
}
}