amirbagh75/smsir-php
A PHP/Laravel package for integrating the SMS.ir API, making it easy to send SMS messages, use verification codes, and manage SMS-related features from your applications.
Install and publish config
composer require amirbagh75/smsir-php
php artisan vendor:publish --provider="Amirbagh75\SmsirPhp\SmsirServiceProvider"
The package follows the standard Laravel package shape, so auto-discovery should register the service provider and a Smsir facade. If discovery fails (or you're on an older Laravel), add the provider and alias to config/app.php manually.
Where to look first
config/smsir.php — the only file you actually need to touch. It will carry an api_key, a default line_number, and likely a timeout/base-URI entry. Map these to .env:SMSIR_API_KEY=your-panel-api-key
SMSIR_LINE_NUMBER=3000xxxx
src/ — read the client class before writing anything on top of it. This package is at 17 stars with a last release in February 2023; the exact method surface (sendMessage, sendVerifyCode, sendBulk, getCredit, getLineNumbers, or variations) is what you must confirm rather than assume. Compare what you find against the current sms.ir REST docs — the vendor has moved endpoints between v1 and v2, and a two-year-old wrapper may target the older path.
The facade accessor string. Confirm it in the provider's register() before you type Smsir:: in application code.
First use case: OTP delivery
For an Iranian-facing app this is almost certainly why you're here. The pattern is template-based (sendVerifyCode-style), not free-text — the panel holds a pre-approved template with placeholders, and you pass a template ID plus a parameter array:
use Amirbagh75\SmsirPhp\Facades\Smsir;
Smsir::sendVerifyCode($mobile, $templateId, [
'Code' => $code,
]);
Get that one path working end-to-end against a staging mobile number before building anything else. Free-text promo sending is a separate, later concern.
Wrap the facade — don't call it from controllers
Given the maintenance profile, treat amirbagh75/smsir-php as an implementation detail behind your own contract from day one. This is the single highest-leverage decision in this assessment.
interface SmsGateway
{
public function sendOtp(string $mobile, string $code): void;
public function sendText(string $mobile, string $message): void;
public function balance(): int;
}
final class SmsIrGateway implements SmsGateway
{
public function __construct(private readonly int $otpTemplateId) {}
public function sendOtp(string $mobile, string $code): void
{
Smsir::sendVerifyCode($this->normalize($mobile), $this->otpTemplateId, [
'Code' => $code,
]);
}
}
Bind it in a service provider. Swapping providers later becomes a one-line rebind instead of a grep across the codebase.
Use it as a Laravel notification channel
If the package exposes a notification channel class, register it and use toSmsir() on notifications so OTP and transactional messages share the queue, retry, and logging infrastructure:
public function via($notifiable): array
{
return [SmsirChannel::class];
}
public function toSmsir($notifiable): SmsirMessage
{
return SmsirMessage::create()
->template($this->templateId)
->parameters(['Code' => $this->code]);
}
If no channel ships with the package, write a 30-line one that delegates to your SmsGateway — that's cheaper than coupling your notification classes to the facade.
Always queue OTP and bulk sends
The sms.ir API is an external HTTP call with real latency; blocking a login request on it is a self-inflicted outage. Put sends behind ShouldQueue:
final class SendOtp implements ShouldQueue
{
use InteractsWithQueue, Queueable;
public int $tries = 3;
public int $backoff = 5;
public function handle(SmsGateway $gateway): void
{
$gateway->sendOtp($this->mobile, $this->code);
}
}
Note the tradeoff: queueing OTP introduces a delivery delay. If your product needs sub-second code delivery, send synchronously but wrap in an explicit timeout and have a fallback (voice call, resend button with cooldown).
Bulk sends: chunk and throttle
There is almost certainly a per-request recipient cap and a per-second rate limit on the panel. Chunk with array_chunk($numbers, 100), and throttle the job:
foreach (array_chunk($numbers, 100) as $batch) {
Smsir::sendBulk($batch, $message);
usleep(200_000);
}
Scheduled credit monitoring
getCredit() is cheap and worth a daily scheduled command that alerts your team when balance drops below a threshold. Silent credit exhaustion is the classic way an SMS-dependent app breaks in production — the sends start failing and nobody notices until support tickets arrive.
$credits = app(SmsGateway::class)->balance();
if ($credits < config('sms.low_balance_threshold')) {
// Slack / email the on-call dev
}
Normalize phone numbers at the boundary
Do this once, in the gateway, not at every call site: strip spaces and dashes, accept +989…, 00989…, 989…, and 09…, and always emit 09xxxxxxxxx. The API is strict, and a single non-normalized path produces the most confusing class of bug you'll hit with this package.
Test with a fake gateway, not with HTTP fakes
Because the package's internal HTTP client is not your code, don't try to Http::fake() its calls. Bind a FakeSmsGateway in tests that records messages:
$this->app->instance(SmsGateway::class, $fake = new FakeSmsGateway());
// ...
$fake->assertSentTo('09121234567');
The package is two years stale — audit the endpoints before trusting them
Last release 2023-02-05. sms.ir has revised its API since; verify each call you depend on against current vendor documentation and be prepared to bypass the wrapper for any endpoint that has moved. Pin the version in composer.json ("amirbagh75/smsir-php": "1.*") and do not run composer update on it casually in a working system.
Template parameters are exact-match and unforgiving
The parameter keys ('Code', 'Name', …) must match the template placeholders defined in the sms.ir panel character-for-character, including case. Mismatches typically do not throw a helpful exception — you get a failed send or a delivered message with literal blank spaces where the value should be. Keep template IDs in config, not hardcoded, and sanity-check every new template with a real phone.
Persian text changes your billing math
Non-ASCII (Persian) characters are billed at roughly 70 characters per SMS segment versus ~160 for Latin. A "short" Persian message can silently become two or three segments. If you're estimating cost or building a character counter in an admin UI, use the Persian limit.
Never log the API key or full recipient numbers
Iranian data-protection expectations (and general good hygiene) mean OTP codes and full mobile numbers should not land in your logs or in queued-job payloads that get serialized to the queue driver's storage. Hash or mask numbers in logs; keep the code out of exception messages.
Config caching will bite you
After publishing, php artisan config:cache in deploy is mandatory — but remember that any runtime config(['smsir...' => ...]) mutation stops working once cached. Read config in constructors, not at call time.
Check the HTTP client for timeout and exception behavior
Open the client class and find out: does it set a timeout? Does it throw on non-2xx, or return an array with an error field? A wrapper that returns ['status' => 1, ...] on error and ['status' => 0, ...] on success (a common convention in this ecosystem — note the inversion) will let failures pass silently if you only check for exceptions. Wrap calls and assert on the response explicitly.
try {
$result = Smsir::sendVerifyCode($mobile, $templateId, $params);
} catch (\Throwable $e) {
report($e); // log it
throw new SmsDeliveryFailed(...); // and surface it
}
Delivery reports are likely pull-only
Do not assume webhook/callback support. If your product needs per-message delivery status, you will probably be polling a report endpoint on a schedule and reconciling against your own message_id. Store the vendor's returned message ID on your own model at send time — it is the only join key you'll have later.
Extension points are thin, so build them yourself
Don't expect events, macros, or a pluggable transport. The realistic extension surface is: your SmsGateway contract, your notification channel, and your own queued jobs. That's sufficient — but it means any behavior you want (retry policy, circuit breaker, per-tenant API keys) lives in your app, not the package.
Multi-tenancy caveat
If different tenants need different panel credentials, the facade's single-config model won't help. Instantiate the underlying client directly with per-tenant credentials rather than relying on the singleton Smsir:: binding.
How can I help you explore Laravel packages today?