├── .editorconfig ├── .github ├── FUNDING.yml └── workflows │ └── tests.yml ├── .php-cs-fixer.dist.php ├── LICENSE ├── README.md ├── composer.json ├── psalm.xml └── src ├── Contracts ├── GatewayInterface.php ├── MessageInterface.php ├── PhoneNumberInterface.php └── StrategyInterface.php ├── EasySms.php ├── Exceptions ├── Exception.php ├── GatewayErrorException.php ├── InvalidArgumentException.php └── NoGatewayAvailableException.php ├── Gateways ├── AliyunGateway.php ├── AliyunIntlGateway.php ├── AliyunrestGateway.php ├── BaiduGateway.php ├── ChuanglanGateway.php ├── Chuanglanv1Gateway.php ├── CtyunGateway.php ├── ErrorlogGateway.php ├── Gateway.php ├── HuaweiGateway.php ├── HuaxinGateway.php ├── HuyiGateway.php ├── JuheGateway.php ├── KingttoGateway.php ├── LuosimaoGateway.php ├── MaapGateway.php ├── ModuyunGateway.php ├── NowcnGateway.php ├── QcloudGateway.php ├── QiniuGateway.php ├── RongcloudGateway.php ├── RongheyunGateway.php ├── SendcloudGateway.php ├── SmsbaoGateway.php ├── SubmailGateway.php ├── TianyiwuxianGateway.php ├── TiniyoGateway.php ├── TinreeGateway.php ├── TwilioGateway.php ├── UcloudGateway.php ├── Ue35Gateway.php ├── VolcengineGateway.php ├── YidongmasblackGateway.php ├── YunpianGateway.php ├── YuntongxunGateway.php ├── YunxinGateway.php ├── YunzhixunGateway.php └── ZzyunGateway.php ├── Message.php ├── Messenger.php ├── PhoneNumber.php ├── Strategies ├── OrderStrategy.php └── RandomStrategy.php ├── Support └── Config.php └── Traits └── HasHttpRequest.php /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = false 10 | 11 | [*.{vue,js,scss}] 12 | charset = utf-8 13 | indent_style = space 14 | indent_size = 2 15 | end_of_line = lf 16 | insert_final_newline = true 17 | trim_trailing_whitespace = true 18 | 19 | [*.md] 20 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [overtrue] 4 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | 8 | jobs: 9 | phpunit: 10 | strategy: 11 | matrix: 12 | php_version: [8.0, 8.1, 8.2, 8.3] 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Setup PHP environment 17 | uses: shivammathur/setup-php@v2 18 | with: 19 | php-version: ${{ matrix.php_version }} 20 | coverage: xdebug 21 | - name: Install dependencies 22 | run: composer install 23 | - name: PHPUnit check 24 | run: ./vendor/bin/phpunit --coverage-text 25 | -------------------------------------------------------------------------------- /.php-cs-fixer.dist.php: -------------------------------------------------------------------------------- 1 | setRules([ 5 | '@Symfony' => true, 6 | ]) 7 | ->setFinder( 8 | PhpCsFixer\Finder::create() 9 | ->exclude('vendor') 10 | ->in([__DIR__.'/src/', __DIR__.'/tests/']) 11 | ) 12 | ; 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 overtrue 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Easy SMS

2 | 3 |

:calling: 一款满足你的多种发送需求的短信发送组件

4 | 5 |

6 | Latest Stable Version 7 | Latest Unstable Version 8 | Code Coverage 9 | Total Downloads 10 | License 11 |

12 | 13 |

14 | Sponsor me 15 |

