Creating Custom Providers
The SMS Client library is designed to be extensible. You can easily create custom providers to integrate with any SMS service.
Provider Interface
To create a custom provider, implement the ProviderInterface:
namespace ByJG\SmsClient\Provider;
use ByJG\SmsClient\Message;
use ByJG\SmsClient\Phone;
use ByJG\SmsClient\ReturnObject;
use ByJG\Util\Uri;
use ByJG\WebRequest\HttpPromise;
interface ProviderInterface
{
/**
* Returns the URI schema for this provider
* @return array
*/
public static function schema(): array;
/**
* Setup the provider with connection details
* @param Uri $uri Connection URI
*/
public function setUp(Uri $uri): void;
/**
* Send an SMS message
* @param string|Phone $to Recipient phone number
* @param Message $envelope Message to send
* @return ReturnObject Result of the send operation
*/
public function send(string|Phone $to, Message $envelope): ReturnObject;
/**
* Send without waiting: the promise is fulfilled with the ReturnObject,
* or rejected with any error (it does not throw)
*/
public function sendAsync(string|Phone $to, Message $envelope): HttpPromise;
}
ProviderBase implements sendAsync() with your send(): the message is sent at once and the promise is already
settled. To send in parallel, override sendAsync() with sendHttpRequestAsync() (below) and implement send() as
return $this->sendAsync($to, $envelope)->await();, as the built-in providers do.
Using ProviderBase
The easiest way to create a custom provider is to extend the ProviderBase class:
namespace MyApp\SmsProviders;
use ByJG\SmsClient\Message;
use ByJG\SmsClient\Phone;
use ByJG\SmsClient\Provider\ProviderBase;
use ByJG\SmsClient\ReturnObject;
use ByJG\Util\Uri;
class CustomSmsProvider extends ProviderBase
{
protected string $apiKey;
protected string $apiSecret;
/**
* Define the URI schema for your provider
*/
public static function schema(): array
{
return [
'customsms' // Your custom URI scheme
];
}
/**
* Setup connection from URI
* URI format: customsms://apiKey:apiSecret@default
*/
public function setUp(Uri $uri): void
{
$this->apiKey = $uri->getUsername();
$this->apiSecret = $uri->getPassword();
}
/**
* Send the SMS message
*/
#[\Override]
public function send(string|Phone $to, Message $envelope): ReturnObject
{
// Normalize phone number
$phoneNumber = $to instanceof Phone ? $to->hydrate() : $to;
// Get message body
$messageBody = $envelope->getBody();
// Get sender (if set)
$sender = $envelope->getSender();
if ($sender instanceof Phone) {
$sender = $sender->hydrate();
}
// Make API call to your SMS service
try {
$result = $this->makeApiCall($phoneNumber, $messageBody, $sender);
return new ReturnObject(
true, // Message sent successfully
[
'message_id' => $result['id'],
'status' => $result['status']
]
);
} catch (\Exception $e) {
return new ReturnObject(
false, // Message failed
['error' => $e->getMessage()]
);
}
}
/**
* Your custom API call logic
*/
protected function makeApiCall(string $to, string $message, ?string $from): array
{
// Implement your API call here
// This is just an example
$data = [
'to' => $to,
'message' => $message,
'from' => $from,
'api_key' => $this->apiKey
];
// Make HTTP request to your SMS API
$response = $this->sendHttpRequest(/* ... */);
return json_decode($response, true);
}
}
Complete Example
Here's a complete example of a custom provider for a fictional SMS service:
namespace MyApp\SmsProviders;
use ByJG\SmsClient\Message;
use ByJG\SmsClient\Phone;
use ByJG\SmsClient\Provider\ProviderBase;
use ByJG\SmsClient\ReturnObject;
use ByJG\Util\Uri;
use ByJG\WebRequest\HttpClient;
use ByJG\WebRequest\Psr7\MemoryStream;
use ByJG\WebRequest\Psr7\Request;
class AcmeProvider extends ProviderBase
{
protected string $apiToken;
protected string $apiEndpoint = 'https://api.acmesms.com/v1';
public static function schema(): array
{
return ['acmesms'];
}
public function setUp(Uri $uri): void
{
// URI format: acmesms://token@endpoint
$this->apiToken = $uri->getUsername();
if ($uri->getHost() !== 'default') {
$this->apiEndpoint = 'https://' . $uri->getHost();
}
}
#[\Override]
public function send(string|Phone $to, Message $envelope): ReturnObject
{
$to = $to instanceof Phone ? $to->hydrate() : $to;
$from = $envelope->getSender();
if ($from instanceof Phone) {
$from = $from->hydrate();
}
// Prepare request
$request = Request::getInstance(new Uri("{$this->apiEndpoint}/send"))
->withMethod('POST')
->withHeader('Authorization', "Bearer {$this->apiToken}")
->withHeader('Content-Type', 'application/json')
->withBody(new MemoryStream(json_encode([
'to' => $to,
'from' => $from,
'message' => $envelope->getBody(),
'properties' => $envelope->getProperties()
])));
try {
$client = HttpClient::getInstance();
$response = $this->sendHttpRequest($client, $request);
$data = json_decode($response->getBody()->getContents(), true);
$success = $response->getStatusCode() === 200
&& isset($data['status'])
&& $data['status'] === 'sent';
return new ReturnObject($success, $data);
} catch (\Exception $e) {
return new ReturnObject(false, [
'error' => $e->getMessage()
]);
}
}
}
Registering Your Custom Provider
Once you've created your provider, register it with the ProviderFactory:
use ByJG\SmsClient\Provider\ProviderFactory;
use MyApp\SmsProviders\AcmeProvider;
// Register the provider
ProviderFactory::registerProvider(AcmeProvider::class);
// Use it
$provider = ProviderFactory::create(new Uri("acmesms://your-api-token@default"));
$response = $provider->send(
"+12221234567",
(new Message("Hello from Acme SMS!"))->withSender("+12223217654")
);
Using with Service Registration
You can also register your custom provider for automatic selection:
use ByJG\SmsClient\Provider\ProviderFactory;
use MyApp\SmsProviders\AcmeProvider;
ProviderFactory::registerProvider(AcmeProvider::class);
// Register for specific country codes (one call per prefix)
ProviderFactory::registerServices("acmesms://your-api-token@default", "+44"); // UK
ProviderFactory::registerServices("acmesms://your-api-token@default", "+49"); // Germany
// Automatically uses your provider for UK/German numbers
$response = ProviderFactory::createAndSend(
"+447911123456",
new Message("Hello UK!")
);
Helper Methods
The ProviderBase class provides the helper methods below, and HttpPromise::try() builds the promise. To convert a Phone object to a string, call
$phone->hydrate() (see Phone Formatting).
sendHttpRequest()
Makes HTTP requests (you can override for custom behavior):
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
protected function sendHttpRequest(
ClientInterface $client,
RequestInterface $request
): ResponseInterface {
return $client->sendRequest($request);
}
sendHttpRequestAsync()
Sends the request without waiting and returns an HttpPromise of the response: in parallel with the ByJG
HttpClient (sendAsync()), or already settled with another PSR-18 client. Override it in a test mock to return
HttpPromise::resolve($response).
HttpPromise::try()
HttpPromise::try(fn() => ...) (byjg/webrequest) returns a promise resolved with the closure's return value (a
returned promise is followed), or rejected with the exception it throws, so sendAsync() does not throw:
#[\Override]
public function sendAsync(string|Phone $to, Message $envelope): HttpPromise
{
return HttpPromise::try(fn() => $this->sendHttpRequestAsync(HttpClient::getInstance(), $this->buildRequest($to, $envelope))
->then(fn(ResponseInterface $response) => new ReturnObject($response->getStatusCode() == 200, (string)$response->getBody())));
}
Best Practices
-
Error Handling: Always wrap API calls in try-catch blocks and return appropriate
ReturnObjectinstances -
Phone Number Validation: Use
Phone::hydrate()(it validates the number) to ensure proper phone number format -
Configuration: Store API credentials and endpoints in the
setUp()method -
Response Data: Include useful information in the
ReturnObjectraw message -
Testing: Create a mock version of your provider for testing (extend your provider and override
sendHttpRequest())
Testing Your Provider
Create a mock version for testing:
namespace Tests\Providers;
use MyApp\SmsProviders\AcmeProvider;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use ByJG\WebRequest\Psr7\Response;
use ByJG\WebRequest\Psr7\MemoryStream;
class AcmeProviderMock extends AcmeProvider
{
#[\Override]
protected function sendHttpRequest(
ClientInterface $client,
RequestInterface $request
): ResponseInterface {
// Return mock response
return (new Response(200))->withBody(new MemoryStream(json_encode([
'status' => 'sent',
'message_id' => 'mock-12345'
])));
}
}
Then use it in your tests:
use Tests\Providers\AcmeProviderMock;
use ByJG\SmsClient\Provider\ProviderFactory;
ProviderFactory::registerProvider(AcmeProviderMock::class);
$provider = ProviderFactory::create(new Uri("acmesms://test-token@default"));
$response = $provider->send("+12221234567", new Message("Test"));
$this->assertTrue($response->isSent());