textify
Send SMS via multiple providers with a unified API, queue support, Laravel notifications, fallback routing, and activity tracking.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Send SMS via multiple providers with a unified API, queue support, Laravel notifications, fallback routing, and activity tracking.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
| name | textify |
| description | Send SMS via multiple providers with a unified API, queue support, Laravel notifications, fallback routing, and activity tracking. |
Activate this skill when:
ALWAYS use the Textify facade — never instantiate providers directly:
use DevWizard\Textify\Facades\Textify;
// CORRECT
$response = Textify::send('01712345678', 'Your OTP is 123456');
// WRONG — bypasses lifecycle, events, tracking
$provider = new DhorolaSmsProvider($config);
$provider->send($message);
ALWAYS check the response after sending:
$response = Textify::send($phone, $message);
if ($response->isSuccessful()) {
$messageId = $response->getMessageId();
$cost = $response->getCost();
} else {
$error = $response->getErrorMessage();
$code = $response->getErrorCode();
Log::error("SMS failed: {$error}", ['code' => $code, 'to' => $phone]);
}
Use the fluent builder for complex sends. Methods like via(), to(), message(), and from() return a clone; fallback() mutates the current instance:
// With specific provider
$response = Textify::via('twilio')
->to('+1234567890')
->message('Hello from Twilio!')
->from('MyApp')
->send();
// With fallback
$response = Textify::via('dhorola')
->fallback('bulksmsbd')
->to('01712345678')
->message('Critical alert!')
->sendWithFallback();
For multiple recipients, pass an array to to():
$responses = Textify::to(['01712345678', '01812345678', '01912345678'])
->message('System maintenance at 2 AM.')
->send();
// Returns array of TextifyResponse — one per recipient
ALWAYS queue non-critical SMS to avoid blocking HTTP requests:
// CORRECT — queued, non-blocking
Textify::to($user->phone)
->message('Your weekly report is ready.')
->queue();
// With specific queue name
Textify::to($user->phone)
->message('Your weekly report is ready.')
->queue('sms');
// Only send synchronously for critical, time-sensitive messages (OTP)
$response = Textify::send($phone, "Your OTP is {$otp}");
ALWAYS use fallback() for critical messages (OTP, password reset, alerts). Use sendWithFallback() to enable automatic fallback routing:
$response = Textify::via('dhorola')
->fallback('bulksmsbd')
->to($phone)
->message("Your verification code is {$code}")
->sendWithFallback();
NEVER hardcode provider names in application code when possible. Use the config default:
// CORRECT — uses TEXTIFY_PROVIDER from .env
Textify::send($phone, $message);
// OK — when you explicitly need a specific provider for this message
Textify::via('twilio')->send($phone, $message);
// WRONG — hardcoded everywhere, hard to change
Textify::via('dhorola')->send($phone, $message); // in every controller
ALWAYS use the textify notification channel for user-facing SMS (order updates, reminders, alerts):
use Illuminate\Notifications\Notification;
use DevWizard\Textify\Notifications\TextifyMessage;
class OrderShipped extends Notification
{
public function __construct(private Order $order) {}
public function via($notifiable): array
{
return ['textify'];
}
public function toTextify($notifiable): TextifyMessage
{
return TextifyMessage::create(
__('Your order #:id has been shipped!', ['id' => $this->order->id])
);
}
}
ALWAYS implement routeNotificationForTextify() on your notifiable model for explicit phone number routing:
class User extends Authenticatable
{
use Notifiable;
public function routeNotificationForTextify($notification): string
{
return $this->phone_number;
}
}
The channel auto-detects these attributes if the method is not defined: phone_number, phone, mobile, phn, mobile_number, cell. But explicit routing is preferred.
To specify a provider per notification, use ->driver() on the message:
public function toTextify($notifiable): TextifyMessage
{
return TextifyMessage::create(__('Your OTP is :code', ['code' => $this->code]))
->from('MyApp')
->driver('twilio'); // Use Twilio for this specific notification
}
Send notifications the standard Laravel way:
// Single user
$user->notify(new OrderShipped($order));
// Multiple users
Notification::send($users, new OrderShipped($order));
ALWAYS extend TextifyProvider (simplified base) for custom providers:
use DevWizard\Textify\Providers\TextifyProvider;
use DevWizard\Textify\DTOs\TextifyMessage;
use DevWizard\Textify\DTOs\TextifyResponse;
class MySmsProvider extends TextifyProvider
{
public function getProviderName(): string
{
return 'mysms';
}
protected function getRequiredConfigKeys(): array
{
return ['api_key', 'sender_id'];
}
protected function sendSms(TextifyMessage $message): array
{
$response = Http::post('https://api.mysms.com/send', [
'api_key' => $this->config['api_key'],
'to' => $message->getTo(),
'message' => $message->getMessage(),
'from' => $message->getFrom() ?? $this->config['sender_id'],
]);
return $response->json();
}
protected function parseApiResponse(array $response): TextifyResponse
{
if ($response['status'] === 'success') {
return TextifyResponse::success(
messageId: $response['id'],
cost: $response['cost'] ?? null,
status: 'sent',
rawResponse: $response,
);
}
return TextifyResponse::failed(
errorMessage: $response['error'] ?? 'Unknown error',
errorCode: $response['code'] ?? 'UNKNOWN',
rawResponse: $response,
);
}
}
Register custom providers in config, not in service providers:
// config/textify.php → providers
'mysms' => [
'driver' => 'mysms',
'class' => \App\Services\MySmsProvider::class,
'api_key' => env('MYSMS_API_KEY'),
'sender_id' => env('MYSMS_SENDER_ID'),
],
ALWAYS use TextifyResponse::success() and TextifyResponse::failed() static factories in custom providers. Never construct TextifyResponse directly.
ALWAYS set TEXTIFY_PROVIDER per environment in .env:
# .env (development)
TEXTIFY_PROVIDER=log
# .env (testing)
TEXTIFY_PROVIDER=array
# .env (production)
TEXTIFY_PROVIDER=dhorola
TEXTIFY_FALLBACK_PROVIDER=bulksmsbd
ALWAYS use the array driver in tests, then assert against stored messages:
use DevWizard\Textify\Providers\ArrayProvider;
// In test setup or phpunit.xml
// TEXTIFY_PROVIDER=array
public function test_otp_is_sent(): void
{
ArrayProvider::clearMessages();
// Trigger the action that sends SMS
$this->post('/send-otp', ['phone' => '01712345678']);
$messages = ArrayProvider::getMessages();
$this->assertCount(1, $messages);
$this->assertStringContainsString('OTP', $messages[0]['message']);
$this->assertEquals('01712345678', $messages[0]['to']);
}
protected function tearDown(): void
{
ArrayProvider::clearMessages();
parent::tearDown();
}
NEVER send real SMS in tests. The array driver stores messages in memory without making HTTP calls.
Listen to SMS events for logging, metrics, or side effects:
use DevWizard\Textify\Events\TextifySending;
use DevWizard\Textify\Events\TextifySent;
use DevWizard\Textify\Events\TextifyFailed;
// In a listener or EventServiceProvider
Event::listen(TextifySent::class, function (TextifySent $event) {
// $event->message — TextifyMessage DTO
// $event->response — TextifyResponse DTO
// $event->provider — string provider name
});
Event::listen(TextifyFailed::class, function (TextifyFailed $event) {
// $event->exception — ?\Throwable
// Alert ops team, retry logic, etc.
});
For production monitoring, enable database activity tracking:
// config/textify.php
'activity_tracking' => [
'enabled' => true,
'driver' => 'database',
],
php artisan textify:table
php artisan migrate
Query activity tracking via the TextifyActivity model:
use DevWizard\Textify\Models\TextifyActivity;
// Today's stats
$sent = TextifyActivity::successful()->today()->count();
$failed = TextifyActivity::failed()->today()->count();
// By provider
$twilioFailed = TextifyActivity::failed()->byProvider('twilio')->lastDays(7)->get();
// By recipient
$history = TextifyActivity::byRecipient('01712345678')->latest()->get();
// Controller
class OtpController extends Controller
{
public function send(Request $request)
{
$request->validate(['phone' => 'required|string']);
$otp = random_int(100000, 999999);
cache()->put("otp:{$request->phone}", $otp, now()->addMinutes(5));
cache()->put("otp_attempts:{$request->phone}", 0, now()->addMinutes(5));
$response = Textify::via(config('textify.default'))
->fallback(config('textify.fallback'))
->to($request->phone)
->message(__('Your verification code is :code. Valid for 5 minutes.', ['code' => $otp]))
->sendWithFallback();
if ($response->isFailed()) {
return back()->withErrors(['phone' => __('Failed to send OTP. Please try again.')]);
}
return redirect()->route('otp.verify');
}
public function verify(Request $request)
{
$request->validate([
'phone' => 'required|string',
'otp' => 'required|digits:6',
]);
// Rate limit: max 5 attempts per phone number
$attemptsKey = "otp_attempts:{$request->phone}";
$attempts = (int) cache()->get($attemptsKey, 0);
if ($attempts >= 5) {
cache()->forget("otp:{$request->phone}");
cache()->forget($attemptsKey);
return back()->withErrors(['otp' => __('Too many attempts. Please request a new OTP.')]);
}
cache()->increment($attemptsKey);
$cached = cache()->pull("otp:{$request->phone}");
if (!$cached || (int) $request->otp !== $cached) {
// Put back so subsequent attempts can still match (pull removes it)
if ($cached) {
cache()->put("otp:{$request->phone}", $cached, now()->addMinutes(5));
}
return back()->withErrors(['otp' => __('Invalid or expired OTP.')]);
}
cache()->forget($attemptsKey);
// OTP verified — proceed with action
}
}
// app/Notifications/OrderStatusChanged.php
class OrderStatusChanged extends Notification
{
public function __construct(
private Order $order,
private string $status,
) {}
public function via($notifiable): array
{
return ['textify', 'mail'];
}
public function toTextify($notifiable): TextifyMessage
{
$message = match ($this->status) {
'confirmed' => __('Order #:id confirmed! We are preparing it.', ['id' => $this->order->id]),
'shipped' => __('Order #:id shipped! Track: :url', ['id' => $this->order->id, 'url' => $this->order->tracking_url]),
'delivered' => __('Order #:id delivered! Thank you for shopping.', ['id' => $this->order->id]),
};
return TextifyMessage::create($message)->from('MyStore');
}
}
// Usage in controller/service
$order->customer->notify(new OrderStatusChanged($order, 'shipped'));
// app/Jobs/SendPromotionalSms.php
class SendPromotionalSms implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(
private string $message,
private array $phoneNumbers,
) {}
public function handle(): void
{
foreach (array_chunk($this->phoneNumbers, 50) as $batch) {
Textify::to($batch)
->message($this->message)
->queue('sms-bulk');
}
}
}
// Dispatch
SendPromotionalSms::dispatch('Flash sale! 50% off everything today.', $phones);
| Provider | Driver Key | Region | Required Config |
|---|---|---|---|
| Dhorola | dhorola | BD | api_key, sender_id |
| BulkSMS BD | bulksmsbd | BD | api_key, sender_id |
| MIM SMS | mimsms | BD | username, apikey |
| eSMS | esms | BD | api_token, sender_id |
| Reve SMS | revesms | BD | apikey, secretkey, client_id |
| Alpha SMS | alphasms | BD | api_key |
| Twilio | twilio | Global | account_sid, auth_token, from |
| Nexmo/Vonage | nexmo | Global | api_key, api_secret, from |
| Log | log | Dev | — (writes to Laravel log) |
| Array | array | Test | — (stores in memory) |
Phone number formatting: Bangladeshi providers auto-format to their required format (01XXXXXXXXX or 8801XXXXXXXXX). Global providers use +E.164. You do NOT need to pre-format numbers.
TextifyResponse)| Method | Returns | Description |
|---|---|---|
isSuccessful() | bool | Was the send successful |
isFailed() | bool | Did the send fail |
getMessageId() | ?string | Provider's message ID |
getErrorMessage() | ?string | Human-readable error |
getErrorCode() | ?string | Machine-readable error code |
getCost() | ?float | Cost of the message |
getStatus() | ?string | Delivery status string |
getRawResponse() | array | Full provider response |
php artisan textify:test --to=01712345678 --message="Test" # Send test SMS
php artisan textify:test --driver=twilio # Test specific provider
php artisan textify:table # Publish activity migration
| Anti-Pattern | Correct Pattern |
|---|---|
| Instantiating providers directly | Use Textify:: facade or Textify::via('name') |
| Sending SMS synchronously in web requests for non-critical messages | Use ->queue() for non-critical SMS |
| No fallback for OTP/critical messages | Use ->fallback('provider')->sendWithFallback() |
Hardcoding Textify::via('dhorola') everywhere | Set TEXTIFY_PROVIDER in .env, use default |
| Running real SMS in tests | Set TEXTIFY_PROVIDER=array in test env |
Not checking $response->isSuccessful() | ALWAYS check response and handle failures |
| Manual phone number formatting | Providers auto-format — pass raw numbers |
Using Textify::send() in Notification classes | Use toTextify() returning TextifyMessage |
Not clearing ArrayProvider::clearMessages() between tests | ALWAYS clear in tearDown() |