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

Sms Laravel Package

tencentcloud/sms

Send SMS messages through Tencent Cloud from your Laravel or Lumen app. This package wraps the Tencent Cloud SMS SDK in a simple, configurable service, so you can dispatch verification codes and notifications with clean, framework-friendly code.

View on GitHub
Deep Wiki
Context7

Getting Started

tencentcloud/sms is an auto-generated subtree split of the tencentcloud-sdk-php monorepo — almost everything you need lives in tencentcloud/common, which is pulled in automatically. Treat it as a raw transport SDK, not a Laravel package: no service provider, no facade, no config file, no auto-discovery.

composer require tencentcloud/sms
# pulls tencentcloud/common as a dependency

Everything you touch lives in one namespace that encodes the API version:

use TencentCloud\Sms\V20210111\SmsClient;
use TencentCloud\Sms\V20210111\Models\SendSmsRequest;

Where to look first

  1. vendor/tencentcloud/sms/src/TencentCloud/Sms/V20210111/Models/ — every request/response field is a setX() method here. This is your real documentation.
  2. vendor/tencentcloud/common/src/TencentCloud/Common/ — Credential, ClientProfile, HttpProfile, TencentCloudSDKException.
  3. Official docs at cloud.tencent.com/document/api/382/ for error codes and template/sign rules — the repo itself has none.

First use case: an OTP send. Add config first:

// config/services.php
'tencent_sms' => [
    'secret_id'  => env('TENCENT_SMS_SECRET_ID'),
    'secret_key' => env('TENCENT_SMS_SECRET_KEY'),
    'sdk_app_id' => env('TENCENT_SMS_SDK_APP_ID'),
    'sign_name'  => env('TENCENT_SMS_SIGN_NAME'),
    'region'     => env('TENCENT_SMS_REGION', 'ap-guangzhou'),
    'endpoint'   => 'sms.tencentcloudapi.com',
],

Then a thin wrapper — this is the boundary you will mock, log, and retry at:

namespace App\Services;

use TencentCloud\Common\Credential;
use TencentCloud\Common\Profile\ClientProfile;
use TencentCloud\Common\Profile\HttpProfile;
use TencentCloud\Sms\V20210111\SmsClient;
use TencentCloud\Sms\V20210111\Models\SendSmsRequest;

class TencentSms
{
    private SmsClient $client;

    public function __construct(array $cfg)
    {
        $http = new HttpProfile();
        $http->setEndpoint($cfg['endpoint']);
        $http->setReqTimeout(10); // seconds; default is much longer

        $profile = new ClientProfile();
        $profile->setHttpProfile($http);
        // $profile->setSignMethod('TC3-HMAC-SHA256'); // already the default

        $this->client = new SmsClient(
            new Credential($cfg['secret_id'], $cfg['secret_key']),
            $cfg['region'],
            $profile
        );
    }

    /** @return array<int, array{Code:string, Message:string, PhoneNumber:string, SerialNo:string}> */
    public function send(string $templateId, array $params, array $phoneNumbers, ?string $context = null): array
    {
        $req = new SendSmsRequest();
        $req->setSmsSdkAppId(config('services.tencent_sms.sdk_app_id'));
        $req->setSignName(config('services.tencent_sms.sign_name'));
        $req->setTemplateId($templateId);
        $req->setTemplateParamSet(array_map('strval', $params)); // strings only
        $req->setPhoneNumberSet($phoneNumbers);
        if ($context) {
            $req->setSessionContext($context);
        }

        $resp = $this->client->SendSms($req);

        return json_decode($resp->toJsonString(), true)['SendStatusSet'] ?? [];
    }
}
// AppServiceProvider::register()
$this->app->singleton(TencentSms::class, fn () => new TencentSms(config('services.tencent_sms')));

Call it: app(TencentSms::class)->send('1234567', [$code], ['8613800138000'], 'otp:'.$userId).

Implementation Patterns

1. Interface first, vendor second. The single highest-value thing you can do with this package is never let it leak past one class. Define your own contract and bind Tencent as one driver:

