fd6130/exabytes-sms-bundle
A Symfony bundle for sending SMS through the Exabytes SMS gateway. Integrates the Exabytes API into your Symfony app with simple configuration and service injection, so you can dispatch transactional or notification messages from anywhere in your codebase without writing HTTP client boilerplate.
1. Install and confirm what you actually got.
composer require fd6130/exabytes-sms-bundle
Before writing a single line of app code, open the package's composer.json and check for:
extra.laravel.providers / extra.laravel.aliases (auto-discovery) — if absent, you must register the service provider manually in bootstrap/providers.php (Laravel 11+) or config/app.php.require — a symfony/* requirement list means this is a Symfony bundle that happens to be installable in Laravel, not a native Laravel package. The name ...-bundle is a strong hint. Confirm there's a real Illuminate\Support\ServiceProvider subclass before assuming php artisan vendor:publish will work.2. Publish config and set credentials.
php artisan vendor:publish --provider="Fd6130\ExabytesSms\ExabytesSmsServiceProvider"
(Publish tag and provider FQCN are indicative — grep src/ for publishes( to get the exact tag.) Then in .env:
EXABYTES_API_KEY=
EXABYTES_SENDER_ID=
EXABYTES_API_URL=
EXABYTES_TIMEOUT=10
Confirm the config file path (config/exabytes.php vs config/sms.php) and cache it after changes: php artisan config:clear.
3. First use case: a single OTP to a Malaysian mobile number.
The fastest sanity check is a tinker send, not an integration:
php artisan tinker
>>> app(/* the client class from src/ */::class)->send('60123456789', 'Test OTP 123456');
Watch for: is the number accepted in E.164 (60..., no leading 0, no +)? Is the sender ID registered against the Exabytes account? Does the response object expose a message ID, or just a boolean?
Where to look first, in order: config/ (what's actually configurable), the service provider's register()/boot() (what's bound and whether it's a singleton), the client class (HTTP calls, retry/exception behaviour), then README.md for the expected response shape.
Wrap the package, don't scatter it. Bind it behind your own contract so the vendor can be swapped and so tests don't hit Exabytes:
// app/Contracts/SmsSender.php
interface SmsSender { public function send(string $to, string $message): string; }
// app/Services/ExabytesSender.php
final class ExabytesSender implements SmsSender
{
public function __construct(private readonly ExabytesClient $client) {}
public function send(string $to, string $message): string
{
$response = $this->client->send($this->normalise($to), $message);
if (! $response->isSuccess()) { // shape varies — verify
throw new SmsFailedException($response->message());
}
return $response->messageId();
}
private function normalise(string $to): string
{
return ltrim(preg_replace('/\D+/', '', $to), '0') |> /* prefix 60 */;
}
}
Laravel Notification channel. For anything user-facing, expose SMS as a notification channel rather than calling the client from controllers. One toExabytes($notifiable) method returning a message object gives you queueing, throttling, and ShouldQueue for free.
Queue everything, always. HTTP calls to an SMS gateway are the classic source of 30-second checkout requests. Dispatch a job, set $tries = 3 and an explicit backoff(). Do not retry blindly on timeout — a timed-out request may have been delivered; retrying sends a duplicate and double-bills. Retry only on connection failures and 5xx, and prefer an idempotency key if the API supports one.
Bulk sends. Chunk recipients (array_chunk($numbers, 100) or the API's batch limit) and dispatch one job per chunk with Bus::batch(). Add your own rate limiter — the gateway's throughput cap is usually per-second and is documented only in the vendor portal, not in the package.
Delivery receipts / webhooks. If the package ships a webhook handler, register it behind your own route so you control middleware: exclude it from CSRF, verify the signature (or at minimum a shared secret in the query string), return 200 fast, and hand the payload to a job. Persist the provider's message ID at send time — that's the only join key you'll have.
Testing. Point the client at a fake. Either Http::fake() (if the package uses the Laravel HTTP client — check src/ for Illuminate\Support\Facades\Http vs raw Guzzle) or swap the binding:
$this->mock(SmsSender::class, fn ($m) => $m->shouldReceive('send')->once()->andReturn('msg_123'));
Assert on the normalised number — that's where the bugs live.
Multi-tenant. If tenants bring their own Exabytes credentials, you can't rely on the singleton config binding. Either rebuild the client per tenant from a config array, or use Config::set() inside a scoped binding and app()->forgetInstance() after.
composer update fd6130/exabytes-sms-bundle --dry-run then inspect the commits). A patch bump is probably safe, but a 1.0.0 → 1.0.1 on a young package often carries silent config-key renames. Pin to ^1.0 and let the lockfile do its job.012-345 6789, +60123456789, 60123456789, and 123456789. Normalise once, centrally, and reject anything that doesn't match ^60\d{8,10}$ before it reaches the API.EXABYTES_TIMEOUT is actually wired into the Guzzle/HTTP client options; if not, the default is effectively infinite and one hung request ties up a queue worker.array_merge won't deep-merge nested arrays), the HTTP client binding (swap it to add middleware for logging/metrics), and the exception class (catch narrowly; a generic \Exception catch here will mask auth failures as delivery failures).tinker with a real number you own, then move to a queued job, then Http::fake() in the test suite. If the client throws an opaque exception, enable HTTP client logging via Http::globalRequestMiddleware() or a Guzzle Middleware::log() to see the raw request and response.How can I help you explore Laravel packages today?