toplan/phpsms
PHP SMS toolkit for Laravel and standalone apps with a unified API, multiple gateway drivers, templates, and simple configuration. Send, queue, and manage SMS messages reliably through popular providers.
Install
composer require toplan/phpsms
Laravel 5.5+ auto-discovers Toplan\PhpSms\PhpSmsServiceProvider. On older versions register the provider and the Sms alias manually in config/app.php:
'providers' => [Toplan\PhpSms\PhpSmsServiceProvider::class],
'aliases' => ['Sms' => Toplan\PhpSms\Facades\Sms::class],
Then publish the config:
php artisan vendor:publish --provider="Toplan\PhpSms\PhpSmsServiceProvider"
This gives you config/phpsms.php — the single most important file in the package.
Where to look first
config/phpsms.php — the agents map (credentials, weight, retry, sign, default template) and the global scheme/timeout keys.src/Agents/ — one class per provider (Luosimao, YunPian, SubMail, Aliyun, YunTongXun, Chuanglan, a Log agent, etc.). Read the one you actually plan to use; each documents its own config keys.src/Agent.php — the abstract base: this is the contract you implement if you need a provider that isn't bundled.First use case — send one message, log it locally
Start with the Log agent so you can build and test the whole flow without spending money or holding real credentials:
use Toplan\PhpSms\Sms;
$sms = Sms::make(['Log'])
->to('13800138000')
->content('Your verification code is 1234')
->send();
if ($sms->success) {
// delivered (or, for Log, written to the log)
}
Once that works, swap ['Log'] for a real agent name and fill in its credentials in config/phpsms.php.
Useful when a provider is contractually mandated or you need a specific sender ID:
$sms = Sms::make(['YunPian'])
->to($mobile)
->content('...')
->send();
Pass several agents and let the package walk them in weight order, retrying each per its retry setting:
// config/phpsms.php
'agents' => [
'YunPian' => ['send_key' => env('YUNPIAN_KEY'), 'weight' => 10, 'retry' => 2],
'Luosimao' => ['send_key' => env('LUOSIMAO_KEY'), 'weight' => 5, 'retry' => 1],
],
// code
$sms = Sms::make(['YunPian', 'Luosimao'])
->to($mobile)
->content('...')
->send();
You can also override per-agent options at call time by passing an associative array, so one flow can use a different template than the configured default:
Sms::make([
'YunPian' => ['template' => config('sms.tpl.notify')],
'Luosimao' => ['weight' => 1],
])->to($mobile)->data(['name' => 'Ann'])->send();
Chinese providers almost always require a pre-approved template. Where an agent supports it, prefer the template path over raw content:
$sms = Sms::make(['YunPian'])
->to($mobile)
->template($templateId, ['code' => $code]) // positional data fills {placeholders}
->send();
Keep content sending for providers/regions that allow it, and centralize your copy in config or a translation file so the {placeholder} names stay in sync with what the provider approved.
Verification flows often fall back to a robocall when SMS fails. The package exposes a voice entry point on agents that support it — inspect the agent class before assuming:
$sms = Sms::make(['YunTongXun'])
->to($mobile)
->voice($code)
->send();
SMS gateways are slow and flaky; never block a user-facing response on them. Push the send onto the queue and consume the result asynchronously:
// Controller
Sms::queue()->to($mobile)->content('...'); // returns a closure/job for your queue
// Or wrap in your own job for full control
dispatch(new SendSmsJob($mobile, $code));
The package's queue helper only enqueues; retry/backoff semantics are yours unless you enable per-agent retry.
Static hooks let you centralize observability without touching every call site. Register them once in a service provider:
Sms::beforeSend(function ($sms, $agent) { /* ... */ });
Sms::afterSend(function ($sms, $agent) { /* ... */ });
Sms::sendSuccess(function ($sms, $agent) { Log::info(...); });
Sms::sendFail(function ($sms, $agent) { Log::warning(...); });
These fire per agent attempt, so they're the right place to count failures per provider and alert on a degraded gateway.
send() never throws on provider failure — it records an outcome per agent:
$sms = Sms::make([...])->to($mobile)->content('...')->send();
if (! $sms->success) {
// per-agent detail: which agent, success flag, provider message/timings
foreach ($sms->result as $agent => $detail) { /* ... */ }
}
Treat $sms->result as the audit record: persist it against the notification so support can answer "did it go out, and through which gateway?".
When you need a provider that isn't bundled (or a regional gateway), extend Toplan\PhpSms\Agent, implement the send methods it declares abstract, and assign to $this->result inside each. Register it under agents in config/phpsms.php with the same key you'll pass to Sms::make(). Read the abstract class first — the exact set of methods and result shape is small but specific, and it varies by which capabilities (content, template, voice) your agent claims.
composer.json and run your test suite on the exact PHP version you deploy. The release predates PHP 7.4/8.x, so expect deprecation noise (dynamic properties, nullable signatures) or outright breakage on modern PHP. If you must run PHP 8, budget for a vendor fork or a thin adapter rather than assuming it works.send() returns an object; it does not throw. If you skip the $sms->success check your code will happily report "sent" when every gateway rejected you. Lint for unchecked sends in review.Sms class. Hooks and other globals live on a static facade. In long-running processes (queue workers, Octane, Swoole) they accumulate across jobs. Reset between jobs/tests — the package ships a static reset helper (cleanRuntime() in recent versions); verify the name against your pinned tag.env() inside config/phpsms.php freeze after php artisan config:cache. Deploy with env set, then cache; don't cache locally and ship the file.weight orders attempts; it does not re-route on a provider timeout unless retry is high enough to burn through. Set an overall timeout short enough that the worst case (N agents × M retries × timeout) still fits your queue worker's job timeout.src/Content.php before trusting the cost estimate.template ID and its {placeholder} names must match exactly what was approved in the provider console; a mismatch usually surfaces as a generic provider error inside $sms->result, not as a validation error.sign/sign_name in config. It's easy to forget in staging and get a rejection that reads like an auth failure.Log agent in local/testing (AppServiceProvider: if (app()->environment('local')) { config(['phpsms.scheme' => ['Log']]); }) and never let test suites hit a paid gateway.Agent base class plus a config entry is the escape hatch for a new provider, IP allowlisting differences, or a proprietary internal gateway — cheaper than forking the library.How can I help you explore Laravel packages today?