moffhub/sms-handler
Composer 安装命令:
composer require moffhub/sms-handler
包简介
Unified SMS gateway for Laravel with multi-provider support (Advanta, Africa's Talking, Twilio, Nexmo, Onfon). Queue-ready, delivery tracking, fallback chains, templating, and analytics.
关键字:
README 文档
README
A simple, unified SMS integration library for Laravel. Send SMS messages through multiple providers with a consistent API, automatic fallback, rate limiting, templating, cost estimation, and analytics.
Features
- Send SMS, Bulk SMS, and Scheduled SMS
- Multiple provider support (Advanta, Africa's Talking, Twilio, Nexmo, Onfon Media)
- Custom provider extensibility
- Automatic fallback provider chain
- Per-provider rate limiting
- SMS templating with variable interpolation
- Cost estimation and segment counting
- Analytics and success rate tracking
- Webhook delivery report handling with signature validation
- Events (SmsSent, SmsFailed, DeliveryReportReceived)
- Structured logging with credential scrubbing
- Laravel Notification channel support
- Phone number and message validation
- Database or file logging
Supported Providers
- Advanta - Kenya SMS gateway
- Africa's Talking - Pan-African SMS gateway
- Twilio - Global SMS provider
- Nexmo/Vonage - Global SMS provider
- Onfon Media - Kenya SMS gateway
- Custom - Build your own provider
Installation
composer require moffhub/sms-handler
Configuration
Publish the config and migrations:
php artisan vendor:publish --provider="Moffhub\SmsHandler\SmsHandlerServiceProvider" --tag=sms-config
php artisan vendor:publish --tag=sms-migrations
php artisan migrate
Environment Variables
Add the following to your .env file based on your provider:
# Provider selection SMS_PROVIDER=at # Options: advanta, at, onfon, twilio, nexmo # Africa's Talking AT_USERNAME=sandbox # Use 'sandbox' for testing, your app username for production AT_API_KEY=your_api_key AT_FROM=YOUR_SENDER_ID # Optional: Your registered sender ID/short code AT_API_URL= # Optional: Custom API URL (auto-detected based on username) AT_FALLBACK_PROVIDER= # Optional: Fallback provider name (e.g., 'advanta') AT_RATE_LIMIT= # Optional: Messages per minute (null = unlimited) AT_PER_SEGMENT_COST=0.80 # Optional: Cost per SMS segment # Advanta ADVANTA_API_KEY= ADVANTA_API_URL= ADVANTA_BULK_API_URL= ADVANTA_PARTNER_ID= ADVANTA_SHORT_CODE= ADVANTA_FALLBACK_PROVIDER= ADVANTA_RATE_LIMIT= ADVANTA_PER_SEGMENT_COST=1.50 # Onfon Media ONFON_API_KEY= ONFON_API_URL= ONFON_SENDER_ID= ONFON_CLIENT_ID= ONFON_FALLBACK_PROVIDER= ONFON_RATE_LIMIT= ONFON_PER_SEGMENT_COST= # Nexmo/Vonage NEXMO_KEY= NEXMO_SECRET= NEXMO_FROM=NEXMO NEXMO_API_URL=https://rest.nexmo.com/sms/json NEXMO_FALLBACK_PROVIDER= NEXMO_RATE_LIMIT= NEXMO_PER_SEGMENT_COST= # Twilio TWILIO_SID= TWILIO_TOKEN= TWILIO_FROM= TWILIO_API_URL=https://api.twilio.com TWILIO_FALLBACK_PROVIDER= TWILIO_RATE_LIMIT= TWILIO_PER_SEGMENT_COST= # Logging SMS_LOG_CHANNEL=log # Options: log, model SMS_STRUCTURED_LOG_CHANNEL= # Optional: Separate log channel for structured SMS logs # Webhooks SMS_WEBHOOKS_ENABLED=false SMS_WEBHOOKS_PREFIX=sms/webhooks SMS_WEBHOOKS_RATE_LIMIT=60 SMS_WEBHOOK_SECRET_ADVANTA= SMS_WEBHOOK_SECRET_AFRICASTALKING= SMS_WEBHOOK_SECRET_ONFON= SMS_WEBHOOK_SECRET_NEXMO= SMS_WEBHOOK_SECRET_TWILIO= # Queue SMS_QUEUE_NAME=default SMS_QUEUE_TIMEOUT=30 SMS_QUEUE_MAX_TRIES=3
Usage
Using the Facade
use Moffhub\SmsHandler\Facades\Sms; // Send a single SMS Sms::sendSms('+254712345678', 'Hello World'); // Send bulk SMS Sms::sendBulkSms(['+254712345678', '+254712345679'], 'Hello everyone!'); // Send scheduled SMS Sms::sendScheduledSms('+254712345678', 'Reminder!', '2024-12-25 09:00:00'); // Check delivery status $status = Sms::getSmsDeliveryStatus('message_id_here');
Using Dependency Injection
use Moffhub\SmsHandler\Services\SmsService; class NotificationController extends Controller { public function __construct(protected SmsService $smsService) {} public function notify(Request $request) { $this->smsService->sendSms( $request->phone, $request->message ); } }
Switching Providers at Runtime
use Moffhub\SmsHandler\SmsManager; $manager = app(SmsManager::class); // Use Africa's Talking for this message $manager->driver('at')->sendSms('+254712345678', 'Via AT'); // Use Twilio for this message $manager->driver('twilio')->sendSms('+1234567890', 'Via Twilio');
Fallback Provider Chain
Configure a fallback provider that is used automatically when the primary provider fails with a ProviderException. Fallback is limited to one level (no chaining beyond the fallback).
# In .env
ADVANTA_FALLBACK_PROVIDER=africastalking
Or in config/sms.php:
'providers' => [ 'advanta' => [ // ...credentials... 'fallback' => 'africastalking', ], ],
When the primary provider fails, the package will:
- Log the failure
- Automatically retry with the fallback provider
- Log the fallback activation
- Return the fallback result (or null if the fallback also fails)
Non-ProviderException errors (e.g., validation errors) are not retried and are rethrown.
Rate Limiting
Configure per-provider rate limits (messages per minute). When the limit is exceeded, messages are automatically queued for later delivery instead of being rejected.
# In .env - allow 100 messages per minute for Advanta
ADVANTA_RATE_LIMIT=100
Or in config/sms.php:
'providers' => [ 'advanta' => [ // ...credentials... 'rate_limit' => 100, // messages per minute, null = unlimited ], ],
Programmatic access:
use Moffhub\SmsHandler\Facades\Sms; // Check remaining attempts $remaining = Sms::rateLimiter()->remainingAttempts('advanta'); // Clear the rate limiter for a provider Sms::rateLimiter()->clear('advanta');
Rate Limiting Recommendations
- Set rate limits based on your provider's API quotas to avoid being blocked.
- Start with conservative limits and increase as needed.
- Monitor your queue to ensure rate-limited messages are being delivered.
- Use
null(unlimited) only for providers with no known API rate limits.
SMS Templating
Define reusable SMS templates with {{ variable }} interpolation:
// config/sms.php 'templates' => [ 'otp' => 'Your verification code is {{ code }}. Valid for {{ minutes }} minutes.', 'welcome' => 'Welcome {{ name }}! Thanks for joining us.', 'order_shipped' => ['body' => 'Hi {{ name }}, your order #{{ order_id }} has shipped.'], ],
Send a templated SMS:
use Moffhub\SmsHandler\Facades\Sms; // Fluent API Sms::template('otp', ['code' => '1234', 'minutes' => '5']) ->to('+254712345678') ->send(); // Check if a template exists Sms::templateService()->exists('otp'); // true // Get all template names Sms::templateService()->getTemplateNames(); // ['otp', 'welcome', 'order_shipped']
Message length is validated after interpolation. If the rendered message exceeds the configured max_message_length, an InvalidMessageException is thrown.
Cost Estimation
Estimate the cost of sending an SMS before dispatching. The estimator calculates SMS segment count based on message encoding (GSM-7 vs UCS-2) and multiplies by the configured per-segment cost.
use Moffhub\SmsHandler\Facades\Sms; $estimate = Sms::estimateCost('Hello world', 100, 'advanta'); // Returns: // [ // 'segments' => 1, // 'per_segment_cost' => 1.50, // 'total_cost' => 150.0, // 'recipient_count' => 100, // 'is_unicode' => false, // ]
Segment Counting Rules
| Encoding | Single SMS | Multi-part (per segment) |
|---|---|---|
| GSM-7 | 160 chars | 153 chars |
| UCS-2 | 70 chars | 67 chars |
Unicode characters (emoji, CJK, Arabic, etc.) force UCS-2 encoding, which reduces the per-segment capacity.
Configure per-segment cost in your provider config:
'providers' => [ 'advanta' => [ // ...credentials... 'per_segment_cost' => 1.50, ], ],
When logging to the database (SMS_LOG_CHANNEL=model), the estimated_cost and segment_count columns are automatically populated on each SmsLog record.
Analytics & Success Rate Tracking
Query SMS analytics aggregated from the sms_logs table:
use Moffhub\SmsHandler\Facades\Sms; // Overall summary $summary = Sms::analytics()->summary(); // ['total_sent' => 1000, 'total_delivered' => 950, 'total_failed' => 50, 'success_rate' => 95.0, ...] // Filter by provider $summary = Sms::analytics()->forProvider('twilio')->summary(); // Filter by date range $summary = Sms::analytics()->last30Days()->summary(); $summary = Sms::analytics()->last7Days()->summary(); $summary = Sms::analytics()->between($from, $to)->summary(); // Combine filters $summary = Sms::analytics()->forProvider('advanta')->last30Days()->summary(); // Daily breakdown $breakdown = Sms::analytics()->last30Days()->dailyBreakdown(); // Collection of ['date' => '2024-01-15', 'sent' => 100, 'delivered' => 95, 'failed' => 5] // Per-provider summary $providers = Sms::analytics()->perProviderSummary(); // Collection of ['provider' => 'advanta', 'sent' => 500, 'delivered' => 490, 'failed' => 10, 'success_rate' => 98.0]
CLI Stats Command
View SMS statistics from the command line:
# Last 30 days (default) php artisan sms:stats # Last 7 days php artisan sms:stats --days=7 # Filter by provider php artisan sms:stats --provider=advanta # Combined php artisan sms:stats --provider=twilio --days=14
Error Handling
Exception Types
The package defines a hierarchy of exceptions:
| Exception | Description |
|---|---|
SmsException |
Base exception class for all SMS errors |
ProviderException |
Provider-level failures (API errors, timeouts). Triggers fallback if configured. |
InvalidPhoneNumberException |
Invalid, empty, or malformed phone numbers |
InvalidMessageException |
Empty or too-long messages |
Handling Exceptions
use Moffhub\SmsHandler\Exceptions\ProviderException; use Moffhub\SmsHandler\Exceptions\InvalidPhoneNumberException; use Moffhub\SmsHandler\Exceptions\InvalidMessageException; try { Sms::sendSms($phone, $message); } catch (InvalidPhoneNumberException $e) { // Phone number validation failed // e.g., "Invalid phone number '123': Phone number must have at least 9 digits" } catch (InvalidMessageException $e) { // Message validation failed // e.g., "SMS message cannot be empty" // e.g., "Message exceeds maximum of 918 characters" } catch (ProviderException $e) { // Provider API error — fallback was already attempted if configured // e.g., "SMS provider 'advanta' failed to send: Connection timeout" }
Events
Listen for SMS lifecycle events:
use Moffhub\SmsHandler\Events\SmsSent; use Moffhub\SmsHandler\Events\SmsFailed; use Moffhub\SmsHandler\Events\DeliveryReportReceived; // In EventServiceProvider protected $listen = [ SmsSent::class => [SmsSuccessListener::class], SmsFailed::class => [SmsFailureListener::class], DeliveryReportReceived::class => [DeliveryReportListener::class], ]; // SmsSent carries: provider, to, message, messageId, response // SmsFailed carries: provider, to, message, exception // DeliveryReportReceived carries: provider, messageId, status, phoneNumber, payload
Webhook Setup
Enable webhooks to receive delivery reports from providers:
SMS_WEBHOOKS_ENABLED=true
SMS_WEBHOOKS_PREFIX=sms/webhooks # URL prefix
This registers POST routes for each provider:
| Provider | Endpoint | Route Name |
|---|---|---|
| Advanta | POST /sms/webhooks/advanta |
sms.webhooks.advanta |
| Africa's Talking | POST /sms/webhooks/africastalking |
sms.webhooks.africastalking |
| Onfon | POST /sms/webhooks/onfon |
sms.webhooks.onfon |
| Nexmo | POST /sms/webhooks/nexmo |
sms.webhooks.nexmo |
| Twilio | POST /sms/webhooks/twilio |
sms.webhooks.twilio |
Webhook Signature Validation
Set webhook secrets to validate incoming requests:
SMS_WEBHOOK_SECRET_TWILIO=your_twilio_auth_token SMS_WEBHOOK_SECRET_AFRICASTALKING=your_callback_token SMS_WEBHOOK_SECRET_ADVANTA=your_shared_secret SMS_WEBHOOK_SECRET_NEXMO=your_signature_secret SMS_WEBHOOK_SECRET_ONFON=your_api_key
If a secret is configured, the package validates the signature header on incoming webhook requests. Invalid signatures receive a 403 response.
Provider Webhook Setup
Twilio:
- Go to your Twilio Console > Phone Numbers > Active Numbers
- Click your number and set the "A MESSAGE COMES IN" webhook URL to your endpoint
- Set the Status Callback URL to
https://yourdomain.com/sms/webhooks/twilio
Africa's Talking:
- Go to your AT Dashboard > SMS > SMS Callback URLs
- Set the Delivery Reports URL to
https://yourdomain.com/sms/webhooks/africastalking
Advanta:
- Configure the callback URL in your Advanta dashboard or pass it in the API request
- Set it to
https://yourdomain.com/sms/webhooks/advanta
Nexmo/Vonage:
- Go to Vonage Dashboard > Settings
- Set the SMS Delivery Receipt URL to
https://yourdomain.com/sms/webhooks/nexmo
Onfon:
- Contact Onfon support to configure your callback URL
- Set it to
https://yourdomain.com/sms/webhooks/onfon
Delivery Report Payload Examples
Twilio:
{
"MessageSid": "SM1234567890",
"MessageStatus": "delivered",
"To": "+254712345678",
"ErrorCode": null
}
Africa's Talking:
{
"id": "ATXid_123",
"status": "Success",
"phoneNumber": "+254712345678",
"failureReason": ""
}
Advanta:
{
"messageId": "msg123",
"status": "DeliveredToTerminal",
"phoneNumber": "254712345678"
}
Nexmo/Vonage:
{
"messageId": "0C000000217B7F02",
"status": "delivered",
"to": "254712345678",
"err-code": "0"
}
Onfon:
{
"MessageId": "12345",
"Status": "DELIVERED",
"Number": "254712345678"
}
Africa's Talking Integration
The library fully supports the Africa's Talking Bulk SMS API:
Sandbox Testing
AT_USERNAME=sandbox AT_API_KEY=your_sandbox_api_key
Production
AT_USERNAME=your_app_username AT_API_KEY=your_production_api_key AT_FROM=YOUR_SENDER_ID
Features
- Automatic sandbox/production URL detection
- Phone number formatting (supports 0712..., 254712..., +254712...)
- Bulk SMS with enqueue support
- Sender ID/Short code support
- Detailed response handling with message IDs and costs
Custom Providers
Create your own provider by extending CustomProvider:
use Moffhub\SmsHandler\Providers\CustomProvider; use Illuminate\Support\Collection; class MySmsProvider extends CustomProvider { protected function getApiUrl(): string { return 'https://api.custom.com/send'; } protected function buildPayload(string $to, string $message): array { return [ 'to' => $to, 'text' => $message, 'api_key' => $this->config['key'], ]; } protected function handleResponse(mixed $response): ?Collection { return collect([ 'status' => $response['status'] ?? 'unknown', ]); } }
Register your provider:
// In a service provider use Moffhub\SmsHandler\SmsManager; $this->app->make(SmsManager::class)->extend('custom', function ($app) { return new MySmsProvider([ 'key' => config('sms.providers.custom.key'), ]); });
Add config:
// config/sms.php 'providers' => [ 'custom' => [ 'key' => env('MY_CUSTOM_API_KEY'), ], ],
Update .env:
SMS_PROVIDER=custom MY_CUSTOM_API_KEY=super-secret
Laravel Notifications
Use SMS in Laravel notifications:
use Moffhub\SmsHandler\Notifications\SmsChannel; class OrderShipped extends Notification { public function via($notifiable): array { return [SmsChannel::class]; } public function toSms($notifiable): string { return 'Your order has been shipped!'; } }
Ensure your notifiable model has a routeNotificationForSms method:
public function routeNotificationForSms(): string { return $this->phone; }
Logging
SMS messages can be logged to file or database:
# Log to Laravel's log file SMS_LOG_CHANNEL=log # Log to database (requires migration) SMS_LOG_CHANNEL=model
When using database logging, each SmsLog record includes:
provider- The provider class usedto- Recipient phone numbermessage- Message contentsuccess- Boolean success statusmessage_id- Provider message IDdelivery_status- Updated via webhooksestimated_cost- Calculated cost based on segments and provider ratesegment_count- Number of SMS segmentsscheduled_at- For scheduled messagesresponse- Raw provider response
Structured Logging
The package logs structured events to a configurable log channel:
SMS_STRUCTURED_LOG_CHANNEL=sms # Optional: dedicated log channel
Log events include:
sms.sent- Successful send with provider, recipient, message_idsms.failed- Failed send with provider, recipient, error detailssms.bulk_failed- Bulk send failuresms.delivery_report- Incoming delivery report
All log entries are scrubbed of sensitive data (API keys, tokens, secrets).
Testing & Mocking
Faking HTTP Requests
Use Laravel's HTTP faking to test SMS sending without making real API calls:
use Illuminate\Support\Facades\Http; use Moffhub\SmsHandler\Facades\Sms; Http::fake([ '*' => Http::response([ 'responses' => [ [ 'response-code' => 200, 'response-description' => 'Success', 'mobile' => '254712345678', 'messageid' => 'msg123', ], ], ]), ]); Sms::sendSms('+254712345678', 'Test message'); Http::assertSentCount(1);
Faking Events
Test that events are dispatched correctly:
use Illuminate\Support\Facades\Event; use Moffhub\SmsHandler\Events\SmsSent; use Moffhub\SmsHandler\Events\SmsFailed; Event::fake([SmsSent::class, SmsFailed::class]); Sms::sendSms('+254712345678', 'Test'); Event::assertDispatched(SmsSent::class, function ($event) { return $event->to === '+254712345678'; });
Testing Templates
use Moffhub\SmsHandler\Services\TemplateService; $service = new TemplateService(); $rendered = $service->render('otp', ['code' => '1234']); $this->assertEquals('Your code is 1234.', $rendered);
Testing Cost Estimation
use Moffhub\SmsHandler\Facades\Sms; $estimate = Sms::estimateCost('Short message', 1, 'advanta'); $this->assertEquals(1, $estimate['segments']); $this->assertEquals(1.50, $estimate['total_cost']);
Troubleshooting
Common Issues
SMS not sending:
- Check your provider credentials in
.env - Verify
SMS_PROVIDERis set to a valid provider name - Check
SMS_LOG_CHANNEL=logand review Laravel logs for errors - Ensure your queue worker is running if using queued sending
Validation errors:
- Phone numbers must have at least 9 digits and no more than 15 (E.164)
- Messages cannot be empty or exceed
SMS_MAX_MESSAGE_LENGTH(default: 918 characters) - Phone numbers are auto-formatted; supported formats:
0712345678,254712345678,+254712345678
Rate limiting:
- If messages are being queued unexpectedly, check
rate_limitconfig per provider - Use
Sms::rateLimiter()->remainingAttempts('provider')to check remaining quota - Clear the limiter with
Sms::rateLimiter()->clear('provider')
Webhooks not receiving reports:
- Ensure
SMS_WEBHOOKS_ENABLED=true - Verify your webhook URL is publicly accessible (use ngrok for local dev)
- Check webhook secrets match what the provider expects
- Review logs for 403 responses (signature validation failures)
Fallback not activating:
- Fallback only triggers on
ProviderException, not on validation errors - Verify the fallback provider name matches a configured driver
- Fallback is limited to 1 level -- the fallback provider's own fallback is not used
- Check logs for "SMS fallback activated" messages
Cost estimation showing 0:
- Ensure
per_segment_costis configured for the provider inconfig/sms.php - Cost is 0.0 by default if not configured
Debug Logging
Enable debug-level structured logging:
// config/logging.php 'channels' => [ 'sms' => [ 'driver' => 'daily', 'path' => storage_path('logs/sms.log'), 'level' => 'debug', ], ],
SMS_STRUCTURED_LOG_CHANNEL=sms
Testing
composer test
License
MIT License. See LICENSE for details.
moffhub/sms-handler 适用场景与选型建议
moffhub/sms-handler 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 3.21k 次下载、GitHub Stars 达 0, 最近一次更新时间为 2024 年 09 月 04 日, 在 PHP 生态内属于活跃度较高的组件。
它主要适用于以下技术方向: 「sms」 「twilio」 「laravel」 「nexmo」 「africa」 「vonage」 等业务场景。在实际项目中,围绕这些方向常见需要落地的问题包括:接口对接、性能调优、并发安全、与既有框架(Laravel / ThinkPHP / Yii / Webman 等)的兼容适配,以及生产环境的日志埋点与稳定性保障。
我们在过去多个企业项目中使用过 moffhub/sms-handler 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。
基于 moffhub/sms-handler 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。
线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。
承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。
与 moffhub/sms-handler 相关的其它包
同方向 / 同关键字的高下载量 PHP Composer 包推荐,方便对比选型:
SDK for payment gateway PlatbaMobilom.sk for PHP7.0
Componente para integração com serviços de mensageria com APIs externas como Twilio, SendGrid, AWS SES, etc.
Extensible library for building notifications and sending them via different delivery channels.
yii2-sms expand
Alfabank REST API integration
统计信息
- 总下载量: 3.21k
- 月度下载量: 0
- 日度下载量: 0
- 收藏数: 0
- 点击次数: 37
- 依赖项目数: 0
- 推荐数: 0
其他信息
- 授权协议: MIT
- 更新时间: 2024-09-04