alibabacloud/dysmsapi-20170525
Official Alibaba Cloud Dysmsapi SDK for PHP, providing a simple client for Alibaba Cloud SMS: send verification, notification, and marketing messages, query delivery status, and manage signatures and templates via the Dysmsapi 2017-05-25 API.
This is an official, auto-generated Alibaba Cloud SDK — not a Laravel package. There is no service provider, no config file, no facade, and no testing fake. You get one client class and ~hundreds of request/response model classes under AlibabaCloud\SDK\Dysmsapi\V20170525\. Plan to wrap it yourself on day one.
1. Install
composer require alibabacloud/dysmsapi-20170525
It pulls in alibabacloud/darabonba-openapi and tea/tea (the OpenAPI runtime + Guzzle transport). PHP 7.2+ in current releases; check the tag you pin.
2. Credentials — use a RAM user with only dysms:SendSms / dysms:QuerySendDetails, never your root AK.
ALIBABA_CLOUD_ACCESS_KEY_ID=LTAI...
ALIBABA_CLOUD_ACCESS_KEY_SECRET=...
ALIBABA_SMS_SIGN=YourApprovedSign
ALIBABA_SMS_ENDPOINT=dysmsapi.aliyuncs.com
Note the endpoint is regional: mainland China uses dysmsapi.aliyuncs.com, international uses dysmsapi.ap-southeast-1.aliyuncs.com. Sending to a number that doesn't match the endpoint's region fails.
3. Where to look first (in this order)
src/Dysmsapi.php — the whole API surface as methods: sendSms, sendBatchSms, querySendDetails, querySendStatistics, sendSmsVerifyCode, checkSmsVerifyCode, getSmsTemplateList, getSmsSignList.src/Models/SendSmsRequest.php — the required fields you'll actually touch.src/Models/SendSmsResponseBody.php — what a "successful" HTTP 200 that failed looks like.4. First send (verified working shape)
use AlibabaCloud\Darabonba\OpenApi\Models\Config;
use AlibabaCloud\SDK\Dysmsapi\V20170525\Dysmsapi;
use AlibabaCloud\SDK\Dysmsapi\V20170525\Models\SendSmsRequest;
$config = new Config([
'accessKeyId' => config('services.alibaba_sms.key'),
'accessKeySecret' => config('services.alibaba_sms.secret'),
]);
$config->endpoint = config('services.alibaba_sms.endpoint');
$client = new Dysmsapi($config);
$request = new SendSmsRequest([
'phoneNumbers' => '8613800000000',
'signName' => config('services.alibaba_sms.sign'),
'templateCode' => 'SMS_123456789',
'templateParam' => json_encode(['code' => '482913']),
]);
$response = $client->sendSms($request);
$response->body->code; // "OK" on success, "isv.xxx" on business failure
$response->body->bizId; // keep this; it's your receipt for QuerySendDetails
(On older tags the Config class lives at AlibabaCloud\SDK\Dysmsapi\V20170525\Config — same constructor. If the import above 404s, try that.)
First realistic use case: an OTP/login code. Approve a sign and a 验证码 template in the Alibaba Cloud console before writing code — SMS is one of the few services where you cannot test anything until a human approves your sign and template (usually minutes to a couple of hours).
You will want to swap this out in tests, and you may want a fallback provider. Don't scatter new Dysmsapi(...) through controllers.
// app/Services/Sms/SmsGateway.php
interface SmsGateway {
public function send(SmsMessage $message): SmsResult;
}
// app/Providers/AlibabaSmsServiceProvider.php
$this->app->singleton(Dysmsapi::class, function () {
$config = new Config([
'accessKeyId' => config('services.alibaba_sms.key'),
'accessKeySecret' => config('services.alibaba_sms.secret'),
]);
$config->endpoint = config('services.alibaba_sms.endpoint');
$config->connectTimeout = 3; // seconds
$config->readTimeout = 5;
return new Dysmsapi($config);
});
$this->app->bind(SmsGateway::class, AlibabaSmsGateway::class);
Setting connectTimeout/readTimeout explicitly matters — the default is generous, and a hung SMS call inside a request will hold a PHP-FPM worker.
SendSms returns HTTP 200 even when it rejects the message. Your gateway should never leak $response->body->code to callers.
public function send(SmsMessage $m): SmsResult
{
try {
$res = $this->client->sendSms(new SendSmsRequest([...]));
} catch (TeaError | TeaUnableRetryError $e) {
return SmsResult::transportError($e->getMessage());
}
if ($res->body->code !== 'OK') {
Log::warning('sms.rejected', ['code' => $res->body->code, 'msg' => $res->body->message]);
return SmsResult::rejected($res->body->code, $res->body->message);
}
return SmsResult::sent($res->body->bizId);
}
SMS is a network call to another continent. Push to a job so a slow/retrying send never blocks a login request.
final class SendSmsJob implements ShouldQueue, ShouldBeUnique
{
public int $tries = 3;
public array $backoff = [5, 30, 120];
public int $uniqueFor = 60;
public function handle(SmsGateway $sms): void { /* ... */ }
}
Cap $tries low; if Alibaba is throttling you, retrying harder is counterproductive (see gotchas).
The idiomatic Laravel hookup is a notification channel so OTPs live next to your other user-facing messages:
// app/Notifications/Channels/AlibabaSmsChannel.php
public function send($notifiable, Notification $notification)
{
$message = $notification->toAlibabaSms($notifiable);
app(SmsGateway::class)->send($message->to($notifiable->routeNotificationFor('sms')));
}
Your SmsMessage should hold templateCode, params (array), signName. Let the message class own the domain knowledge (SmsMessage::loginCode($code) → SMS_123456789 + ['code' => $code]), so template codes aren't littered through the codebase.
QuerySendDetails only retains roughly the last 7 days and is awkward to query. Write your own sms_logs table (phone, template, params JSON, biz_id, response code, provider, user_id, ip) in the same job. This becomes your audit trail, your debugging tool, and your per-user rate-limiting source.
Do rate limiting in your app — per phone number, per IP, per user — before the SDK call. Alibaba's own SendSmsVerifyCode also has Interval and DuplicatePolicy options, but you still need your own limits to protect your budget and reputation.
RateLimiter::attempt("sms:{$phone}", 1, fn () => ..., 60); // 1 per minute per number
sendSmsVerifyCode / checkSmsVerifyCode push code generation, expiry, and retry rules to the server, so you don't need Redis for the code. Good when you have a single code flow and want less infra; less good when you need the code for your own business logic or want a provider-agnostic auth layer.
sendBatchSms takes parallel arrays (phoneNumberJson, signNameJson, templateParamJson) — it's easy to get out of sync. Build them from a single collection:
$recipients = collect($list)->map(fn ($r) => [
'phone' => $r->phone,
'sign' => $r->sign ?? config('services.alibaba_sms.sign'),
'params' => json_encode(['name' => $r->name]),
]);
// then pluck each column into its own json_encode() payload
If you send to both mainland China and international numbers you need two clients with two endpoints and two sign sets. Bind a SmsClientFactory::for(string $region) rather than a single singleton, and route by phone prefix (+86 → CN, otherwise → international).
templateParam is a JSON string, not an array. Passing an array yields a signature/serialization error that reads nothing like "you passed the wrong type". Single-variable templates still need {"code":"1234"}, not "1234".
HTTP 200 does not mean sent. The client only throws on transport/parse errors. Business failures come back as body->code !== 'OK'. If you only catch exceptions, you will silently believe every SMS went out.
The error codes you will actually hit:
| Code | Meaning | Retry? |
|---|---|---|
isv.BUSINESS_LIMIT_CONTROL |
Per-number/per-day flow control | No — backoff or drop |
isv.MOBILE_NUMBER_ILLEGAL |
Bad number format | No |
isv.SMS_TEMPLATE_ILLEGAL / isv.SMS_SIGNATURE_ILLEGAL |
Not approved / mismatched | No |
isv.OUT_OF_SERVICE |
Account in arrears | No |
isp.RAM_PERMISSION_DENY |
AK lacks dysms:SendSms |
No |
isv.AMOUNT_NOT_ENOUGH |
Out of credit | No |
Retrying BUSINESS_LIMIT_CONTROL is the classic self-inflicted outage — it escalates the throttle.
Phone number format. Mainland numbers need the 86 prefix and no + (8613800000000). International numbers use country code without +. There is no normalization done for you, and a leading + or a missing country code reliably produces MOBILE_NUMBER_ILLEGAL.
Signs and templates are region- and account-scoped. A sign approved for China cannot be used from the Singapore endpoint. When you clone config between staging and production, the template codes usually don't exist in the other account.
Testing is the sharpest edge. The SDK uses Guzzle directly through the Darabonba runtime, so Http::fake() does not intercept it. Do not try to spy on it. Instead:
FakeSmsGateway implementing your interface in tests (this is why the interface exists), andsms_logs rows rather than on SDK responses.Composer/autoload weight. These generated SDKs contain thousands of small model classes. Run composer dump-autoload -o in production — without it, the classmap lookups get noticeably slow on cold containers. In CI, --classmap-authoritative helps; just be aware the classmap generation step is slower than for a typical package.
Versioning is a firehose. New tags land constantly (the metadata lists a 2026 release). Pin the exact version in composer.json ("alibabacloud/dysmsapi-20170525": "3.x.y") rather than ^. Generated SDKs occasionally reshape request models between majors.
Config namespace has moved. Older versions expose AlibabaCloud\SDK\Dysmsapi\V20170525\Config; newer ones use the shared AlibabaCloud\Darabonba\OpenApi\Models\Config. If you copy a snippet from an old blog post and get a "class not found", this is why — check vendor/alibabacloud/dysmsapi-20170525/src/ for what actually exists.
Log the bizId. It is the only key that lets support trace a delivery. QuerySendDetails needs it plus sendDate (YYYYMMDD) and the phone number, and gives you SendStatus (1 waiting, 2 failed, 3 delivered). Build a "resend receipt" screen on top of it — but note the 7-day retention.
Extension point for a second provider: because everything funnels through SmsGateway, a FailoverSmsGateway that tries Alibaba then falls back to another provider is ~30 lines. Do this if SMS is in your login path — a single-provider OTP flow is a single point of account lockout.
Zero dependents (at the time of writing) is a signal, not a verdict. This is a vendor-generated package consumed as a transitive dependency of other Alibaba SDKs, so it isn't "unmaintained" — but it also means no community issues, no Stack Overflow answers, and no Laravel idioms. Read src/Models/*Request.php for the real contract; that's the only reliable documentation for each call's required fields.
How can I help you explore Laravel packages today?