16 | 17 | ## 特点 18 | 19 | 1. 支持目前市面多家服务商 20 | 1. 一套写法兼容所有平台 21 | 1. 简单配置即可灵活增减服务商 22 | 1. 内置多种服务商轮询策略、支持自定义轮询策略 23 | 1. 统一的返回值格式,便于日志与监控 24 | 1. 自动轮询选择可用的服务商 25 | 1. 更多等你去发现与改进... 26 | 27 | ## 平台支持 28 | 29 | - [腾讯云 SMS](https://cloud.tencent.com/product/sms) 30 | - [Ucloud](https://www.ucloud.cn) 31 | - [七牛云](https://www.qiniu.com/) 32 | - [SendCloud](http://www.sendcloud.net/) 33 | - [阿里云](https://www.aliyun.com/) 34 | - [云片](https://www.yunpian.com) 35 | - [Submail](https://www.mysubmail.com) 36 | - [螺丝帽](https://luosimao.com/) 37 | - [容联云通讯](http://www.yuntongxun.com) 38 | - [互亿无线](http://www.ihuyi.com) 39 | - [聚合数据](https://www.juhe.cn) 40 | - [百度云](https://cloud.baidu.com/) 41 | - [华信短信平台](http://www.ipyy.com/) 42 | - [253云通讯(创蓝)](https://www.253.com/) 43 | - [创蓝云智](https://www.chuanglan.com/) 44 | - [融云](http://www.rongcloud.cn) 45 | - [天毅无线](http://www.85hu.com/) 46 | - [华为云](https://www.huaweicloud.com/product/msgsms.html) 47 | - [网易云信](https://yunxin.163.com/sms) 48 | - [云之讯](https://www.ucpaas.com/index.html) 49 | - [凯信通](http://www.kingtto.cn/) 50 | - [UE35.net](http://uesms.ue35.cn/) 51 | - [短信宝](http://www.smsbao.com/) 52 | - [Tiniyo](https://tiniyo.com/) 53 | - [摩杜云](https://www.moduyun.com/) 54 | - [融合云(助通)](https://www.ztinfo.cn/products/sms) 55 | - [蜘蛛云](https://zzyun.com/) 56 | - [融合云信](https://maap.wo.cn/) 57 | - [天瑞云](http://cms.tinree.com/) 58 | - [时代互联](https://www.now.cn/) 59 | - [火山引擎](https://console.volcengine.com/sms/) 60 | - [移动云MAS(黑名单模式)](https://mas.10086.cn) 61 | - [电信天翼云](https://www.ctyun.cn/document/10020426/10021544) 62 | 63 | ## 环境需求 64 | 65 | - PHP >= 5.6 66 | 67 | ## 安装 68 | 69 | ```shell 70 | composer require "overtrue/easy-sms" 71 | ``` 72 | 73 | **For Laravel notification** 74 | 75 | 如果你喜欢使用 [Laravel Notification](https://laravel.com/docs/5.8/notifications), 可以考虑直接使用朋友封装的拓展包: 76 | 77 | 78 | 79 | ## 使用 80 | 81 | ```php 82 | use Overtrue\EasySms\EasySms; 83 | 84 | $config = [ 85 | // HTTP 请求的超时时间(秒) 86 | 'timeout' => 5.0, 87 | 88 | // 默认发送配置 89 | 'default' => [ 90 | // 网关调用策略,默认:顺序调用 91 | 'strategy' => \Overtrue\EasySms\Strategies\OrderStrategy::class, 92 | 93 | // 默认可用的发送网关 94 | 'gateways' => [ 95 | 'yunpian', 'aliyun', 96 | ], 97 | ], 98 | // 可用的网关配置 99 | 'gateways' => [ 100 | 'errorlog' => [ 101 | 'file' => '/tmp/easy-sms.log', 102 | ], 103 | 'yunpian' => [ 104 | 'api_key' => '824f0ff2f71cab52936axxxxxxxxxx', 105 | ], 106 | 'aliyun' => [ 107 | 'access_key_id' => '', 108 | 'access_key_secret' => '', 109 | 'sign_name' => '', 110 | ], 111 | //... 112 | ], 113 | ]; 114 | 115 | $easySms = new EasySms($config); 116 | 117 | $easySms->send(13188888888, [ 118 | 'content' => '您的验证码为: 6379', 119 | 'template' => 'SMS_001', 120 | 'data' => [ 121 | 'code' => 6379 122 | ], 123 | ]); 124 | ``` 125 | 126 | ## 短信内容 127 | 128 | 由于使用多网关发送,所以一条短信要支持多平台发送,每家的发送方式不一样,但是我们抽象定义了以下公用属性: 129 | 130 | - `content` 文字内容,使用在像云片类似的以文字内容发送的平台 131 | - `template` 模板 ID,使用在以模板ID来发送短信的平台 132 | - `data` 模板变量,使用在以模板ID来发送短信的平台 133 | 134 | 所以,在使用过程中你可以根据所要使用的平台定义发送的内容。 135 | 136 | ```php 137 | $easySms->send(13188888888, [ 138 | 'content' => '您的验证码为: 6379', 139 | 'template' => 'SMS_001', 140 | 'data' => [ 141 | 'code' => 6379 142 | ], 143 | ]); 144 | ``` 145 | 146 | 你也可以使用闭包来返回对应的值: 147 | 148 | ```php 149 | $easySms->send(13188888888, [ 150 | 'content' => function($gateway){ 151 | return '您的验证码为: 6379'; 152 | }, 153 | 'template' => function($gateway){ 154 | return 'SMS_001'; 155 | }, 156 | 'data' => function($gateway){ 157 | return [ 158 | 'code' => 6379 159 | ]; 160 | }, 161 | ]); 162 | ``` 163 | 164 | 你可以根据 `$gateway` 参数类型来判断返回值,例如: 165 | 166 | ```php 167 | $easySms->send(13188888888, [ 168 | 'content' => function($gateway){ 169 | if ($gateway->getName() == 'yunpian') { 170 | return '云片专用验证码:1235'; 171 | } 172 | return '您的验证码为: 6379'; 173 | }, 174 | 'template' => function($gateway){ 175 | if ($gateway->getName() == 'aliyun') { 176 | return 'TP2818'; 177 | } 178 | return 'SMS_001'; 179 | }, 180 | 'data' => function($gateway){ 181 | return [ 182 | 'code' => 6379 183 | ]; 184 | }, 185 | ]); 186 | ``` 187 | 188 | ## 发送网关 189 | 190 | 默认使用 `default` 中的设置来发送,如果某一条短信你想要覆盖默认的设置。在 `send` 方法中使用第三个参数即可: 191 | 192 | ```php 193 | $easySms->send(13188888888, [ 194 | 'content' => '您的验证码为: 6379', 195 | 'template' => 'SMS_001', 196 | 'data' => [ 197 | 'code' => 6379 198 | ], 199 | ], ['yunpian', 'juhe']); // 这里的网关配置将会覆盖全局默认值 200 | ``` 201 | 202 | ## 返回值 203 | 204 | 由于使用多网关发送,所以返回值为一个数组,结构如下: 205 | 206 | ```php 207 | [ 208 | 'yunpian' => [ 209 | 'gateway' => 'yunpian', 210 | 'status' => 'success', 211 | 'result' => [...] // 平台返回值 212 | ], 213 | 'juhe' => [ 214 | 'gateway' => 'juhe', 215 | 'status' => 'failure', 216 | 'exception' => \Overtrue\EasySms\Exceptions\GatewayErrorException 对象 217 | ], 218 | //... 219 | ] 220 | ``` 221 | 222 | 如果所选网关列表均发送失败时,将会抛出 `Overtrue\EasySms\Exceptions\NoGatewayAvailableException` 异常,你可以使用 `$e->results` 获取发送结果。 223 | 224 | 你也可以使用 `$e` 提供的更多便捷方法: 225 | 226 | ```php 227 | $e->getResults(); // 返回所有 API 的结果,结构同上 228 | $e->getExceptions(); // 返回所有调用异常列表 229 | $e->getException($gateway); // 返回指定网关名称的异常对象 230 | $e->getLastException(); // 获取最后一个失败的异常对象 231 | ``` 232 | 233 | ## 自定义网关 234 | 235 | 本拓展已经支持用户自定义网关,你可以很方便的配置即可当成与其它拓展一样的使用: 236 | 237 | ```php 238 | $config = [ 239 | ... 240 | 'default' => [ 241 | 'gateways' => [ 242 | 'mygateway', // 配置你的网站到可用的网关列表 243 | ], 244 | ], 245 | 'gateways' => [ 246 | 'mygateway' => [...], // 你网关所需要的参数,如果没有可以不配置 247 | ], 248 | ]; 249 | 250 | $easySms = new EasySms($config); 251 | 252 | // 注册 253 | $easySms->extend('mygateway', function($gatewayConfig){ 254 | // $gatewayConfig 来自配置文件里的 `gateways.mygateway` 255 | return new MyGateway($gatewayConfig); 256 | }); 257 | 258 | $easySms->send(13188888888, [ 259 | 'content' => '您的验证码为: 6379', 260 | 'template' => 'SMS_001', 261 | 'data' => [ 262 | 'code' => 6379 263 | ], 264 | ]); 265 | ``` 266 | 267 | ## 国际短信 268 | 269 | 国际短信与国内短信的区别是号码前面需要加国际码,但是由于各平台对国际号码的写法不一致,所以在发送国际短信的时候有一点区别: 270 | 271 | ```php 272 | use Overtrue\EasySms\PhoneNumber; 273 | 274 | // 发送到国际码为 31 的国际号码 275 | $number = new PhoneNumber(13188888888, 31); 276 | 277 | $easySms->send($number, [ 278 | 'content' => '您的验证码为: 6379', 279 | 'template' => 'SMS_001', 280 | 'data' => [ 281 | 'code' => 6379 282 | ], 283 | ]); 284 | ``` 285 | 286 | ## 定义短信 287 | 288 | 你可以根据发送场景的不同,定义不同的短信类,从而实现一处定义多处调用,你可以继承 `Overtrue\EasySms\Message` 来定义短信模型: 289 | 290 | ```php 291 | order = $order; 306 | } 307 | 308 | // 定义直接使用内容发送平台的内容 309 | public function getContent(GatewayInterface $gateway = null) 310 | { 311 | return sprintf('您的订单:%s, 已经完成付款', $this->order->no); 312 | } 313 | 314 | // 定义使用模板发送方式平台所需要的模板 ID 315 | public function getTemplate(GatewayInterface $gateway = null) 316 | { 317 | return 'SMS_003'; 318 | } 319 | 320 | // 模板参数 321 | public function getData(GatewayInterface $gateway = null) 322 | { 323 | return [ 324 | 'order_no' => $this->order->no 325 | ]; 326 | } 327 | } 328 | ``` 329 | 330 | > 更多自定义方式请参考:[`Overtrue\EasySms\Message`](Overtrue\EasySms\Message;) 331 | 332 | 发送自定义短信: 333 | 334 | ```php 335 | $order = ...; 336 | $message = new OrderPaidMessage($order); 337 | 338 | $easySms->send(13188888888, $message); 339 | ``` 340 | 341 | ## 各平台配置说明 342 | 343 | ### [阿里云](https://www.aliyun.com/) 344 | 345 | 短信内容使用 `template` + `data` 346 | 347 | ```php 348 | 'aliyun' => [ 349 | 'access_key_id' => '', 350 | 'access_key_secret' => '', 351 | 'sign_name' => '', 352 | ], 353 | ``` 354 | 355 | ### [阿里云Rest](https://www.aliyun.com/) 356 | 357 | 短信内容使用 `template` + `data` 358 | 359 | ```php 360 | 'aliyunrest' => [ 361 | 'app_key' => '', 362 | 'app_secret_key' => '', 363 | 'sign_name' => '', 364 | ], 365 | ``` 366 | 367 | ### [阿里云国际](https://www.alibabacloud.com/help/zh/doc-detail/160524.html) 368 | 369 | 短信内容使用 `template` + `data` 370 | 371 | ```php 372 | 'aliyunintl' => [ 373 | 'access_key_id' => '', 374 | 'access_key_secret' => '', 375 | 'sign_name' => '', 376 | ], 377 | ``` 378 | 379 | 发送示例: 380 | 381 | ```php 382 | use Overtrue\EasySms\PhoneNumber; 383 | 384 | $easySms = new EasySms($config); 385 | $phone_number = new PhoneNumber(18888888888, 86); 386 | 387 | $easySms->send($phone_number, [ 388 | 'content' => '您好:先生/女士!您的验证码为${code},有效时间是5分钟,请及时验证。', 389 | 'template' => 'SMS_00000001', // 模板ID 390 | 'data' => [ 391 | "code" => 521410, 392 | ], 393 | ]); 394 | ``` 395 | 396 | ### [云片](https://www.yunpian.com) 397 | 398 | 短信内容使用 `content` 399 | 400 | ```php 401 | 'yunpian' => [ 402 | 'api_key' => '', 403 | 'signature' => '【默认签名】', // 内容中无签名时使用 404 | ], 405 | ``` 406 | 407 | ### [Submail](https://www.mysubmail.com) 408 | 409 | 短信内容使用 `data` 410 | 411 | ```php 412 | 'submail' => [ 413 | 'app_id' => '', 414 | 'app_key' => '', 415 | 'project' => '', // 默认 project,可在发送时 data 中指定 416 | ], 417 | ``` 418 | 419 | ### [螺丝帽](https://luosimao.com/) 420 | 421 | 短信内容使用 `content` 422 | 423 | ```php 424 | 'luosimao' => [ 425 | 'api_key' => '', 426 | ], 427 | ``` 428 | 429 | ### [容联云通讯](http://www.yuntongxun.com) 430 | 431 | 短信内容使用 `template` + `data` 432 | 433 | ```php 434 | 'yuntongxun' => [ 435 | 'app_id' => '', 436 | 'account_sid' => '', 437 | 'account_token' => '', 438 | 'is_sub_account' => false, 439 | ], 440 | ``` 441 | 442 | ### [互亿无线](http://www.ihuyi.com) 443 | 444 | 短信内容使用 `content` 445 | 446 | ```php 447 | 'huyi' => [ 448 | 'api_id' => '', 449 | 'api_key' => '', 450 | 'signature' => '', 451 | ], 452 | ``` 453 | 454 | ### [聚合数据](https://www.juhe.cn) 455 | 456 | 短信内容使用 `template` + `data` 457 | 458 | ```php 459 | 'juhe' => [ 460 | 'app_key' => '', 461 | ], 462 | ``` 463 | 464 | ### [SendCloud](http://www.sendcloud.net/) 465 | 466 | 短信内容使用 `template` + `data` 467 | 468 | ```php 469 | 'sendcloud' => [ 470 | 'sms_user' => '', 471 | 'sms_key' => '', 472 | 'timestamp' => false, // 是否启用时间戳 473 | ], 474 | ``` 475 | 476 | ### [百度云](https://cloud.baidu.com/) 477 | 478 | 短信内容使用 `template` + `data` 479 | 480 | ```php 481 | 'baidu' => [ 482 | 'ak' => '', 483 | 'sk' => '', 484 | 'invoke_id' => '', 485 | 'domain' => '', 486 | ], 487 | ``` 488 | 489 | ### [华信短信平台](http://www.ipyy.com/) 490 | 491 | 短信内容使用 `content` 492 | 493 | ```php 494 | 'huaxin' => [ 495 | 'user_id' => '', 496 | 'password' => '', 497 | 'account' => '', 498 | 'ip' => '', 499 | 'ext_no' => '', 500 | ], 501 | ``` 502 | 503 | ### [253云通讯(创蓝)](https://www.253.com/) 504 | 505 | 短信内容使用 `content` 506 | 507 | ```php 508 | 'chuanglan' => [ 509 | 'account' => '', 510 | 'password' => '', 511 | 512 | // 国际短信时必填 513 | 'intel_account' => '', 514 | 'intel_password' => '', 515 | 516 | // \Overtrue\EasySms\Gateways\ChuanglanGateway::CHANNEL_VALIDATE_CODE => 验证码通道(默认) 517 | // \Overtrue\EasySms\Gateways\ChuanglanGateway::CHANNEL_PROMOTION_CODE => 会员营销通道 518 | 'channel' => \Overtrue\EasySms\Gateways\ChuanglanGateway::CHANNEL_VALIDATE_CODE, 519 | 520 | // 会员营销通道 特定参数。创蓝规定:api提交营销短信的时候,需要自己加短信的签名及退订信息 521 | 'sign' => '【通讯云】', 522 | 'unsubscribe' => '回TD退订', 523 | ], 524 | ``` 525 | 526 | ### [创蓝云智](https://www.chuanglan.com/) 527 | 528 | 普通短信发送内容使用 `content` 529 | 530 | ```php 531 | 'chuanglanv1' => [ 532 | 'account' => '', 533 | 'password' => '', 534 | 'needstatus' => false, 535 | 'channel' => \Overtrue\EasySms\Gateways\Chuanglanv1Gateway::CHANNEL_NORMAL_CODE, 536 | ], 537 | ``` 538 | 539 | 发送示例: 540 | 541 | ```php 542 | $easySms->send(18888888888, [ 543 | 'content' => xxxxxxx 544 | ]); 545 | ``` 546 | 547 | 变量短信发送内容使用 `template` + `data` 548 | 549 | ```php 550 | 'chuanglanv1' => [ 551 | 'account' => '', 552 | 'password' => '', 553 | 'needstatus' => false, 554 | 'channel' => \Overtrue\EasySms\Gateways\Chuanglanv1Gateway::CHANNEL_VARIABLE_CODE, 555 | ], 556 | ``` 557 | 558 | 发送示例: 559 | 560 | ```php 561 | $easySms->send(18888888888, [ 562 | 'template' => xxxxxx, // 模板内容 563 | 'data' => 'phone":"15800000000,1234;15300000000,4321', 564 | ]); 565 | ``` 566 | 567 | ### [融云](http://www.rongcloud.cn) 568 | 569 | 短信分为两大类,验证类和通知类短信。 发送验证类短信使用 `template` + `data` 570 | 571 | ```php 572 | 'rongcloud' => [ 573 | 'app_key' => '', 574 | 'app_secret' => '', 575 | ] 576 | ``` 577 | 578 | ### [天毅无线](http://www.85hu.com/) 579 | 580 | 短信内容使用 `content` 581 | 582 | ```php 583 | 'tianyiwuxian' => [ 584 | 'username' => '', //用户名 585 | 'password' => '', //密码 586 | 'gwid' => '', //网关ID 587 | ] 588 | ``` 589 | 590 | ### [twilio](https://www.twilio.com) 591 | 592 | 短信使用 `content` 593 | 发送对象需要 使用`+`添加区号 594 | 595 | ```php 596 | 'twilio' => [ 597 | 'account_sid' => '', // sid 598 | 'from' => '', // 发送的号码 可以在控制台购买 599 | 'token' => '', // apitoken 600 | ], 601 | ``` 602 | 603 | ### [tiniyo](https://www.tiniyo.com) 604 | 605 | 短信使用 `content` 606 | 发送对象需要 使用`+`添加区号 607 | 608 | ```php 609 | 'tiniyo' => [ 610 | 'account_sid' => '', // auth_id from https://tiniyo.com 611 | 'from' => '', // 发送的号码 可以在控制台购买 612 | 'token' => '', // auth_secret from https://tiniyo.com 613 | ], 614 | ``` 615 | 616 | ### [腾讯云 SMS](https://cloud.tencent.com/product/sms) 617 | 618 | 短信内容使用 `template` + `data` 619 | 620 | ```php 621 | 'qcloud' => [ 622 | 'sdk_app_id' => '', // 短信应用的 SDK APP ID 623 | 'secret_id' => '', // SECRET ID 624 | 'secret_key' => '', // SECRET KEY 625 | 'sign_name' => '腾讯CoDesign', // 短信签名 626 | ], 627 | ``` 628 | 629 | 发送示例: 630 | 631 | ```php 632 | $easySms->send(18888888888, [ 633 | 'template' => 101234, // 模板ID 634 | 'data' => [ 635 | "a", 'b', 'c', 'd', //按占位顺序给值 636 | ], 637 | ]); 638 | ``` 639 | 640 | ### [华为云 SMS](https://www.huaweicloud.com/product/msgsms.html) 641 | 642 | 短信内容使用 `template` + `data` 643 | 644 | ```php 645 | 'huawei' => [ 646 | 'endpoint' => '', // APP接入地址 647 | 'app_key' => '', // APP KEY 648 | 'app_secret' => '', // APP SECRET 649 | 'from' => [ 650 | 'default' => '1069012345', // 默认使用签名通道号 651 | 'custom' => 'csms12345', // 其他签名通道号 可以在 data 中定义 from 来指定 652 | 'abc' => 'csms67890', // 其他签名通道号 653 | ... 654 | ], 655 | 'callback' => '' // 短信状态回调地址 656 | ], 657 | ``` 658 | 659 | 使用默认签名通道 `default` 660 | 661 | ```php 662 | $easySms->send(13188888888, [ 663 | 'template' => 'SMS_001', 664 | 'data' => [ 665 | 6379 666 | ], 667 | ]); 668 | ``` 669 | 670 | 使用指定签名通道 671 | 672 | ```php 673 | $easySms->send(13188888888, [ 674 | 'template' => 'SMS_001', 675 | 'data' => [ 676 | 6379, 677 | 'from' => 'custom' // 对应 config 中的 from 数组中 custom 678 | ], 679 | ]); 680 | ``` 681 | 682 | ### [网易云信](https://yunxin.163.com/sms) 683 | 684 | 短信内容使用 `template` + `data` 685 | 686 | ```php 687 | 'yunxin' => [ 688 | 'app_key' => '', 689 | 'app_secret' => '', 690 | 'code_length' => 4, // 随机验证码长度,范围 4~10,默认为 4 691 | 'need_up' => false, // 是否需要支持短信上行 692 | ], 693 | ``` 694 | 695 | ```php 696 | $easySms->send(18888888888, [ 697 | 'template' => 'SMS_001', // 不填则使用默认模板 698 | 'data' => [ 699 | 'code' => 8946, // 如果设置了该参数,则 code_length 参数无效 700 | 'action' => 'sendCode', // 默认为 `sendCode`,校验短信验证码使用 `verifyCode` 701 | ], 702 | ]); 703 | ``` 704 | 705 | 通知模板短信 706 | 707 | ```php 708 | $easySms->send(18888888888, [ 709 | 'template' => 'templateid', // 模板编号(由客户顾问配置之后告知开发者) 710 | 'data' => [ 711 | 'action' => 'sendTemplate', // 默认为 `sendCode`,校验短信验证码使用 `verifyCode` 712 | 'params' => [1,2,3], //短信参数列表,用于依次填充模板 713 | ], 714 | ]); 715 | ``` 716 | 717 | ### [云之讯](https://www.ucpaas.com/index.html) 718 | 719 | 短信内容使用 `template` + `data` 720 | 721 | ```php 722 | 'yunzhixun' => [ 723 | 'sid' => '', 724 | 'token' => '', 725 | 'app_id' => '', 726 | ], 727 | ``` 728 | 729 | ```php 730 | $easySms->send(18888888888, [ 731 | 'template' => 'SMS_001', 732 | 'data' => [ 733 | 'params' => '8946,3', // 模板参数,多个参数使用 `,` 分割,模板无参数时可为空 734 | 'uid' => 'hexianghui', // 用户 ID,随状态报告返回,可为空 735 | 'mobiles' => '18888888888,188888888889', // 批量发送短信,手机号使用 `,` 分割,不使用批量发送请不要设置该参数 736 | ], 737 | ]); 738 | ``` 739 | 740 | ### [凯信通](http://www.kingtto.cn/) 741 | 742 | 短信内容使用 `content` 743 | 744 | ```php 745 | 'kingtto' => [ 746 | 'userid' => '', 747 | 'account' => '', 748 | 'password' => '', 749 | ], 750 | ``` 751 | 752 | ```php 753 | $easySms->send(18888888888, [ 754 | 'content' => '您的验证码为: 6379', 755 | ]); 756 | ``` 757 | 758 | ### [七牛云](https://www.qiniu.com/) 759 | 760 | 短信内容使用 `template` + `data` 761 | 762 | ```php 763 | 'qiniu' => [ 764 | 'secret_key' => '', 765 | 'access_key' => '', 766 | ], 767 | ``` 768 | 769 | ```php 770 | $easySms->send(18888888888, [ 771 | 'template' => '1231234123412341234', 772 | 'data' => [ 773 | 'code' => 1234, 774 | ], 775 | ]); 776 | ``` 777 | 778 | ### [Ucloud](https://www.ucloud.cn/) 779 | 780 | 短信使用 `template` + `data` 781 | 782 | ```php 783 | 'ucloud' => [ 784 | 'private_key' => '', //私钥 785 | 'public_key' => '', //公钥 786 | 'sig_content' => '', // 短信签名, 787 | 'project_id' => '', //项目ID,子账号才需要该参数 788 | ], 789 | ``` 790 | 791 | ```php 792 | $easySms->send(18888888888, [ 793 | 'template' => 'UTAXXXXX', //短信模板 794 | 'data' => [ 795 | 'code' => 1234, //模板参数,模板没有参数不用则填写,有多个参数请用数组,[1111,1111] 796 | 'mobiles' =>'', //同时发送多个手机短信,请用数组[xxx,xxx] 797 | ], 798 | ]); 799 | 800 | ``` 801 | 802 | ### [短信宝](http://www.smsbao.com/) 803 | 804 | 短信使用 `content` 805 | 806 | ```php 807 | 'smsbao' => [ 808 | 'user' => '', //账号 809 | 'password' => '' //密码 810 | ], 811 | ``` 812 | 813 | ```php 814 | $easySms->send(18888888888, [ 815 | 'content' => '您的验证码为: 6379', //短信模板 816 | ]); 817 | 818 | ``` 819 | 820 | ### [摩杜云](https://www.moduyun.com/) 821 | 822 | 短信使用 `template` + `data` 823 | 824 | ```php 825 | 'moduyun' => [ 826 | 'accesskey' => '', //必填 ACCESS KEY 827 | 'secretkey' => '', //必填 SECRET KEY 828 | 'signId' => '', //选填 短信签名,如果使用默认签名,该字段可缺省 829 | 'type' => 0, //选填 0:普通短信;1:营销短信 830 | ], 831 | ``` 832 | 833 | ```php 834 | $easySms->send(18888888888, [ 835 | 'template' => '5a95****b953', //短信模板 836 | 'data' => [ 837 | 1234, //模板参数,对应模板的{1} 838 | 30 //模板参数,对应模板的{2} 839 | //... 840 | ], 841 | ]); 842 | 843 | ``` 844 | 845 | ### [融合云(助通)](https://www.ztinfo.cn/products/sms) 846 | 847 | 短信使用 `template` + `data` 848 | 849 | ```php 850 | 'rongheyun' => [ 851 | 'username' => '', //必填 用户名 852 | 'password' => '', //必填 密码 853 | 'signature'=> '', //必填 已报备的签名 854 | ], 855 | ``` 856 | 857 | ```php 858 | $easySms->send(18888888888, [ 859 | 'template' => '31874', //短信模板 860 | 'data' => [ 861 | 'valid_code' => '888888', //模板参数,对应模板的{valid_code} 862 | //... 863 | ], 864 | ]); 865 | 866 | ``` 867 | 868 | ### [蜘蛛云](https://zzyun.com/) 869 | 870 | 短信使用 `template` + `data` 871 | 872 | ```php 873 | 'zzyun' => [ 874 | 'user_id' => '', //必填 会员ID 875 | 'secret' => '', //必填 接口密钥 876 | 'sign_name'=> '', //必填 短信签名 877 | ], 878 | ``` 879 | 880 | ```php 881 | $easySms->send(18888888888, [ 882 | 'template' => 'SMS_210317****', //短信模板 883 | 'data' => [ 884 | 'code' => '888888', //模板参数,对应模板的{code} 885 | //... 886 | ], 887 | ]); 888 | 889 | ``` 890 | 891 | ### [融合云信](https://maap.wo.cn/) 892 | 893 | 短信使用 `template` + `data` 894 | 895 | ```php 896 | 'maap' => [ 897 | 'cpcode' => '', //必填 商户编码 898 | 'key' => '', //必填 接口密钥 899 | 'excode'=> '', //选填 扩展名 900 | ], 901 | ``` 902 | 903 | ```php 904 | $easySms->send(18888888888, [ 905 | 'template' => '356120', //短信模板 906 | 'data' => [ 907 | '123465' 908 | ],//模板参数 909 | ]); 910 | 911 | ``` 912 | 913 | ### [天瑞云](http://cms.tinree.com/) 914 | 915 | 短信内容使用 `template` + `data` 916 | 917 | ```php 918 | 'tinree' => [ 919 | 'accesskey' => '', // 平台分配给用户的accesskey 920 | 'secret' => '', // 平台分配给用户的secret 921 | 'sign' => '', // 平台上申请的接口短信签名或者签名ID 922 | ], 923 | ``` 924 | 925 | 发送示例: 926 | 927 | ```php 928 | $easySms->send(18888888888, [ 929 | 'template' => '123456', // 模板ID 930 | 'data' => [ 931 | "a", 'b', 'c', //按模板变量占位顺序 932 | ], 933 | ]); 934 | ``` 935 | 936 | ### [时代互联](https://www.now.cn/) 937 | 938 | 短信使用 `content` 939 | 940 | ```php 941 | 'nowcn' => [ 942 | 'key' => '', //用户ID 943 | 'secret' => '', //开发密钥 944 | 'api_type' => '', // 短信通道, 945 | ], 946 | ``` 947 | 948 | 发送示例: 949 | 950 | ```php 951 | $easySms->send(18888888888, [ 952 | 'content' => '您的验证码为: 6379', 953 | ]); 954 | ``` 955 | 956 | ### [火山引擎](https://console.volcengine.com/sms/) 957 | 958 | 短信内容使用 `template` + `data` 959 | 960 | ```php 961 | 'volcengine' => [ 962 | 'access_key_id' => '', // 平台分配给用户的access_key_id 963 | 'access_key_secret' => '', // 平台分配给用户的access_key_secret 964 | 'region_id' => 'cn-north-1', // 国内节点 cn-north-1,国外节点 ap-singapore-1,不填或填错,默认使用国内节点 965 | 'sign_name' => '', // 平台上申请的接口短信签名或者签名ID,可不填,发送短信时data中指定 966 | 'sms_account' => '', // 消息组帐号,火山短信页面右上角,短信应用括号中的字符串,可不填,发送短信时data中指定 967 | ], 968 | ``` 969 | 970 | 发送示例1: 971 | 972 | ```php 973 | $easySms->send(18888888888, [ 974 | 'template' => 'SMS_123456', // 模板ID 975 | 'data' => [ 976 | "code" => 1234 // 模板变量 977 | ], 978 | ]); 979 | ``` 980 | 981 | 发送示例2: 982 | 983 | ```php 984 | $easySms->send(18888888888, [ 985 | 'template' => 'SMS_123456', // 模板ID 986 | 'data' => [ 987 | "template_param" => ["code" => 1234], // 模板变量参数 988 | "sign_name" => "yoursignname", // 签名,覆盖配置文件中的sign_name 989 | "sms_account" => "yoursmsaccount", // 消息组帐号,覆盖配置文件中的sms_account 990 | "phone_numbers" => "18888888888,18888888889", // 手机号,批量发送,英文的逗号连接多个手机号,覆盖发送方法中的填入的手机号 991 | ], 992 | ]); 993 | ``` 994 | 995 | ### [移动云MAS(黑名单模式)](https://mas.10086.cn/) 996 | 997 | 短信内容使用 `template` + `data` 998 | 999 | ```php 1000 | 'yidongmasblack' => [ 1001 | 'ecName' => '', // 机构名称 1002 | 'secretKey' => '', // 密钥 1003 | 'apId' => '', // 应用ID 1004 | 'sign' => '', // 签名 1005 | 'addSerial' => '', // 通道号默认空 1006 | ], 1007 | ``` 1008 | 1009 | 发送示例: 1010 | 1011 | ```php 1012 | $easySms->send(18888888888, [ 1013 | 'content' => '您的验证码为: 6379', 1014 | ]); 1015 | ``` 1016 | 1017 | ### [电信天翼云](https://www.ctyun.cn/) 1018 | 1019 | 短信使用 `content` 1020 | 1021 | ```php 1022 | 'ctyun' => [ 1023 | 'access_key' => '', //用户access 1024 | 'secret_key' => '', //开发密钥secret 1025 | 'sign' => '验证码测试', // 短信下发签名, 1026 | ], 1027 | ``` 1028 | 1029 | 发送示例: 1030 | 1031 | ```php 1032 | $easySms->send(18888888888, [ 1033 | 'content' => $content, 1034 | 'template' => 'SMS64124870510', // 模板ID 1035 | 'data' => [ 1036 | "code" => 123456, 1037 | ], 1038 | ]); 1039 | ``` 1040 | 1041 | ## :heart: 支持我 1042 | 1043 | [![Sponsor me](https://github.com/overtrue/overtrue/blob/master/sponsor-me.svg?raw=true)](https://github.com/sponsors/overtrue) 1044 | 1045 | 如果你喜欢我的项目并想支持它,[点击这里 :heart:](https://github.com/sponsors/overtrue) 1046 | 1047 | ## Project supported by JetBrains 1048 | 1049 | Many thanks to Jetbrains for kindly providing a license for me to work on this and other open-source projects. 1050 | 1051 | [![](https://resources.jetbrains.com/storage/products/company/brand/logos/jb_beam.svg)](https://www.jetbrains.com/?from=https://github.com/overtrue) 1052 | 1053 | ## PHP 扩展包开发 1054 | 1055 | > 想知道如何从零开始构建 PHP 扩展包? 1056 | > 1057 | > 请关注我的实战课程,我会在此课程中分享一些扩展开发经验 —— [《PHP 扩展包实战教程 - 从入门到发布》](https://learnku.com/courses/creating-package) 1058 | 1059 | ## License 1060 | 1061 | MIT 1062 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "overtrue/easy-sms", 3 | "description": "The easiest way to send short message.", 4 | "type": "library", 5 | "require": { 6 | "guzzlehttp/guzzle": "^6.2 || ^7.0", 7 | "php": ">=8.0", 8 | "ext-json": "*" 9 | }, 10 | "require-dev": { 11 | "phpunit/phpunit": "^9.5.8", 12 | "mockery/mockery": "^1.4.2", 13 | "jetbrains/phpstorm-attributes": "^1.0", 14 | "friendsofphp/php-cs-fixer": "^3.54" 15 | }, 16 | "autoload": { 17 | "psr-4": { 18 | "Overtrue\\EasySms\\": "src" 19 | } 20 | }, 21 | "autoload-dev": { 22 | "psr-4": { 23 | "Overtrue\\EasySms\\Tests\\": "tests" 24 | } 25 | }, 26 | "license": "MIT", 27 | "authors": [ 28 | { 29 | "name": "overtrue", 30 | "email": "i@overtrue.me" 31 | } 32 | ], 33 | "scripts": { 34 | "phpstan": "phpstan analyse", 35 | "check-style": "php-cs-fixer fix --using-cache=no --diff --config=.php-cs-fixer.dist.php --dry-run --allow-risky=yes --ansi", 36 | "fix-style": "php-cs-fixer fix --using-cache=no --config=.php-cs-fixer.dist.php --allow-risky=yes --ansi", 37 | "test": "phpunit --colors", 38 | "psalm": "psalm --show-info=true --no-cache", 39 | "psalm-fix": "psalm --no-cache --alter --issues=MissingReturnType,MissingParamType" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /psalm.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/Contracts/GatewayInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Overtrue\EasySms\Contracts; 13 | 14 | use Overtrue\EasySms\Support\Config; 15 | 16 | /** 17 | * Class GatewayInterface. 18 | */ 19 | interface GatewayInterface 20 | { 21 | /** 22 | * Get gateway name. 23 | * 24 | * @return string 25 | */ 26 | public function getName(); 27 | 28 | /** 29 | * Send a short message. 30 | * 31 | * @return array 32 | */ 33 | public function send(PhoneNumberInterface $to, MessageInterface $message, Config $config); 34 | } 35 | -------------------------------------------------------------------------------- /src/Contracts/MessageInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Overtrue\EasySms\Contracts; 13 | 14 | /** 15 | * Interface MessageInterface. 16 | */ 17 | interface MessageInterface 18 | { 19 | public const TEXT_MESSAGE = 'text'; 20 | 21 | public const VOICE_MESSAGE = 'voice'; 22 | 23 | /** 24 | * Return the message type. 25 | */ 26 | public function getMessageType(): string; 27 | 28 | /** 29 | * Return message content. 30 | */ 31 | public function getContent(?GatewayInterface $gateway = null): ?string; 32 | 33 | /** 34 | * Return the template id of message. 35 | */ 36 | public function getTemplate(?GatewayInterface $gateway = null): ?string; 37 | 38 | /** 39 | * Return the template data of message. 40 | */ 41 | public function getData(?GatewayInterface $gateway = null): array; 42 | 43 | /** 44 | * Return message supported gateways. 45 | */ 46 | public function getGateways(): array; 47 | } 48 | -------------------------------------------------------------------------------- /src/Contracts/PhoneNumberInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Overtrue\EasySms\Contracts; 13 | 14 | /** 15 | * Interface PhoneNumberInterface. 16 | * 17 | * @author overtrue 18 | */ 19 | interface PhoneNumberInterface extends \JsonSerializable 20 | { 21 | /** 22 | * 86. 23 | */ 24 | public function getIDDCode(): ?int; 25 | 26 | /** 27 | * 18888888888. 28 | */ 29 | public function getNumber(): int|string; 30 | 31 | /** 32 | * +8618888888888. 33 | */ 34 | public function getUniversalNumber(): string; 35 | 36 | /** 37 | * 008618888888888. 38 | */ 39 | public function getZeroPrefixedNumber(): string; 40 | 41 | /** 42 | * @return string 43 | */ 44 | public function __toString(); 45 | } 46 | -------------------------------------------------------------------------------- /src/Contracts/StrategyInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Overtrue\EasySms\Contracts; 13 | 14 | /** 15 | * Interface StrategyInterface. 16 | */ 17 | interface StrategyInterface 18 | { 19 | /** 20 | * Apply the strategy and return result. 21 | * 22 | * @return array 23 | */ 24 | public function apply(array $gateways); 25 | } 26 | -------------------------------------------------------------------------------- /src/EasySms.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Overtrue\EasySms; 13 | 14 | use Closure; 15 | use Overtrue\EasySms\Contracts\GatewayInterface; 16 | use Overtrue\EasySms\Contracts\MessageInterface; 17 | use Overtrue\EasySms\Contracts\PhoneNumberInterface; 18 | use Overtrue\EasySms\Contracts\StrategyInterface; 19 | use Overtrue\EasySms\Exceptions\InvalidArgumentException; 20 | use Overtrue\EasySms\Exceptions\NoGatewayAvailableException; 21 | use Overtrue\EasySms\Gateways\Gateway; 22 | use Overtrue\EasySms\Strategies\OrderStrategy; 23 | use Overtrue\EasySms\Support\Config; 24 | 25 | /** 26 | * Class EasySms. 27 | */ 28 | class EasySms 29 | { 30 | protected Config $config; 31 | 32 | protected string $defaultGateway; 33 | 34 | protected array $customCreators = []; 35 | 36 | protected array $gateways = []; 37 | 38 | protected Messenger $messenger; 39 | 40 | protected array $strategies = []; 41 | 42 | /** 43 | * Constructor. 44 | */ 45 | public function __construct(array $config) 46 | { 47 | $this->config = new Config($config); 48 | } 49 | 50 | /** 51 | * Send a message. 52 | * 53 | * @throws NoGatewayAvailableException 54 | * @throws InvalidArgumentException 55 | */ 56 | public function send(array|string $to, MessageInterface|array $message, array $gateways = []): array 57 | { 58 | $to = $this->formatPhoneNumber($to); 59 | $message = $this->formatMessage($message); 60 | $gateways = empty($gateways) ? $message->getGateways() : $gateways; 61 | 62 | if (empty($gateways)) { 63 | $gateways = $this->config->get('default.gateways', []); 64 | } 65 | 66 | return $this->getMessenger()->send($to, $message, $this->formatGateways($gateways)); 67 | } 68 | 69 | /** 70 | * Create a gateway. 71 | * 72 | * @throws InvalidArgumentException 73 | */ 74 | public function gateway(?string $name): GatewayInterface 75 | { 76 | if (!isset($this->gateways[$name])) { 77 | $this->gateways[$name] = $this->createGateway($name); 78 | } 79 | 80 | return $this->gateways[$name]; 81 | } 82 | 83 | /** 84 | * Get a strategy instance. 85 | * 86 | * @throws InvalidArgumentException 87 | */ 88 | public function strategy(?string $strategy = null): StrategyInterface 89 | { 90 | if (\is_null($strategy)) { 91 | $strategy = $this->config->get('default.strategy', OrderStrategy::class); 92 | } 93 | 94 | if (!\class_exists($strategy)) { 95 | $strategy = __NAMESPACE__ . '\Strategies\\' . \ucfirst($strategy); 96 | } 97 | 98 | if (!\class_exists($strategy)) { 99 | throw new InvalidArgumentException("Unsupported strategy \"{$strategy}\""); 100 | } 101 | 102 | if (empty($this->strategies[$strategy]) || !($this->strategies[$strategy] instanceof StrategyInterface)) { 103 | $this->strategies[$strategy] = new $strategy($this); 104 | } 105 | 106 | return $this->strategies[$strategy]; 107 | } 108 | 109 | /** 110 | * Register a custom driver creator Closure. 111 | * 112 | * @return $this 113 | */ 114 | public function extend(string $name, \Closure $callback): static 115 | { 116 | $this->customCreators[$name] = $callback; 117 | 118 | return $this; 119 | } 120 | 121 | public function getConfig(): Config 122 | { 123 | return $this->config; 124 | } 125 | 126 | public function getMessenger(): Messenger 127 | { 128 | return $this->messenger ??= new Messenger($this); 129 | } 130 | 131 | /** 132 | * Create a new driver instance. 133 | * 134 | * @throws \InvalidArgumentException 135 | * @throws InvalidArgumentException 136 | */ 137 | protected function createGateway(string $name): GatewayInterface 138 | { 139 | $config = $this->config->get("gateways.{$name}", []); 140 | 141 | if (!isset($config['timeout'])) { 142 | $config['timeout'] = $this->config->get('timeout', Gateway::DEFAULT_TIMEOUT); 143 | } 144 | 145 | $config['options'] = $this->config->get('options', []); 146 | 147 | if (isset($this->customCreators[$name])) { 148 | $gateway = $this->callCustomCreator($name, $config); 149 | } else { 150 | $className = $this->formatGatewayClassName($name); 151 | $gateway = $this->makeGateway($className, $config); 152 | } 153 | 154 | if (!($gateway instanceof GatewayInterface)) { 155 | throw new InvalidArgumentException(\sprintf('Gateway "%s" must implement interface %s.', $name, GatewayInterface::class)); 156 | } 157 | 158 | return $gateway; 159 | } 160 | 161 | /** 162 | * Make gateway instance. 163 | * 164 | * @throws InvalidArgumentException 165 | */ 166 | protected function makeGateway(string $gateway, array $config): GatewayInterface 167 | { 168 | if (!\class_exists($gateway) || !\in_array(GatewayInterface::class, \class_implements($gateway))) { 169 | throw new InvalidArgumentException(\sprintf('Class "%s" is a invalid easy-sms gateway.', $gateway)); 170 | } 171 | 172 | return new $gateway($config); 173 | } 174 | 175 | /** 176 | * Format gateway name. 177 | */ 178 | protected function formatGatewayClassName(string $name): string 179 | { 180 | if (\class_exists($name) && \in_array(GatewayInterface::class, \class_implements($name))) { 181 | return $name; 182 | } 183 | 184 | $name = \ucfirst(\str_replace(['-', '_', ''], '', $name)); 185 | 186 | return __NAMESPACE__ . "\\Gateways\\{$name}Gateway"; 187 | } 188 | 189 | /** 190 | * Call a custom gateway creator. 191 | */ 192 | protected function callCustomCreator(string $gateway, array $config): mixed 193 | { 194 | return \call_user_func($this->customCreators[$gateway], $config); 195 | } 196 | 197 | protected function formatPhoneNumber(PhoneNumberInterface|string $number): PhoneNumberInterface 198 | { 199 | if ($number instanceof PhoneNumberInterface) { 200 | return $number; 201 | } 202 | 203 | return new PhoneNumber(\trim($number)); 204 | } 205 | 206 | protected function formatMessage(MessageInterface|array|string $message): MessageInterface 207 | { 208 | if (!($message instanceof MessageInterface)) { 209 | if (!\is_array($message)) { 210 | $message = [ 211 | 'content' => $message, 212 | 'template' => $message, 213 | ]; 214 | } 215 | 216 | $message = new Message($message); 217 | } 218 | 219 | return $message; 220 | } 221 | 222 | /** 223 | * @throws InvalidArgumentException 224 | */ 225 | protected function formatGateways(array $gateways): array 226 | { 227 | $formatted = []; 228 | 229 | foreach ($gateways as $gateway => $setting) { 230 | if (\is_int($gateway) && \is_string($setting)) { 231 | $gateway = $setting; 232 | $setting = []; 233 | } 234 | 235 | $formatted[$gateway] = $setting; 236 | $globalSettings = $this->config->get("gateways.{$gateway}", []); 237 | 238 | if (\is_string($gateway) && !empty($globalSettings) && \is_array($setting)) { 239 | $formatted[$gateway] = new Config(\array_merge($globalSettings, $setting)); 240 | } 241 | } 242 | 243 | $result = []; 244 | 245 | foreach ($this->strategy()->apply($formatted) as $name) { 246 | $result[$name] = $formatted[$name]; 247 | } 248 | 249 | return $result; 250 | } 251 | } -------------------------------------------------------------------------------- /src/Exceptions/Exception.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Overtrue\EasySms\Exceptions; 13 | 14 | /** 15 | * Class Exception. 16 | * 17 | * @author overtrue 18 | */ 19 | class Exception extends \Exception 20 | { 21 | } 22 | -------------------------------------------------------------------------------- /src/Exceptions/GatewayErrorException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Overtrue\EasySms\Exceptions; 13 | 14 | /** 15 | * Class GatewayErrorException. 16 | */ 17 | class GatewayErrorException extends Exception 18 | { 19 | /** 20 | * @var array 21 | */ 22 | public $raw = []; 23 | 24 | /** 25 | * GatewayErrorException constructor. 26 | * 27 | * @param string $message 28 | * @param int $code 29 | */ 30 | public function __construct($message, $code, array $raw = []) 31 | { 32 | parent::__construct($message, intval($code)); 33 | 34 | $this->raw = $raw; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Exceptions/InvalidArgumentException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Overtrue\EasySms\Exceptions; 13 | 14 | /** 15 | * Class InvalidArgumentException. 16 | */ 17 | class InvalidArgumentException extends Exception 18 | { 19 | } 20 | -------------------------------------------------------------------------------- /src/Exceptions/NoGatewayAvailableException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Overtrue\EasySms\Exceptions; 13 | 14 | /** 15 | * Class NoGatewayAvailableException. 16 | * 17 | * @author overtrue 18 | */ 19 | class NoGatewayAvailableException extends Exception 20 | { 21 | /** 22 | * @var array 23 | */ 24 | public $results = []; 25 | 26 | /** 27 | * @var array 28 | */ 29 | public $exceptions = []; 30 | 31 | /** 32 | * NoGatewayAvailableException constructor. 33 | * 34 | * @param int $code 35 | */ 36 | public function __construct(array $results = [], $code = 0, ?\Throwable $previous = null) 37 | { 38 | $this->results = $results; 39 | $this->exceptions = \array_column($results, 'exception', 'gateway'); 40 | 41 | parent::__construct('All the gateways have failed. You can get error details by `$exception->getExceptions()`', $code, $previous); 42 | } 43 | 44 | /** 45 | * @return array 46 | */ 47 | public function getResults() 48 | { 49 | return $this->results; 50 | } 51 | 52 | /** 53 | * @param string $gateway 54 | * 55 | * @return mixed|null 56 | */ 57 | public function getException($gateway) 58 | { 59 | return isset($this->exceptions[$gateway]) ? $this->exceptions[$gateway] : null; 60 | } 61 | 62 | /** 63 | * @return array 64 | */ 65 | public function getExceptions() 66 | { 67 | return $this->exceptions; 68 | } 69 | 70 | public function getLastException() 71 | { 72 | return end($this->exceptions); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Gateways/AliyunGateway.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Overtrue\EasySms\Gateways; 13 | 14 | use Overtrue\EasySms\Contracts\MessageInterface; 15 | use Overtrue\EasySms\Contracts\PhoneNumberInterface; 16 | use Overtrue\EasySms\Exceptions\GatewayErrorException; 17 | use Overtrue\EasySms\Support\Config; 18 | use Overtrue\EasySms\Traits\HasHttpRequest; 19 | 20 | /** 21 | * Class AliyunGateway. 22 | * 23 | * @author carson 24 | * 25 | * @see https://help.aliyun.com/document_detail/55451.html 26 | */ 27 | class AliyunGateway extends Gateway 28 | { 29 | use HasHttpRequest; 30 | 31 | public const ENDPOINT_URL = 'http://dysmsapi.aliyuncs.com'; 32 | 33 | public const ENDPOINT_METHOD = 'SendSms'; 34 | 35 | public const ENDPOINT_VERSION = '2017-05-25'; 36 | 37 | public const ENDPOINT_FORMAT = 'JSON'; 38 | 39 | public const ENDPOINT_REGION_ID = 'cn-hangzhou'; 40 | 41 | public const ENDPOINT_SIGNATURE_METHOD = 'HMAC-SHA1'; 42 | 43 | public const ENDPOINT_SIGNATURE_VERSION = '1.0'; 44 | 45 | /** 46 | * @return array 47 | * 48 | * @throws GatewayErrorException ; 49 | */ 50 | public function send(PhoneNumberInterface $to, MessageInterface $message, Config $config) 51 | { 52 | $data = $message->getData($this); 53 | 54 | $signName = !empty($data['sign_name']) ? $data['sign_name'] : $config->get('sign_name'); 55 | 56 | unset($data['sign_name']); 57 | 58 | $params = [ 59 | 'RegionId' => self::ENDPOINT_REGION_ID, 60 | 'AccessKeyId' => $config->get('access_key_id'), 61 | 'Format' => self::ENDPOINT_FORMAT, 62 | 'SignatureMethod' => self::ENDPOINT_SIGNATURE_METHOD, 63 | 'SignatureVersion' => self::ENDPOINT_SIGNATURE_VERSION, 64 | 'SignatureNonce' => uniqid(), 65 | 'Timestamp' => gmdate('Y-m-d\TH:i:s\Z'), 66 | 'Action' => self::ENDPOINT_METHOD, 67 | 'Version' => self::ENDPOINT_VERSION, 68 | 'PhoneNumbers' => !\is_null($to->getIDDCode()) ? strval($to->getZeroPrefixedNumber()) : $to->getNumber(), 69 | 'SignName' => $signName, 70 | 'TemplateCode' => $message->getTemplate($this), 71 | 'TemplateParam' => json_encode($data, JSON_FORCE_OBJECT), 72 | ]; 73 | 74 | $params['Signature'] = $this->generateSign($params); 75 | 76 | $result = $this->get(self::ENDPOINT_URL, $params); 77 | 78 | if (!empty($result['Code']) && 'OK' != $result['Code']) { 79 | throw new GatewayErrorException($result['Message'], $result['Code'], $result); 80 | } 81 | 82 | return $result; 83 | } 84 | 85 | /** 86 | * Generate Sign. 87 | * 88 | * @param array $params 89 | * 90 | * @return string 91 | * 92 | * @see https://help.aliyun.com/document_detail/101343.html 93 | */ 94 | protected function generateSign($params) 95 | { 96 | ksort($params); 97 | $accessKeySecret = $this->config->get('access_key_secret'); 98 | $stringToSign = 'GET&%2F&'.urlencode(http_build_query($params, '', '&', PHP_QUERY_RFC3986)); 99 | $stringToSign = str_replace('%7E', '~', $stringToSign); 100 | 101 | return base64_encode(hash_hmac('sha1', $stringToSign, $accessKeySecret.'&', true)); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Gateways/AliyunIntlGateway.php: -------------------------------------------------------------------------------- 1 | getData($this); 42 | 43 | $signName = !empty($data['sign_name']) ? $data['sign_name'] : $config->get('sign_name'); 44 | 45 | unset($data['sign_name']); 46 | 47 | $params = [ 48 | 'RegionId' => self::ENDPOINT_REGION_ID, 49 | 'AccessKeyId' => $config->get('access_key_id'), 50 | 'Format' => self::ENDPOINT_FORMAT, 51 | 'SignatureMethod' => self::ENDPOINT_SIGNATURE_METHOD, 52 | 'SignatureVersion' => self::ENDPOINT_SIGNATURE_VERSION, 53 | 'SignatureNonce' => uniqid('', true), 54 | 'Timestamp' => gmdate('Y-m-d\TH:i:s\Z'), 55 | 'Version' => self::ENDPOINT_VERSION, 56 | 'To' => !\is_null($to->getIDDCode()) ? (int) $to->getZeroPrefixedNumber() : $to->getNumber(), 57 | 'Action' => self::ENDPOINT_ACTION, 58 | 'From' => $signName, 59 | 'TemplateCode' => $message->getTemplate($this), 60 | 'TemplateParam' => json_encode($data, JSON_FORCE_OBJECT), 61 | ]; 62 | 63 | $params['Signature'] = $this->generateSign($params); 64 | 65 | $result = $this->get(self::ENDPOINT_URL, $params); 66 | 67 | if ('OK' !== $result['ResponseCode']) { 68 | throw new GatewayErrorException($result['ResponseDescription'], $result['ResponseCode'], $result); 69 | } 70 | 71 | return $result; 72 | } 73 | 74 | /** 75 | * Generate sign. 76 | * 77 | * @return string 78 | */ 79 | protected function generateSign(array $params) 80 | { 81 | ksort($params); 82 | $accessKeySecret = $this->config->get('access_key_secret'); 83 | $stringToSign = 'GET&%2F&'.urlencode(http_build_query($params, '', '&', PHP_QUERY_RFC3986)); 84 | 85 | return base64_encode(hash_hmac('sha1', $stringToSign, $accessKeySecret.'&', true)); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Gateways/AliyunrestGateway.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Overtrue\EasySms\Gateways; 13 | 14 | use Overtrue\EasySms\Contracts\MessageInterface; 15 | use Overtrue\EasySms\Contracts\PhoneNumberInterface; 16 | use Overtrue\EasySms\Exceptions\GatewayErrorException; 17 | use Overtrue\EasySms\Support\Config; 18 | use Overtrue\EasySms\Traits\HasHttpRequest; 19 | 20 | /** 21 | * Class AliyunrestGateway. 22 | */ 23 | class AliyunrestGateway extends Gateway 24 | { 25 | use HasHttpRequest; 26 | 27 | public const ENDPOINT_URL = 'http://gw.api.taobao.com/router/rest'; 28 | 29 | public const ENDPOINT_VERSION = '2.0'; 30 | 31 | public const ENDPOINT_FORMAT = 'json'; 32 | 33 | public const ENDPOINT_METHOD = 'alibaba.aliqin.fc.sms.num.send'; 34 | 35 | public const ENDPOINT_SIGNATURE_METHOD = 'md5'; 36 | 37 | public const ENDPOINT_PARTNER_ID = 'EasySms'; 38 | 39 | /** 40 | * @return array|void 41 | */ 42 | public function send(PhoneNumberInterface $to, MessageInterface $message, Config $config) 43 | { 44 | $urlParams = [ 45 | 'app_key' => $config->get('app_key'), 46 | 'v' => self::ENDPOINT_VERSION, 47 | 'format' => self::ENDPOINT_FORMAT, 48 | 'sign_method' => self::ENDPOINT_SIGNATURE_METHOD, 49 | 'method' => self::ENDPOINT_METHOD, 50 | 'timestamp' => date('Y-m-d H:i:s'), 51 | 'partner_id' => self::ENDPOINT_PARTNER_ID, 52 | ]; 53 | 54 | $params = [ 55 | 'extend' => '', 56 | 'sms_type' => 'normal', 57 | 'sms_free_sign_name' => $config->get('sign_name'), 58 | 'sms_param' => json_encode($message->getData($this)), 59 | 'rec_num' => !\is_null($to->getIDDCode()) ? strval($to->getZeroPrefixedNumber()) : $to->getNumber(), 60 | 'sms_template_code' => $message->getTemplate($this), 61 | ]; 62 | $urlParams['sign'] = $this->generateSign(array_merge($params, $urlParams)); 63 | 64 | $result = $this->post($this->getEndpointUrl($urlParams), $params); 65 | 66 | if (isset($result['error_response']) && 0 != $result['error_response']['code']) { 67 | throw new GatewayErrorException($result['error_response']['msg'], $result['error_response']['code'], $result); 68 | } 69 | 70 | return $result; 71 | } 72 | 73 | /** 74 | * @param array $params 75 | * 76 | * @return string 77 | */ 78 | protected function getEndpointUrl($params) 79 | { 80 | return self::ENDPOINT_URL.'?'.http_build_query($params); 81 | } 82 | 83 | /** 84 | * @param array $params 85 | * 86 | * @return string 87 | */ 88 | protected function generateSign($params) 89 | { 90 | ksort($params); 91 | 92 | $stringToBeSigned = $this->config->get('app_secret_key'); 93 | foreach ($params as $k => $v) { 94 | if (!is_array($v) && '@' != substr($v, 0, 1)) { 95 | $stringToBeSigned .= "$k$v"; 96 | } 97 | } 98 | unset($k, $v); 99 | $stringToBeSigned .= $this->config->get('app_secret_key'); 100 | 101 | return strtoupper(md5($stringToBeSigned)); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Gateways/BaiduGateway.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Overtrue\EasySms\Gateways; 13 | 14 | use Overtrue\EasySms\Contracts\MessageInterface; 15 | use Overtrue\EasySms\Contracts\PhoneNumberInterface; 16 | use Overtrue\EasySms\Exceptions\GatewayErrorException; 17 | use Overtrue\EasySms\Support\Config; 18 | use Overtrue\EasySms\Traits\HasHttpRequest; 19 | 20 | /** 21 | * Class BaiduGateway. 22 | * 23 | * @see https://cloud.baidu.com/doc/SMS/index.html 24 | */ 25 | class BaiduGateway extends Gateway 26 | { 27 | use HasHttpRequest; 28 | 29 | public const ENDPOINT_HOST = 'smsv3.bj.baidubce.com'; 30 | 31 | public const ENDPOINT_URI = '/api/v3/sendSms'; 32 | 33 | public const BCE_AUTH_VERSION = 'bce-auth-v1'; 34 | 35 | public const DEFAULT_EXPIRATION_IN_SECONDS = 1800; // 签名有效期默认1800秒 36 | 37 | public const SUCCESS_CODE = 1000; 38 | 39 | /** 40 | * Send message. 41 | * 42 | * @return array 43 | * 44 | * @throws GatewayErrorException ; 45 | */ 46 | public function send(PhoneNumberInterface $to, MessageInterface $message, Config $config) 47 | { 48 | $params = [ 49 | 'signatureId' => $config->get('invoke_id'), 50 | 'mobile' => $to->getNumber(), 51 | 'template' => $message->getTemplate($this), 52 | 'contentVar' => $message->getData($this), 53 | ]; 54 | if (!empty($params['contentVar']['custom'])) { 55 | // 用户自定义参数,格式为字符串,状态回调时会回传该值 56 | $params['custom'] = $params['contentVar']['custom']; 57 | unset($params['contentVar']['custom']); 58 | } 59 | if (!empty($params['contentVar']['userExtId'])) { 60 | // 通道自定义扩展码,上行回调时会回传该值,其格式为纯数字串。默认为不开通,请求时无需设置该参数。如需开通请联系客服申请 61 | $params['userExtId'] = $params['contentVar']['userExtId']; 62 | unset($params['contentVar']['userExtId']); 63 | } 64 | 65 | $datetime = gmdate('Y-m-d\TH:i:s\Z'); 66 | 67 | $headers = [ 68 | 'host' => self::ENDPOINT_HOST, 69 | 'content-type' => 'application/json', 70 | 'x-bce-date' => $datetime, 71 | ]; 72 | // 获得需要签名的数据 73 | $signHeaders = $this->getHeadersToSign($headers, ['host', 'x-bce-date']); 74 | 75 | $headers['Authorization'] = $this->generateSign($signHeaders, $datetime, $config); 76 | 77 | $result = $this->request('post', self::buildEndpoint($config), ['headers' => $headers, 'json' => $params]); 78 | 79 | if (self::SUCCESS_CODE != $result['code']) { 80 | throw new GatewayErrorException($result['message'], $result['code'], $result); 81 | } 82 | 83 | return $result; 84 | } 85 | 86 | /** 87 | * Build endpoint url. 88 | * 89 | * @return string 90 | */ 91 | protected function buildEndpoint(Config $config) 92 | { 93 | return 'http://'.$config->get('domain', self::ENDPOINT_HOST).self::ENDPOINT_URI; 94 | } 95 | 96 | /** 97 | * Generate Authorization header. 98 | * 99 | * @param int $datetime 100 | * 101 | * @return string 102 | */ 103 | protected function generateSign(array $signHeaders, $datetime, Config $config) 104 | { 105 | // 生成 authString 106 | $authString = self::BCE_AUTH_VERSION.'/'.$config->get('ak').'/' 107 | .$datetime.'/'.self::DEFAULT_EXPIRATION_IN_SECONDS; 108 | 109 | // 使用 sk 和 authString 生成 signKey 110 | $signingKey = hash_hmac('sha256', $authString, $config->get('sk')); 111 | // 生成标准化 URI 112 | // 根据 RFC 3986,除了:1.大小写英文字符 2.阿拉伯数字 3.点'.'、波浪线'~'、减号'-'以及下划线'_' 以外都要编码 113 | $canonicalURI = str_replace('%2F', '/', rawurlencode(self::ENDPOINT_URI)); 114 | 115 | // 生成标准化 QueryString 116 | $canonicalQueryString = ''; // 此 api 不需要此项。返回空字符串 117 | 118 | // 整理 headersToSign,以 ';' 号连接 119 | $signedHeaders = empty($signHeaders) ? '' : strtolower(trim(implode(';', array_keys($signHeaders)))); 120 | 121 | // 生成标准化 header 122 | $canonicalHeader = $this->getCanonicalHeaders($signHeaders); 123 | 124 | // 组成标准请求串 125 | $canonicalRequest = "POST\n{$canonicalURI}\n{$canonicalQueryString}\n{$canonicalHeader}"; 126 | 127 | // 使用 signKey 和标准请求串完成签名 128 | $signature = hash_hmac('sha256', $canonicalRequest, $signingKey); 129 | 130 | // 组成最终签名串 131 | return "{$authString}/{$signedHeaders}/{$signature}"; 132 | } 133 | 134 | /** 135 | * 生成标准化 http 请求头串. 136 | * 137 | * @return string 138 | */ 139 | protected function getCanonicalHeaders(array $headers) 140 | { 141 | $headerStrings = []; 142 | foreach ($headers as $name => $value) { 143 | // trim后再encode,之后使用':'号连接起来 144 | $headerStrings[] = rawurlencode(strtolower(trim($name))).':'.rawurlencode(trim($value)); 145 | } 146 | 147 | sort($headerStrings); 148 | 149 | return implode("\n", $headerStrings); 150 | } 151 | 152 | /** 153 | * 根据 指定的 keys 过滤应该参与签名的 header. 154 | * 155 | * @return array 156 | */ 157 | protected function getHeadersToSign(array $headers, array $keys) 158 | { 159 | return array_intersect_key($headers, array_flip($keys)); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/Gateways/ChuanglanGateway.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Overtrue\EasySms\Gateways; 13 | 14 | use Overtrue\EasySms\Contracts\MessageInterface; 15 | use Overtrue\EasySms\Contracts\PhoneNumberInterface; 16 | use Overtrue\EasySms\Exceptions\GatewayErrorException; 17 | use Overtrue\EasySms\Exceptions\InvalidArgumentException; 18 | use Overtrue\EasySms\Support\Config; 19 | use Overtrue\EasySms\Traits\HasHttpRequest; 20 | 21 | /** 22 | * Class ChuanglanGateway. 23 | * 24 | * @see https://zz.253.com/v5.html#/api_doc 25 | */ 26 | class ChuanglanGateway extends Gateway 27 | { 28 | use HasHttpRequest; 29 | 30 | /** 31 | * URL模板 32 | */ 33 | public const ENDPOINT_URL_TEMPLATE = 'https://%s.253.com/msg/send/json'; 34 | 35 | /** 36 | * 国际短信 37 | */ 38 | public const INT_URL = 'http://intapi.253.com/send/json'; 39 | 40 | /** 41 | * 验证码渠道code. 42 | */ 43 | public const CHANNEL_VALIDATE_CODE = 'smsbj1'; 44 | 45 | /** 46 | * 会员营销渠道code. 47 | */ 48 | public const CHANNEL_PROMOTION_CODE = 'smssh1'; 49 | 50 | /** 51 | * @return array 52 | * 53 | * @throws GatewayErrorException 54 | * @throws InvalidArgumentException 55 | */ 56 | public function send(PhoneNumberInterface $to, MessageInterface $message, Config $config) 57 | { 58 | $IDDCode = !empty($to->getIDDCode()) ? $to->getIDDCode() : 86; 59 | 60 | $params = [ 61 | 'account' => $config->get('account'), 62 | 'password' => $config->get('password'), 63 | 'phone' => $to->getNumber(), 64 | 'msg' => $this->wrapChannelContent($message->getContent($this), $config, $IDDCode), 65 | ]; 66 | 67 | if (86 != $IDDCode) { 68 | $params['mobile'] = $to->getIDDCode().$to->getNumber(); 69 | $params['account'] = $config->get('intel_account') ?: $config->get('account'); 70 | $params['password'] = $config->get('intel_password') ?: $config->get('password'); 71 | } 72 | 73 | $result = $this->postJson($this->buildEndpoint($config, $IDDCode), $params); 74 | 75 | if (!isset($result['code']) || '0' != $result['code']) { 76 | throw new GatewayErrorException(json_encode($result, JSON_UNESCAPED_UNICODE), isset($result['code']) ? $result['code'] : 0, $result); 77 | } 78 | 79 | return $result; 80 | } 81 | 82 | /** 83 | * @param int $IDDCode 84 | * 85 | * @return string 86 | * 87 | * @throws InvalidArgumentException 88 | */ 89 | protected function buildEndpoint(Config $config, $IDDCode = 86) 90 | { 91 | $channel = $this->getChannel($config, $IDDCode); 92 | 93 | if (self::INT_URL === $channel) { 94 | return $channel; 95 | } 96 | 97 | return sprintf(self::ENDPOINT_URL_TEMPLATE, $channel); 98 | } 99 | 100 | /** 101 | * @param int $IDDCode 102 | * 103 | * @throws InvalidArgumentException 104 | */ 105 | protected function getChannel(Config $config, $IDDCode) 106 | { 107 | if (86 != $IDDCode) { 108 | return self::INT_URL; 109 | } 110 | $channel = $config->get('channel', self::CHANNEL_VALIDATE_CODE); 111 | 112 | if (!in_array($channel, [self::CHANNEL_VALIDATE_CODE, self::CHANNEL_PROMOTION_CODE])) { 113 | throw new InvalidArgumentException('Invalid channel for ChuanglanGateway.'); 114 | } 115 | 116 | return $channel; 117 | } 118 | 119 | /** 120 | * @param string $content 121 | * @param int $IDDCode 122 | * 123 | * @return string|string 124 | * 125 | * @throws InvalidArgumentException 126 | */ 127 | protected function wrapChannelContent($content, Config $config, $IDDCode) 128 | { 129 | $channel = $this->getChannel($config, $IDDCode); 130 | 131 | if (self::CHANNEL_PROMOTION_CODE == $channel) { 132 | $sign = (string) $config->get('sign', ''); 133 | if (empty($sign)) { 134 | throw new InvalidArgumentException('Invalid sign for ChuanglanGateway when using promotion channel'); 135 | } 136 | 137 | $unsubscribe = (string) $config->get('unsubscribe', ''); 138 | if (empty($unsubscribe)) { 139 | throw new InvalidArgumentException('Invalid unsubscribe for ChuanglanGateway when using promotion channel'); 140 | } 141 | 142 | $content = $sign.$content.$unsubscribe; 143 | } 144 | 145 | return $content; 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/Gateways/Chuanglanv1Gateway.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Overtrue\EasySms\Gateways; 13 | 14 | use Overtrue\EasySms\Contracts\MessageInterface; 15 | use Overtrue\EasySms\Contracts\PhoneNumberInterface; 16 | use Overtrue\EasySms\Exceptions\GatewayErrorException; 17 | use Overtrue\EasySms\Exceptions\InvalidArgumentException; 18 | use Overtrue\EasySms\Support\Config; 19 | use Overtrue\EasySms\Traits\HasHttpRequest; 20 | 21 | /** 22 | * Class ChuanglanGateway. 23 | * 24 | * @see https://www.chuanglan.com/document/6110e57909fd9600010209de/62b3dc1d272e290001af3e75 25 | */ 26 | class Chuanglanv1Gateway extends Gateway 27 | { 28 | use HasHttpRequest; 29 | 30 | /** 31 | * 国际短信 32 | */ 33 | public const INT_URL = 'http://intapi.253.com/send/json'; 34 | 35 | /** 36 | * URL模板 37 | */ 38 | public const ENDPOINT_URL_TEMPLATE = 'https://smssh1.253.com/msg/%s/json'; 39 | 40 | /** 41 | * 支持单发、群发短信 42 | */ 43 | public const CHANNEL_NORMAL_CODE = 'v1/send'; 44 | 45 | /** 46 | * 单号码对应单内容批量下发. 47 | */ 48 | public const CHANNEL_VARIABLE_CODE = 'variable'; 49 | 50 | /** 51 | * @return array 52 | * 53 | * @throws GatewayErrorException 54 | * @throws InvalidArgumentException 55 | */ 56 | public function send(PhoneNumberInterface $to, MessageInterface $message, Config $config) 57 | { 58 | $IDDCode = !empty($to->getIDDCode()) ? $to->getIDDCode() : 86; 59 | 60 | $params = [ 61 | 'account' => $config->get('account'), 62 | 'password' => $config->get('password'), 63 | 'report' => $config->get('needstatus') ?? false, 64 | ]; 65 | 66 | if (86 != $IDDCode) { 67 | $params['mobile'] = $to->getIDDCode().$to->getNumber(); 68 | $params['account'] = $config->get('intel_account') ?: $config->get('account'); 69 | $params['password'] = $config->get('intel_password') ?: $config->get('password'); 70 | } 71 | 72 | if (self::CHANNEL_VARIABLE_CODE == $this->getChannel($config, $IDDCode)) { 73 | $params['params'] = $message->getData($this); 74 | $params['msg'] = $this->wrapChannelContent($message->getTemplate($this), $config, $IDDCode); 75 | } else { 76 | $params['phone'] = $to->getNumber(); 77 | $params['msg'] = $this->wrapChannelContent($message->getContent($this), $config, $IDDCode); 78 | } 79 | 80 | $result = $this->postJson($this->buildEndpoint($config, $IDDCode), $params); 81 | 82 | if (!isset($result['code']) || '0' != $result['code']) { 83 | throw new GatewayErrorException(json_encode($result, JSON_UNESCAPED_UNICODE), isset($result['code']) ? $result['code'] : 0, $result); 84 | } 85 | 86 | return $result; 87 | } 88 | 89 | /** 90 | * @param int $IDDCode 91 | * 92 | * @return string 93 | * 94 | * @throws InvalidArgumentException 95 | */ 96 | protected function buildEndpoint(Config $config, $IDDCode = 86) 97 | { 98 | $channel = $this->getChannel($config, $IDDCode); 99 | 100 | if (self::INT_URL === $channel) { 101 | return $channel; 102 | } 103 | 104 | return sprintf(self::ENDPOINT_URL_TEMPLATE, $channel); 105 | } 106 | 107 | /** 108 | * @param int $IDDCode 109 | * 110 | * @throws InvalidArgumentException 111 | */ 112 | protected function getChannel(Config $config, $IDDCode) 113 | { 114 | if (86 != $IDDCode) { 115 | return self::INT_URL; 116 | } 117 | $channel = $config->get('channel', self::CHANNEL_NORMAL_CODE); 118 | 119 | if (!in_array($channel, [self::CHANNEL_NORMAL_CODE, self::CHANNEL_VARIABLE_CODE])) { 120 | throw new InvalidArgumentException('Invalid channel for ChuanglanGateway.'); 121 | } 122 | 123 | return $channel; 124 | } 125 | 126 | /** 127 | * @param string $content 128 | * @param int $IDDCode 129 | * 130 | * @return string|string 131 | * 132 | * @throws InvalidArgumentException 133 | */ 134 | protected function wrapChannelContent($content, Config $config, $IDDCode) 135 | { 136 | return $content; 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/Gateways/CtyunGateway.php: -------------------------------------------------------------------------------- 1 | getData($this); 34 | $endpoint = self::ENDPOINT_HOST.'/sms/api/v1'; 35 | 36 | return $this->execute($endpoint, [ 37 | 'phoneNumber' => (string) $to, 38 | 'templateCode' => $this->config->get('template_code'), 39 | 'templateParam' => '{"code":"'.$data['code'].'"}', 40 | 'signName' => $this->config->get('sign_name'), 41 | 'action' => 'SendSms', 42 | ]); 43 | } 44 | 45 | /** 46 | * @return array 47 | * 48 | * @throws GatewayErrorException 49 | */ 50 | protected function execute(string $url, array $data) 51 | { 52 | $uuid = date('ymdHis', time()).substr(microtime(), 2, 6).sprintf('%03d', rand(0, 999)); 53 | $time = date('Ymd', time()).'T'.date('His').'Z'; 54 | $timeDate = substr($time, 0, 8); 55 | 56 | $body = bin2hex(hash('sha256', json_encode($data), true)); 57 | $query = ''; 58 | $strSignature = 'ctyun-eop-request-id:'.$uuid."\n".'eop-date:'.$time."\n\n".$query."\n".$body; 59 | 60 | $kTime = $this->sha256($time, $this->config->get('secret_key')); 61 | $kAk = $this->sha256($this->config->get('access_key'), $kTime); 62 | 63 | $kDate = $this->sha256($timeDate, $kAk); 64 | 65 | $signature = base64_encode($this->sha256($strSignature, $kDate)); 66 | $headers['Content-Type'] = 'application/json'; 67 | $headers['ctyun-eop-request-id'] = $uuid; 68 | $headers['Eop-Authorization'] = $this->config->get('access_key').' Headers=ctyun-eop-request-id;eop-date Signature='.$signature; 69 | $headers['eop-date'] = $time; 70 | 71 | $result = $this->postJson($url, $data, $headers); 72 | if (self::SUCCESS_CODE !== $result['code']) { 73 | throw new GatewayErrorException($result['message'], $result['code'], $result); 74 | } 75 | 76 | return $result; 77 | } 78 | 79 | public function sha256($str, $pass): string 80 | { 81 | return hash_hmac('sha256', $str, $pass, true); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Gateways/ErrorlogGateway.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Overtrue\EasySms\Gateways; 13 | 14 | use Overtrue\EasySms\Contracts\MessageInterface; 15 | use Overtrue\EasySms\Contracts\PhoneNumberInterface; 16 | use Overtrue\EasySms\Support\Config; 17 | 18 | /** 19 | * Class ErrorlogGateway. 20 | */ 21 | class ErrorlogGateway extends Gateway 22 | { 23 | /** 24 | * @return array 25 | */ 26 | public function send(PhoneNumberInterface|array $to, MessageInterface $message, Config $config) 27 | { 28 | if (is_array($to)) { 29 | $to = implode(',', $to); 30 | } 31 | 32 | $message = sprintf( 33 | "[%s] to: %s | message: \"%s\" | template: \"%s\" | data: %s\n", 34 | date('Y-m-d H:i:s'), 35 | $to, 36 | $message->getContent($this), 37 | $message->getTemplate($this), 38 | json_encode($message->getData($this)) 39 | ); 40 | 41 | $file = $this->config->get('file', ini_get('error_log')); 42 | $status = error_log($message, 3, $file); 43 | 44 | return compact('status', 'file'); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Gateways/Gateway.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Overtrue\EasySms\Gateways; 13 | 14 | use Overtrue\EasySms\Contracts\GatewayInterface; 15 | use Overtrue\EasySms\Support\Config; 16 | 17 | /** 18 | * Class Gateway. 19 | */ 20 | abstract class Gateway implements GatewayInterface 21 | { 22 | public const DEFAULT_TIMEOUT = 5.0; 23 | 24 | /** 25 | * @var Config 26 | */ 27 | protected $config; 28 | 29 | /** 30 | * @var array 31 | */ 32 | protected $options; 33 | 34 | /** 35 | * @var float 36 | */ 37 | protected $timeout; 38 | 39 | /** 40 | * Gateway constructor. 41 | */ 42 | public function __construct(array $config) 43 | { 44 | $this->config = new Config($config); 45 | } 46 | 47 | /** 48 | * Return timeout. 49 | * 50 | * @return int|mixed 51 | */ 52 | public function getTimeout() 53 | { 54 | return $this->timeout ?: $this->config->get('timeout', self::DEFAULT_TIMEOUT); 55 | } 56 | 57 | /** 58 | * Set timeout. 59 | * 60 | * @param int $timeout 61 | * 62 | * @return $this 63 | */ 64 | public function setTimeout($timeout) 65 | { 66 | $this->timeout = floatval($timeout); 67 | 68 | return $this; 69 | } 70 | 71 | /** 72 | * @return Config 73 | */ 74 | public function getConfig() 75 | { 76 | return $this->config; 77 | } 78 | 79 | /** 80 | * @return $this 81 | */ 82 | public function setConfig(Config $config) 83 | { 84 | $this->config = $config; 85 | 86 | return $this; 87 | } 88 | 89 | /** 90 | * @return $this 91 | */ 92 | public function setGuzzleOptions($options) 93 | { 94 | $this->options = $options; 95 | 96 | return $this; 97 | } 98 | 99 | /** 100 | * @return array 101 | */ 102 | public function getGuzzleOptions() 103 | { 104 | return $this->options ?: $this->config->get('options', []); 105 | } 106 | 107 | public function getName() 108 | { 109 | return \strtolower(str_replace([__NAMESPACE__.'\\', 'Gateway'], '', \get_class($this))); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/Gateways/HuaweiGateway.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Overtrue\EasySms\Gateways; 13 | 14 | use GuzzleHttp\Exception\RequestException; 15 | use Overtrue\EasySms\Contracts\MessageInterface; 16 | use Overtrue\EasySms\Contracts\PhoneNumberInterface; 17 | use Overtrue\EasySms\Exceptions\GatewayErrorException; 18 | use Overtrue\EasySms\Exceptions\InvalidArgumentException; 19 | use Overtrue\EasySms\Support\Config; 20 | use Overtrue\EasySms\Traits\HasHttpRequest; 21 | 22 | class HuaweiGateway extends Gateway 23 | { 24 | use HasHttpRequest; 25 | 26 | public const ENDPOINT_HOST = 'https://api.rtc.huaweicloud.com:10443'; 27 | 28 | public const ENDPOINT_URI = '/sms/batchSendSms/v1'; 29 | 30 | public const SUCCESS_CODE = '000000'; 31 | 32 | /** 33 | * 发送信息. 34 | * 35 | * @return array 36 | * 37 | * @throws GatewayErrorException 38 | * @throws InvalidArgumentException 39 | */ 40 | public function send(PhoneNumberInterface $to, MessageInterface $message, Config $config) 41 | { 42 | $appKey = $config->get('app_key'); 43 | $appSecret = $config->get('app_secret'); 44 | $channels = $config->get('from'); 45 | $statusCallback = $config->get('callback', ''); 46 | 47 | $endpoint = $this->getEndpoint($config); 48 | $headers = $this->getHeaders($appKey, $appSecret); 49 | 50 | $templateId = $message->getTemplate($this); 51 | $messageData = $message->getData($this); 52 | 53 | // 短信签名通道号码 54 | $from = 'default'; 55 | if (isset($messageData['from'])) { 56 | $from = $messageData['from']; 57 | unset($messageData['from']); 58 | } 59 | $channel = isset($channels[$from]) ? $channels[$from] : ''; 60 | 61 | if (empty($channel)) { 62 | throw new InvalidArgumentException("From Channel [{$from}] Not Exist"); 63 | } 64 | 65 | $params = [ 66 | 'from' => $channel, 67 | 'to' => $to->getUniversalNumber(), 68 | 'templateId' => $templateId, 69 | 'templateParas' => json_encode($messageData), 70 | 'statusCallback' => $statusCallback, 71 | ]; 72 | 73 | try { 74 | $result = $this->request('post', $endpoint, [ 75 | 'headers' => $headers, 76 | 'form_params' => $params, 77 | // 为防止因HTTPS证书认证失败造成API调用失败,需要先忽略证书信任问题 78 | 'verify' => false, 79 | ]); 80 | } catch (RequestException $e) { 81 | $result = $this->unwrapResponse($e->getResponse()); 82 | } 83 | 84 | if (self::SUCCESS_CODE != $result['code']) { 85 | throw new GatewayErrorException($result['description'], ltrim($result['code'], 'E'), $result); 86 | } 87 | 88 | return $result; 89 | } 90 | 91 | /** 92 | * 构造 Endpoint. 93 | * 94 | * @return string 95 | */ 96 | protected function getEndpoint(Config $config) 97 | { 98 | $endpoint = rtrim($config->get('endpoint', self::ENDPOINT_HOST), '/'); 99 | 100 | return $endpoint.self::ENDPOINT_URI; 101 | } 102 | 103 | /** 104 | * 获取请求 Headers 参数. 105 | * 106 | * @param string $appKey 107 | * @param string $appSecret 108 | * 109 | * @return array 110 | */ 111 | protected function getHeaders($appKey, $appSecret) 112 | { 113 | return [ 114 | 'Content-Type' => 'application/x-www-form-urlencoded', 115 | 'Authorization' => 'WSSE realm="SDP",profile="UsernameToken",type="Appkey"', 116 | 'X-WSSE' => $this->buildWsseHeader($appKey, $appSecret), 117 | ]; 118 | } 119 | 120 | /** 121 | * 构造X-WSSE参数值 122 | * 123 | * @param string $appKey 124 | * @param string $appSecret 125 | * 126 | * @return string 127 | */ 128 | protected function buildWsseHeader($appKey, $appSecret) 129 | { 130 | $now = date('Y-m-d\TH:i:s\Z'); 131 | $nonce = uniqid(); 132 | $passwordDigest = base64_encode(hash('sha256', $nonce.$now.$appSecret)); 133 | 134 | return sprintf( 135 | 'UsernameToken Username="%s",PasswordDigest="%s",Nonce="%s",Created="%s"', 136 | $appKey, 137 | $passwordDigest, 138 | $nonce, 139 | $now 140 | ); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/Gateways/HuaxinGateway.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Overtrue\EasySms\Gateways; 13 | 14 | use Overtrue\EasySms\Contracts\MessageInterface; 15 | use Overtrue\EasySms\Contracts\PhoneNumberInterface; 16 | use Overtrue\EasySms\Exceptions\GatewayErrorException; 17 | use Overtrue\EasySms\Support\Config; 18 | use Overtrue\EasySms\Traits\HasHttpRequest; 19 | 20 | /** 21 | * Class HuaxinGateway. 22 | * 23 | * @see http://www.ipyy.com/help/ 24 | */ 25 | class HuaxinGateway extends Gateway 26 | { 27 | use HasHttpRequest; 28 | 29 | public const ENDPOINT_TEMPLATE = 'http://%s/smsJson.aspx'; 30 | 31 | /** 32 | * @return array 33 | * 34 | * @throws GatewayErrorException ; 35 | */ 36 | public function send(PhoneNumberInterface $to, MessageInterface $message, Config $config) 37 | { 38 | $endpoint = $this->buildEndpoint($config->get('ip')); 39 | 40 | $result = $this->post($endpoint, [ 41 | 'userid' => $config->get('user_id'), 42 | 'account' => $config->get('account'), 43 | 'password' => $config->get('password'), 44 | 'mobile' => $to->getNumber(), 45 | 'content' => $message->getContent($this), 46 | 'sendTime' => '', 47 | 'action' => 'send', 48 | 'extno' => $config->get('ext_no'), 49 | ]); 50 | 51 | if ('Success' !== $result['returnstatus']) { 52 | throw new GatewayErrorException($result['message'], 400, $result); 53 | } 54 | 55 | return $result; 56 | } 57 | 58 | /** 59 | * Build endpoint url. 60 | * 61 | * @param string $ip 62 | * 63 | * @return string 64 | */ 65 | protected function buildEndpoint($ip) 66 | { 67 | return sprintf(self::ENDPOINT_TEMPLATE, $ip); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Gateways/HuyiGateway.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Overtrue\EasySms\Gateways; 13 | 14 | use Overtrue\EasySms\Contracts\MessageInterface; 15 | use Overtrue\EasySms\Contracts\PhoneNumberInterface; 16 | use Overtrue\EasySms\Exceptions\GatewayErrorException; 17 | use Overtrue\EasySms\Support\Config; 18 | use Overtrue\EasySms\Traits\HasHttpRequest; 19 | 20 | /** 21 | * Class HuyiGateway. 22 | * 23 | * @see http://www.ihuyi.com/api/sms.html 24 | */ 25 | class HuyiGateway extends Gateway 26 | { 27 | use HasHttpRequest; 28 | 29 | public const ENDPOINT_URL = 'http://106.ihuyi.com/webservice/sms.php?method=Submit'; 30 | 31 | public const ENDPOINT_FORMAT = 'json'; 32 | 33 | public const SUCCESS_CODE = 2; 34 | 35 | /** 36 | * @return array 37 | * 38 | * @throws GatewayErrorException ; 39 | */ 40 | public function send(PhoneNumberInterface $to, MessageInterface $message, Config $config) 41 | { 42 | $params = [ 43 | 'account' => $config->get('api_id'), 44 | 'mobile' => $to->getIDDCode() ? \sprintf('%s %s', $to->getIDDCode(), $to->getNumber()) : $to->getNumber(), 45 | 'content' => $message->getContent($this), 46 | 'time' => time(), 47 | 'format' => self::ENDPOINT_FORMAT, 48 | 'sign' => $config->get('signature'), 49 | ]; 50 | 51 | $params['password'] = $this->generateSign($params); 52 | 53 | $result = $this->post(self::ENDPOINT_URL, $params); 54 | 55 | if (self::SUCCESS_CODE != $result['code']) { 56 | throw new GatewayErrorException($result['msg'], $result['code'], $result); 57 | } 58 | 59 | return $result; 60 | } 61 | 62 | /** 63 | * Generate Sign. 64 | * 65 | * @param array $params 66 | * 67 | * @return string 68 | */ 69 | protected function generateSign($params) 70 | { 71 | return md5($params['account'].$this->config->get('api_key').$params['mobile'].$params['content'].$params['time']); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Gateways/JuheGateway.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Overtrue\EasySms\Gateways; 13 | 14 | use Overtrue\EasySms\Contracts\MessageInterface; 15 | use Overtrue\EasySms\Contracts\PhoneNumberInterface; 16 | use Overtrue\EasySms\Exceptions\GatewayErrorException; 17 | use Overtrue\EasySms\Support\Config; 18 | use Overtrue\EasySms\Traits\HasHttpRequest; 19 | 20 | /** 21 | * Class JuheGateway. 22 | * 23 | * @see https://www.juhe.cn/docs/api/id/54 24 | */ 25 | class JuheGateway extends Gateway 26 | { 27 | use HasHttpRequest; 28 | 29 | public const ENDPOINT_URL = 'http://v.juhe.cn/sms/send'; 30 | 31 | public const ENDPOINT_FORMAT = 'json'; 32 | 33 | /** 34 | * @return array 35 | * 36 | * @throws GatewayErrorException ; 37 | */ 38 | public function send(PhoneNumberInterface $to, MessageInterface $message, Config $config) 39 | { 40 | $params = [ 41 | 'mobile' => $to->getNumber(), 42 | 'tpl_id' => $message->getTemplate($this), 43 | 'tpl_value' => $this->formatTemplateVars($message->getData($this)), 44 | 'dtype' => self::ENDPOINT_FORMAT, 45 | 'key' => $config->get('app_key'), 46 | ]; 47 | 48 | $result = $this->get(self::ENDPOINT_URL, $params); 49 | 50 | if ($result['error_code']) { 51 | throw new GatewayErrorException($result['reason'], $result['error_code'], $result); 52 | } 53 | 54 | return $result; 55 | } 56 | 57 | /** 58 | * @return string 59 | */ 60 | protected function formatTemplateVars(array $vars) 61 | { 62 | $formatted = []; 63 | 64 | foreach ($vars as $key => $value) { 65 | $formatted[sprintf('#%s#', trim($key, '#'))] = $value; 66 | } 67 | 68 | return urldecode(http_build_query($formatted)); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Gateways/KingttoGateway.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Overtrue\EasySms\Gateways; 13 | 14 | use Overtrue\EasySms\Contracts\MessageInterface; 15 | use Overtrue\EasySms\Contracts\PhoneNumberInterface; 16 | use Overtrue\EasySms\Exceptions\GatewayErrorException; 17 | use Overtrue\EasySms\Support\Config; 18 | use Overtrue\EasySms\Traits\HasHttpRequest; 19 | 20 | /** 21 | * Class KingttoGateWay. 22 | * 23 | * @see http://www.kingtto.cn/ 24 | */ 25 | class KingttoGateway extends Gateway 26 | { 27 | use HasHttpRequest; 28 | 29 | public const ENDPOINT_URL = 'http://101.201.41.194:9999/sms.aspx'; 30 | 31 | public const ENDPOINT_METHOD = 'send'; 32 | 33 | /** 34 | * @return \Psr\Http\Message\ResponseInterface|array|string 35 | * 36 | * @throws GatewayErrorException 37 | */ 38 | public function send(PhoneNumberInterface $to, MessageInterface $message, Config $config) 39 | { 40 | $params = [ 41 | 'action' => self::ENDPOINT_METHOD, 42 | 'userid' => $config->get('userid'), 43 | 'account' => $config->get('account'), 44 | 'password' => $config->get('password'), 45 | 'mobile' => $to->getNumber(), 46 | 'content' => $message->getContent(), 47 | ]; 48 | 49 | $result = $this->post(self::ENDPOINT_URL, $params); 50 | 51 | if ('Success' != $result['returnstatus']) { 52 | throw new GatewayErrorException($result['message'], $result['remainpoint'], $result); 53 | } 54 | 55 | return $result; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Gateways/LuosimaoGateway.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Overtrue\EasySms\Gateways; 13 | 14 | use Overtrue\EasySms\Contracts\MessageInterface; 15 | use Overtrue\EasySms\Contracts\PhoneNumberInterface; 16 | use Overtrue\EasySms\Exceptions\GatewayErrorException; 17 | use Overtrue\EasySms\Support\Config; 18 | use Overtrue\EasySms\Traits\HasHttpRequest; 19 | 20 | /** 21 | * Class LuosimaoGateway. 22 | * 23 | * @see https://luosimao.com/docs/api/ 24 | */ 25 | class LuosimaoGateway extends Gateway 26 | { 27 | use HasHttpRequest; 28 | 29 | public const ENDPOINT_TEMPLATE = 'https://%s.luosimao.com/%s/%s.%s'; 30 | 31 | public const ENDPOINT_VERSION = 'v1'; 32 | 33 | public const ENDPOINT_FORMAT = 'json'; 34 | 35 | /** 36 | * @return array 37 | * 38 | * @throws GatewayErrorException ; 39 | */ 40 | public function send(PhoneNumberInterface $to, MessageInterface $message, Config $config) 41 | { 42 | $endpoint = $this->buildEndpoint('sms-api', 'send'); 43 | 44 | $result = $this->post($endpoint, [ 45 | 'mobile' => $to->getNumber(), 46 | 'message' => $message->getContent($this), 47 | ], [ 48 | 'Authorization' => 'Basic '.base64_encode('api:key-'.$config->get('api_key')), 49 | ]); 50 | 51 | if ($result['error']) { 52 | throw new GatewayErrorException($result['msg'], $result['error'], $result); 53 | } 54 | 55 | return $result; 56 | } 57 | 58 | /** 59 | * Build endpoint url. 60 | * 61 | * @param string $type 62 | * @param string $function 63 | * 64 | * @return string 65 | */ 66 | protected function buildEndpoint($type, $function) 67 | { 68 | return sprintf(self::ENDPOINT_TEMPLATE, $type, self::ENDPOINT_VERSION, $function, self::ENDPOINT_FORMAT); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Gateways/MaapGateway.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Overtrue\EasySms\Gateways; 13 | 14 | use Overtrue\EasySms\Contracts\MessageInterface; 15 | use Overtrue\EasySms\Contracts\PhoneNumberInterface; 16 | use Overtrue\EasySms\Exceptions\GatewayErrorException; 17 | use Overtrue\EasySms\Support\Config; 18 | use Overtrue\EasySms\Traits\HasHttpRequest; 19 | 20 | /** 21 | * Class MaapGateway. 22 | * 23 | * @see https://maap.wo.cn/ 24 | */ 25 | class MaapGateway extends Gateway 26 | { 27 | use HasHttpRequest; 28 | 29 | public const ENDPOINT_URL = 'http://rcsapi.wo.cn:8000/umcinterface/sendtempletmsg'; 30 | 31 | /** 32 | * Send message. 33 | * 34 | * @return array 35 | * 36 | * @throws GatewayErrorException 37 | */ 38 | public function send(PhoneNumberInterface $to, MessageInterface $message, Config $config) 39 | { 40 | $params = [ 41 | 'cpcode' => $config->get('cpcode'), 42 | 'msg' => implode(',', $message->getData($this)), 43 | 'mobiles' => $to->getNumber(), 44 | 'excode' => $config->get('excode', ''), 45 | 'templetid' => $message->getTemplate($this), 46 | ]; 47 | $params['sign'] = $this->generateSign($params, $config->get('key')); 48 | 49 | $result = $this->postJson(self::ENDPOINT_URL, $params); 50 | 51 | if (0 != $result['resultcode']) { 52 | throw new GatewayErrorException($result['resultmsg'], $result['resultcode'], $result); 53 | } 54 | 55 | return $result; 56 | } 57 | 58 | /** 59 | * Generate Sign. 60 | * 61 | * @param array $params 62 | * @param string $key 签名Key 63 | * 64 | * @return string 65 | */ 66 | protected function generateSign($params, $key) 67 | { 68 | return md5($params['cpcode'].$params['msg'].$params['mobiles'].$params['excode'].$params['templetid'].$key); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Gateways/ModuyunGateway.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Overtrue\EasySms\Gateways; 13 | 14 | use Overtrue\EasySms\Contracts\MessageInterface; 15 | use Overtrue\EasySms\Contracts\PhoneNumberInterface; 16 | use Overtrue\EasySms\Exceptions\GatewayErrorException; 17 | use Overtrue\EasySms\Support\Config; 18 | use Overtrue\EasySms\Traits\HasHttpRequest; 19 | 20 | /** 21 | * Class ModuyunGateway. 22 | * 23 | * @see https://www.moduyun.com/doc/index.html#10002 24 | */ 25 | class ModuyunGateway extends Gateway 26 | { 27 | use HasHttpRequest; 28 | 29 | public const ENDPOINT_URL = 'https://live.moduyun.com/sms/v2/sendsinglesms'; 30 | 31 | /** 32 | * @return array 33 | * 34 | * @throws GatewayErrorException ; 35 | */ 36 | public function send(PhoneNumberInterface $to, MessageInterface $message, Config $config) 37 | { 38 | $urlParams = [ 39 | 'accesskey' => $config->get('accesskey'), 40 | 'random' => rand(100000, 999999), 41 | ]; 42 | 43 | $params = [ 44 | 'tel' => [ 45 | 'mobile' => $to->getNumber(), 46 | 'nationcode' => $to->getIDDCode() ?: '86', 47 | ], 48 | 'signId' => $config->get('signId', ''), 49 | 'templateId' => $message->getTemplate($this), 50 | 'time' => time(), 51 | 'type' => $config->get('type', 0), 52 | 'params' => array_values($message->getData($this)), 53 | 'ext' => '', 54 | 'extend' => '', 55 | ]; 56 | $params['sig'] = $this->generateSign($params, $urlParams['random']); 57 | 58 | $result = $this->postJson($this->getEndpointUrl($urlParams), $params); 59 | $result = is_string($result) ? json_decode($result, true) : $result; 60 | if (0 != $result['result']) { 61 | throw new GatewayErrorException($result['errmsg'], $result['result'], $result); 62 | } 63 | 64 | return $result; 65 | } 66 | 67 | /** 68 | * @param array $params 69 | * 70 | * @return string 71 | */ 72 | protected function getEndpointUrl($params) 73 | { 74 | return self::ENDPOINT_URL.'?'.http_build_query($params); 75 | } 76 | 77 | /** 78 | * Generate Sign. 79 | * 80 | * @param array $params 81 | * @param string $random 82 | * 83 | * @return string 84 | */ 85 | protected function generateSign($params, $random) 86 | { 87 | return hash('sha256', sprintf( 88 | 'secretkey=%s&random=%d&time=%d&mobile=%s', 89 | $this->config->get('secretkey'), 90 | $random, 91 | $params['time'], 92 | $params['tel']['mobile'] 93 | )); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Gateways/NowcnGateway.php: -------------------------------------------------------------------------------- 1 | get('key')) { 22 | throw new GatewayErrorException('key not found', -2, []); 23 | } 24 | $params = [ 25 | 'mobile' => $to->getNumber(), 26 | 'content' => $message->getContent($this), 27 | 'userId' => $config->get('key'), 28 | 'password' => $config->get('secret'), 29 | 'apiType' => $config->get('api_type'), 30 | ]; 31 | $result = $this->get(self::ENDPOINT_URL, $params); 32 | $result = is_string($result) ? json_decode($result, true) : $result; 33 | if (self::SUCCESS_CODE != $result['code']) { 34 | throw new GatewayErrorException($result['msg'], $result['code'], $result); 35 | } 36 | 37 | return $result; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Gateways/QcloudGateway.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Overtrue\EasySms\Gateways; 13 | 14 | use GuzzleHttp\Psr7\Uri; 15 | use Overtrue\EasySms\Contracts\MessageInterface; 16 | use Overtrue\EasySms\Contracts\PhoneNumberInterface; 17 | use Overtrue\EasySms\Exceptions\GatewayErrorException; 18 | use Overtrue\EasySms\Support\Config; 19 | use Overtrue\EasySms\Traits\HasHttpRequest; 20 | 21 | /** 22 | * Class QcloudGateway. 23 | * 24 | * @see https://cloud.tencent.com/document/api/382/55981 25 | */ 26 | class QcloudGateway extends Gateway 27 | { 28 | use HasHttpRequest; 29 | 30 | public const ENDPOINT_URL = 'https://sms.tencentcloudapi.com'; 31 | 32 | public const ENDPOINT_SERVICE = 'sms'; 33 | 34 | public const ENDPOINT_METHOD = 'SendSms'; 35 | 36 | public const ENDPOINT_VERSION = '2021-01-11'; 37 | 38 | public const ENDPOINT_REGION = 'ap-guangzhou'; 39 | 40 | public const ENDPOINT_FORMAT = 'json'; 41 | 42 | /** 43 | * @return array 44 | * 45 | * @throws GatewayErrorException ; 46 | */ 47 | public function send(PhoneNumberInterface $to, MessageInterface $message, Config $config) 48 | { 49 | $data = $message->getData($this); 50 | 51 | $signName = !empty($data['sign_name']) ? $data['sign_name'] : $config->get('sign_name', ''); 52 | 53 | unset($data['sign_name']); 54 | 55 | $phone = !\is_null($to->getIDDCode()) ? strval($to->getUniversalNumber()) : $to->getNumber(); 56 | $params = [ 57 | 'PhoneNumberSet' => [ 58 | $phone, 59 | ], 60 | 'SmsSdkAppId' => $this->config->get('sdk_app_id'), 61 | 'SignName' => $signName, 62 | 'TemplateId' => (string) $message->getTemplate($this), 63 | 'TemplateParamSet' => array_map('strval', array_values($data)), 64 | ]; 65 | 66 | $time = time(); 67 | 68 | $endpoint = $this->config->get('endpoint', self::ENDPOINT_URL); 69 | 70 | $result = $this->request('post', $endpoint, [ 71 | 'headers' => [ 72 | 'Authorization' => $this->generateSign($params, $time), 73 | 'Host' => (new Uri($endpoint))->getHost(), 74 | 'Content-Type' => 'application/json; charset=utf-8', 75 | 'X-TC-Action' => self::ENDPOINT_METHOD, 76 | 'X-TC-Region' => $this->config->get('region', self::ENDPOINT_REGION), 77 | 'X-TC-Timestamp' => $time, 78 | 'X-TC-Version' => self::ENDPOINT_VERSION, 79 | ], 80 | 'json' => $params, 81 | ]); 82 | 83 | if (!empty($result['Response']['Error']['Code'])) { 84 | throw new GatewayErrorException($result['Response']['Error']['Message'], 400, $result); 85 | } 86 | 87 | if (!empty($result['Response']['SendStatusSet'])) { 88 | foreach ($result['Response']['SendStatusSet'] as $group) { 89 | if ('Ok' != $group['Code']) { 90 | throw new GatewayErrorException($group['Message'], 400, $result); 91 | } 92 | } 93 | } 94 | 95 | return $result; 96 | } 97 | 98 | /** 99 | * Generate Sign. 100 | * 101 | * @param array $params 102 | * 103 | * @return string 104 | */ 105 | protected function generateSign($params, $timestamp) 106 | { 107 | $date = gmdate('Y-m-d', $timestamp); 108 | $secretKey = $this->config->get('secret_key'); 109 | $secretId = $this->config->get('secret_id'); 110 | $endpoint = $this->config->get('endpoint', self::ENDPOINT_URL); 111 | $host = (new Uri($endpoint))->getHost(); 112 | 113 | $canonicalRequest = 'POST'."\n". 114 | '/'."\n". 115 | ''."\n". 116 | 'content-type:application/json; charset=utf-8'."\n". 117 | 'host:'.$host."\n\n". 118 | 'content-type;host'."\n". 119 | hash('SHA256', json_encode($params)); 120 | 121 | $stringToSign = 122 | 'TC3-HMAC-SHA256'."\n". 123 | $timestamp."\n". 124 | $date.'/'.self::ENDPOINT_SERVICE.'/tc3_request'."\n". 125 | hash('SHA256', $canonicalRequest); 126 | 127 | $secretDate = hash_hmac('SHA256', $date, 'TC3'.$secretKey, true); 128 | $secretService = hash_hmac('SHA256', self::ENDPOINT_SERVICE, $secretDate, true); 129 | $secretSigning = hash_hmac('SHA256', 'tc3_request', $secretService, true); 130 | $signature = hash_hmac('SHA256', $stringToSign, $secretSigning); 131 | 132 | return 'TC3-HMAC-SHA256' 133 | .' Credential='.$secretId.'/'.$date.'/'.self::ENDPOINT_SERVICE.'/tc3_request' 134 | .', SignedHeaders=content-type;host, Signature='.$signature; 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/Gateways/QiniuGateway.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Overtrue\EasySms\Gateways; 13 | 14 | use Overtrue\EasySms\Contracts\MessageInterface; 15 | use Overtrue\EasySms\Contracts\PhoneNumberInterface; 16 | use Overtrue\EasySms\Exceptions\GatewayErrorException; 17 | use Overtrue\EasySms\Support\Config; 18 | use Overtrue\EasySms\Traits\HasHttpRequest; 19 | 20 | /** 21 | * Class QiniuGateway. 22 | * 23 | * @see https://developer.qiniu.com/sms/api/5897/sms-api-send-message 24 | */ 25 | class QiniuGateway extends Gateway 26 | { 27 | use HasHttpRequest; 28 | 29 | public const ENDPOINT_TEMPLATE = 'https://%s.qiniuapi.com/%s/%s'; 30 | 31 | public const ENDPOINT_VERSION = 'v1'; 32 | 33 | /** 34 | * @return array 35 | * 36 | * @throws GatewayErrorException ; 37 | */ 38 | public function send(PhoneNumberInterface $to, MessageInterface $message, Config $config) 39 | { 40 | $endpoint = $this->buildEndpoint('sms', 'message/single'); 41 | 42 | $data = $message->getData($this); 43 | 44 | $params = [ 45 | 'template_id' => $message->getTemplate($this), 46 | 'mobile' => $to->getNumber(), 47 | ]; 48 | 49 | if (!empty($data)) { 50 | $params['parameters'] = $data; 51 | } 52 | 53 | $headers = [ 54 | 'Content-Type' => 'application/json', 55 | ]; 56 | 57 | $headers['Authorization'] = $this->generateSign($endpoint, 'POST', json_encode($params), $headers['Content-Type'], $config); 58 | 59 | $result = $this->postJson($endpoint, $params, $headers); 60 | 61 | if (isset($result['error'])) { 62 | throw new GatewayErrorException($result['message'], $result['error'], $result); 63 | } 64 | 65 | return $result; 66 | } 67 | 68 | /** 69 | * Build endpoint url. 70 | * 71 | * @param string $type 72 | * @param string $function 73 | * 74 | * @return string 75 | */ 76 | protected function buildEndpoint($type, $function) 77 | { 78 | return sprintf(self::ENDPOINT_TEMPLATE, $type, self::ENDPOINT_VERSION, $function); 79 | } 80 | 81 | /** 82 | * Build endpoint url. 83 | * 84 | * @param string $url 85 | * @param string $method 86 | * @param string $body 87 | * @param string $contentType 88 | * 89 | * @return string 90 | */ 91 | protected function generateSign($url, $method, $body, $contentType, Config $config) 92 | { 93 | $urlItems = parse_url($url); 94 | $host = $urlItems['host']; 95 | if (isset($urlItems['port'])) { 96 | $port = $urlItems['port']; 97 | } else { 98 | $port = ''; 99 | } 100 | $path = $urlItems['path']; 101 | if (isset($urlItems['query'])) { 102 | $query = $urlItems['query']; 103 | } else { 104 | $query = ''; 105 | } 106 | // write request uri 107 | $toSignStr = $method.' '.$path; 108 | if (!empty($query)) { 109 | $toSignStr .= '?'.$query; 110 | } 111 | // write host and port 112 | $toSignStr .= "\nHost: ".$host; 113 | if (!empty($port)) { 114 | $toSignStr .= ':'.$port; 115 | } 116 | // write content type 117 | if (!empty($contentType)) { 118 | $toSignStr .= "\nContent-Type: ".$contentType; 119 | } 120 | $toSignStr .= "\n\n"; 121 | // write body 122 | if (!empty($body)) { 123 | $toSignStr .= $body; 124 | } 125 | 126 | $hmac = hash_hmac('sha1', $toSignStr, $config->get('secret_key'), true); 127 | 128 | return 'Qiniu '.$config->get('access_key').':'.$this->base64UrlSafeEncode($hmac); 129 | } 130 | 131 | /** 132 | * @param string $data 133 | * 134 | * @return string 135 | */ 136 | protected function base64UrlSafeEncode($data) 137 | { 138 | $find = ['+', '/']; 139 | $replace = ['-', '_']; 140 | 141 | return str_replace($find, $replace, base64_encode($data)); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/Gateways/RongcloudGateway.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Overtrue\EasySms\Gateways; 13 | 14 | use GuzzleHttp\Exception\ClientException; 15 | use Overtrue\EasySms\Contracts\MessageInterface; 16 | use Overtrue\EasySms\Contracts\PhoneNumberInterface; 17 | use Overtrue\EasySms\Exceptions\GatewayErrorException; 18 | use Overtrue\EasySms\Support\Config; 19 | use Overtrue\EasySms\Traits\HasHttpRequest; 20 | 21 | /** 22 | * Class RongcloudGateway. 23 | * 24 | * @author Darren Gao 25 | * 26 | * @see http://www.rongcloud.cn/docs/sms_service.html#send_sms_code 27 | */ 28 | class RongcloudGateway extends Gateway 29 | { 30 | use HasHttpRequest; 31 | 32 | public const ENDPOINT_TEMPLATE = 'http://api.sms.ronghub.com/%s.%s'; 33 | 34 | public const ENDPOINT_ACTION = 'sendCode'; 35 | 36 | public const ENDPOINT_FORMAT = 'json'; 37 | 38 | public const ENDPOINT_REGION = '86'; // 中国区,目前只支持此国别 39 | 40 | public const SUCCESS_CODE = 200; 41 | 42 | /** 43 | * @return array 44 | * 45 | * @throws GatewayErrorException ; 46 | */ 47 | public function send(PhoneNumberInterface $to, MessageInterface $message, Config $config) 48 | { 49 | $data = $message->getData(); 50 | $action = array_key_exists('action', $data) ? $data['action'] : self::ENDPOINT_ACTION; 51 | $endpoint = $this->buildEndpoint($action); 52 | 53 | $headers = [ 54 | 'Nonce' => uniqid(), 55 | 'App-Key' => $config->get('app_key'), 56 | 'Timestamp' => time(), 57 | ]; 58 | $headers['Signature'] = $this->generateSign($headers, $config); 59 | 60 | switch ($action) { 61 | case 'sendCode': 62 | $params = [ 63 | 'mobile' => $to->getNumber(), 64 | 'region' => self::ENDPOINT_REGION, 65 | 'templateId' => $message->getTemplate($this), 66 | ]; 67 | 68 | break; 69 | case 'verifyCode': 70 | if (!array_key_exists('code', $data) 71 | or !array_key_exists('sessionId', $data)) { 72 | throw new GatewayErrorException('"code" or "sessionId" is not set', 0); 73 | } 74 | $params = [ 75 | 'code' => $data['code'], 76 | 'sessionId' => $data['sessionId'], 77 | ]; 78 | 79 | break; 80 | case 'sendNotify': 81 | $params = [ 82 | 'mobile' => $to->getNumber(), 83 | 'region' => self::ENDPOINT_REGION, 84 | 'templateId' => $message->getTemplate($this), 85 | ]; 86 | $params = array_merge($params, $data); 87 | 88 | break; 89 | default: 90 | throw new GatewayErrorException(sprintf('action: %s not supported', $action)); 91 | } 92 | 93 | try { 94 | $result = $this->post($endpoint, $params, $headers); 95 | 96 | if (self::SUCCESS_CODE !== $result['code']) { 97 | throw new GatewayErrorException($result['errorMessage'], $result['code'], $result); 98 | } 99 | } catch (ClientException $e) { 100 | throw new GatewayErrorException($e->getMessage(), $e->getCode()); 101 | } 102 | 103 | return $result; 104 | } 105 | 106 | /** 107 | * Generate Sign. 108 | * 109 | * @param array $params 110 | * 111 | * @return string 112 | */ 113 | protected function generateSign($params, Config $config) 114 | { 115 | return sha1(sprintf('%s%s%s', $config->get('app_secret'), $params['Nonce'], $params['Timestamp'])); 116 | } 117 | 118 | /** 119 | * Build endpoint url. 120 | * 121 | * @param string $action 122 | * 123 | * @return string 124 | */ 125 | protected function buildEndpoint($action) 126 | { 127 | return sprintf(self::ENDPOINT_TEMPLATE, $action, self::ENDPOINT_FORMAT); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/Gateways/RongheyunGateway.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Overtrue\EasySms\Gateways; 13 | 14 | use Overtrue\EasySms\Contracts\MessageInterface; 15 | use Overtrue\EasySms\Contracts\PhoneNumberInterface; 16 | use Overtrue\EasySms\Exceptions\GatewayErrorException; 17 | use Overtrue\EasySms\Support\Config; 18 | use Overtrue\EasySms\Traits\HasHttpRequest; 19 | 20 | /** 21 | * Class RongheyunGateway. 22 | * 23 | * @see https://doc.zthysms.com/web/#/1?page_id=13 24 | */ 25 | class RongheyunGateway extends Gateway 26 | { 27 | use HasHttpRequest; 28 | 29 | public const ENDPOINT_URL = 'https://api.mix2.zthysms.com/v2/sendSmsTp'; 30 | 31 | /** 32 | * @return array 33 | * 34 | * @throws GatewayErrorException ; 35 | */ 36 | public function send(PhoneNumberInterface $to, MessageInterface $message, Config $config) 37 | { 38 | $tKey = time(); 39 | $password = md5(md5($config->get('password')).$tKey); 40 | $params = [ 41 | 'username' => $config->get('username', ''), 42 | 'password' => $password, 43 | 'tKey' => $tKey, 44 | 'signature' => $config->get('signature', ''), 45 | 'tpId' => $message->getTemplate($this), 46 | 'ext' => '', 47 | 'extend' => '', 48 | 'records' => [ 49 | 'mobile' => $to->getNumber(), 50 | 'tpContent' => $message->getData($this), 51 | ], 52 | ]; 53 | 54 | $result = $this->postJson( 55 | self::ENDPOINT_URL, 56 | $params, 57 | ['Content-Type' => 'application/json; charset="UTF-8"'] 58 | ); 59 | if (200 != $result['code']) { 60 | throw new GatewayErrorException($result['msg'], $result['code'], $result); 61 | } 62 | 63 | return $result; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Gateways/SendcloudGateway.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Overtrue\EasySms\Gateways; 13 | 14 | use Overtrue\EasySms\Contracts\MessageInterface; 15 | use Overtrue\EasySms\Contracts\PhoneNumberInterface; 16 | use Overtrue\EasySms\Exceptions\GatewayErrorException; 17 | use Overtrue\EasySms\Support\Config; 18 | use Overtrue\EasySms\Traits\HasHttpRequest; 19 | 20 | /** 21 | * Class SendcloudGateway. 22 | * 23 | * @see http://sendcloud.sohu.com/doc/sms/ 24 | */ 25 | class SendcloudGateway extends Gateway 26 | { 27 | use HasHttpRequest; 28 | 29 | public const ENDPOINT_TEMPLATE = 'http://www.sendcloud.net/smsapi/%s'; 30 | 31 | /** 32 | * Send a short message. 33 | * 34 | * @return array 35 | * 36 | * @throws GatewayErrorException 37 | */ 38 | public function send(PhoneNumberInterface $to, MessageInterface $message, Config $config) 39 | { 40 | $params = [ 41 | 'smsUser' => $config->get('sms_user'), 42 | 'templateId' => $message->getTemplate($this), 43 | 'msgType' => $to->getIDDCode() ? 2 : 0, 44 | 'phone' => $to->getZeroPrefixedNumber(), 45 | 'vars' => $this->formatTemplateVars($message->getData($this)), 46 | ]; 47 | 48 | if ($config->get('timestamp', false)) { 49 | $params['timestamp'] = time() * 1000; 50 | } 51 | 52 | $params['signature'] = $this->sign($params, $config->get('sms_key')); 53 | 54 | $result = $this->post(sprintf(self::ENDPOINT_TEMPLATE, 'send'), $params); 55 | 56 | if (!$result['result']) { 57 | throw new GatewayErrorException($result['message'], $result['statusCode'], $result); 58 | } 59 | 60 | return $result; 61 | } 62 | 63 | /** 64 | * @return string 65 | */ 66 | protected function formatTemplateVars(array $vars) 67 | { 68 | $formatted = []; 69 | 70 | foreach ($vars as $key => $value) { 71 | $formatted[sprintf('%%%s%%', trim($key, '%'))] = $value; 72 | } 73 | 74 | return json_encode($formatted, JSON_FORCE_OBJECT); 75 | } 76 | 77 | /** 78 | * @param array $params 79 | * @param string $key 80 | * 81 | * @return string 82 | */ 83 | protected function sign($params, $key) 84 | { 85 | ksort($params); 86 | 87 | return md5(sprintf('%s&%s&%s', $key, urldecode(http_build_query($params)), $key)); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Gateways/SmsbaoGateway.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Overtrue\EasySms\Gateways; 13 | 14 | use Overtrue\EasySms\Contracts\MessageInterface; 15 | use Overtrue\EasySms\Contracts\PhoneNumberInterface; 16 | use Overtrue\EasySms\Exceptions\GatewayErrorException; 17 | use Overtrue\EasySms\Support\Config; 18 | use Overtrue\EasySms\Traits\HasHttpRequest; 19 | 20 | /** 21 | * Class SmsbaoGateway. 22 | * 23 | * @author iwindy <203962638@qq.com> 24 | * 25 | * @see http://www.smsbao.com/openapi/ 26 | */ 27 | class SmsbaoGateway extends Gateway 28 | { 29 | use HasHttpRequest; 30 | 31 | public const ENDPOINT_URL = 'http://api.smsbao.com/%s'; 32 | 33 | public const SUCCESS_CODE = '0'; 34 | 35 | protected $errorStatuses = [ 36 | '0' => '短信发送成功', 37 | '-1' => '参数不全', 38 | '-2' => '服务器空间不支持,请确认支持curl或者fsocket,联系您的空间商解决或者更换空间!', 39 | '30' => '密码错误', 40 | '40' => '账号不存在', 41 | '41' => '余额不足', 42 | '42' => '帐户已过期', 43 | '43' => 'IP地址限制', 44 | '50' => '内容含有敏感词', 45 | ]; 46 | 47 | public function send(PhoneNumberInterface $to, MessageInterface $message, Config $config) 48 | { 49 | $data = $message->getContent($this); 50 | 51 | if (is_null($to->getIDDCode()) || '86' == $to->getIDDCode()) { 52 | $number = $to->getNumber(); 53 | $action = 'sms'; 54 | } else { 55 | $number = $to->getUniversalNumber(); 56 | $action = 'wsms'; 57 | } 58 | 59 | $params = [ 60 | 'u' => $config->get('user'), 61 | 'p' => md5($config->get('password')), 62 | 'm' => $number, 63 | 'c' => $data, 64 | ]; 65 | 66 | $result = $this->get($this->buildEndpoint($action), $params); 67 | 68 | if (self::SUCCESS_CODE !== $result) { 69 | throw new GatewayErrorException($this->errorStatuses[$result], $result); 70 | } 71 | 72 | return $result; 73 | } 74 | 75 | protected function buildEndpoint($type) 76 | { 77 | return sprintf(self::ENDPOINT_URL, $type); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Gateways/SubmailGateway.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Overtrue\EasySms\Gateways; 13 | 14 | use Overtrue\EasySms\Contracts\MessageInterface; 15 | use Overtrue\EasySms\Contracts\PhoneNumberInterface; 16 | use Overtrue\EasySms\Exceptions\GatewayErrorException; 17 | use Overtrue\EasySms\Support\Config; 18 | use Overtrue\EasySms\Traits\HasHttpRequest; 19 | 20 | /** 21 | * Class SubmailGateway. 22 | * 23 | * @see https://www.mysubmail.com/chs/documents/developer/index 24 | */ 25 | class SubmailGateway extends Gateway 26 | { 27 | use HasHttpRequest; 28 | 29 | public const ENDPOINT_TEMPLATE = 'https://api.mysubmail.com/%s.%s'; 30 | 31 | public const ENDPOINT_FORMAT = 'json'; 32 | 33 | /** 34 | * @return array 35 | * 36 | * @throws GatewayErrorException ; 37 | */ 38 | public function send(PhoneNumberInterface $to, MessageInterface $message, Config $config) 39 | { 40 | $isContent = (bool) $message->getContent($this); 41 | if ($isContent) { 42 | $endpoint = $this->buildEndpoint($this->inChineseMainland($to) ? 'sms/send' : 'internationalsms/send'); 43 | $params = [ 44 | 'appid' => $config->get('app_id'), 45 | 'signature' => $config->get('app_key'), 46 | 'content' => $message->getContent($this), 47 | 'to' => $to->getUniversalNumber(), 48 | ]; 49 | } else { 50 | $endpoint = $this->buildEndpoint($this->inChineseMainland($to) ? 'message/xsend' : 'internationalsms/xsend'); 51 | $data = $message->getData($this); 52 | $template_code = $message->getTemplate($this); 53 | $params = [ 54 | 'appid' => $config->get('app_id'), 55 | 'signature' => $config->get('app_key'), 56 | 'project' => !empty($template_code) ? $template_code : (!empty($data['project']) ? $data['project'] : $config->get('project')), 57 | 'to' => $to->getUniversalNumber(), 58 | 'vars' => json_encode($data, JSON_FORCE_OBJECT), 59 | ]; 60 | } 61 | 62 | $result = $this->post($endpoint, $params); 63 | 64 | if ('success' != $result['status']) { 65 | throw new GatewayErrorException($result['msg'], $result['code'], $result); 66 | } 67 | 68 | return $result; 69 | } 70 | 71 | /** 72 | * Build endpoint url. 73 | * 74 | * @param string $function 75 | * 76 | * @return string 77 | */ 78 | protected function buildEndpoint($function) 79 | { 80 | return sprintf(self::ENDPOINT_TEMPLATE, $function, self::ENDPOINT_FORMAT); 81 | } 82 | 83 | /** 84 | * Check if the phone number belongs to chinese mainland. 85 | * 86 | * @param PhoneNumberInterface $to 87 | * 88 | * @return bool 89 | */ 90 | protected function inChineseMainland($to) 91 | { 92 | $code = $to->getIDDCode(); 93 | 94 | return empty($code) || 86 === $code; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/Gateways/TianyiwuxianGateway.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Overtrue\EasySms\Gateways; 13 | 14 | use Overtrue\EasySms\Contracts\MessageInterface; 15 | use Overtrue\EasySms\Contracts\PhoneNumberInterface; 16 | use Overtrue\EasySms\Exceptions\GatewayErrorException; 17 | use Overtrue\EasySms\Support\Config; 18 | use Overtrue\EasySms\Traits\HasHttpRequest; 19 | 20 | /** 21 | * Class TianyiwuxianGateway. 22 | * 23 | * @author Darren Gao 24 | */ 25 | class TianyiwuxianGateway extends Gateway 26 | { 27 | use HasHttpRequest; 28 | 29 | public const ENDPOINT_TEMPLATE = 'http://jk.106api.cn/sms%s.aspx'; 30 | 31 | public const ENDPOINT_ENCODE = 'UTF8'; 32 | 33 | public const ENDPOINT_TYPE = 'send'; 34 | 35 | public const ENDPOINT_FORMAT = 'json'; 36 | 37 | public const SUCCESS_STATUS = 'success'; 38 | 39 | public const SUCCESS_CODE = '0'; 40 | 41 | /** 42 | * @return array 43 | * 44 | * @throws GatewayErrorException ; 45 | */ 46 | public function send(PhoneNumberInterface $to, MessageInterface $message, Config $config) 47 | { 48 | $endpoint = $this->buildEndpoint(); 49 | 50 | $params = [ 51 | 'gwid' => $config->get('gwid'), 52 | 'type' => self::ENDPOINT_TYPE, 53 | 'rece' => self::ENDPOINT_FORMAT, 54 | 'mobile' => $to->getNumber(), 55 | 'message' => $message->getContent($this), 56 | 'username' => $config->get('username'), 57 | 'password' => strtoupper(md5($config->get('password'))), 58 | ]; 59 | 60 | $result = $this->post($endpoint, $params); 61 | 62 | $result = json_decode($result, true); 63 | 64 | if (self::SUCCESS_STATUS !== $result['returnstatus'] || self::SUCCESS_CODE !== $result['code']) { 65 | throw new GatewayErrorException($result['remark'], $result['code']); 66 | } 67 | 68 | return $result; 69 | } 70 | 71 | /** 72 | * Build endpoint url. 73 | * 74 | * @return string 75 | */ 76 | protected function buildEndpoint() 77 | { 78 | return sprintf(self::ENDPOINT_TEMPLATE, self::ENDPOINT_ENCODE); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Gateways/TiniyoGateway.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Overtrue\EasySms\Gateways; 13 | 14 | use Overtrue\EasySms\Contracts\MessageInterface; 15 | use Overtrue\EasySms\Contracts\PhoneNumberInterface; 16 | use Overtrue\EasySms\Exceptions\GatewayErrorException; 17 | use Overtrue\EasySms\Support\Config; 18 | use Overtrue\EasySms\Traits\HasHttpRequest; 19 | 20 | /** 21 | * Class Tiniyo Gateway. 22 | * 23 | * @see https://tiniyo.com/sms.html 24 | */ 25 | class TiniyoGateway extends Gateway 26 | { 27 | use HasHttpRequest; 28 | 29 | public const ENDPOINT_URL = 'https://api.tiniyo.com/v1/Account/%s/Message'; 30 | 31 | public const SUCCESS_CODE = '000000'; 32 | 33 | public function getName() 34 | { 35 | return 'tiniyo'; 36 | } 37 | 38 | /** 39 | * @return array 40 | * 41 | * @throws GatewayErrorException 42 | */ 43 | public function send(PhoneNumberInterface $to, MessageInterface $message, Config $config) 44 | { 45 | $accountSid = $config->get('account_sid'); 46 | $endpoint = $this->buildEndPoint($accountSid); 47 | 48 | $params = [ 49 | 'dst' => $to->getUniversalNumber(), 50 | 'src' => $config->get('from'), 51 | 'text' => $message->getContent($this), 52 | ]; 53 | 54 | $result = $this->request('post', $endpoint, [ 55 | 'json' => $params, 56 | 'headers' => [ 57 | 'Accept' => 'application/json', 58 | 'Content-Type' => 'application/json;charset=utf-8', 59 | 'Authorization' => base64_encode($config->get('account_sid').':'.$config->get('token')), 60 | ], 61 | ]); 62 | 63 | if (self::SUCCESS_CODE != $result['statusCode']) { 64 | throw new GatewayErrorException($result['statusCode'], $result['statusCode'], $result); 65 | } 66 | 67 | return $result; 68 | } 69 | 70 | /** 71 | * build endpoint url. 72 | * 73 | * @param string $accountSid 74 | * 75 | * @return string 76 | */ 77 | protected function buildEndPoint($accountSid) 78 | { 79 | return sprintf(self::ENDPOINT_URL, $accountSid); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Gateways/TinreeGateway.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Overtrue\EasySms\Gateways; 13 | 14 | use Overtrue\EasySms\Contracts\MessageInterface; 15 | use Overtrue\EasySms\Contracts\PhoneNumberInterface; 16 | use Overtrue\EasySms\Exceptions\GatewayErrorException; 17 | use Overtrue\EasySms\Support\Config; 18 | use Overtrue\EasySms\Traits\HasHttpRequest; 19 | 20 | /** 21 | * Class TinreeGateway. 22 | * 23 | * @see http://cms.tinree.com 24 | */ 25 | class TinreeGateway extends Gateway 26 | { 27 | use HasHttpRequest; 28 | 29 | public const ENDPOINT_URL = 'http://api.tinree.com/api/v2/single_send'; 30 | 31 | /** 32 | * @return array 33 | * 34 | * @throws GatewayErrorException 35 | */ 36 | public function send(PhoneNumberInterface $to, MessageInterface $message, Config $config) 37 | { 38 | $params = [ 39 | 'accesskey' => $config->get('accesskey'), 40 | 'secret' => $config->get('secret'), 41 | 'sign' => $config->get('sign'), 42 | 'templateId' => $message->getTemplate($this), 43 | 'mobile' => $to->getNumber(), 44 | 'content' => $this->buildContent($message), 45 | ]; 46 | 47 | $result = $this->post(self::ENDPOINT_URL, $params); 48 | 49 | if (0 != $result['code']) { 50 | throw new GatewayErrorException($result['msg'], $result['code'], $result); 51 | } 52 | 53 | return $result; 54 | } 55 | 56 | /** 57 | * 构建发送内容 58 | * 用 data 数据合成内容,或者直接使用 data 的值 59 | * 60 | * @return string 61 | */ 62 | protected function buildContent(MessageInterface $message) 63 | { 64 | $data = $message->getData($this); 65 | 66 | if (is_array($data)) { 67 | return implode('##', $data); 68 | } 69 | 70 | return $data; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Gateways/TwilioGateway.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Overtrue\EasySms\Gateways; 13 | 14 | use GuzzleHttp\Exception\ClientException; 15 | use Overtrue\EasySms\Contracts\MessageInterface; 16 | use Overtrue\EasySms\Contracts\PhoneNumberInterface; 17 | use Overtrue\EasySms\Exceptions\GatewayErrorException; 18 | use Overtrue\EasySms\Support\Config; 19 | use Overtrue\EasySms\Traits\HasHttpRequest; 20 | 21 | /** 22 | * Class TwilioGateway. 23 | * 24 | * @see https://www.twilio.com/docs/api/messaging/send-messages 25 | */ 26 | class TwilioGateway extends Gateway 27 | { 28 | use HasHttpRequest; 29 | 30 | public const ENDPOINT_URL = 'https://api.twilio.com/2010-04-01/Accounts/%s/Messages.json'; 31 | 32 | protected $errorStatuses = [ 33 | 'failed', 34 | 'undelivered', 35 | ]; 36 | 37 | public function getName() 38 | { 39 | return 'twilio'; 40 | } 41 | 42 | /** 43 | * @return array 44 | * 45 | * @throws GatewayErrorException 46 | */ 47 | public function send(PhoneNumberInterface $to, MessageInterface $message, Config $config) 48 | { 49 | $accountSid = $config->get('account_sid'); 50 | $endpoint = $this->buildEndPoint($accountSid); 51 | 52 | $params = [ 53 | 'To' => $to->getUniversalNumber(), 54 | 'From' => $config->get('from'), 55 | 'Body' => $message->getContent($this), 56 | ]; 57 | 58 | try { 59 | $result = $this->request('post', $endpoint, [ 60 | 'auth' => [ 61 | $accountSid, 62 | $config->get('token'), 63 | ], 64 | 'form_params' => $params, 65 | ]); 66 | if (in_array($result['status'], $this->errorStatuses) || !is_null($result['error_code'])) { 67 | throw new GatewayErrorException($result['message'], $result['error_code'], $result); 68 | } 69 | } catch (ClientException $e) { 70 | throw new GatewayErrorException($e->getMessage(), $e->getCode()); 71 | } 72 | 73 | return $result; 74 | } 75 | 76 | /** 77 | * build endpoint url. 78 | * 79 | * @param string $accountSid 80 | * 81 | * @return string 82 | */ 83 | protected function buildEndPoint($accountSid) 84 | { 85 | return sprintf(self::ENDPOINT_URL, $accountSid); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Gateways/UcloudGateway.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Overtrue\EasySms\Gateways; 13 | 14 | use Overtrue\EasySms\Contracts\MessageInterface; 15 | use Overtrue\EasySms\Contracts\PhoneNumberInterface; 16 | use Overtrue\EasySms\Exceptions\GatewayErrorException; 17 | use Overtrue\EasySms\Support\Config; 18 | use Overtrue\EasySms\Traits\HasHttpRequest; 19 | 20 | /** 21 | * Class UcloudGateway. 22 | */ 23 | class UcloudGateway extends Gateway 24 | { 25 | use HasHttpRequest; 26 | 27 | public const ENDPOINT_URL = 'https://api.ucloud.cn'; 28 | 29 | public const ENDPOINT_Action = 'SendUSMSMessage'; 30 | 31 | public const SUCCESS_CODE = 0; 32 | 33 | /** 34 | * Send Message. 35 | * 36 | * @return array 37 | * 38 | * @throws GatewayErrorException 39 | */ 40 | public function send(PhoneNumberInterface $to, MessageInterface $message, Config $config) 41 | { 42 | $params = $this->buildParams($to, $message, $config); 43 | 44 | $result = $this->get(self::ENDPOINT_URL, $params); 45 | 46 | if (self::SUCCESS_CODE != $result['RetCode']) { 47 | throw new GatewayErrorException($result['Message'], $result['RetCode'], $result); 48 | } 49 | 50 | return $result; 51 | } 52 | 53 | /** 54 | * Build Params. 55 | * 56 | * @return array 57 | */ 58 | protected function buildParams(PhoneNumberInterface $to, MessageInterface $message, Config $config) 59 | { 60 | $data = $message->getData($this); 61 | $params = [ 62 | 'Action' => self::ENDPOINT_Action, 63 | 'SigContent' => !empty($data['sig_content']) ? $data['sig_content'] : $config->get('sig_content', ''), 64 | 'TemplateId' => $message->getTemplate($this), 65 | 'PublicKey' => $config->get('public_key'), 66 | ]; 67 | $code = isset($data['code']) ? $data['code'] : ''; 68 | if (is_array($code) && !empty($code)) { 69 | foreach ($code as $key => $value) { 70 | $params['TemplateParams.'.$key] = $value; 71 | } 72 | } else { 73 | if (!empty($code) || !is_null($code)) { 74 | $params['TemplateParams.0'] = $code; 75 | } 76 | } 77 | 78 | $mobiles = isset($data['mobiles']) ? $data['mobiles'] : ''; 79 | if (!empty($mobiles) && !is_null($mobiles)) { 80 | if (is_array($mobiles)) { 81 | foreach ($mobiles as $key => $value) { 82 | $params['PhoneNumbers.'.$key] = $value; 83 | } 84 | } else { 85 | $params['PhoneNumbers.0'] = $mobiles; 86 | } 87 | } else { 88 | $params['PhoneNumbers.0'] = $to->getNumber(); 89 | } 90 | 91 | if (!is_null($config->get('project_id')) && !empty($config->get('project_id'))) { 92 | $params['ProjectId'] = $config->get('project_id'); 93 | } 94 | 95 | $signature = $this->getSignature($params, $config->get('private_key')); 96 | $params['Signature'] = $signature; 97 | 98 | return $params; 99 | } 100 | 101 | /** 102 | * Generate Sign. 103 | * 104 | * @param array $params 105 | * @param string $privateKey 106 | * 107 | * @return string 108 | */ 109 | protected function getSignature($params, $privateKey) 110 | { 111 | ksort($params); 112 | 113 | $paramsData = ''; 114 | foreach ($params as $key => $value) { 115 | $paramsData .= $key; 116 | $paramsData .= $value; 117 | } 118 | $paramsData .= $privateKey; 119 | 120 | return sha1($paramsData); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/Gateways/Ue35Gateway.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Overtrue\EasySms\Gateways; 13 | 14 | use Overtrue\EasySms\Contracts\MessageInterface; 15 | use Overtrue\EasySms\Contracts\PhoneNumberInterface; 16 | use Overtrue\EasySms\Exceptions\GatewayErrorException; 17 | use Overtrue\EasySms\Support\Config; 18 | use Overtrue\EasySms\Traits\HasHttpRequest; 19 | 20 | /** 21 | * Class Ue35Gateway. 22 | * 23 | * @see https://shimo.im/docs/380b42d8cba24521 24 | */ 25 | class Ue35Gateway extends Gateway 26 | { 27 | use HasHttpRequest; 28 | 29 | public const ENDPOINT_HOST = 'sms.ue35.net:8443'; 30 | 31 | public const ENDPOINT_URI = '/sms/interface/sendmess.htm'; 32 | 33 | public const SUCCESS_CODE = 1; 34 | 35 | /** 36 | * Send message. 37 | * 38 | * @return array 39 | * 40 | * @throws GatewayErrorException ; 41 | */ 42 | public function send(PhoneNumberInterface $to, MessageInterface $message, Config $config) 43 | { 44 | $params = [ 45 | 'username' => $config->get('username'), 46 | 'userpwd' => $config->get('userpwd'), 47 | 'mobiles' => $to->getNumber(), 48 | 'content' => $message->getContent($this), 49 | ]; 50 | 51 | $headers = [ 52 | 'host' => static::ENDPOINT_HOST, 53 | 'content-type' => 'application/json', 54 | 'user-agent' => 'PHP EasySms Client', 55 | ]; 56 | 57 | $result = $this->request('get', self::getEndpointUri().'?'.http_build_query($params), ['headers' => $headers]); 58 | if (is_string($result)) { 59 | $result = json_decode(json_encode(simplexml_load_string($result)), true); 60 | } 61 | 62 | if (self::SUCCESS_CODE != $result['errorcode']) { 63 | throw new GatewayErrorException($result['message'], $result['errorcode'], $result); 64 | } 65 | 66 | return $result; 67 | } 68 | 69 | public static function getEndpointUri() 70 | { 71 | return 'https://'.static::ENDPOINT_HOST.static::ENDPOINT_URI; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Gateways/VolcengineGateway.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Overtrue\EasySms\Gateways; 13 | 14 | use GuzzleHttp\Exception\ClientException; 15 | use GuzzleHttp\HandlerStack; 16 | use GuzzleHttp\Psr7; 17 | use GuzzleHttp\Psr7\Query; 18 | use GuzzleHttp\Psr7\Utils; 19 | use Overtrue\EasySms\Contracts\MessageInterface; 20 | use Overtrue\EasySms\Contracts\PhoneNumberInterface; 21 | use Overtrue\EasySms\Exceptions\GatewayErrorException; 22 | use Overtrue\EasySms\Support\Config; 23 | use Overtrue\EasySms\Traits\HasHttpRequest; 24 | use Psr\Http\Message\RequestInterface; 25 | 26 | /** 27 | * Class VolcengineGateway. 28 | * 29 | * @see https://www.volcengine.com/docs/6361/66704 30 | */ 31 | class VolcengineGateway extends Gateway 32 | { 33 | use HasHttpRequest; 34 | 35 | public const ENDPOINT_ACTION = 'SendSms'; 36 | public const ENDPOINT_VERSION = '2020-01-01'; 37 | public const ENDPOINT_CONTENT_TYPE = 'application/json; charset=utf-8'; 38 | public const ENDPOINT_ACCEPT = 'application/json'; 39 | public const ENDPOINT_USER_AGENT = 'overtrue/easy-sms'; 40 | public const ENDPOINT_SERVICE = 'volcSMS'; 41 | 42 | public const Algorithm = 'HMAC-SHA256'; 43 | 44 | public const ENDPOINT_DEFAULT_REGION_ID = 'cn-north-1'; 45 | 46 | public static $endpoints = [ 47 | 'cn-north-1' => 'https://sms.volcengineapi.com', 48 | 'ap-singapore-1' => 'https://sms.byteplusapi.com', 49 | ]; 50 | 51 | private $regionId = self::ENDPOINT_DEFAULT_REGION_ID; 52 | protected $requestDate; 53 | 54 | public function send(PhoneNumberInterface $to, MessageInterface $message, Config $config) 55 | { 56 | $data = $message->getData($this); 57 | $signName = !empty($data['sign_name']) ? $data['sign_name'] : $config->get('sign_name'); 58 | $smsAccount = !empty($data['sms_account']) ? $data['sms_account'] : $config->get('sms_account'); 59 | $templateId = $message->getTemplate($this); 60 | $phoneNumbers = !empty($data['phone_numbers']) ? $data['phone_numbers'] : $to->getNumber(); 61 | $templateParam = !empty($data['template_param']) ? $data['template_param'] : $message->getData($this); 62 | 63 | $tag = !empty($data['tag']) ? $data['tag'] : ''; 64 | 65 | $payload = [ 66 | 'SmsAccount' => $smsAccount, // 消息组帐号,火山短信页面右上角,短信应用括号中的字符串 67 | 'Sign' => $signName, // 短信签名 68 | 'TemplateID' => $templateId, // 短信模板ID 69 | 'TemplateParam' => json_encode($templateParam), // 短信模板占位符要替换的值 70 | 'PhoneNumbers' => $phoneNumbers, // 手机号,如果有多个使用英文逗号分割 71 | ]; 72 | if ($tag) { 73 | $payload['Tag'] = $tag; 74 | } 75 | $queries = [ 76 | 'Action' => self::ENDPOINT_ACTION, 77 | 'Version' => self::ENDPOINT_VERSION, 78 | ]; 79 | 80 | try { 81 | $stack = HandlerStack::create(); 82 | $stack->push($this->signHandle()); 83 | $this->setGuzzleOptions([ 84 | 'headers' => [ 85 | 'Content-Type' => self::ENDPOINT_CONTENT_TYPE, 86 | 'Accept' => self::ENDPOINT_ACCEPT, 87 | 'User-Agent' => self::ENDPOINT_USER_AGENT, 88 | ], 89 | 'timeout' => $this->getTimeout(), 90 | 'handler' => $stack, 91 | 'base_uri' => $this->getEndpoint(), 92 | ]); 93 | 94 | $response = $this->request('post', $this->getEndpoint().$this->getCanonicalURI(), [ 95 | 'query' => $queries, 96 | 'json' => $payload, 97 | ]); 98 | if ($response instanceof Psr7\Response) { 99 | $response = json_decode($response->getBody()->getContents(), true); 100 | } 101 | if (isset($response['ResponseMetadata']['Error'])) { // 请求错误参数,如果请求没有错误,则不存在该参数返回 102 | // 火山引擎错误码格式为:'ZJ'+ 5位数字,比如 ZJ20009,取出数字部分 103 | preg_match('/\d+/', $response['ResponseMetadata']['Error']['Code'], $matches); 104 | throw new GatewayErrorException($response['ResponseMetadata']['Error']['Code'].':'.$response['ResponseMetadata']['Error']['Message'], $matches[0], $response); 105 | } 106 | 107 | return $response; 108 | } catch (ClientException $exception) { 109 | $responseContent = $exception->getResponse()->getBody()->getContents(); 110 | $response = json_decode($responseContent, true); 111 | if (isset($response['ResponseMetadata']['Error']) && $error = $response['ResponseMetadata']['Error']) { // 请求错误参数,如果请求没有错误,则不存在该参数返回 112 | // 火山引擎公共错误码Error与业务错误码略有不同,比如:"Error":{"CodeN":100004,"Code":"MissingRequestInfo","Message":"The request is missing timestamp information."} 113 | // 此处错误码直接取 CodeN 114 | throw new GatewayErrorException($error['CodeN'].':'.$error['Message'], $error['CodeN'], $response); 115 | } 116 | throw new GatewayErrorException($responseContent, $exception->getCode(), ['content' => $responseContent]); 117 | } 118 | } 119 | 120 | protected function signHandle() 121 | { 122 | return function (callable $handler) { 123 | return function (RequestInterface $request, array $options) use ($handler) { 124 | $request = $request->withHeader('X-Date', $this->getRequestDate()); 125 | list($canonicalHeaders, $signedHeaders) = $this->getCanonicalHeaders($request); 126 | 127 | $queries = Query::parse($request->getUri()->getQuery()); 128 | $canonicalRequest = $request->getMethod()."\n" 129 | .$this->getCanonicalURI()."\n" 130 | .$this->getCanonicalQueryString($queries)."\n" 131 | .$canonicalHeaders."\n" 132 | .$signedHeaders."\n" 133 | .$this->getPayloadHash($request); 134 | 135 | $stringToSign = $this->getStringToSign($canonicalRequest); 136 | 137 | $signingKey = $this->getSigningKey(); 138 | 139 | $signature = hash_hmac('sha256', $stringToSign, $signingKey); 140 | $parsed = $this->parseRequest($request); 141 | 142 | $parsed['headers']['Authorization'] = self::Algorithm. 143 | ' Credential='.$this->getAccessKeyId().'/'.$this->getCredentialScope().', SignedHeaders='.$signedHeaders.', Signature='.$signature; 144 | 145 | $buildRequest = function () use ($parsed) { 146 | if ($parsed['query']) { 147 | $parsed['uri'] = $parsed['uri']->withQuery(Query::build($parsed['query'])); 148 | } 149 | 150 | return new Psr7\Request( 151 | $parsed['method'], 152 | $parsed['uri'], 153 | $parsed['headers'], 154 | $parsed['body'], 155 | $parsed['version'] 156 | ); 157 | }; 158 | 159 | return $handler($buildRequest(), $options); 160 | }; 161 | }; 162 | } 163 | 164 | private function parseRequest(RequestInterface $request) 165 | { 166 | $uri = $request->getUri(); 167 | 168 | return [ 169 | 'method' => $request->getMethod(), 170 | 'path' => $uri->getPath(), 171 | 'query' => Query::parse($uri->getQuery()), 172 | 'uri' => $uri, 173 | 'headers' => $request->getHeaders(), 174 | 'body' => $request->getBody(), 175 | 'version' => $request->getProtocolVersion(), 176 | ]; 177 | } 178 | 179 | public function getPayloadHash(RequestInterface $request) 180 | { 181 | if ($request->hasHeader('X-Content-Sha256')) { 182 | return $request->getHeaderLine('X-Content-Sha256'); 183 | } 184 | 185 | return Utils::hash($request->getBody(), 'sha256'); 186 | } 187 | 188 | public function getRegionId() 189 | { 190 | return $this->config->get('region_id', self::ENDPOINT_DEFAULT_REGION_ID); 191 | } 192 | 193 | public function getEndpoint() 194 | { 195 | $regionId = $this->getRegionId(); 196 | if (!in_array($regionId, array_keys(self::$endpoints))) { 197 | $regionId = self::ENDPOINT_DEFAULT_REGION_ID; 198 | } 199 | 200 | return static::$endpoints[$regionId]; 201 | } 202 | 203 | public function getRequestDate() 204 | { 205 | return $this->requestDate ?: gmdate('Ymd\THis\Z'); 206 | } 207 | 208 | /** 209 | * 指代信任状,格式为:YYYYMMDD/region/service/request. 210 | * 211 | * @return string 212 | */ 213 | public function getCredentialScope() 214 | { 215 | return date('Ymd', strtotime($this->getRequestDate())).'/'.$this->getRegionId().'/'.self::ENDPOINT_SERVICE.'/request'; 216 | } 217 | 218 | /** 219 | * 计算签名密钥 220 | * 在计算签名前,首先从私有访问密钥(Secret Access Key)派生出签名密钥(signing key),而不是直接使用私有访问密钥。具体计算过程如下: 221 | * kSecret = *Your Secret Access Key* 222 | * kDate = HMAC(kSecret, Date) 223 | * kRegion = HMAC(kDate, Region) 224 | * kService = HMAC(kRegion, Service) 225 | * kSigning = HMAC(kService, "request") 226 | * 其中Date精确到日,与RequestDate中YYYYMMDD部分相同。 227 | * 228 | * @return string 229 | */ 230 | protected function getSigningKey() 231 | { 232 | $dateKey = hash_hmac('sha256', date('Ymd', strtotime($this->getRequestDate())), $this->getAccessKeySecret(), true); 233 | $regionKey = hash_hmac('sha256', $this->getRegionId(), $dateKey, true); 234 | $serviceKey = hash_hmac('sha256', self::ENDPOINT_SERVICE, $regionKey, true); 235 | 236 | return hash_hmac('sha256', 'request', $serviceKey, true); 237 | } 238 | 239 | /** 240 | * 创建签名字符串 241 | * 签名字符串主要包含请求以及正规化请求的元数据信息,由签名算法、请求日期、信任状和正规化请求哈希值连接组成,伪代码如下: 242 | * StringToSign = Algorithm + '\n' + RequestDate + '\n' + CredentialScope + '\n' + HexEncode(Hash(CanonicalRequest)). 243 | * 244 | * @return string 245 | */ 246 | public function getStringToSign($canonicalRequest) 247 | { 248 | return self::Algorithm."\n".$this->getRequestDate()."\n".$this->getCredentialScope()."\n".hash('sha256', $canonicalRequest); 249 | } 250 | 251 | /** 252 | * @return string 253 | */ 254 | public function getAccessKeySecret() 255 | { 256 | return $this->config->get('access_key_secret'); 257 | } 258 | 259 | /** 260 | * @return string 261 | */ 262 | public function getAccessKeyId() 263 | { 264 | return $this->config->get('access_key_id'); 265 | } 266 | 267 | /** 268 | * 指代正规化后的Header。 269 | * 其中伪代码如下: 270 | * CanonicalHeaders = CanonicalHeadersEntry0 + CanonicalHeadersEntry1 + ... + CanonicalHeadersEntryN 271 | * 其中CanonicalHeadersEntry = Lowercase(HeaderName) + ':' + Trimall(HeaderValue) + '\n' 272 | * Lowcase代表将Header的名称全部转化成小写,Trimall表示去掉Header的值的前后多余的空格。 273 | * 特别注意:最后需要添加"\n"的换行符,header的顺序是以headerName的小写后ascii排序。 274 | * 275 | * @return array 276 | */ 277 | public function getCanonicalHeaders(RequestInterface $request) 278 | { 279 | $headers = $request->getHeaders(); 280 | ksort($headers); 281 | $canonicalHeaders = ''; 282 | $signedHeaders = []; 283 | foreach ($headers as $key => $val) { 284 | $lowerKey = strtolower($key); 285 | $canonicalHeaders .= $lowerKey.':'.trim($val[0]).PHP_EOL; 286 | $signedHeaders[] = $lowerKey; 287 | } 288 | $signedHeadersString = implode(';', $signedHeaders); 289 | 290 | return [$canonicalHeaders, $signedHeadersString]; 291 | } 292 | 293 | /** 294 | * urlencode(注:同RFC3986方法)每一个querystring参数名称和参数值。 295 | * 按照ASCII字节顺序对参数名称严格排序,相同参数名的不同参数值需保持请求的原始顺序。 296 | * 将排序好的参数名称和参数值用=连接,按照排序结果将“参数对”用&连接。 297 | * 例如:CanonicalQueryString = "Action=ListUsers&Version=2018-01-01". 298 | * 299 | * @return string 300 | */ 301 | public function getCanonicalQueryString(array $query) 302 | { 303 | ksort($query); 304 | 305 | return http_build_query($query); 306 | } 307 | 308 | /** 309 | * 指代正规化后的URI。 310 | * 如果URI为空,那么使用"/"作为绝对路径。 311 | * 在火山引擎中绝大多数接口的URI都为"/"。 312 | * 如果是复杂的path,请通过RFC3986规范进行编码。 313 | * 314 | * @return string 315 | */ 316 | public function getCanonicalURI() 317 | { 318 | return '/'; 319 | } 320 | } 321 | -------------------------------------------------------------------------------- /src/Gateways/YidongmasblackGateway.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Overtrue\EasySms\Gateways; 13 | 14 | use Overtrue\EasySms\Contracts\MessageInterface; 15 | use Overtrue\EasySms\Contracts\PhoneNumberInterface; 16 | use Overtrue\EasySms\Exceptions\GatewayErrorException; 17 | use Overtrue\EasySms\Support\Config; 18 | use Overtrue\EasySms\Traits\HasHttpRequest; 19 | 20 | /** 21 | * Class YidongmasblackGateway. 22 | * 移动MAS黑名单模式(免创建模板). 23 | * 24 | * @author houang 25 | * 26 | * @see https://mas.10086.cn 27 | */ 28 | class YidongmasblackGateway extends Gateway 29 | { 30 | use HasHttpRequest; 31 | 32 | public const ENDPOINT_URL = 'http://112.35.1.155:1992/sms/norsubmit'; 33 | 34 | public const ENDPOINT_METHOD = 'send'; 35 | 36 | /** 37 | * @return \Psr\Http\Message\ResponseInterface|array|string 38 | * 39 | * @throws GatewayErrorException 40 | */ 41 | public function send(PhoneNumberInterface $to, MessageInterface $message, Config $config) 42 | { 43 | $params['ecName'] = $config->get('ecName'); 44 | $params['apId'] = $config->get('apId'); 45 | $params['sign'] = $config->get('sign'); 46 | $params['addSerial'] = $config->get('addSerial'); 47 | $params['mobiles'] = $to->getNumber(); 48 | $params['content'] = $message->getContent(); 49 | $result = $this->postJson(self::ENDPOINT_URL, $this->generateContent($params)); 50 | 51 | if ('true' != $result['success']) { 52 | throw new GatewayErrorException($result['success'], $result['rspcod'], $result); 53 | } 54 | 55 | return $result; 56 | } 57 | 58 | /** 59 | * Generate Content. 60 | * 61 | * @param array $params 62 | * 63 | * @return string 64 | */ 65 | protected function generateContent($params) 66 | { 67 | $secretKey = $this->config->get('secretKey'); 68 | $params['mac'] = md5($params['ecName'].$params['apId'].$secretKey.$params['mobiles'].$params['content'].$params['sign'].$params['addSerial']); 69 | 70 | return base64_encode(json_encode($params)); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Gateways/YunpianGateway.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Overtrue\EasySms\Gateways; 13 | 14 | use Overtrue\EasySms\Contracts\MessageInterface; 15 | use Overtrue\EasySms\Contracts\PhoneNumberInterface; 16 | use Overtrue\EasySms\Exceptions\GatewayErrorException; 17 | use Overtrue\EasySms\Support\Config; 18 | use Overtrue\EasySms\Traits\HasHttpRequest; 19 | 20 | /** 21 | * Class YunpianGateway. 22 | * 23 | * @see https://www.yunpian.com/doc/zh_CN/intl/single_send.html 24 | */ 25 | class YunpianGateway extends Gateway 26 | { 27 | use HasHttpRequest; 28 | 29 | public const ENDPOINT_TEMPLATE = 'https://%s.yunpian.com/%s/%s/%s.%s'; 30 | 31 | public const ENDPOINT_VERSION = 'v2'; 32 | 33 | public const ENDPOINT_FORMAT = 'json'; 34 | 35 | /** 36 | * @return array 37 | * 38 | * @throws GatewayErrorException ; 39 | */ 40 | public function send(PhoneNumberInterface $to, MessageInterface $message, Config $config) 41 | { 42 | $template = $message->getTemplate($this); 43 | $function = 'single_send'; 44 | $option = [ 45 | 'form_params' => [ 46 | 'apikey' => $config->get('api_key'), 47 | 'mobile' => $to->getUniversalNumber(), 48 | ], 49 | 'exceptions' => false, 50 | ]; 51 | 52 | if (!is_null($template)) { 53 | $function = 'tpl_single_send'; 54 | $data = []; 55 | 56 | $templateData = $message->getData($this); 57 | $templateData = isset($templateData) ? $templateData : []; 58 | foreach ($templateData as $key => $value) { 59 | $data[] = urlencode('#'.$key.'#').'='.urlencode($value); 60 | } 61 | 62 | $option['form_params'] = array_merge($option['form_params'], [ 63 | 'tpl_id' => $template, 64 | 'tpl_value' => implode('&', $data), 65 | ]); 66 | } else { 67 | $content = $message->getContent($this); 68 | $signature = $config->get('signature', ''); 69 | $option['form_params'] = array_merge($option['form_params'], [ 70 | 'text' => 0 === \stripos($content, '【') ? $content : $signature.$content, 71 | ]); 72 | } 73 | 74 | $endpoint = $this->buildEndpoint('sms', 'sms', $function); 75 | $result = $this->request('post', $endpoint, $option); 76 | 77 | if ($result['code']) { 78 | throw new GatewayErrorException($result['msg'], $result['code'], $result); 79 | } 80 | 81 | return $result; 82 | } 83 | 84 | /** 85 | * Build endpoint url. 86 | * 87 | * @param string $type 88 | * @param string $resource 89 | * @param string $function 90 | * 91 | * @return string 92 | */ 93 | protected function buildEndpoint($type, $resource, $function) 94 | { 95 | return sprintf(self::ENDPOINT_TEMPLATE, $type, self::ENDPOINT_VERSION, $resource, $function, self::ENDPOINT_FORMAT); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Gateways/YuntongxunGateway.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Overtrue\EasySms\Gateways; 13 | 14 | use Overtrue\EasySms\Contracts\MessageInterface; 15 | use Overtrue\EasySms\Contracts\PhoneNumberInterface; 16 | use Overtrue\EasySms\Exceptions\GatewayErrorException; 17 | use Overtrue\EasySms\Support\Config; 18 | use Overtrue\EasySms\Traits\HasHttpRequest; 19 | 20 | /** 21 | * Class YuntongxunGateway. 22 | * 23 | * @see Chinese Mainland: http://doc.yuntongxun.com/pe/5a533de33b8496dd00dce07c 24 | * @see International: http://doc.yuntongxun.com/pe/604f29eda80948a1006e928d 25 | */ 26 | class YuntongxunGateway extends Gateway 27 | { 28 | use HasHttpRequest; 29 | 30 | public const ENDPOINT_TEMPLATE = 'https://%s:%s/%s/%s/%s/%s/%s?sig=%s'; 31 | 32 | public const SERVER_IP = 'app.cloopen.com'; 33 | 34 | public const DEBUG_SERVER_IP = 'sandboxapp.cloopen.com'; 35 | 36 | public const DEBUG_TEMPLATE_ID = 1; 37 | 38 | public const SERVER_PORT = '8883'; 39 | 40 | public const SDK_VERSION = '2013-12-26'; 41 | 42 | public const SDK_VERSION_INT = 'v2'; 43 | 44 | public const SUCCESS_CODE = '000000'; 45 | 46 | private $international = false; // if international SMS, default false means no. 47 | 48 | /** 49 | * @return array 50 | * 51 | * @throws GatewayErrorException ; 52 | */ 53 | public function send(PhoneNumberInterface $to, MessageInterface $message, Config $config) 54 | { 55 | $datetime = date('YmdHis'); 56 | 57 | $data = [ 58 | 'appId' => $config->get('app_id'), 59 | ]; 60 | 61 | if ($to->inChineseMainland()) { 62 | $type = 'SMS'; 63 | $resource = 'TemplateSMS'; 64 | $data['to'] = $to->getNumber(); 65 | $data['templateId'] = (int) ($this->config->get('debug') ? self::DEBUG_TEMPLATE_ID : $message->getTemplate($this)); 66 | $data['datas'] = $message->getData($this); 67 | } else { 68 | $type = 'international'; 69 | $resource = 'send'; 70 | $this->international = true; 71 | $data['mobile'] = $to->getZeroPrefixedNumber(); 72 | $data['content'] = $message->getContent($this); 73 | } 74 | 75 | $endpoint = $this->buildEndpoint($type, $resource, $datetime, $config); 76 | 77 | $result = $this->request('post', $endpoint, [ 78 | 'json' => $data, 79 | 'headers' => [ 80 | 'Accept' => 'application/json', 81 | 'Content-Type' => 'application/json;charset=utf-8', 82 | 'Authorization' => base64_encode($config->get('account_sid').':'.$datetime), 83 | ], 84 | ]); 85 | 86 | if (self::SUCCESS_CODE != $result['statusCode']) { 87 | throw new GatewayErrorException($result['statusCode'], $result['statusCode'], $result); 88 | } 89 | 90 | return $result; 91 | } 92 | 93 | /** 94 | * Build endpoint url. 95 | * 96 | * @param string $type 97 | * @param string $resource 98 | * @param string $datetime 99 | * 100 | * @return string 101 | */ 102 | protected function buildEndpoint($type, $resource, $datetime, Config $config) 103 | { 104 | $serverIp = $this->config->get('debug') ? self::DEBUG_SERVER_IP : self::SERVER_IP; 105 | 106 | if ($this->international) { 107 | $accountType = 'account'; 108 | $sdkVersion = self::SDK_VERSION_INT; 109 | } else { 110 | $accountType = $this->config->get('is_sub_account') ? 'SubAccounts' : 'Accounts'; 111 | $sdkVersion = self::SDK_VERSION; 112 | } 113 | 114 | $sig = strtoupper(md5($config->get('account_sid').$config->get('account_token').$datetime)); 115 | 116 | return sprintf(self::ENDPOINT_TEMPLATE, $serverIp, self::SERVER_PORT, $sdkVersion, $accountType, $config->get('account_sid'), $type, $resource, $sig); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/Gateways/YunxinGateway.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Overtrue\EasySms\Gateways; 13 | 14 | use Overtrue\EasySms\Contracts\MessageInterface; 15 | use Overtrue\EasySms\Contracts\PhoneNumberInterface; 16 | use Overtrue\EasySms\Exceptions\GatewayErrorException; 17 | use Overtrue\EasySms\Support\Config; 18 | use Overtrue\EasySms\Traits\HasHttpRequest; 19 | 20 | /** 21 | * Class YunxinGateway. 22 | * 23 | * @author her-cat 24 | * 25 | * @see https://dev.yunxin.163.com/docs/product/%E7%9F%AD%E4%BF%A1/%E7%9F%AD%E4%BF%A1%E6%8E%A5%E5%8F%A3%E6%8C%87%E5%8D%97 26 | */ 27 | class YunxinGateway extends Gateway 28 | { 29 | use HasHttpRequest; 30 | 31 | public const ENDPOINT_TEMPLATE = 'https://api.netease.im/%s/%s.action'; 32 | 33 | public const ENDPOINT_ACTION = 'sendCode'; 34 | 35 | public const SUCCESS_CODE = 200; 36 | 37 | /** 38 | * Send a short message. 39 | * 40 | * @return array 41 | * 42 | * @throws GatewayErrorException 43 | */ 44 | public function send(PhoneNumberInterface $to, MessageInterface $message, Config $config) 45 | { 46 | $data = $message->getData($this); 47 | 48 | $action = isset($data['action']) ? $data['action'] : self::ENDPOINT_ACTION; 49 | 50 | $endpoint = $this->buildEndpoint('sms', $action); 51 | 52 | switch ($action) { 53 | case 'sendCode': 54 | $params = $this->buildSendCodeParams($to, $message, $config); 55 | 56 | break; 57 | case 'verifyCode': 58 | $params = $this->buildVerifyCodeParams($to, $message); 59 | 60 | break; 61 | case 'sendTemplate': 62 | $params = $this->buildTemplateParams($to, $message, $config); 63 | 64 | break; 65 | default: 66 | throw new GatewayErrorException(sprintf('action: %s not supported', $action), 0); 67 | } 68 | 69 | $headers = $this->buildHeaders($config); 70 | 71 | try { 72 | $result = $this->post($endpoint, $params, $headers); 73 | 74 | if (!isset($result['code']) || self::SUCCESS_CODE !== $result['code']) { 75 | $code = isset($result['code']) ? $result['code'] : 0; 76 | $error = isset($result['msg']) ? $result['msg'] : json_encode($result, JSON_UNESCAPED_UNICODE); 77 | 78 | throw new GatewayErrorException($error, $code); 79 | } 80 | } catch (\Exception $e) { 81 | throw new GatewayErrorException($e->getMessage(), $e->getCode()); 82 | } 83 | 84 | return $result; 85 | } 86 | 87 | /** 88 | * @return string 89 | */ 90 | protected function buildEndpoint($resource, $function) 91 | { 92 | return sprintf(self::ENDPOINT_TEMPLATE, $resource, strtolower($function)); 93 | } 94 | 95 | /** 96 | * Get the request headers. 97 | * 98 | * @return array 99 | */ 100 | protected function buildHeaders(Config $config) 101 | { 102 | $headers = [ 103 | 'AppKey' => $config->get('app_key'), 104 | 'Nonce' => md5(uniqid('easysms')), 105 | 'CurTime' => (string) time(), 106 | 'Content-Type' => 'application/x-www-form-urlencoded;charset=utf-8', 107 | ]; 108 | 109 | $headers['CheckSum'] = sha1("{$config->get('app_secret')}{$headers['Nonce']}{$headers['CurTime']}"); 110 | 111 | return $headers; 112 | } 113 | 114 | /** 115 | * @return array 116 | */ 117 | public function buildSendCodeParams(PhoneNumberInterface $to, MessageInterface $message, Config $config) 118 | { 119 | $data = $message->getData($this); 120 | $template = $message->getTemplate($this); 121 | 122 | return [ 123 | 'mobile' => $to->getUniversalNumber(), 124 | 'authCode' => array_key_exists('code', $data) ? $data['code'] : '', 125 | 'deviceId' => array_key_exists('device_id', $data) ? $data['device_id'] : '', 126 | 'templateid' => is_string($template) ? $template : '', 127 | 'codeLen' => $config->get('code_length', 4), 128 | 'needUp' => $config->get('need_up', false), 129 | ]; 130 | } 131 | 132 | /** 133 | * @return array 134 | * 135 | * @throws GatewayErrorException 136 | */ 137 | public function buildVerifyCodeParams(PhoneNumberInterface $to, MessageInterface $message) 138 | { 139 | $data = $message->getData($this); 140 | 141 | if (!array_key_exists('code', $data)) { 142 | throw new GatewayErrorException('"code" cannot be empty', 0); 143 | } 144 | 145 | return [ 146 | 'mobile' => $to->getUniversalNumber(), 147 | 'code' => $data['code'], 148 | ]; 149 | } 150 | 151 | /** 152 | * @return array 153 | */ 154 | public function buildTemplateParams(PhoneNumberInterface $to, MessageInterface $message, Config $config) 155 | { 156 | $data = $message->getData($this); 157 | 158 | $template = $message->getTemplate($this); 159 | 160 | return [ 161 | 'templateid' => $template, 162 | 'mobiles' => json_encode([$to->getUniversalNumber()]), 163 | 'params' => array_key_exists('params', $data) ? json_encode($data['params']) : '', 164 | 'needUp' => $config->get('need_up', false), 165 | ]; 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/Gateways/YunzhixunGateway.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Overtrue\EasySms\Gateways; 13 | 14 | use Overtrue\EasySms\Contracts\MessageInterface; 15 | use Overtrue\EasySms\Contracts\PhoneNumberInterface; 16 | use Overtrue\EasySms\Exceptions\GatewayErrorException; 17 | use Overtrue\EasySms\Support\Config; 18 | use Overtrue\EasySms\Traits\HasHttpRequest; 19 | 20 | /** 21 | * Class YunzhixunGateway. 22 | * 23 | * @author her-cat 24 | * 25 | * @see http://docs.ucpaas.com/doku.php?id=%E7%9F%AD%E4%BF%A1:sendsms 26 | */ 27 | class YunzhixunGateway extends Gateway 28 | { 29 | use HasHttpRequest; 30 | 31 | public const SUCCESS_CODE = '000000'; 32 | 33 | public const FUNCTION_SEND_SMS = 'sendsms'; 34 | 35 | public const FUNCTION_BATCH_SEND_SMS = 'sendsms_batch'; 36 | 37 | public const ENDPOINT_TEMPLATE = 'https://open.ucpaas.com/ol/%s/%s'; 38 | 39 | /** 40 | * Send a short message. 41 | * 42 | * @return array 43 | * 44 | * @throws GatewayErrorException 45 | */ 46 | public function send(PhoneNumberInterface $to, MessageInterface $message, Config $config) 47 | { 48 | $data = $message->getData($this); 49 | 50 | $function = isset($data['mobiles']) ? self::FUNCTION_BATCH_SEND_SMS : self::FUNCTION_SEND_SMS; 51 | 52 | $endpoint = $this->buildEndpoint('sms', $function); 53 | 54 | $params = $this->buildParams($to, $message, $config); 55 | 56 | return $this->execute($endpoint, $params); 57 | } 58 | 59 | /** 60 | * @return string 61 | */ 62 | protected function buildEndpoint($resource, $function) 63 | { 64 | return sprintf(self::ENDPOINT_TEMPLATE, $resource, $function); 65 | } 66 | 67 | /** 68 | * @return array 69 | */ 70 | protected function buildParams(PhoneNumberInterface $to, MessageInterface $message, Config $config) 71 | { 72 | $data = $message->getData($this); 73 | 74 | return [ 75 | 'sid' => $config->get('sid'), 76 | 'token' => $config->get('token'), 77 | 'appid' => $config->get('app_id'), 78 | 'templateid' => $message->getTemplate($this), 79 | 'uid' => isset($data['uid']) ? $data['uid'] : '', 80 | 'param' => isset($data['params']) ? $data['params'] : '', 81 | 'mobile' => isset($data['mobiles']) ? $data['mobiles'] : $to->getNumber(), 82 | ]; 83 | } 84 | 85 | /** 86 | * @return array 87 | * 88 | * @throws GatewayErrorException 89 | */ 90 | protected function execute($endpoint, $params) 91 | { 92 | try { 93 | $result = $this->postJson($endpoint, $params); 94 | 95 | if (!isset($result['code']) || self::SUCCESS_CODE !== $result['code']) { 96 | $code = isset($result['code']) ? $result['code'] : 0; 97 | $error = isset($result['msg']) ? $result['msg'] : json_encode($result, JSON_UNESCAPED_UNICODE); 98 | 99 | throw new GatewayErrorException($error, $code); 100 | } 101 | 102 | return $result; 103 | } catch (\Exception $e) { 104 | throw new GatewayErrorException($e->getMessage(), $e->getCode()); 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/Gateways/ZzyunGateway.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Overtrue\EasySms\Gateways; 13 | 14 | use Overtrue\EasySms\Contracts\MessageInterface; 15 | use Overtrue\EasySms\Contracts\PhoneNumberInterface; 16 | use Overtrue\EasySms\Exceptions\GatewayErrorException; 17 | use Overtrue\EasySms\Support\Config; 18 | use Overtrue\EasySms\Traits\HasHttpRequest; 19 | 20 | /** 21 | * Class RongheyunGateway. 22 | * 23 | * @see https://zzyun.com/ 24 | */ 25 | class ZzyunGateway extends Gateway 26 | { 27 | use HasHttpRequest; 28 | 29 | public const ENDPOINT_URL = 'https://zzyun.com/api/sms/sendByTplCode'; 30 | 31 | /** 32 | * @return array 33 | * 34 | * @throws GatewayErrorException ; 35 | */ 36 | public function send(PhoneNumberInterface $to, MessageInterface $message, Config $config) 37 | { 38 | $time = time(); 39 | $user_id = $config->get('user_id'); 40 | $token = md5($time.$user_id.$config->get('secret')); 41 | $params = [ 42 | 'user_id' => $user_id, 43 | 'time' => $time, 44 | 'token' => $token, 45 | 'mobiles' => $to->getNumber(), // 手机号码,多个英文逗号隔开 46 | 'tpl_code' => $message->getTemplate($this), 47 | 'tpl_params' => $message->getData($this), 48 | 'sign_name' => $config->get('sign_name'), 49 | ]; 50 | 51 | $result = $this->post(self::ENDPOINT_URL, $params); 52 | 53 | if ('Success' != $result['Code']) { 54 | throw new GatewayErrorException($result['Message'], $result['Code'], $result); 55 | } 56 | 57 | return $result; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Message.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Overtrue\EasySms; 13 | 14 | use Overtrue\EasySms\Contracts\GatewayInterface; 15 | use Overtrue\EasySms\Contracts\MessageInterface; 16 | 17 | /** 18 | * Class Message. 19 | */ 20 | class Message implements MessageInterface 21 | { 22 | protected array $gateways = []; 23 | 24 | protected string $type; 25 | 26 | protected \Closure|string|null $content = null; 27 | 28 | protected \Closure|string|null $template = null; 29 | 30 | protected array|\Closure $data = []; 31 | 32 | /** 33 | * Message constructor. 34 | */ 35 | public function __construct(array $attributes = [], string $type = MessageInterface::TEXT_MESSAGE) 36 | { 37 | $this->type = $type; 38 | 39 | foreach ($attributes as $property => $value) { 40 | if (property_exists($this, $property)) { 41 | $this->$property = $value; 42 | } 43 | } 44 | } 45 | 46 | /** 47 | * Return the message type. 48 | */ 49 | public function getMessageType(): string 50 | { 51 | return $this->type; 52 | } 53 | 54 | /** 55 | * Return message content. 56 | */ 57 | public function getContent(?GatewayInterface $gateway = null): ?string 58 | { 59 | return is_callable($this->content) ? call_user_func($this->content, $gateway) : $this->content; 60 | } 61 | 62 | /** 63 | * Return the template id of message. 64 | */ 65 | public function getTemplate(?GatewayInterface $gateway = null): ?string 66 | { 67 | return is_callable($this->template) ? call_user_func($this->template, $gateway) : $this->template; 68 | } 69 | 70 | /** 71 | * @return $this 72 | */ 73 | public function setType($type): static 74 | { 75 | $this->type = $type; 76 | 77 | return $this; 78 | } 79 | 80 | /** 81 | * @return $this 82 | */ 83 | public function setContent($content): static 84 | { 85 | $this->content = $content; 86 | 87 | return $this; 88 | } 89 | 90 | /** 91 | * @return $this 92 | */ 93 | public function setTemplate($template): static 94 | { 95 | $this->template = $template; 96 | 97 | return $this; 98 | } 99 | 100 | public function getData(?GatewayInterface $gateway = null): array 101 | { 102 | return is_callable($this->data) ? call_user_func($this->data, $gateway) : $this->data; 103 | } 104 | 105 | /** 106 | * @return $this 107 | */ 108 | public function setData(callable|array $data): static 109 | { 110 | $this->data = $data; 111 | 112 | return $this; 113 | } 114 | 115 | public function getGateways(): array 116 | { 117 | return $this->gateways; 118 | } 119 | 120 | /** 121 | * @return $this 122 | */ 123 | public function setGateways(array $gateways): static 124 | { 125 | $this->gateways = $gateways; 126 | 127 | return $this; 128 | } 129 | 130 | /** 131 | * @return string 132 | */ 133 | public function __get($property) 134 | { 135 | if (property_exists($this, $property)) { 136 | return $this->$property; 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/Messenger.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Overtrue\EasySms; 13 | 14 | use Overtrue\EasySms\Contracts\MessageInterface; 15 | use Overtrue\EasySms\Contracts\PhoneNumberInterface; 16 | use Overtrue\EasySms\Exceptions\InvalidArgumentException; 17 | use Overtrue\EasySms\Exceptions\NoGatewayAvailableException; 18 | 19 | /** 20 | * Class Messenger. 21 | */ 22 | class Messenger 23 | { 24 | public const STATUS_SUCCESS = 'success'; 25 | 26 | public const STATUS_FAILURE = 'failure'; 27 | 28 | protected EasySms $easySms; 29 | 30 | /** 31 | * Messenger constructor. 32 | */ 33 | public function __construct(EasySms $easySms) 34 | { 35 | $this->easySms = $easySms; 36 | } 37 | 38 | /** 39 | * Send a message. 40 | * 41 | * @throws InvalidArgumentException 42 | * @throws NoGatewayAvailableException 43 | */ 44 | public function send(PhoneNumberInterface $to, MessageInterface $message, array $gateways = []): array 45 | { 46 | $results = []; 47 | $isSuccessful = false; 48 | 49 | foreach ($gateways as $gateway => $config) { 50 | try { 51 | $results[$gateway] = [ 52 | 'gateway' => $gateway, 53 | 'status' => self::STATUS_SUCCESS, 54 | 'template' => $message->getTemplate($this->easySms->gateway($gateway)), 55 | 'result' => $this->easySms->gateway($gateway)->send($to, $message, $config), 56 | ]; 57 | $isSuccessful = true; 58 | 59 | break; 60 | } catch (\Exception|\Throwable $e) { 61 | $results[$gateway] = [ 62 | 'gateway' => $gateway, 63 | 'status' => self::STATUS_FAILURE, 64 | 'template' => $message->getTemplate($this->easySms->gateway($gateway)), 65 | 'exception' => $e, 66 | ]; 67 | } 68 | } 69 | 70 | if (!$isSuccessful) { 71 | throw new NoGatewayAvailableException($results); 72 | } 73 | 74 | return $results; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/PhoneNumber.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Overtrue\EasySms; 13 | 14 | /** 15 | * Class PhoneNumberInterface. 16 | * 17 | * @author overtrue 18 | */ 19 | class PhoneNumber implements Contracts\PhoneNumberInterface 20 | { 21 | protected int|string $number; 22 | 23 | protected ?int $IDDCode; 24 | 25 | /** 26 | * PhoneNumberInterface constructor. 27 | */ 28 | public function __construct(int|string $numberWithoutIDDCode, ?string $IDDCode = null) 29 | { 30 | $this->number = $numberWithoutIDDCode; 31 | $this->IDDCode = $IDDCode ? intval(ltrim($IDDCode, '+0')) : null; 32 | } 33 | 34 | /** 35 | * 86. 36 | */ 37 | public function getIDDCode(): ?int 38 | { 39 | return $this->IDDCode; 40 | } 41 | 42 | /** 43 | * 18888888888. 44 | */ 45 | public function getNumber(): int|string 46 | { 47 | return $this->number; 48 | } 49 | 50 | /** 51 | * +8618888888888. 52 | */ 53 | public function getUniversalNumber(): string 54 | { 55 | return $this->getPrefixedIDDCode('+').$this->number; 56 | } 57 | 58 | /** 59 | * 008618888888888. 60 | */ 61 | public function getZeroPrefixedNumber(): string 62 | { 63 | return $this->getPrefixedIDDCode('00').$this->number; 64 | } 65 | 66 | public function getPrefixedIDDCode(string $prefix): ?string 67 | { 68 | return $this->IDDCode ? $prefix.$this->IDDCode : null; 69 | } 70 | 71 | /** 72 | * @return string 73 | */ 74 | public function __toString() 75 | { 76 | return $this->getUniversalNumber(); 77 | } 78 | 79 | /** 80 | * Specify data which should be serialized to JSON. 81 | * 82 | * @see http://php.net/manual/en/jsonserializable.jsonserialize.php 83 | * 84 | * @return mixed data which can be serialized by json_encode, 85 | * which is a value of any type other than a resource 86 | * 87 | * @since 5.4.0 88 | */ 89 | #[\ReturnTypeWillChange] 90 | public function jsonSerialize() 91 | { 92 | return $this->getUniversalNumber(); 93 | } 94 | 95 | /** 96 | * Check if the phone number belongs to chinese mainland. 97 | */ 98 | public function inChineseMainland(): bool 99 | { 100 | return empty($this->IDDCode) || 86 === $this->IDDCode; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/Strategies/OrderStrategy.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Overtrue\EasySms\Strategies; 13 | 14 | use Overtrue\EasySms\Contracts\StrategyInterface; 15 | 16 | /** 17 | * Class OrderStrategy. 18 | */ 19 | class OrderStrategy implements StrategyInterface 20 | { 21 | /** 22 | * Apply the strategy and return result. 23 | */ 24 | public function apply(array $gateways): array 25 | { 26 | return array_keys($gateways); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Strategies/RandomStrategy.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Overtrue\EasySms\Strategies; 13 | 14 | use Overtrue\EasySms\Contracts\StrategyInterface; 15 | 16 | /** 17 | * Class RandomStrategy. 18 | */ 19 | class RandomStrategy implements StrategyInterface 20 | { 21 | public function apply(array $gateways): array 22 | { 23 | try { 24 | $keys = \array_keys($gateways); 25 | $n = \count($keys); 26 | 27 | for ($i = $n - 1; $i > 0; --$i) { 28 | $j = \random_int(0, $i); 29 | [$keys[$i], $keys[$j]] = [$keys[$j], $keys[$i]]; 30 | } 31 | 32 | return $keys; 33 | } catch (\Throwable $exception) { 34 | $keys = \array_keys($gateways); 35 | shuffle($keys); 36 | 37 | return $keys; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Support/Config.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Overtrue\EasySms\Support; 13 | 14 | use ArrayAccess; 15 | 16 | /** 17 | * Class Config. 18 | */ 19 | class Config implements \ArrayAccess 20 | { 21 | protected array $config; 22 | 23 | /** 24 | * Config constructor. 25 | */ 26 | public function __construct(array $config = []) 27 | { 28 | $this->config = $config; 29 | } 30 | 31 | /** 32 | * Get an item from an array using "dot" notation. 33 | * 34 | * @return array|mixed|null 35 | */ 36 | public function get(string $key, mixed $default = null): mixed 37 | { 38 | $config = $this->config; 39 | 40 | if (isset($config[$key])) { 41 | return $config[$key]; 42 | } 43 | 44 | if (!str_contains($key, '.')) { 45 | return $default; 46 | } 47 | 48 | foreach (explode('.', $key) as $segment) { 49 | if (!is_array($config) || !array_key_exists($segment, $config)) { 50 | return $default; 51 | } 52 | $config = $config[$segment]; 53 | } 54 | 55 | return $config; 56 | } 57 | 58 | /** 59 | * Whether a offset exists. 60 | * 61 | * @see http://php.net/manual/en/arrayaccess.offsetexists.php 62 | * 63 | * @param mixed $offset

