Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Easy Sms Laravel Package

overtrue/easy-sms

overtrue/easy-sms is a PHP SMS component with one unified API for dozens of Chinese cloud providers. Configure multiple gateways, use built-in or custom rotation and failover strategies, auto-select an available provider, and get consistent responses for logging and monitoring. Easily add or remove...

View on GitHub
Deep Wiki
Context7

Getting Started

overtrue/easy-sms is framework-agnostic — it ships no Laravel service provider, no publishable config, and no facade. "Getting started" in a Laravel app is therefore three small steps, not one composer require.

1. Install (check your PHP version first). The current major line requires PHP >= 8.4. If your app is on 8.1–8.3 you must pin the older line (composer require overtrue/easy-sms:^1.0) or the install will fail resolution against laravel/framework.

composer require overtrue/easy-sms

2. Create the config and bind a singleton. The config array is the package's entire configuration surface; put it in config/easy-sms.php so it is env-driven and cacheable.

// config/easy-sms.php
return [
    'timeout' => 5.0,
    'default' => [
        'strategy' => \Overtrue\EasySms\Strategies\OrderStrategy::class,
        'gateways' => ['aliyun'],
    ],
    'gateways' => [
        'aliyun' => [
            'access_key_id'     => env('ALIYUN_SMS_KEY_ID'),
            'access_key_secret' => env('ALIYUN_SMS_KEY_SECRET'),
            'sign_name'         => env('ALIYUN_SMS_SIGN_NAME'),
        ],
    ],
];
// app/Providers/AppServiceProvider.php  (boot/register)
$this->app->singleton(\Overtrue\EasySms\EasySms::class, function ($app) {
    return new \Overtrue\EasySms\EasySms($app['config']['easy-sms']);
});

3. First use case: an OTP code for login/verification.

$results = app(\Overtrue\EasySms\EasySms::class)->send($request->string('phone'), [
    'template' => 'SMS_123456789',
    'data'     => ['code' => $code],
]);

Where to look first, in this order:

  1. The README's per-gateway config block for your provider — it tells you which of content / template+data that gateway consumes.
  2. src/Gateways/<YourGateway>Gateway.php — the README is incomplete for several providers; the gateway source is the authoritative list of required keys.
  3. src/Message.php and src/Exceptions/NoGatewayAvailableException.php — these define the two extension seams you will actually use.
  4. If you want the notification system rather than direct calls, look at yl/easysms-notification-channel (linked in the README) instead of hand-rolling a channel.

Implementation Patterns

Message classes per scenario. This is the single biggest maintainability win: one class per business message, callable from controllers, jobs, and notifications.

class LoginCodeMessage extends \Overtrue\EasySms\Message
{
    protected $strategy = \Overtrue\EasySms\Strategies\OrderStrategy::class;
    protected $gateways = ['aliyun', 'yunpian']; // overrides default.gateways for this message

    public function __construct(private string $code) {}

    public function getTemplate(\Overtrue\EasySms\Contracts\GatewayInterface $gateway = null)
    {
        return $gateway?->getName() === 'yunpian' ? 'yunpian_tpl_01' : 'SMS_123456789';
    }

    public function getData(\Overtrue\EasySms\Contracts\GatewayInterface $gateway = null)
    {
        return ['code' => $this->code];
    }

    public function getContent(\Overtrue\EasySms\Contracts\GatewayInterface $gateway = null)
    {
        return "您的验证码为: {$this->code},5分钟内有效。"; // used by content-based gateways
    }
}

Use closures when you don't want a class. The content / template / data keys all accept a closure receiving the gateway, which is the cheapest way to send one logical message across heterogeneous providers:

$easySms->send($phone, [
    'template' => fn ($gateway) => $gateway->getName() === 'aliyun' ? 'SMS_1' : 'yunpian_2',
    'data'     => fn () => ['code' => $code],
]);

Wrap it in one application-level service. Don't call EasySms from controllers. A thin App\Services\SmsSender that normalizes the results array, logs per-gateway outcomes, and converts a total failure into a domain exception gives you one place to add retries, metrics, and masking.

public function send(string $phone, Message $message): array
{
    try {
        return $this->easySms->send($phone, $message);
    } catch (NoGatewayAvailableException $e) {
        Log::error('sms.all_failed', ['exceptions' => array_map(
            fn ($ex) => $ex->getMessage(), $e->getExceptions()
        )]);
        throw new SmsFailedException('SMS could not be delivered', previous: $e);
    }
}

