xi/sms
A Laravel package for sending SMS messages through a unified API with configurable gateway drivers, queue support, and simple notification integration.
xi/sms is a small, framework-agnostic SMS abstraction: a message value object plus a gateway contract you implement per provider. It is not a Laravel package — there is no service provider, facade, config file, or notification channel — so "getting started" means wrapping it yourself in about 50 lines. With 10 stars, zero dependents, and no recent activity, treat it as a reference interface for a legacy codebase, not a library to build a new product on.
Check installability before anything else. The package predates modern PHP; its composer.json constraint will likely block PHP 8.x.
composer show xi/sms --all # inspect versions + PHP constraint
composer why-not php 8.2 # see the exact conflict
On a legacy PHP 7.x app, composer require xi/sms is fine. On PHP 8, either fork and patch the constraint (after reading the source for removed functions) or skip it — the abstraction is small enough to reproduce.
Read the source in this order. There is no documentation worth relying on beyond the README:
GatewayInterface-style class under src/) — this is the only real extension point;from, to, and body or just holds strings;file_get_contents, or an HTTP client;Create the Laravel seam. Keep package types out of your domain code:
// app/Contracts/SmsSender.php
interface SmsSender { public function send(string $to, string $body, ?string $from = null): void; }
// app/Services/XiSmsSender.php
final class XiSmsSender implements SmsSender
{
public function __construct(private \Xi\Sms\Gateway\GatewayInterface $gateway) {}
public function send(string $to, string $body, ?string $from = null): void
{
$this->gateway->send(new \Xi\Sms\SmsMessage($from, $to, $body));
}
}
Bind the contract in AppServiceProvider (or a dedicated SmsServiceProvider) and pull credentials from config/services.php / env — never hardcode them.
First use case: one transactional path only — OTP or "order shipped" — dispatched from a queued job. That exercises config, the gateway, error handling, and testing without over-engineering.
SmsSender contract is the stable API; the xi gateway is an implementation detail. This lets you swap in a maintained provider later without touching call sites.config('sms.default'), mirroring Laravel's manager pattern. Instantiate lazily (in the binding closure), not at boot.ShouldQueue job or a queued notification with $tries, $backoff, and $timeout set. SMS gateways are external HTTP calls; doing this synchronously in a request is the most common production mistake.
final class SendSms implements ShouldQueue
{
public $tries = 3;
public $backoff = [10, 60, 300];
public function __construct(public string $to, public string $body) {}
public function handle(SmsSender $sms): void { $sms->send($this->to, $this->body); }
}
SmsSender) with an in-memory fake that records messages, and rebind it in TestCase::setUp. Assert on the fake rather than on HTTP.
$this->app->instance(SmsSender::class, $fake = new FakeSmsSender());
// ... act ...
$fake->assertSentTo('+36301234567', fn ($body) => str_contains($body, 'kód'));
\Exception or a false return) and rethrow as one retryable SmsDeliveryException vs. a permanent InvalidRecipientException. Laravel's retry logic can then distinguish $this->release() from $this->fail().try, secondary in catch, with a circuit breaker to avoid hammering a dead provider.NOASSERTION. GitHub could not classify it; open the LICENSE file before shipping in a commercial product. This is a legal blocker, not a formality.each(), curly-brace string offsets, missing return types, and possibly mcrypt in gateway code. Run the test suite on your target PHP version before committing.file_get_contents/raw cURL. That means no timeouts by default, no connection reuse, and no way to intercept in tests. If you keep the package, inject your own transport or confine it behind your adapter.SmsSender.Cast, and reject invalid numbers before they reach a paid gateway.order:123:shipped) and check it in the job.register() using env() directly, php artisan config:cache will freeze stale values. Go through config() only.How can I help you explore Laravel packages today?