mrgoon/aliyun-sms-sdk
mrgoon/aliyun-sms-sdk is a lightweight PHP/Laravel wrapper for Alibaba Cloud (Aliyun) SMS. It simplifies sending verification codes and notifications through the Aliyun SMS API, with straightforward configuration and easy integration into Laravel applications via Composer.
Install the package.
composer require mrgoon/aliyun-sms-sdk
The package is a thin Composer wrapper around Aliyun’s SMS core code (the "核心代码" from the official SDK). It ships no Laravel service provider, no config file, and no facade, so treat it as a library you wire up yourself.
Read vendor/mrgoon/aliyun-sms-sdk/src/ before writing anything. The README is minimal. Look for the request class (typically SendSmsRequest), the client/profile classes, and the response object. Confirm the exact namespace (usually Mrgoon\AliyunSmsSdk\... or a mirrored Aliyun\Core\... structure) and the constructor signatures — they differ between forks of the official SDK.
Prepare the Aliyun console first. You need:
AliyunDysmsFullAccess, not the root key).MyApp).SMS_123456789) whose variable placeholders (${code}) match your payload exactly.
Without approved sign + template, every call fails with isv.SMS_SIGNATURE_ILLEGAL / isv.SMS_TEMPLATE_ILLEGAL regardless of code correctness.First use case: OTP login. Put credentials in config/services.php so they are not scattered in code:
// config/services.php
'aliyun_sms' => [
'access_key_id' => env('ALIYUN_SMS_ACCESS_KEY_ID'),
'access_key_secret' => env('ALIYUN_SMS_ACCESS_KEY_SECRET'),
'region' => env('ALIYUN_SMS_REGION', 'cn-hangzhou'),
'sign_name' => env('ALIYUN_SMS_SIGN_NAME'),
],
Then send:
$request = new SendSmsRequest();
$request->setPhoneNumbers('13800138000');
$request->setSignName(config('services.aliyun_sms.sign_name'));
$request->setTemplateCode('SMS_123456789');
$request->setTemplateParam(json_encode(['code' => '482913'], JSON_UNESCAPED_UNICODE));
$response = $client->getAcsResponse($request);
Success is $response->Code === 'OK'; anything else is a business error to map.
Wrap it in an app-level interface, not raw SDK calls. This is the single most valuable pattern for a 4-star, zero-dependency package: you get a swap point when the package dies.
// app/Services/Sms/SmsSender.php
interface SmsSender
{
public function send(string $phone, string $templateCode, array $params): SmsResult;
}
// app/Services/Sms/AliyunSmsSender.php
final class AliyunSmsSender implements SmsSender
{
public function __construct(private DefaultAcsClient $client, private string $signName) {}
public function send(string $phone, string $templateCode, array $params): SmsResult
{
$req = new SendSmsRequest();
$req->setPhoneNumbers($phone);
$req->setSignName($this->signName);
$req->setTemplateCode($templateCode);
$req->setTemplateParam(json_encode($params, JSON_UNESCAPED_UNICODE));
try {
$res = $this->client->getAcsResponse($req);
} catch (\Throwable $e) {
return SmsResult::transportError($e->getMessage());
}
return new SmsResult(
ok: ($res->Code ?? null) === 'OK',
requestId: $res->RequestId ?? null,
bizId: $res->BizId ?? null,
code: $res->Code ?? 'Unknown',
message: $res->Message ?? '',
);
}
}
Bind it once in AppServiceProvider::register():
$this->app->singleton(SmsSender::class, function ($app) {
$cfg = $app['config']->get('services.aliyun_sms');
// Config::load(); DefaultProfile::getProfile(...); DefaultProfile::addEndpoint(...);
return new AliyunSmsSender($client, $cfg['sign_name']);
});
Queue the send. SMS calls are 100–500 ms plus occasional timeouts. Never block a login request on Aliyun:
class SendOtpSms implements ShouldQueue
{
use Queueable, InteractsWithQueue;
public int $tries = 3;
public array $backoff = [5, 30, 120];
public function __construct(public string $phone, public string $code) {}
public function handle(SmsSender $sms): void
{
$sms->send($this->phone, 'SMS_123456789', ['code' => $this->code]);
}
}
OTP pattern. Generate → store in cache with TTL → dispatch. Verify with a constant-time comparison and increment an attempt counter on the same cache key.
$code = (string) random_int(100000, 999999);
Cache::put("otp:{$phone}", $code, now()->addMinutes(5));
SendOtpSms::dispatch($phone, $code);
Delivery audit via QuerySendDetails. After sends, persist BizId and RequestId. Support requests about "I never got the code" are answered in seconds with QuerySendDetails using the phone, date (YYYYMMDD), and BizId.
Batch sends. There is no bulk endpoint — the API is one-phone-per-call. Loop with a delay (5–10 ms), a RateLimiter, or split into a queued fan-out. Do not foreach 10k numbers synchronously.
Fake it in tests. Bind a FakeSmsSender in TestCase::setUp() that records (phone, template, params), and assert against the fake. The real client should never run in CI.
composer.json, keep the SmsSender interface boundary tight, and plan a migration to alibabacloud/dysmsapi-20170525 (official, maintained) when it hurts. The interface wrapper is what makes that a one-file change.vendor/.setTemplateParam takes a JSON string, not an array. Passing an array silently produces isv.TEMPLATE_MISSING_PARAMETERS or an empty-param template. Always json_encode(...) and cast values to strings — numeric 482913 vs "482913" can behave differently in some templates.isv.BUSINESS_LIMIT_CONTROL — per-number throttling (default 1/min, 5/hour, 10/day). Show "try again later"; do not retry blindly.isv.DAY_LIMIT_CONTROL — daily cap per number.isv.MOBILE_NUMBER_ILLEGAL — bad number or non-mainland format.isv.SMS_SIGNATURE_ILLEGAL / isv.SMS_TEMPLATE_ILLEGAL — not approved or wrong code.isv.OUT_OF_SERVICE — account suspended or arrears.InvalidAccessKeyId.NotFound / SignatureDoesNotMatch — wrong key or wrong region signing.Throttling.User — per-account QPS; this is the only one worth exponential-backoff retrying.dysmsapi.aliyuncs.com and pass the matching region to DefaultProfile::getProfile() and addEndpoint(). A mismatched region produces SignatureDoesNotMatch, which looks like a credential bug but isn’t.cURL error 60, set curl.cainfo in php.ini or pass a CA bundle — the 2017 SDK doesn’t always bundle one.RequestId, BizId, Code, and a masked number (138****8000). OTPs in logs are a compliance problem.QuerySendDetails are YYYYMMDD in the account’s timezone; use Carbon::now('Asia/Shanghai')->format('Ymd'), not now() in UTC.app/ — never edit vendor/ (a composer update wipes it, and this package will get reinstalled from a stale artifact).How can I help you explore Laravel packages today?