diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4be6042..84e2aab 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,11 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).
## [Unreleased]
+### Added
+- Support for API token authentication. Username and password can still be used
+ as a fallback.
+### Changed
+- Deprecate `getConfig()` method. Packages can maintain their config internally.
## [6.0.0] - 2025-02-14
### Added
@@ -14,6 +19,13 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- **BC break**: Removed support for PHP versions <= v8.0 as they are no longer
[actively supported](https://php.net/supported-versions.php) by the PHP project.
+## [5.2.0] - 2025-06-25
+### Added
+- Support for API token authentication. Username and password can still be used
+ as a fallback.
+### Changed
+- Deprecate `getConfig()` method. Packages can maintain their config internally.
+
## [5.1.1] - 2021-08-10
### Fixed
- Clean-up temporary local file for any errors while trying to download a file.
diff --git a/README.md b/README.md
index 13cce08..485f8af 100644
--- a/README.md
+++ b/README.md
@@ -36,8 +36,7 @@ $ composer require maxemail/api-php
```php
// Instantiate Client:
$config = [
- 'username' => 'api@user.com',
- 'password' => 'apipass'
+ 'token' => 'apitoken',
];
$api = new \Maxemail\Api\Client($config);
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 07e8dd3..c313011 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -12,8 +12,7 @@
-
-
+
diff --git a/src/Client.php b/src/Client.php
index de8f5e7..22091d3 100644
--- a/src/Client.php
+++ b/src/Client.php
@@ -5,6 +5,7 @@
namespace Maxemail\Api;
use GuzzleHttp\Client as GuzzleClient;
+use GuzzleHttp\ClientInterface as GuzzleClientInterface;
use GuzzleHttp\HandlerStack;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerInterface;
@@ -63,6 +64,8 @@ class Client implements LoggerAwareInterface
private string $uri = 'https://mxm.xtremepush.com/';
+ private readonly string $token;
+
private readonly string $username;
private readonly string $password;
@@ -76,26 +79,37 @@ class Client implements LoggerAwareInterface
private LoggerInterface $logger;
- private GuzzleClient $httpClient;
+ private GuzzleClientInterface $httpClient;
private bool $debugLoggingEnabled = false;
+ /**
+ * @var \Closure(array):GuzzleClientInterface
+ */
+ private \Closure $httpClientFactory;
+
/**
* @param array{
- * username: string, // Required
- * password: string, // Required
+ * token: string, // Required, or username & password
+ * username: string, // Required, if no token
+ * password: string, // Required, if no token
* uri: string, // Optional. Default https://mxm.xtremepush.com/
* debugLogging: bool, // Optional. Enable logging of request/response. Default false
* } $config
*/
public function __construct(array $config)
{
- // Must have user/pass
- if (!isset($config['username']) || !isset($config['password'])) {
- throw new Exception\InvalidArgumentException('API config requires username & password');
+ // Must have API token
+ if (!isset($config['token'])) {
+ // Must have user/pass
+ if (!isset($config['username']) || !isset($config['password'])) {
+ throw new Exception\InvalidArgumentException('API config requires token OR username & password');
+ }
+ $this->username = $config['username'];
+ $this->password = $config['password'];
+ } else {
+ $this->token = $config['token'];
}
- $this->username = $config['username'];
- $this->password = $config['password'];
if (isset($config['uri'])) {
$parsed = parse_url($config['uri']);
@@ -127,7 +141,7 @@ private function getInstance(string $serviceName): Service
return $this->services[$serviceName];
}
- private function getClient(): GuzzleClient
+ private function getClient(): GuzzleClientInterface
{
if (!isset($this->httpClient)) {
$stack = HandlerStack::create();
@@ -136,19 +150,31 @@ private function getClient(): GuzzleClient
if ($this->debugLoggingEnabled) {
Middleware::addLogging($stack, $this->getLogger());
}
- $this->httpClient = new GuzzleClient([
+
+ $clientConfig = [
'base_uri' => $this->uri . 'api/json/',
- 'auth' => [
- $this->username,
- $this->password,
- ],
'headers' => [
'User-Agent' => 'MxmApiClient/' . self::VERSION . ' PHP/' . PHP_VERSION,
'Content-Type' => 'application/x-www-form-urlencoded',
'Accept' => 'application/json',
],
'handler' => $stack,
- ]);
+ ];
+
+ if (isset($this->token)) {
+ $clientConfig['headers']['Authorization'] = 'Bearer ' . $this->token;
+ } else {
+ $clientConfig['auth'] = [
+ $this->username,
+ $this->password,
+ ];
+ }
+
+ if (!isset($this->httpClientFactory)) {
+ $this->httpClient = new GuzzleClient($clientConfig);
+ } else {
+ $this->httpClient = ($this->httpClientFactory)($clientConfig);
+ }
}
return $this->httpClient;
@@ -157,6 +183,7 @@ private function getClient(): GuzzleClient
/**
* Get API connection config
*
+ * @deprecated v5.2 No replacement; packages can maintain their own config; to be removed in v7.
* @return array{
* uri: string,
* username: string,
@@ -167,8 +194,8 @@ public function getConfig(): array
{
return [
'uri' => $this->uri,
- 'username' => $this->username,
- 'password' => $this->password,
+ 'username' => $this->username ?? null,
+ 'password' => $this->password ?? null,
];
}
@@ -194,4 +221,12 @@ public function getLogger(): LoggerInterface
return $this->logger;
}
+
+ /**
+ * @internal This method is not part of the BC promise. Used for DI for unit tests only.
+ */
+ public function setHttpClientFactory(\Closure $httpClientFactory): void
+ {
+ $this->httpClientFactory = $httpClientFactory;
+ }
}
diff --git a/src/Helper.php b/src/Helper.php
index ae10027..e99dca8 100644
--- a/src/Helper.php
+++ b/src/Helper.php
@@ -4,7 +4,7 @@
namespace Maxemail\Api;
-use GuzzleHttp\Client as GuzzleClient;
+use GuzzleHttp\ClientInterface as GuzzleClient;
use Psr\Log\LogLevel;
/**
diff --git a/src/Service.php b/src/Service.php
index 09aaf07..fc4de1c 100644
--- a/src/Service.php
+++ b/src/Service.php
@@ -4,7 +4,7 @@
namespace Maxemail\Api;
-use GuzzleHttp\Client as GuzzleClient;
+use GuzzleHttp\ClientInterface as GuzzleClient;
/**
* Maxemail API Client
diff --git a/tests/ClientTest.php b/tests/ClientTest.php
index e1eb4d8..4c3dbe0 100644
--- a/tests/ClientTest.php
+++ b/tests/ClientTest.php
@@ -4,6 +4,7 @@
namespace Maxemail\Api;
+use GuzzleHttp\ClientInterface as GuzzleClient;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
@@ -18,40 +19,75 @@ class ClientTest extends TestCase
{
private array $testConfig = [
'uri' => 'https://maxemail.example.com/',
- 'username' => 'api@user.com',
- 'password' => 'apipass',
+ 'token' => 'apitoken',
];
public function testConfigValid(): void
{
$api = new Client($this->testConfig);
- static::assertSame($this->testConfig, $api->getConfig());
+ $factory = function (array $actual): GuzzleClient {
+ $expectedUri = $this->testConfig['uri'] . 'api/json/';
+ static::assertSame($expectedUri, $actual['base_uri']);
+
+ $expectedHeaders = [
+ 'User-Agent' => 'MxmApiClient/' . Client::VERSION . ' PHP/' . PHP_VERSION,
+ 'Content-Type' => 'application/x-www-form-urlencoded',
+ 'Accept' => 'application/json',
+ 'Authorization' => 'Bearer ' . $this->testConfig['token'],
+ ];
+ static::assertSame($expectedHeaders, $actual['headers']);
+
+ return $this->createMock(GuzzleClient::class);
+ };
+
+ $api->setHttpClientFactory($factory);
+
+ // Get a service, to trigger the HTTP Client factory
+ $api->folder;
}
public function testConfigDefaultHost(): void
{
$config = [
- 'username' => 'api@user.com',
- 'password' => 'apipass',
+ 'token' => 'apitoken',
];
$api = new Client($config);
- static::assertSame('https://mxm.xtremepush.com/', $api->getConfig()['uri']);
+ $factory = function (array $actual) use ($config): GuzzleClient {
+ $expectedUri = 'https://mxm.xtremepush.com/api/json/';
+ static::assertSame($expectedUri, $actual['base_uri']);
+
+ return $this->createMock(GuzzleClient::class);
+ };
+
+ $api->setHttpClientFactory($factory);
+
+ // Get a service, to trigger the HTTP Client factory
+ $api->folder;
}
public function testConfigStripsUriPath(): void
{
$config = [
- 'uri' => 'http://maxemail.example.com/some/extra/path',
- 'username' => 'api@user.com',
- 'password' => 'apipass',
+ 'uri' => 'https://maxemail.example.com/some/extra/path',
+ 'token' => 'apitoken',
];
$api = new Client($config);
- static::assertSame('http://maxemail.example.com/', $api->getConfig()['uri']);
+ $factory = function (array $actual) use ($config): GuzzleClient {
+ $expectedUri = 'https://maxemail.example.com/api/json/';
+ static::assertSame($expectedUri, $actual['base_uri']);
+
+ return $this->createMock(GuzzleClient::class);
+ };
+
+ $api->setHttpClientFactory($factory);
+
+ // Get a service, to trigger the HTTP Client factory
+ $api->folder;
}
public function testConfigInvalidUri(): void
@@ -61,8 +97,7 @@ public function testConfigInvalidUri(): void
$config = [
'uri' => '//',
- 'username' => 'api@user.com',
- 'password' => 'apipass',
+ 'token' => 'apitoken',
];
new Client($config);
@@ -75,17 +110,49 @@ public function testConfigMissingUriProtocol(): void
$config = [
'uri' => 'maxemail.example.com',
+ 'token' => 'apitoken',
+ ];
+
+ new Client($config);
+ }
+
+ public function testConfigLegacyAuthentication(): void
+ {
+ $config = [
'username' => 'api@user.com',
'password' => 'apipass',
];
- new Client($config);
+ $api = new Client($config);
+
+ $factory = function (array $actual) use ($config): GuzzleClient {
+ $expectedAuth = [
+ $config['username'],
+ $config['password'],
+ ];
+ static::assertSame($expectedAuth, $actual['auth']);
+
+ return $this->createMock(GuzzleClient::class);
+ };
+
+ $api->setHttpClientFactory($factory);
+
+ // Get a service, to trigger the HTTP Client factory
+ $api->folder;
+ }
+
+ public function testConfigMissingToken(): void
+ {
+ $this->expectException(Exception\InvalidArgumentException::class);
+ $this->expectExceptionMessage('API config requires token OR username & password');
+
+ new Client([]);
}
public function testConfigMissingUsername(): void
{
$this->expectException(Exception\InvalidArgumentException::class);
- $this->expectExceptionMessage('API config requires username & password');
+ $this->expectExceptionMessage('API config requires token OR username & password');
$config = [
'password' => 'apipass',
@@ -97,7 +164,7 @@ public function testConfigMissingUsername(): void
public function testConfigMissingPassword(): void
{
$this->expectException(Exception\InvalidArgumentException::class);
- $this->expectExceptionMessage('API config requires username & password');
+ $this->expectExceptionMessage('API config requires token OR username & password');
$config = [
'username' => 'api@user.com',
@@ -106,6 +173,32 @@ public function testConfigMissingPassword(): void
new Client($config);
}
+ public function testGetConfigWithToken(): void
+ {
+ $api = new Client($this->testConfig);
+
+ $expected = [
+ 'uri' => $this->testConfig['uri'],
+ 'username' => null,
+ 'password' => null,
+ ];
+
+ static::assertSame($expected, $api->getConfig());
+ }
+
+ public function testGetConfigWithLegacyAuthentication(): void
+ {
+ $config = [
+ 'uri' => 'https://maxemail.example.com/',
+ 'username' => 'api@user.com',
+ 'password' => 'apipass',
+ ];
+
+ $api = new Client($config);
+
+ static::assertSame($config, $api->getConfig());
+ }
+
public function testSetGetLogger(): void
{
/** @var \Psr\Log\LoggerInterface|MockObject $logger */
diff --git a/tests/FunctionalTest.php b/tests/FunctionalTest.php
index d11fa51..ccb23d9 100644
--- a/tests/FunctionalTest.php
+++ b/tests/FunctionalTest.php
@@ -27,8 +27,7 @@ protected function setUp(): void
$config = [
'uri' => getenv('FUNC_API_URI'),
- 'username' => getenv('FUNC_API_USERNAME'),
- 'password' => getenv('FUNC_API_PASSWORD'),
+ 'token' => getenv('FUNC_API_TOKEN'),
];
$this->client = new Client($config);
}