interface SmsSender { public function send(string $template, array $params, string $phone): void; }
$this->app->bind(SmsSender::class, match (config('sms.driver')) {
    'tencent' => fn () => new TencentSmsDriver(app(TencentSms::class), config('sms.templates')),
    default   => fn () => new LogSmsDriver(),
});

Now SmsSender::class can be faked in tests and swapped in local dev with zero SDK mocking pain.

2. Template IDs belong in config, keyed by intent, per environment. Tencent template IDs and sign names differ between approval environments and are effectively immutable once approved.

// config/sms.php
'templates' => [
    'otp'      => env('TENCENT_SMS_TPL_OTP'),      // params: [code, expire_minutes]
    'order'    => env('TENCENT_SMS_TPL_ORDER'),    // params: [order_no, amount]
    'return'   => env('TENCENT_SMS_TPL_RETURN'),
],

Callers ask for $sms->send('otp', [$code, '5'], $phone) — they never see a numeric template ID.

3. Normalise phone numbers at the edge. Tencent wants E.164 without the +: 8613800138000, 85298765432, 14155552671. Do this in a single helper and validate before hitting the API — a malformed number is a billed, failed request.

function tencent_phone(string $e164): string {
    return ltrim(preg_replace('/[^0-9]/', '', $e164), '+');
}

4. OTP flow: the SDK is the last step, not the flow. Rate limit and store the code before sending, so a failed send doesn't leave a valid-but-undelivered code, and a slow API call doesn't hold a DB transaction:

$key = "otp:{$phone}";
RateLimiter::hit("sms:{$phone}", 3600);

if (RateLimiter::tooManyAttempts("sms:{$phone}", 5)) {
    throw ValidationException::withMessages(['phone' => 'Too many attempts.']);
}

$code = (string) random_int(100000, 999999);
Cache::put($key, Hash::make($code), now()->addMinutes(5));

$statuses = app(TencentSms::class)->send(
    'otp', [$code, '5'], [tencent_phone($phone)], 'otp:'.$phone
);

if (($statuses[0]['Code'] ?? null) !== 'Ok') {
    Log::warning('sms.failed', ['phone' => mask($phone), 'status' => $statuses[0] ?? null]);
    Cache::forget($key);
    throw new \RuntimeException('SMS delivery failed');
}

5. Queue anything that isn't a login OTP. SendSms is a blocking HTTP call (~100–500 ms). Marketing/notification SMS should go through a job with $tries = 3, backoff = [5, 30, 120], and a failed() handler that writes a row to your sms_logs table. For OTP you often want it inline — but still wrap it in a try/catch with a short timeout (5–10 s) so a slow Tencent endpoint doesn't stall your login request.

6. Build a Notification channel for anything user-facing:

class SmsChannel
{
    public function send($notifiable, Notification $notification): void
    {
        $msg = $notification->toSms($notifiable);
        app(SmsSender::class)->send($msg->template, $msg->params, $notifiable->phone);
    }
}

Route via via() returning [SmsChannel::class, 'mail'] — multi-channel fallback for free.

7. Pass SessionContext with something traceable ("otp:{$userId}:{$requestId}"). It round-trips into SendStatusSet and into the console's send records, giving you a join key between your logs and Tencent's when a user says "I never got it."

8. Batch deliberately. PhoneNumberSet accepts many numbers, but one bad entry doesn't fail the call — you get a per-number result array. Batch in chunks of ~50 (well under the 200 cap), inspect every status, and split successes from failures into your delivery log.

9. Reporting uses the same client: PullSmsSendStatus / PullSmsSendStatusByPhoneNumber for delivery receipts, SendStatusStatistics for daily volume + failure rate. Put these in a scheduled command and feed a dashboard rather than querying per-request.

10. Testing. Mock your own interface or TencentSms — do not try to fake the SDK's HTTP layer. Http::fake() does nothing here; the common package uses its own Guzzle client. For a smoke test against the real API, use a dedicated TENCENT_SMS_SDK_APP_ID with a test template and one real phone.