64 | * An offset to check for. 65 | *

66 | * 67 | * @return bool true on success or false on failure. 68 | *

69 | *

70 | * The return value will be casted to boolean if non-boolean was returned 71 | * 72 | * @since 5.0.0 73 | */ 74 | #[\ReturnTypeWillChange] 75 | public function offsetExists(mixed $offset): bool 76 | { 77 | return array_key_exists($offset, $this->config); 78 | } 79 | 80 | /** 81 | * Offset to retrieve. 82 | * 83 | * @see http://php.net/manual/en/arrayaccess.offsetget.php 84 | * 85 | * @param mixed $offset

86 | * The offset to retrieve. 87 | *

88 | * 89 | * @return mixed Can return all value types 90 | * 91 | * @since 5.0.0 92 | */ 93 | #[\ReturnTypeWillChange] 94 | public function offsetGet(mixed $offset): mixed 95 | { 96 | return $this->get($offset); 97 | } 98 | 99 | /** 100 | * Offset to set. 101 | * 102 | * @see http://php.net/manual/en/arrayaccess.offsetset.php 103 | * 104 | * @param mixed $offset

105 | * The offset to assign the value to. 106 | *

107 | * @param mixed $value

108 | * The value to set. 109 | *

110 | * 111 | * @since 5.0.0 112 | */ 113 | #[\ReturnTypeWillChange] 114 | public function offsetSet(mixed $offset, mixed $value): void 115 | { 116 | if (isset($this->config[$offset])) { 117 | $this->config[$offset] = $value; 118 | } 119 | } 120 | 121 | /** 122 | * Offset to unset. 123 | * 124 | * @see http://php.net/manual/en/arrayaccess.offsetunset.php 125 | * 126 | * @param mixed $offset

