Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,19 @@
use Utopia\Messaging\Messages\SMS;
use Utopia\Messaging\Response;

class Vonage extends SMSAdapter
class VonageLegacy extends SMSAdapter
{
protected const NAME = 'Vonage';
protected const NAME = 'Vonage Legacy';

/**
* @param string $apiKey Vonage API Key
* @param string $apiSecret Vonage API Secret
*/
public function __construct(
private readonly string $apiKey,
private readonly string $apiSecret,
private readonly ?string $from = null,
private string $apiKey,
private string $apiSecret,
private ?string $from = null
) {
parent::__construct();
}
Comment on lines 20 to 25

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Missing parent::__construct() causes fatal error on every send

The original Vonage.php called parent::__construct(), which this PR explicitly removes. The base Adapter class initializes two typed properties there — $sendCounter (non-nullable Counter) and $clientFactory (readonly ?Closure) — both via constructor promotion or direct assignment. Without that call, both properties remain uninitialized. PHP throws Fatal error: Typed property Utopia\Messaging\Adapter::$sendCounter must not be accessed before initialization the first time send() calls recordResponse(), which is every successful delivery. Every other SMS adapter in the codebase (Twilio, Clickatell, Sinch, etc.) calls parent::__construct().

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/Utopia/Messaging/Adapter/SMS/VonageLegacy.php
Line: 20-25

Comment:
**Missing `parent::__construct()` causes fatal error on every send**

The original `Vonage.php` called `parent::__construct()`, which this PR explicitly removes. The base `Adapter` class initializes two typed properties there — `$sendCounter` (non-nullable `Counter`) and `$clientFactory` (readonly `?Closure`) — both via constructor promotion or direct assignment. Without that call, both properties remain uninitialized. PHP throws `Fatal error: Typed property Utopia\Messaging\Adapter::$sendCounter must not be accessed before initialization` the first time `send()` calls `recordResponse()`, which is every successful delivery. Every other SMS adapter in the codebase (`Twilio`, `Clickatell`, `Sinch`, etc.) calls `parent::__construct()`.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Claude Code Fix in Codex


public function getName(): string
Expand All @@ -40,9 +39,9 @@ public function getMaxMessagesPerRequest(): int
*/
protected function process(SMS $message): array
{
$to = array_map(
fn(string $to): string => ltrim($to, '+'),
$message->getTo(),
$to = \array_map(
fn ($to) => \ltrim($to, '+'),
$message->getTo()
);

$response = new Response($this->getType());
Expand All @@ -64,10 +63,12 @@ protected function process(SMS $message): array
if (($result['response']['messages'][0]['status'] ?? null) === 0) {
$response->setDeliveredTo(1);
$response->addResult($result['response']['messages'][0]['to']);
} elseif (!\is_null($result['response']['messages'][0]['error-text'] ?? null)) {
$response->addResult($message->getTo()[0], $result['response']['messages'][0]['error-text']);
} else {
$response->addResult($message->getTo()[0], 'Unknown error');
if (!\is_null($result['response']['messages'][0]['error-text'] ?? null)) {
$response->addResult($message->getTo()[0], $result['response']['messages'][0]['error-text']);
} else {
$response->addResult($message->getTo()[0], 'Unknown error');
}
}

return $response->toArray();
Expand Down
102 changes: 102 additions & 0 deletions src/Utopia/Messaging/Adapter/SMS/VonageMessages.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
<?php

namespace Utopia\Messaging\Adapter\SMS;

use Utopia\Messaging\Adapter\SMS as SMSAdapter;
use Utopia\Messaging\Messages\SMS as SMSMessage;
use Utopia\Messaging\Response;

class VonageMessages extends SMSAdapter
{
protected const NAME = 'Vonage Messages';

public function __construct(
private string $apiKey,
private string $apiSecret,
private ?string $from = null
) {
}

protected function getApiEndpoint(): string
{
return 'https://api.vonage.com/v1/messages';
}

protected function getAuthorizationHeader(): string
{
return 'Basic ' . \base64_encode("{$this->apiKey}:{$this->apiSecret}");
}

/**
* @return array<string>
*/
protected function getRequestHeaders(): array
{
return [
"Authorization: {$this->getAuthorizationHeader()}",
'Content-Type: application/json',
'Accept: application/json',
'User-Agent: Utopia Messaging',
];
}

public function getName(): string
{
return static::NAME;
}

public function getMaxMessagesPerRequest(): int
{
return 1;
}

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());

if (empty($from)) {
$response->addResult($message->getTo()[0], 'The "from" field is required for the Vonage Messages API.');
return $response->toArray();
}

$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 = "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);
}

return $response->toArray();
}
}
35 changes: 35 additions & 0 deletions tests/Messaging/Adapter/SMS/VonageLegacyTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

namespace Utopia\Tests\Adapter\SMS;

use Utopia\Tests\Adapter\Base;

class VonageLegacyTest extends Base
{
/**
* @throws \Exception
*/
public function testSendSMS(): void
{
$this->markTestSkipped('Vonage credentials are not available.');

/*
$apiKey = \getenv('VONAGE_API_KEY');
$apiSecret = \getenv('VONAGE_API_SECRET');

$sender = new Vonage($apiKey, $apiSecret);

$message = new SMS(
to: [\getenv('VONAGE_TO')],
content: 'Test Content',
from: \getenv('VONAGE_FROM')
);

$response = $sender->send($message);

$result = \json_decode($response, true);

$this->assertResponse($result);
*/
}
}
36 changes: 36 additions & 0 deletions tests/Messaging/Adapter/SMS/VonageMessagesTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

namespace Utopia\Tests\Adapter\SMS;

use Utopia\Messaging\Adapter\SMS\VonageMessages;
use Utopia\Messaging\Messages\SMS;
use Utopia\Tests\Adapter\Base;

class VonageMessagesTest extends Base
{
/**
* @throws \Exception
*/
public function testSendSMS(): void
{
$this->markTestSkipped('Vonage Messages credentials are not available.');

/*
$apiKey = \getenv('VONAGE_MESSAGES_API_KEY');
$apiSecret = \getenv('VONAGE_MESSAGES_API_SECRET');

$sender = new VonageMessages($apiKey, $apiSecret);

$message = new SMS(
to: [\getenv('VONAGE_MESSAGES_TO')],
content: 'Test Content',
from: \getenv('VONAGE_MESSAGES_FROM'),
);

$response = $sender->send($message);

$this->assertNotEmpty($response['results']);
$this->assertNotEmpty($response['results'][0]['success']);
*/
}
}