From 386e78e67387bda3d4a81adc20816221817334dd Mon Sep 17 00:00:00 2001
From: Chris Minett <1084019+chrisminett@users.noreply.github.com>
Date: Fri, 13 Jun 2025 09:56:40 +0100
Subject: [PATCH 1/4] Deprecate `getConfig()` method
Update unit tests to validate the values used to construct the HTTP client.
---
CHANGELOG.md | 2 ++
src/Client.php | 30 +++++++++++++++---
src/Helper.php | 2 +-
src/Service.php | 2 +-
tests/ClientTest.php | 75 ++++++++++++++++++++++++++++++++++++++++----
5 files changed, 99 insertions(+), 12 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3e4f3e5..06851aa 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).
## [Unreleased]
+### Changed
+- Deprecate `getConfig()` method. Packages can maintain their config internally.
## [5.1.1] - 2021-08-10
### Fixed
diff --git a/src/Client.php b/src/Client.php
index b041f32..237c002 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\LoggerInterface;
@@ -91,7 +92,7 @@ class Client implements \Psr\Log\LoggerAwareInterface
private $logger;
/**
- * @var GuzzleClient
+ * @var GuzzleClientInterface
*/
private $httpClient;
@@ -100,6 +101,11 @@ class Client implements \Psr\Log\LoggerAwareInterface
*/
private $debugLoggingEnabled = false;
+ /**
+ * @var \Closure(array):GuzzleClientInterface
+ */
+ private $httpClientFactory;
+
/**
* @param array $config {
* @var string $username Required
@@ -157,7 +163,7 @@ private function getInstance(string $serviceName): Service
return $this->services[$serviceName];
}
- private function getClient(): GuzzleClient
+ private function getClient(): GuzzleClientInterface
{
if ($this->httpClient === null) {
$stack = HandlerStack::create();
@@ -166,7 +172,8 @@ 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,
@@ -178,7 +185,13 @@ private function getClient(): GuzzleClient
'Accept' => 'application/json',
],
'handler' => $stack,
- ]);
+ ];
+
+ if (!isset($this->httpClientFactory)) {
+ $this->httpClient = new GuzzleClient($clientConfig);
+ } else {
+ $this->httpClient = ($this->httpClientFactory)($clientConfig);
+ }
}
return $this->httpClient;
@@ -187,6 +200,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 {
* @var string $uri
* @var string $username
@@ -224,4 +238,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 5c940ba..262f534 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 e56beeb..13b0f30 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 47d0256..f00982b 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;
@@ -26,7 +27,30 @@ public function testConfigValid()
{
$api = new Client($this->testConfig);
- $this->assertSame($this->testConfig, $api->getConfig());
+ $factory = function (array $actual): GuzzleClient {
+ $expectedUri = $this->testConfig['uri'] . 'api/json/';
+ static::assertSame($expectedUri, $actual['base_uri']);
+
+ $expectedAuth = [
+ $this->testConfig['username'],
+ $this->testConfig['password'],
+ ];
+ static::assertSame($expectedAuth, $actual['auth']);
+
+ $expectedHeaders = [
+ 'User-Agent' => 'MxmApiClient/' . Client::VERSION . ' PHP/' . PHP_VERSION,
+ 'Content-Type' => 'application/x-www-form-urlencoded',
+ 'Accept' => 'application/json',
+ ];
+ 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 testConfigSupportDeprecatedUserPass()
@@ -38,8 +62,20 @@ public function testConfigSupportDeprecatedUserPass()
$api = new Client($config);
- $this->assertSame($config['user'], $api->getConfig()['username']);
- $this->assertSame($config['pass'], $api->getConfig()['password']);
+ $factory = function (array $actual) use ($config): GuzzleClient {
+ $expectedAuth = [
+ $config['user'],
+ $config['pass'],
+ ];
+ 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 testConfigDefaultHost()
@@ -51,20 +87,40 @@ public function testConfigDefaultHost()
$api = new Client($config);
- $this->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()
{
$config = [
- 'uri' => 'http://maxemail.example.com/some/extra/path',
+ 'uri' => 'https://maxemail.example.com/some/extra/path',
'username' => 'api@user.com',
'password' => 'apipass',
];
$api = new Client($config);
- $this->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()
@@ -119,6 +175,13 @@ public function testConfigMissingPassword(): void
new Client($config);
}
+ public function testGetConfig(): void
+ {
+ $api = new Client($this->testConfig);
+
+ static::assertSame($this->testConfig, $api->getConfig());
+ }
+
public function testSetGetLogger()
{
/** @var \Psr\Log\LoggerInterface|MockObject $logger */
From 74b07a7388ac97e6a16a706b883e19be49e2890d Mon Sep 17 00:00:00 2001
From: Chris Minett <1084019+chrisminett@users.noreply.github.com>
Date: Thu, 12 Jun 2025 15:58:22 +0100
Subject: [PATCH 2/4] Add support for API token authentication for Maxemail
v145
---
CHANGELOG.md | 3 ++
README.md | 3 +-
phpunit.xml.dist | 3 +-
src/Client.php | 47 +++++++++++++++--------
tests/ClientTest.php | 80 ++++++++++++++++++++++++++++++----------
tests/FunctionalTest.php | 3 +-
6 files changed, 98 insertions(+), 41 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 06851aa..a0c9ec0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,9 @@ 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.
diff --git a/README.md b/README.md
index 1f40d73..f8d47c4 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 e6230c1..8a738f1 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -12,8 +12,7 @@
-
-
+
diff --git a/src/Client.php b/src/Client.php
index 237c002..5ae1fa6 100644
--- a/src/Client.php
+++ b/src/Client.php
@@ -66,6 +66,11 @@ class Client implements \Psr\Log\LoggerAwareInterface
*/
private $uri = 'https://mxm.xtremepush.com/';
+ /**
+ * @var string
+ */
+ private $token;
+
/**
* @var string
*/
@@ -118,20 +123,25 @@ class Client implements \Psr\Log\LoggerAwareInterface
*/
public function __construct(array $config)
{
- // Support deprecated key names from v3
- if (!isset($config['username']) && isset($config['user'])) {
- $config['username'] = $config['user'];
- }
- if (!isset($config['password']) && isset($config['pass'])) {
- $config['password'] = $config['pass'];
- }
+ // Must have API token
+ if (!isset($config['token'])) {
+ // Support deprecated key names from v3
+ if (!isset($config['username']) && isset($config['user'])) {
+ $config['username'] = $config['user'];
+ }
+ if (!isset($config['password']) && isset($config['pass'])) {
+ $config['password'] = $config['pass'];
+ }
- // Must have user/pass
- if (!isset($config['username']) || !isset($config['password'])) {
- throw new Exception\InvalidArgumentException('API config requires username & password');
+ // 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']);
@@ -175,10 +185,6 @@ private function getClient(): GuzzleClientInterface
$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',
@@ -187,6 +193,15 @@ private function getClient(): GuzzleClientInterface
'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 {
diff --git a/tests/ClientTest.php b/tests/ClientTest.php
index f00982b..370a511 100644
--- a/tests/ClientTest.php
+++ b/tests/ClientTest.php
@@ -19,8 +19,7 @@ class ClientTest extends TestCase
{
private $testConfig = [
'uri' => 'https://maxemail.example.com/',
- 'username' => 'api@user.com',
- 'password' => 'apipass',
+ 'token' => 'apitoken',
];
public function testConfigValid()
@@ -31,16 +30,11 @@ public function testConfigValid()
$expectedUri = $this->testConfig['uri'] . 'api/json/';
static::assertSame($expectedUri, $actual['base_uri']);
- $expectedAuth = [
- $this->testConfig['username'],
- $this->testConfig['password'],
- ];
- static::assertSame($expectedAuth, $actual['auth']);
-
$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']);
@@ -81,8 +75,7 @@ public function testConfigSupportDeprecatedUserPass()
public function testConfigDefaultHost()
{
$config = [
- 'username' => 'api@user.com',
- 'password' => 'apipass',
+ 'token' => 'apitoken',
];
$api = new Client($config);
@@ -104,8 +97,7 @@ public function testConfigStripsUriPath()
{
$config = [
'uri' => 'https://maxemail.example.com/some/extra/path',
- 'username' => 'api@user.com',
- 'password' => 'apipass',
+ 'token' => 'apitoken',
];
$api = new Client($config);
@@ -130,8 +122,7 @@ public function testConfigInvalidUri()
$config = [
'uri' => '//',
- 'username' => 'api@user.com',
- 'password' => 'apipass',
+ 'token' => 'apitoken',
];
new Client($config);
@@ -144,17 +135,49 @@ public function testConfigMissingUriProtocol()
$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',
@@ -166,7 +189,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',
@@ -175,11 +198,30 @@ public function testConfigMissingPassword(): void
new Client($config);
}
- public function testGetConfig(): void
+ public function testGetConfigWithToken(): void
{
$api = new Client($this->testConfig);
- static::assertSame($this->testConfig, $api->getConfig());
+ $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()
diff --git a/tests/FunctionalTest.php b/tests/FunctionalTest.php
index 5df4ca7..21567bd 100644
--- a/tests/FunctionalTest.php
+++ b/tests/FunctionalTest.php
@@ -30,8 +30,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);
}
From c3202e2db3c1c380af168fec9362e8f47bb9c204 Mon Sep 17 00:00:00 2001
From: Chris Minett <1084019+chrisminett@users.noreply.github.com>
Date: Tue, 17 Jun 2025 11:29:33 +0100
Subject: [PATCH 3/4] Update Client docblock
---
src/Client.php | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/src/Client.php b/src/Client.php
index 5ae1fa6..1ec8620 100644
--- a/src/Client.php
+++ b/src/Client.php
@@ -113,8 +113,9 @@ class Client implements \Psr\Log\LoggerAwareInterface
/**
* @param array $config {
- * @var string $username Required
- * @var string $password Required
+ * @var string $token Required, or username & password
+ * @var string $username Required, if no token
+ * @var string $password Required, if no token
* @var string $uri Optional. Default https://mxm.xtremepush.com/
* @var string $user @deprecated See username
* @var string $pass @deprecated See password
From 6379d104d9d34e8a02acbf5239f8c74229b410ad Mon Sep 17 00:00:00 2001
From: Chris Minett <1084019+chrisminett@users.noreply.github.com>
Date: Tue, 17 Jun 2025 11:11:37 +0100
Subject: [PATCH 4/4] Tag for v5.2.0
---
CHANGELOG.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a0c9ec0..1b2cf46 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).
## [Unreleased]
+
+## [5.2.0] - 2025-06-25
### Added
- Support for API token authentication. Username and password can still be used
as a fallback.