Always send queued, never inline in a request. An SMS call is a 1–5 s external round-trip. Use a queued notification or dispatch(new SendSms(...))->onQueue('notifications'), then pair it with a RateLimiter / WithoutOverlapping middleware per phone number to avoid double-sends and provider quota bursts.

Per-call gateway override is the failover switch. The third argument to send() overrides default.gateways, which is useful for an emergency "use provider B only" toggle driven by an env flag rather than a deploy.

Custom gateways go through extend() and belong in the service provider, not in a controller — register once:

$easySms->extend('mygateway', fn ($config) => new MyGateway($config));

Testing. Never let the test suite hit a real provider. Register a fake gateway in the test case via extend() that records calls and returns a canned result, or assert on the PhoneNumber + message payload after calling getContent(). For feature tests, swap the singleton binding with a mock that returns a fixed results array.

International numbers. Use new PhoneNumber($number, 31) — plain strings default to 86-style handling and formatting differs per gateway.

Gotchas and Tips

  • errorlog is a trap in production. It's a real gateway that "sends" by writing to a file and returns a success status. If it lands in default.gateways by accident you get green logs and zero delivered SMS. Keep it to local/testing config only, or better, use a custom fake gateway.
  • send() does not throw on partial failure. It returns an array keyed by gateway name; individual failures are buried in ['status' => 'failure', 'exception' => ...]. NoGatewayAvailableException is thrown only when every gateway fails. If you only try/catch, you will silently drop messages. Always inspect the results array and log per-gateway status.
  • php artisan config:cache will break if your config contains closures. Any closure in config/easy-sms.php (a strategy callback, a custom gateway factory) is unserializable. Keep closures in code, keep the config file scalar-only.
  • Content model mismatch is the #1 runtime error. Template-based gateways (aliyun, juhe, sendcloud) need template + data; content-based gateways (yunpian, luosimao, huyi) need content. Passing only content to Aliyun fails at the provider with an opaque error. Either supply all three keys or branch per gateway with closures.
  • Template variable names must match exactly. Aliyun/Submail treat data keys as template placeholders — ['code' => ...] vs a template expecting ${code}/code is provider-specific. A missing key typically returns a success-shaped HTTP response with a non-zero business code; check result, not just the exception.
  • Signatures double up. Yunpian appends signature only when the content lacks 【】. If you hardcode a signature in content and set signature, you get two signatures in one message.
  • No built-in retry. OrderStrategy/RandomStrategy only choose which gateway to try, once. A transient 5xx from provider A does not roll over to B within the same gateway entry. Implement retry/backoff in your queued job ($tries, backoff()), not in the package.
  • Pass phone numbers as strings. An int phone column loses leading zeros and breaks E.164 handling; also validate/normalize before calling, since the package performs no validation.
  • Timeout is global and floats. 'timeout' => 5.0 applies to every gateway HTTP call; with 3 gateways a fully failing send can take 15 s. Keep queues and job timeouts aligned.
  • Cost/segmentation is on you. Long Chinese content is split into multiple billable segments at the provider. Trim OTP messages and keep template variables short; the library does no length checking.
  • Don't log raw results indiscriminately. The result payload contains the recipient number and often the code/message body. Mask numbers and never log OTP values — log gateway name, status, and a hash of the number instead.
  • Version drift in docs. The README is Chinese-first and some examples predate 2.x (PhoneNumber, config key names, per-gateway required params). When something doesn't work, trust src/Gateways/<name>Gateway.php over the README snippet.
  • Extension point for strategy: implement Overtrue\EasySms\Contracts\StrategyInterface::apply(array $gateways): array to get cost-aware or health-check-driven routing, then set it as default.strategy or on a Message's $strategy property.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
adachsoft/command-executor-lib
tagixo/document-builder
tagixo/page-builder
tagixo/mail-builder
tagixo/form-builder
primix/primix
tagixo/core
alesima/laravel-azure-service-bus
tomshaw/shopcart
likyly/laravel
darthsoup/shoppingcart
tdt/core
labyrinthe/connect-php
encore2/laravel-admin2
cboxdk/laravel-console-kit
shapecode/doctrine-dbal-datetimeutc
gilads-otiannoh254/inertia-protocol
dakiquang/laravel-oauth-refresh-lock
woda/laravel-worktrees
dalefranks/ramasite