Gotchas and Tips

  • Low stars ≠ low quality. This is a mechanically split repo; the upstream monorepo is the real project. Pin it, don't judge it. For projects using COS/OCR/SMS together, depend on tencentcloud/tencentcloud-sdk-php instead to avoid two copies of tencentcloud/common at different versions.
  • The API version is in the namespace. TencentCloud\Sms\V20210111 — when Tencent ships a new API version, your imports change. This is why you pin an exact version ("tencentcloud/sms": "3.x.y") and commit composer.lock. composer update on an auto-generated SDK will silently change request/response classes.
  • SendSms returning 200 does not mean it sent. The top-level response is a batch envelope; each SendStatusSet entry has its own Code / Message. Only Code === 'Ok' counts. The classic bug is $response->getSendStatusSet() truthy → treat as delivered.
  • Errors inside the batch are not exceptions; errors at the API level are. Auth failures, quota exhaustion, invalid template, and rate limits throw TencentCloudSDKException (getErrorCode(), getRequestId(), getMessage()). You need both a try/catch and per-entry status checking.
  • TemplateParamSet must be strings, in template order. {1}, {2} in the approved template map to array index 0, 1. An int 123456 may serialize inconsistently — always array_map('strval', …). Never include newlines, URLs, or marketing language in a verification-code template; it violates the approved content and gets rejected silently at send time.
  • Templates and signatures need manual console approval. Nothing sends until both are approved for that SmsSdkAppId. Plan a day of lead time per environment. Error codes to watch: FailedOperation.TemplateIncorrectOrUnapproved, FailedOperation.SignatureIncorrectOrUnapproved.
  • Set ReqTimeout to 5–10 s. The SDK default is generous; a hung socket on your checkout page is worse than a failed send. The SDK does not retry. If you add retries, do it at the job level, and only with your own dedupe guard — Tencent has no idempotency key, so a retry after a timeout genuinely double-sends (and double-bills).
  • No sandbox. Every successful send is billed. Build a LogSmsDriver and a sms.driver=fake env flag early; use it for all local and CI runs.
  • Reuse the client. Construct SmsClient once as a singleton. Building a new credential + Guzzle stack per message inside a loop is measurably expensive.
  • Http::fake() and Laravel's HTTP events do not apply. No middleware, no Http::retry, no proxy settings from config('app'). If you need a corporate proxy, set it on the SDK's HttpProfile or via HTTP_PROXY/HTTPS_PROXY env, which Guzzle reads.
  • Region is required even for global SMS, and the endpoint must be sms.tencentcloudapi.com — not the region-scoped endpoints some other Tencent products use.
  • Credentials: sub-account with SMS-only policy, keys from env, rotate. If you're on a CVM/COS role setup, prefer temporary credentials (Credential supports setToken() from STS) over long-lived secrets. Never log the secret key; never commit .env variations.
  • Phone numbers are PII under PIPL. Log masked numbers (138****8000) plus SerialNo, not the full number. Keep the send record — regulators and support both want it — but store it in your own table with a retention policy, not just in laravel.log.
  • Rate limits are per account and stingy on new accounts. Expect LimitExceeded.PhoneNumberDailyLimit (same number, per day), LimitExceeded.PhoneNumberOneHourLimit, and app-level QPS caps. Your rate limiter should sit in front of the SDK so you get a clean 429 to the user instead of an exception from Tencent.
  • Response objects are not arrays. Use toJsonString() and json_decode(..., true), or serialize(). There's no ArrayAccess, no Laravel data_get() ergonomics.
  • Octane-safe as long as you register the client as a singleton properly and don't stash per-request state on it. It's stateless, so the main hazard is accidentally resolving a fresh client per request in a hot loop.
  • Extension points are all on ClientProfile/HttpProfile — timeout, protocol, proxy, sign method. If you need more (tracing, metrics), wrap the client; there is no middleware hook.
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.
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package