From 34654af717784f20f69a089cf7b5a0a376749eb6 Mon Sep 17 00:00:00 2001 From: bhardwajparth51 <196071556+bhardwajparth51@users.noreply.github.com> Date: Tue, 24 Mar 2026 02:03:44 +0530 Subject: [PATCH 1/9] feat: add Vonage Messages API adapter --- .../Messaging/Adapter/SMS/VonageMessages.php | 88 +++++++++++++++++++ .../Messaging/Adapter/VonageMessagesBase.php | 45 ++++++++++ .../Adapter/SMS/VonageMessagesTest.php | 39 ++++++++ 3 files changed, 172 insertions(+) create mode 100644 src/Utopia/Messaging/Adapter/SMS/VonageMessages.php create mode 100644 src/Utopia/Messaging/Adapter/VonageMessagesBase.php create mode 100644 tests/Messaging/Adapter/SMS/VonageMessagesTest.php diff --git a/src/Utopia/Messaging/Adapter/SMS/VonageMessages.php b/src/Utopia/Messaging/Adapter/SMS/VonageMessages.php new file mode 100644 index 00000000..eed43878 --- /dev/null +++ b/src/Utopia/Messaging/Adapter/SMS/VonageMessages.php @@ -0,0 +1,88 @@ +getTo()[0], '+'); + $from = $this->from ?? $message->getFrom(); + + $response = new Response($this->getType()); + + $result = $this->request( + method: 'POST', + url: $this->getApiEndpoint(), + headers: $this->getRequestHeaders(), + body: [ + 'message_type' => 'text', + 'to' => $to, + 'from' => $from, + 'text' => $message->getContent(), + 'channel' => 'sms', + ], + ); + + if ($result['statusCode'] === 202) { + $response->setDeliveredTo(1); + $response->addResult($message->getTo()[0]); + } else { + $errorMessage = 'Unknown error'; + if (isset($result['response']['detail'])) { + $errorMessage = $result['response']['detail']; + } elseif (isset($result['response']['title'])) { + $errorMessage = $result['response']['title']; + } elseif (!empty($result['error'])) { + $errorMessage = $result['error']; + } + + $response->addResult($message->getTo()[0], $errorMessage); + } + + return $response->toArray(); + } +} diff --git a/src/Utopia/Messaging/Adapter/VonageMessagesBase.php b/src/Utopia/Messaging/Adapter/VonageMessagesBase.php new file mode 100644 index 00000000..577a2a44 --- /dev/null +++ b/src/Utopia/Messaging/Adapter/VonageMessagesBase.php @@ -0,0 +1,45 @@ +apiKey}:{$this->apiSecret}"); + } + + /** + * Build the request headers for the Messages API. + * + * @return array + */ + protected function getRequestHeaders(): array + { + return [ + "Authorization: {$this->getAuthorizationHeader()}", + 'Content-Type: application/json', + 'Accept: application/json', + ]; + } +} diff --git a/tests/Messaging/Adapter/SMS/VonageMessagesTest.php b/tests/Messaging/Adapter/SMS/VonageMessagesTest.php new file mode 100644 index 00000000..6eeaca94 --- /dev/null +++ b/tests/Messaging/Adapter/SMS/VonageMessagesTest.php @@ -0,0 +1,39 @@ +markTestSkipped('Vonage Messages credentials are not available.'); + } + + $sender = new VonageMessages( + apiKey: $apiKey, + apiSecret: $apiSecret, + from: \getenv('VONAGE_FROM') ?: 'Vonage', + ); + + $message = new SMS( + to: [\getenv('VONAGE_TO')], + content: 'Test Content', + from: \getenv('VONAGE_FROM') + ); + + $response = $sender->send($message); + + $this->assertResponse($response); + } +} From 5f92f6452f7b541739f7db5f62faa6c457febb8b Mon Sep 17 00:00:00 2001 From: bhardwajparth51 <196071556+bhardwajparth51@users.noreply.github.com> Date: Wed, 1 Apr 2026 13:55:24 +0530 Subject: [PATCH 2/9] fix: address code review feedback (Vonage domain, from formatting, test coverage) --- .../Messaging/Adapter/SMS/VonageMessages.php | 9 ++++- .../Messaging/Adapter/VonageMessagesBase.php | 5 ++- .../Adapter/SMS/VonageMessagesTest.php | 37 +++++++++++++++++-- 3 files changed, 45 insertions(+), 6 deletions(-) diff --git a/src/Utopia/Messaging/Adapter/SMS/VonageMessages.php b/src/Utopia/Messaging/Adapter/SMS/VonageMessages.php index eed43878..286a9f1d 100644 --- a/src/Utopia/Messaging/Adapter/SMS/VonageMessages.php +++ b/src/Utopia/Messaging/Adapter/SMS/VonageMessages.php @@ -26,9 +26,7 @@ class VonageMessages extends SMSAdapter * @param string $apiSecret Vonage API Secret */ public function __construct( - /** @phpstan-ignore property.onlyWritten */ private string $apiKey, - /** @phpstan-ignore property.onlyWritten */ private string $apiSecret, private ?string $from = null ) { @@ -54,6 +52,13 @@ protected function process(SMSMessage $message): array $response = new Response($this->getType()); + if (empty($from)) { + $response->addResult($message->getTo()[0], 'The "from" field is required for the Vonage Messages API.'); + return $response->toArray(); + } + + $from = \ltrim($from, '+'); + $result = $this->request( method: 'POST', url: $this->getApiEndpoint(), diff --git a/src/Utopia/Messaging/Adapter/VonageMessagesBase.php b/src/Utopia/Messaging/Adapter/VonageMessagesBase.php index 577a2a44..09ce5e2f 100644 --- a/src/Utopia/Messaging/Adapter/VonageMessagesBase.php +++ b/src/Utopia/Messaging/Adapter/VonageMessagesBase.php @@ -21,9 +21,12 @@ trait VonageMessagesBase */ protected function getApiEndpoint(): string { - return 'https://api.nexmo.com/v1/messages'; + return 'https://api.vonage.com/v1/messages'; } + /** + * @todo Implement JWT authentication for non-SMS channels + */ protected function getAuthorizationHeader(): string { return 'Basic ' . \base64_encode("{$this->apiKey}:{$this->apiSecret}"); diff --git a/tests/Messaging/Adapter/SMS/VonageMessagesTest.php b/tests/Messaging/Adapter/SMS/VonageMessagesTest.php index 6eeaca94..25890d4b 100644 --- a/tests/Messaging/Adapter/SMS/VonageMessagesTest.php +++ b/tests/Messaging/Adapter/SMS/VonageMessagesTest.php @@ -15,9 +15,10 @@ public function testSendSMS(): void { $apiKey = \getenv('VONAGE_API_KEY'); $apiSecret = \getenv('VONAGE_API_SECRET'); + $to = \getenv('VONAGE_TO'); - if (!$apiKey || !$apiSecret) { - $this->markTestSkipped('Vonage Messages credentials are not available.'); + if (!$apiKey || !$apiSecret || !$to) { + $this->markTestSkipped('Vonage Messages credentials or recipient are not available.'); } $sender = new VonageMessages( @@ -27,7 +28,7 @@ public function testSendSMS(): void ); $message = new SMS( - to: [\getenv('VONAGE_TO')], + to: [$to], content: 'Test Content', from: \getenv('VONAGE_FROM') ); @@ -36,4 +37,34 @@ public function testSendSMS(): void $this->assertResponse($response); } + + /** + * @throws \Exception + */ + public function testSendSMSWithFallbackFrom(): void + { + $apiKey = \getenv('VONAGE_API_KEY'); + $apiSecret = \getenv('VONAGE_API_SECRET'); + $to = \getenv('VONAGE_TO'); + $from = \getenv('VONAGE_FROM'); + + if (!$apiKey || !$apiSecret || !$to || !$from) { + $this->markTestSkipped('Vonage Messages credentials or sender/recipient are not available.'); + } + + $sender = new VonageMessages( + apiKey: $apiKey, + apiSecret: $apiSecret, + ); + + $message = new SMS( + to: [$to], + content: 'Test Content', + from: $from + ); + + $response = $sender->send($message); + + $this->assertResponse($response); + } } From 3b8e2c413c402ac5aa6dcafb5c2850e4cf380298 Mon Sep 17 00:00:00 2001 From: bhardwajparth51 <196071556+bhardwajparth51@users.noreply.github.com> Date: Wed, 1 Apr 2026 14:07:17 +0530 Subject: [PATCH 3/9] test: refine type safety and isolate fallback logic in Vonage tests --- tests/Messaging/Adapter/SMS/VonageMessagesTest.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/Messaging/Adapter/SMS/VonageMessagesTest.php b/tests/Messaging/Adapter/SMS/VonageMessagesTest.php index 25890d4b..b26a758a 100644 --- a/tests/Messaging/Adapter/SMS/VonageMessagesTest.php +++ b/tests/Messaging/Adapter/SMS/VonageMessagesTest.php @@ -30,7 +30,6 @@ public function testSendSMS(): void $message = new SMS( to: [$to], content: 'Test Content', - from: \getenv('VONAGE_FROM') ); $response = $sender->send($message); @@ -46,7 +45,7 @@ public function testSendSMSWithFallbackFrom(): void $apiKey = \getenv('VONAGE_API_KEY'); $apiSecret = \getenv('VONAGE_API_SECRET'); $to = \getenv('VONAGE_TO'); - $from = \getenv('VONAGE_FROM'); + $from = \getenv('VONAGE_FROM') ?: null; if (!$apiKey || !$apiSecret || !$to || !$from) { $this->markTestSkipped('Vonage Messages credentials or sender/recipient are not available.'); @@ -60,7 +59,7 @@ public function testSendSMSWithFallbackFrom(): void $message = new SMS( to: [$to], content: 'Test Content', - from: $from + from: $from, ); $response = $sender->send($message); From c1ef16ed854cc3de8e9d3ba3f41f0835ad24868e Mon Sep 17 00:00:00 2001 From: bhardwajparth51 <196071556+bhardwajparth51@users.noreply.github.com> Date: Wed, 1 Apr 2026 14:12:52 +0530 Subject: [PATCH 4/9] docs: improve docstring coverage for Vonage methods to meet CI threshold --- src/Utopia/Messaging/Adapter/SMS/VonageMessages.php | 6 ++++++ src/Utopia/Messaging/Adapter/VonageMessagesBase.php | 2 ++ 2 files changed, 8 insertions(+) diff --git a/src/Utopia/Messaging/Adapter/SMS/VonageMessages.php b/src/Utopia/Messaging/Adapter/SMS/VonageMessages.php index 286a9f1d..a316513e 100644 --- a/src/Utopia/Messaging/Adapter/SMS/VonageMessages.php +++ b/src/Utopia/Messaging/Adapter/SMS/VonageMessages.php @@ -32,11 +32,17 @@ public function __construct( ) { } + /** + * Get adapter name. + */ public function getName(): string { return static::NAME; } + /** + * Get max messages per request. + */ public function getMaxMessagesPerRequest(): int { return 1; diff --git a/src/Utopia/Messaging/Adapter/VonageMessagesBase.php b/src/Utopia/Messaging/Adapter/VonageMessagesBase.php index 09ce5e2f..22659fa0 100644 --- a/src/Utopia/Messaging/Adapter/VonageMessagesBase.php +++ b/src/Utopia/Messaging/Adapter/VonageMessagesBase.php @@ -25,6 +25,8 @@ protected function getApiEndpoint(): string } /** + * Get the authorization header value for the API request. + * * @todo Implement JWT authentication for non-SMS channels */ protected function getAuthorizationHeader(): string From 6fa3699a5f0bcb3f46f8a3d76dd7c0f55f0de557 Mon Sep 17 00:00:00 2001 From: bhardwajparth51 <196071556+bhardwajparth51@users.noreply.github.com> Date: Wed, 1 Apr 2026 14:37:22 +0530 Subject: [PATCH 5/9] fix: move from field trimming before empty guard --- src/Utopia/Messaging/Adapter/SMS/VonageMessages.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Utopia/Messaging/Adapter/SMS/VonageMessages.php b/src/Utopia/Messaging/Adapter/SMS/VonageMessages.php index a316513e..396180ef 100644 --- a/src/Utopia/Messaging/Adapter/SMS/VonageMessages.php +++ b/src/Utopia/Messaging/Adapter/SMS/VonageMessages.php @@ -55,6 +55,7 @@ protected function process(SMSMessage $message): array { $to = \ltrim($message->getTo()[0], '+'); $from = $this->from ?? $message->getFrom(); + $from = $from !== null ? \ltrim($from, '+') : null; $response = new Response($this->getType()); @@ -63,8 +64,6 @@ protected function process(SMSMessage $message): array return $response->toArray(); } - $from = \ltrim($from, '+'); - $result = $this->request( method: 'POST', url: $this->getApiEndpoint(), From 449c941f28aa47f1d478d22ef13d080147c04b78 Mon Sep 17 00:00:00 2001 From: bhardwajparth51 <196071556+bhardwajparth51@users.noreply.github.com> Date: Fri, 10 Apr 2026 21:18:27 +0530 Subject: [PATCH 6/9] docs: add Vonage Messages to supported SMS adapters list --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index ac0deabf..1f059586 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,7 @@ $messaging->send($message); - [x] [Textmagic](https://www.textmagic.com/) - [x] [Msg91](https://msg91.com/) - [x] [Vonage](https://www.vonage.com/) +- [x] [Vonage Messages](https://www.vonage.com/) - [x] [Plivo](https://www.plivo.com/) - [x] [Infobip](https://www.infobip.com/) - [x] [Clickatell](https://www.clickatell.com/) From 4bcbabd501fcd7a71b41e50e6dedd66e83c2957b Mon Sep 17 00:00:00 2001 From: bhardwajparth51 <196071556+bhardwajparth51@users.noreply.github.com> Date: Fri, 10 Apr 2026 21:21:17 +0530 Subject: [PATCH 7/9] fix: address review feedback (remove base class/trait for Vonage) --- .../Messaging/Adapter/SMS/VonageMessages.php | 36 +++++++++++-- .../Messaging/Adapter/VonageMessagesBase.php | 50 ------------------- 2 files changed, 33 insertions(+), 53 deletions(-) delete mode 100644 src/Utopia/Messaging/Adapter/VonageMessagesBase.php diff --git a/src/Utopia/Messaging/Adapter/SMS/VonageMessages.php b/src/Utopia/Messaging/Adapter/SMS/VonageMessages.php index 396180ef..c8e34223 100644 --- a/src/Utopia/Messaging/Adapter/SMS/VonageMessages.php +++ b/src/Utopia/Messaging/Adapter/SMS/VonageMessages.php @@ -3,7 +3,6 @@ namespace Utopia\Messaging\Adapter\SMS; use Utopia\Messaging\Adapter\SMS as SMSAdapter; -use Utopia\Messaging\Adapter\VonageMessagesBase; use Utopia\Messaging\Messages\SMS as SMSMessage; use Utopia\Messaging\Response; @@ -17,8 +16,6 @@ */ class VonageMessages extends SMSAdapter { - use VonageMessagesBase; - protected const NAME = 'Vonage Messages'; /** @@ -32,6 +29,39 @@ public function __construct( ) { } + /** + * Get the API endpoint for the Vonage Messages API. + */ + protected function getApiEndpoint(): string + { + return 'https://api.vonage.com/v1/messages'; + } + + /** + * Get the authorization header value for the API request. + * + * @todo Implement JWT authentication for non-SMS channels + */ + protected function getAuthorizationHeader(): string + { + return 'Basic ' . \base64_encode("{$this->apiKey}:{$this->apiSecret}"); + } + + /** + * Build the request headers for the Messages API. + * + * @return array + */ + protected function getRequestHeaders(): array + { + return [ + "Authorization: {$this->getAuthorizationHeader()}", + 'Content-Type: application/json', + 'Accept: application/json', + 'User-Agent: Utopia Messaging', + ]; + } + /** * Get adapter name. */ diff --git a/src/Utopia/Messaging/Adapter/VonageMessagesBase.php b/src/Utopia/Messaging/Adapter/VonageMessagesBase.php deleted file mode 100644 index 22659fa0..00000000 --- a/src/Utopia/Messaging/Adapter/VonageMessagesBase.php +++ /dev/null @@ -1,50 +0,0 @@ -apiKey}:{$this->apiSecret}"); - } - - /** - * Build the request headers for the Messages API. - * - * @return array - */ - protected function getRequestHeaders(): array - { - return [ - "Authorization: {$this->getAuthorizationHeader()}", - 'Content-Type: application/json', - 'Accept: application/json', - ]; - } -} From a1d89cef7b073209a04ad3498a047427a542e619 Mon Sep 17 00:00:00 2001 From: bhardwajparth51 <196071556+bhardwajparth51@users.noreply.github.com> Date: Fri, 10 Apr 2026 21:22:42 +0530 Subject: [PATCH 8/9] revert: remove documentation changes for Vonage Messages --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 1f059586..ac0deabf 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,6 @@ $messaging->send($message); - [x] [Textmagic](https://www.textmagic.com/) - [x] [Msg91](https://msg91.com/) - [x] [Vonage](https://www.vonage.com/) -- [x] [Vonage Messages](https://www.vonage.com/) - [x] [Plivo](https://www.plivo.com/) - [x] [Infobip](https://www.infobip.com/) - [x] [Clickatell](https://www.clickatell.com/) From 31fa7f1e31d2d6c521ed6f426776b001c2496818 Mon Sep 17 00:00:00 2001 From: bhardwajparth51 <196071556+bhardwajparth51@users.noreply.github.com> Date: Fri, 10 Apr 2026 21:38:58 +0530 Subject: [PATCH 9/9] fix: address review feedback and harden error handling --- .../Messaging/Adapter/SMS/VonageMessages.php | 58 ++++++------------- .../Adapter/SMS/VonageMessagesTest.php | 8 +-- 2 files changed, 21 insertions(+), 45 deletions(-) diff --git a/src/Utopia/Messaging/Adapter/SMS/VonageMessages.php b/src/Utopia/Messaging/Adapter/SMS/VonageMessages.php index c8e34223..5dc2cdd1 100644 --- a/src/Utopia/Messaging/Adapter/SMS/VonageMessages.php +++ b/src/Utopia/Messaging/Adapter/SMS/VonageMessages.php @@ -6,22 +6,14 @@ use Utopia\Messaging\Messages\SMS as SMSMessage; use Utopia\Messaging\Response; -/** - * Vonage Messages API SMS Adapter. - * - * Uses the newer Vonage Messages API (V1) instead of the older SMS API. - * The Messages API is cheaper and supports multiple message types. - * - * Reference: https://developer.vonage.com/en/api/messages - */ +// Vonage Messages API SMS Adapter. +// This adapter uses the modern Vonage Messages API (V1) which is more cost-effective +// and versatile than the legacy SMS API. +// https://developer.vonage.com/en/api/messages class VonageMessages extends SMSAdapter { protected const NAME = 'Vonage Messages'; - /** - * @param string $apiKey Vonage API Key - * @param string $apiSecret Vonage API Secret - */ public function __construct( private string $apiKey, private string $apiSecret, @@ -29,29 +21,19 @@ public function __construct( ) { } - /** - * Get the API endpoint for the Vonage Messages API. - */ + // Vonage Messages API endpoint. protected function getApiEndpoint(): string { return 'https://api.vonage.com/v1/messages'; } - /** - * Get the authorization header value for the API request. - * - * @todo Implement JWT authentication for non-SMS channels - */ + // Generates the Basic Authorization header. protected function getAuthorizationHeader(): string { return 'Basic ' . \base64_encode("{$this->apiKey}:{$this->apiSecret}"); } - /** - * Build the request headers for the Messages API. - * - * @return array - */ + // Sets common headers for the API request. protected function getRequestHeaders(): array { return [ @@ -62,25 +44,18 @@ protected function getRequestHeaders(): array ]; } - /** - * Get adapter name. - */ + // Get adapter name. public function getName(): string { return static::NAME; } - /** - * Get max messages per request. - */ + // Get max messages per request. public function getMaxMessagesPerRequest(): int { return 1; } - /** - * {@inheritdoc} - */ protected function process(SMSMessage $message): array { $to = \ltrim($message->getTo()[0], '+'); @@ -111,13 +86,18 @@ protected function process(SMSMessage $message): array $response->setDeliveredTo(1); $response->addResult($message->getTo()[0]); } else { - $errorMessage = 'Unknown error'; - if (isset($result['response']['detail'])) { - $errorMessage = $result['response']['detail']; - } elseif (isset($result['response']['title'])) { - $errorMessage = $result['response']['title']; + $errorMessage = "Error {$result['statusCode']}"; + + if (\is_array($result['response'])) { + if (isset($result['response']['detail'])) { + $errorMessage = $result['response']['detail']; + } elseif (isset($result['response']['title'])) { + $errorMessage = $result['response']['title']; + } } elseif (!empty($result['error'])) { $errorMessage = $result['error']; + } elseif (\is_string($result['response']) && !empty($result['response'])) { + $errorMessage = "Error {$result['statusCode']}: " . \mb_strimwidth(\strip_tags($result['response']), 0, 100, '...'); } $response->addResult($message->getTo()[0], $errorMessage); diff --git a/tests/Messaging/Adapter/SMS/VonageMessagesTest.php b/tests/Messaging/Adapter/SMS/VonageMessagesTest.php index b26a758a..6711ed2f 100644 --- a/tests/Messaging/Adapter/SMS/VonageMessagesTest.php +++ b/tests/Messaging/Adapter/SMS/VonageMessagesTest.php @@ -8,9 +8,7 @@ class VonageMessagesTest extends Base { - /** - * @throws \Exception - */ + // Tests sending an SMS with the 'from' number set directly in the adapter. public function testSendSMS(): void { $apiKey = \getenv('VONAGE_API_KEY'); @@ -37,9 +35,7 @@ public function testSendSMS(): void $this->assertResponse($response); } - /** - * @throws \Exception - */ + // Tests sending an SMS where the 'from' number is provided by the message object. public function testSendSMSWithFallbackFrom(): void { $apiKey = \getenv('VONAGE_API_KEY');