127 | * The offset to unset. 128 | *

129 | * 130 | * @since 5.0.0 131 | */ 132 | #[\ReturnTypeWillChange] 133 | public function offsetUnset(mixed $offset): void 134 | { 135 | if (isset($this->config[$offset])) { 136 | unset($this->config[$offset]); 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/Traits/HasHttpRequest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Overtrue\EasySms\Traits; 13 | 14 | use GuzzleHttp\Client; 15 | use Psr\Http\Message\ResponseInterface; 16 | 17 | /** 18 | * Trait HasHttpRequest. 19 | */ 20 | trait HasHttpRequest 21 | { 22 | /** 23 | * Make a get request. 24 | * 25 | * @param string $endpoint 26 | * @param array $query 27 | * @param array $headers 28 | * 29 | * @return ResponseInterface|array|string 30 | */ 31 | protected function get($endpoint, $query = [], $headers = []) 32 | { 33 | return $this->request('get', $endpoint, [ 34 | 'headers' => $headers, 35 | 'query' => $query, 36 | ]); 37 | } 38 | 39 | /** 40 | * Make a post request. 41 | * 42 | * @param string $endpoint 43 | * @param array $params 44 | * @param array $headers 45 | * 46 | * @return ResponseInterface|array|string 47 | */ 48 | protected function post($endpoint, $params = [], $headers = []) 49 | { 50 | return $this->request('post', $endpoint, [ 51 | 'headers' => $headers, 52 | 'form_params' => $params, 53 | ]); 54 | } 55 | 56 | /** 57 | * Make a post request with json params. 58 | * 59 | * @param array $params 60 | * @param array $headers 61 | * 62 | * @return ResponseInterface|array|string 63 | */ 64 | protected function postJson($endpoint, $params = [], $headers = []) 65 | { 66 | return $this->request('post', $endpoint, [ 67 | 'headers' => $headers, 68 | 'json' => $params, 69 | ]); 70 | } 71 | 72 | /** 73 | * Make a http request. 74 | * 75 | * @param string $method 76 | * @param string $endpoint 77 | * @param array $options http://docs.guzzlephp.org/en/latest/request-options.html 78 | * 79 | * @return ResponseInterface|array|string 80 | */ 81 | protected function request($method, $endpoint, $options = []) 82 | { 83 | return $this->unwrapResponse($this->getHttpClient($this->getBaseOptions())->{$method}($endpoint, $options)); 84 | } 85 | 86 | /** 87 | * Return base Guzzle options. 88 | * 89 | * @return array 90 | */ 91 | protected function getBaseOptions() 92 | { 93 | $options = method_exists($this, 'getGuzzleOptions') ? $this->getGuzzleOptions() : []; 94 | 95 | return \array_merge($options, [ 96 | 'base_uri' => method_exists($this, 'getBaseUri') ? $this->getBaseUri() : '', 97 | 'timeout' => method_exists($this, 'getTimeout') ? $this->getTimeout() : 5.0, 98 | ]); 99 | } 100 | 101 | /** 102 | * Return http client. 103 | * 104 | * @return Client 105 | * 106 | * @codeCoverageIgnore 107 | */ 108 | protected function getHttpClient(array $options = []) 109 | { 110 | return new Client($options); 111 | } 112 | 113 | /** 114 | * Convert response contents to json. 115 | * 116 | * @return ResponseInterface|array|string 117 | */ 118 | protected function unwrapResponse(ResponseInterface $response) 119 | { 120 | $contentType = $response->getHeaderLine('Content-Type'); 121 | $contents = $response->getBody()->getContents(); 122 | 123 | if (false !== stripos($contentType, 'json') || stripos($contentType, 'javascript')) { 124 | return json_decode($contents, true); 125 | } elseif (false !== stripos($contentType, 'xml')) { 126 | return json_decode(json_encode(simplexml_load_string($contents)), true); 127 | } 128 | 129 | return $contents; 130 | } 131 | } 132 | --------------------------------------------------------------------------------