Skip to content

Commit

Permalink
22: Refactors to use middleware and deprecate InvalidOptionsException
Browse files Browse the repository at this point in the history
  • Loading branch information
mradcliffe committed Jun 14, 2024
1 parent 5601be8 commit 3dc8bfe
Show file tree
Hide file tree
Showing 6 changed files with 73 additions and 52 deletions.
12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,15 @@ $provider = new \Radcliffe\Xero\XeroProvider([
$url = $provider->getAuthorizationUrl();
```

### Create a guzzle client from an authorization code (see above)
### Create with a guzzle client from an authorization code (see above)

```php
$client = \Radcliffe\Xero\XeroClient::createFromToken('my consumer key', 'my consumer secret', $code, 'authorization_code', 'accounting');
// Store the access token for the next 30 minutes or so if making additional requests.
$tokens = $client->getRefreshedToken();
```

### Create a guzzle client with an access token
### Create with a guzzle client with an access token

```php
$client = \Radcliffe\Xero\XeroClient::createFromToken(
Expand All @@ -60,7 +60,7 @@ $client = \Radcliffe\Xero\XeroClient::createFromToken(
);
```

### Create a guzzle client with a refresh token
### Create with a guzzle client with a refresh token

Access tokens expire after 30 minutes so you can create a new client with a stored refresh token too.

Expand Down Expand Up @@ -97,6 +97,12 @@ try {

```

### Error handling

If the configured client does not have a valid Xero API URL or if an auth_token is not provided, then XeroRequestException is thrown as part of the Guzzle request.

Previously XeroClient would throw an exception on instantiation, but this is no longer the case. If the initialize method is used directly, XeroClient will probably fail for other reasons.

### Use with a legacy OAuth1 application

Please see the 0.2 branch and versions < 0.3.0.
Expand Down
7 changes: 7 additions & 0 deletions src/Exception/InvalidOptionsException.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

namespace Radcliffe\Xero\Exception;

/**
* An exception to throw when the client has invalid options.
*
* @deprecated in 0.5.0 and removed in 0.6.0. Use a guzzle request exception
* instead.
* @see \Radcliffe\Xero\Exception\XeroRequestException
*/
class InvalidOptionsException extends \Exception
{
}
9 changes: 9 additions & 0 deletions src/Exception/XeroRequestException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

namespace Radcliffe\Xero\Exception;

use GuzzleHttp\Exception\RequestException;

class XeroRequestException extends RequestException
{
}
38 changes: 19 additions & 19 deletions src/XeroClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;
use League\OAuth2\Client\Token\AccessTokenInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\UriInterface;
use Radcliffe\Xero\Exception\InvalidOptionsException;
use Radcliffe\Xero\Exception\XeroRequestException;

class XeroClient implements XeroClientInterface
{
Expand Down Expand Up @@ -90,22 +92,10 @@ public function getConnections(): array
*
* @throws \Radcliffe\Xero\Exception\InvalidOptionsException
*/
public static function createFromConfig(array $config): static
public static function createFromConfig(array $config, array $options = []): static
{
if (!isset($config['base_uri']) ||
!$config['base_uri'] ||
!in_array($config['base_uri'], self::getValidUrls())) {
throw new InvalidOptionsException('API URL is not valid.');
}

// Use OAuth2 work flow.
if (!isset($config['auth_token'])) {
throw new InvalidOptionsException('Missing required parameter auth_token');
}
$options['headers']['Authorization'] = 'Bearer ' . $config['auth_token'];

if (isset($config['tenant'])) {
$options['headers']['xero-tenant-id'] = $config['tenant'];
if (isset($options['tenant'])) {
$config['headers']['xero-tenant-id'] = $options['tenant'];
}

if (isset($config['handler']) && is_a($config['handler'], '\GuzzleHttp\HandlerStack')) {
Expand All @@ -114,6 +104,18 @@ public static function createFromConfig(array $config): static
$stack = HandlerStack::create();
}

$stack->push(Middleware::mapRequest(function (RequestInterface $request) use ($options) {
$validUrls = array_filter(self::getValidUrls(), fn ($url) => str_starts_with($request->getUri(), $url));
if (empty($validUrls)) {
throw new XeroRequestException('API URL is not valid', $request);
}

if (!isset($options['auth_token'])) {
throw new XeroRequestException('Missing required parameter auth_token', $request);
}
return $request->withHeader('Authorization', 'Bearer ' . $options['auth_token']);
}));

$client = new Client($config + [
'handler' => $stack,
]);
Expand Down Expand Up @@ -160,9 +162,7 @@ public static function createFromToken(
}

// Create a new static instance.
$instance = self::createFromConfig($options + [
'auth_token' => $token,
]);
$instance = self::createFromConfig($options, ['auth_token' => $token]);

$instance->tenantIds = $instance->getConnections();

Expand Down
6 changes: 5 additions & 1 deletion src/XeroClientInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,12 +94,16 @@ public static function createFromToken(
*
* @param array<string,mixed> $config
* The guzzle options.
* @param array<string,mixed> $options
* The XeroClient Options:
* - auth_token: the access or refresh token.
* - tenant: an optional tenant id.
*
* @return static
*
* @see \GuzzleHttp\Client::__construct().
*/
public static function createFromConfig(array $config): static;
public static function createFromConfig(array $config, array $options = []): static;

/**
* Makes a request to the Xero API.
Expand Down
53 changes: 24 additions & 29 deletions tests/src/XeroClientTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Response;
use Radcliffe\Xero\Exception\InvalidOptionsException;
use Radcliffe\Xero\XeroClient;

/**
Expand All @@ -16,28 +15,14 @@
class XeroClientTest extends XeroClientTestBase
{

/**
* @param array<string,mixed> $options
* Invalid options to pass to the consructor.
*
* @dataProvider invalidOptionsExceptionProvider
*/
public function testInvalidOptionsException(array $options): void
{
$this->expectException(InvalidOptionsException::class);
$client = XeroClient::createFromConfig($options);

$this->assertNull($client);
}

/**
* Asserts public application instantiation.
*/
public function testPublicApplication(): void
{
$options = $this->createConfiguration();
$client = XeroClient::createFromConfig($options + [
'auth_token' => $this->createRandomString(),
'auth_token' => self::createRandomString(),
]);
$this->assertNotNull($client);
}
Expand All @@ -60,11 +45,12 @@ public function testGet(int $statusCode, array $headers, string $body): void
]
);
$options['handler'] = new HandlerStack($mock);
$options['auth_token'] = $this->createRandomString();

$client = XeroClient::createFromConfig($options);
$client = XeroClient::createFromConfig($options, [
'auth_token' => self::createRandomString(),
]);

$response = $client->get('/BrandingThemes');
$response = $client->request('GET', 'BrandingThemes');
$this->assertEquals(200, $response->getStatusCode());
}

Expand All @@ -90,23 +76,32 @@ public function testGetConnections(int $statusCode, array $response, int $expect
]);
$client = XeroClient::createFromConfig([
'base_uri' => 'https://api.xero.com/connections',
'scheme' => 'oauth2',
'auth_token' => $this->createRandomString(),
'handler' => new HandlerStack($mock),
]);
], ['auth_token' => self::createRandomString()]);

$connections = $client->getConnections();
$this->assertEquals($expectedCount, count($connections));
}

/**
* @return array<int,mixed>
*/
public static function invalidOptionsExceptionProvider(): array
public function testWithInvalidUrl(): void
{
return [
[[]],
];
$this->expectException('\Radcliffe\Xero\Exception\XeroRequestException');

$options = $this->createConfiguration();
$options['base_uri'] = 'https://example.com/';
$client = XeroClient::createFromConfig($options, [
'auth_token' => self::createRandomString(),
]);
$client->request('GET', 'Accounts');
}

public function testWithoutAuthToken(): void
{
$this->expectException('\Radcliffe\Xero\Exception\XeroRequestException');

$options = $this->createConfiguration();
$client = XeroClient::createFromConfig($options, []);
$client->request('GET', 'Accounts');
}

/**
Expand Down

0 comments on commit 3dc8bfe

Please sign in to comment.