diff --git a/.travis.yml b/.travis.yml index 4173b0b21afd5c08423be34103bc841b9b1cdc97..c64694a8bfd1dd22ab3c8e9f990d235b9aab844f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,6 +6,7 @@ php: - 7.1 - hhvm +dist: trusty sudo: false install: travis_retry composer install --no-interaction --prefer-source diff --git a/README.md b/README.md index a4de1b153640a29d90882ab760a6d4a6925f33eb..49937eec89588a9f40652459c94c5ea94613ec08 100644 --- a/README.md +++ b/README.md @@ -12,29 +12,58 @@ License

+## 特点 -# 环境需求 +1. 支持目前市面多家服务商 +1. 一套写法兼容所有平台 +1. 简单配置即可灵活增减服务商 +1. 内置多种服务商轮询策略、支持自定义轮询策略 +1. 统一的返回值格式,便于日志与监控 +1. 自动轮询选择可用的服务商 +1. 更多等你去发现与改进... + +## 平台支持 + +- [云片](https://www.yunpian.com) +- [Submail](https://www.mysubmail.com) +- [螺丝帽](https://luosimao.com/) +- [阿里大于](https://www.alidayu.com/) +- [容联云通讯](http://www.yuntongxun.com) +- [互亿无线](http://www.ihuyi.com) +- [聚合数据](https://www.juhe.cn) +- [SendCloud](http://www.sendcloud.net/) + + +## 环境需求 - PHP >= 5.6 -# 安装 +## 安装 ```shell $ composer require "overtrue/easy-sms" ``` -# 使用 +## 使用 ```php use Overtrue\EasySms\EasySms; $config = [ + // HTTP 请求的超时时间(秒) 'timeout' => 5.0, + + // 默认发送配置 'default' => [ + // 网关调用策略,默认:顺序调用 + 'strategy' => \Overtrue\EasySms\Strategies\OrderStrategy::class + + // 默认可用的发送网关 'gateways' => [ 'yunpian', 'alidayu', ], ], + // 可用的网关配置 'gateways' => [ 'errorlog' => [ 'file' => '/tmp/easy-sms.log', @@ -49,10 +78,58 @@ $config = [ ]; $easySms = new EasySms($config); -$easySms->send(13188888888, 'hello world!'); + +$easySms->send(13188888888, + 'content' => '您的验证码为: 6379', + 'template' => 'SMS_001', + 'data' => [ + 'code' => 6379 + ], + ]); ``` -# 定义短信 +## 短信内容 + +由于使用多网关发送,所以一条短信要支持多平台发送,每家的发送方式不一样,但是我们抽象定义了以下公用属性: + +- `content` 文字内容,使用在像云片类似的以文字内容发送的平台 +- `template` 模板 ID,使用在以模板ID来发送短信的平台 +- `data` 模板变量,使用在以模板ID来发送短信的平台 + +所以,在使用过程中你可以根据所要使用的平台定义发送的内容。 + +## 发送网关 + +默认使用 `default` 中的设置来发送,如果某一条短信你想要覆盖默认的设置。在 `send` 方法中使用第三个参数即可: + +```php +$easySms->send(13188888888, [ + 'content' => '您的验证码为: 6379', + 'template' => 'SMS_001', + 'data' => [ + 'code' => 6379 + ], + ], ['yunpian', 'juhe']); // 这里的网关配置将会覆盖全局默认值 +``` + +## 返回值 + +由于使用多网关发送,所以返回值为一个数组,结构如下: +```php +[ + 'yunpian' => [ + 'status' => 'success', + 'result' => [...] // 平台返回值 + ], + 'juhe' => [ + 'status' => 'erred', + 'exception' => \Overtrue\EasySms\Exceptions\GatewayErrorException 对象 + ], + //... +] +``` + +## 定义短信 你可以根本发送场景的不同,定义不同的短信类,从而实现一处定义多处调用,你可以继承 `Overtrue\EasySms\Message` 来定义短信模型: @@ -60,11 +137,14 @@ $easySms->send(13188888888, 'hello world!'); order->no); } // 定义使用模板发送方式平台所需要的模板 ID - public function getTemplate() + public function getTemplate(GatewayInterface $gateway = null) { return 'SMS_003'; } // 模板参数 - public function getData() + public function getData(GatewayInterface $gateway = null) { return [ 'order_no' => $this->order->no @@ -104,17 +184,81 @@ $message = new OrderPaidMessage($order); $easySms->send(13188888888, $message); ``` -# 平台支持 +## 各平台配置说明 + + + +### [阿里大于](https://www.alidayu.com/) + +```php + 'alidayu' => [ + 'app_key' => '', + 'sign_name' => '', + ], +``` + +### [云片](https://www.yunpian.com) -- [云片](https://github.com/overtrue/easy-sms/wiki/GateWays---Yunpian) -- [Submail](https://github.com/overtrue/easy-sms/wiki/GateWays---Submail) -- [螺丝帽](https://github.com/overtrue/easy-sms/wiki/GateWays---Luosimao) -- [阿里大鱼](https://github.com/overtrue/easy-sms/wiki/GateWays---AliDayu) -- [容联云通讯](https://github.com/overtrue/easy-sms/wiki/GateWays---Yuntongxun) -- [互亿无线](https://github.com/overtrue/easy-sms/wiki/GateWays---Huyi) -- [聚合数据](https://github.com/overtrue/easy-sms/wiki/GateWays---Juhe) -- SendCloud +```php + 'yunpian' => [ + 'api_key' => '', + ], +``` + +### [Submail](https://www.mysubmail.com) + +```php + 'submail' => [ + 'app_id' => '', + 'app_key' => '', + 'project' => '', + ], +``` + +### [螺丝帽](https://luosimao.com/) + +```php + 'luosimao' => [ + 'api_key' => '', + ], +``` + +### [容联云通讯](http://www.yuntongxun.com) + +```php + 'yuntongxun' => [ + 'app_id' => '', + 'account_sid' => '', + 'account_token' => '', + 'is_sub_account' => false, + ], +``` + +### [互亿无线](http://www.ihuyi.com) + +```php + 'huyi' => [ + 'api_id' => '', + ], +``` + +### [聚合数据](https://www.juhe.cn) + +```php + 'juhe' => [ + 'app_key' => '', + ], +``` + +### [SendCloud](http://www.sendcloud.net/) + +```php + 'sendcloud' => [ + 'sms_user' => '', + 'sms_key' => '', + ], +``` -# License +## License MIT diff --git a/src/Gateways/AlidayuGateway.php b/src/Gateways/AlidayuGateway.php index 93a7ac84662613908f7c057857dcfa514fbb35fc..006a58a1e4a3f175579311fc60cf54afead0b791 100644 --- a/src/Gateways/AlidayuGateway.php +++ b/src/Gateways/AlidayuGateway.php @@ -33,7 +33,7 @@ class AlidayuGateway extends Gateway * @param \Overtrue\EasySms\Contracts\MessageInterface $message * @param \Overtrue\EasySms\Support\Config $config * - * @return mixed + * @return array * * @throws \Overtrue\EasySms\Exceptions\GatewayErrorException; */ @@ -48,7 +48,7 @@ class AlidayuGateway extends Gateway 'sms_type' => 'normal', 'sms_free_sign_name' => $config->get('sign_name'), 'app_key' => $config->get('app_key'), - 'sms_template_code' => $config->get('template_code'), + 'sms_template_code' => $message->getTemplate(), 'rec_num' => strval($to), 'sms_param' => json_encode($message->getData()), ]; diff --git a/src/Gateways/ErrorlogGateway.php b/src/Gateways/ErrorlogGateway.php index 30439c7435519533ac90d71ab36ba25e235ef497..80d21911676b3ec9a13f56dd1ff9e72aa8691398 100644 --- a/src/Gateways/ErrorlogGateway.php +++ b/src/Gateways/ErrorlogGateway.php @@ -22,7 +22,7 @@ class ErrorlogGateway extends Gateway * @param \Overtrue\EasySms\Contracts\MessageInterface $message * @param \Overtrue\EasySms\Support\Config $config * - * @return bool + * @return array */ public function send($to, MessageInterface $message, Config $config) { @@ -39,6 +39,9 @@ class ErrorlogGateway extends Gateway json_encode($message->getData()) ); - return error_log($message, 3, $this->config->get('file', ini_get('error_log'))); + $file = $this->config->get('file', ini_get('error_log')); + $status = error_log($message, 3, $file); + + return compact('status', 'file'); } } diff --git a/src/Gateways/HuyiGateway.php b/src/Gateways/HuyiGateway.php index 86590b4f13c664fea3da6ed5455b775d667c3eae..aff5fa6db61b2792dd23906bbbc34dfcda096838 100644 --- a/src/Gateways/HuyiGateway.php +++ b/src/Gateways/HuyiGateway.php @@ -32,7 +32,7 @@ class HuyiGateway extends Gateway * @param \Overtrue\EasySms\Contracts\MessageInterface $message * @param \Overtrue\EasySms\Support\Config $config * - * @return mixed + * @return array * * @throws \Overtrue\EasySms\Exceptions\GatewayErrorException; */ diff --git a/src/Gateways/JuheGateway.php b/src/Gateways/JuheGateway.php index 3b7c0733ae0408d91b3923dadb49943fc267b216..f244d88a14699db3d03303c18e0e4f016c845f7b 100644 --- a/src/Gateways/JuheGateway.php +++ b/src/Gateways/JuheGateway.php @@ -14,6 +14,11 @@ use Overtrue\EasySms\Exceptions\GatewayErrorException; use Overtrue\EasySms\Support\Config; use Overtrue\EasySms\Traits\HasHttpRequest; +/** + * Class JuheGateway. + * + * @see https://www.juhe.cn/docs/api/id/54 + */ class JuheGateway extends Gateway { use HasHttpRequest; diff --git a/src/Gateways/LuosimaoGateway.php b/src/Gateways/LuosimaoGateway.php index b9c4134dfab9e95abdaa4c8724bf645fc86c540f..f8c4e8a698ec262528f05a9ac4f4725da8c487ad 100644 --- a/src/Gateways/LuosimaoGateway.php +++ b/src/Gateways/LuosimaoGateway.php @@ -32,7 +32,7 @@ class LuosimaoGateway extends Gateway * @param \Overtrue\EasySms\Contracts\MessageInterface $message * @param \Overtrue\EasySms\Support\Config $config * - * @return mixed + * @return array * * @throws \Overtrue\EasySms\Exceptions\GatewayErrorException; */ diff --git a/src/Gateways/SendcloudGateway.php b/src/Gateways/SendcloudGateway.php new file mode 100644 index 0000000000000000000000000000000000000000..84e662cbfc3fb9f2c08e04223495e7cb546129f8 --- /dev/null +++ b/src/Gateways/SendcloudGateway.php @@ -0,0 +1,88 @@ + + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Overtrue\EasySms\Gateways; + +use Overtrue\EasySms\Contracts\MessageInterface; +use Overtrue\EasySms\Exceptions\GatewayErrorException; +use Overtrue\EasySms\Support\Config; +use Overtrue\EasySms\Traits\HasHttpRequest; + +/** + * Class SendcloudGateway. + * + * @see http://sendcloud.sohu.com/doc/sms/ + */ +class SendcloudGateway extends Gateway +{ + use HasHttpRequest; + + const ENDPOINT_TEMPLATE = 'http://www.sendcloud.net/smsapi/%s'; + + /** + * Send a short message. + * + * @param int|string|array $to + * @param \Overtrue\EasySms\Contracts\MessageInterface $message + * @param \Overtrue\EasySms\Support\Config $config + * + * @return array + * + * @throws \Overtrue\EasySms\Exceptions\GatewayErrorException + */ + public function send($to, MessageInterface $message, Config $config) + { + $params = [ + 'smsUser' => $config->get('sms_user'), + 'templateId' => $message->getTemplate(), + 'phone' => is_array($to) ? join(',', $to) : $to, + 'vars' => $this->formatTemplateVars($message->getData()), + 'timestamp' => time(), + ]; + + $params['signature'] = $this->sign($params, $config->get('sms_key')); + + $result = $this->post(sprintf(self::ENDPOINT_TEMPLATE, 'send'), $params); + + if (!$result['result']) { + throw new GatewayErrorException($result['message'], $result['statusCode'], $result); + } + + return $result; + } + + /** + * @param array $vars + * + * @return string + */ + protected function formatTemplateVars(array $vars) + { + $formatted = []; + + foreach ($vars as $key => $value) { + $formatted[sprintf('%%%s%%', trim($key, '%'))] = $value; + } + + return json_encode($formatted); + } + + /** + * @param array $params + * @param string $key + * + * @return string + */ + protected function sign($params, $key) + { + ksort($params); + + return md5(sprintf('%s&%s&%s', $key, urldecode(http_build_query($params)), $key)); + } +} diff --git a/src/Gateways/SubmailGateway.php b/src/Gateways/SubmailGateway.php index 5442cc95978a7d00d51eb85f9dc73f85c6805aa5..f2e8b339ccadbc43378ff5c908154f75b0fa5be6 100644 --- a/src/Gateways/SubmailGateway.php +++ b/src/Gateways/SubmailGateway.php @@ -31,7 +31,7 @@ class SubmailGateway extends Gateway * @param \Overtrue\EasySms\Contracts\MessageInterface $message * @param \Overtrue\EasySms\Support\Config $config * - * @return mixed + * @return array * * @throws \Overtrue\EasySms\Exceptions\GatewayErrorException; */ diff --git a/src/Gateways/YuntongxunGateway.php b/src/Gateways/YuntongxunGateway.php index 81acd72d86a5d7bd1cf00a05160bb3a380bc5af9..69fb0aa3aab7ecbdb24ac5439e651c5e8d5905d7 100644 --- a/src/Gateways/YuntongxunGateway.php +++ b/src/Gateways/YuntongxunGateway.php @@ -36,7 +36,7 @@ class YuntongxunGateway extends Gateway * @param \Overtrue\EasySms\Contracts\MessageInterface $message * @param \Overtrue\EasySms\Support\Config $config * - * @return mixed + * @return array * * @throws \Overtrue\EasySms\Exceptions\GatewayErrorException; */ @@ -44,7 +44,7 @@ class YuntongxunGateway extends Gateway { $datetime = date('YmdHis'); - $endpoint = $this->buildEndpoint('SMS', 'TemplateSMS', $datetime); + $endpoint = $this->buildEndpoint('SMS', 'TemplateSMS', $datetime, $config); $result = $this->request('post', $endpoint, [ 'json' => [ @@ -70,20 +70,21 @@ class YuntongxunGateway extends Gateway /** * Build endpoint url. * - * @param string $type - * @param string $resource - * @param string $datetime + * @param string $type + * @param string $resource + * @param string $datetime + * @param \Overtrue\EasySms\Support\Config $config * * @return string */ - protected function buildEndpoint($type, $resource, $datetime) + protected function buildEndpoint($type, $resource, $datetime, Config $config) { $serverIp = $this->config->get('debug') ? self::DEBUG_SERVER_IP : self::SERVER_IP; $accountType = $this->config->get('is_sub_account') ? 'SubAccounts' : 'Accounts'; - $sig = strtoupper(md5($this->config->get('account_sid').$this->config->get('account_token').$datetime)); + $sig = strtoupper(md5($config->get('account_sid').$config->get('account_token').$datetime)); - return sprintf(self::ENDPOINT_TEMPLATE, $serverIp, self::SERVER_PORT, self::SDK_VERSION, $accountType, $this->config->get('account_sid'), $type, $resource, $sig); + return sprintf(self::ENDPOINT_TEMPLATE, $serverIp, self::SERVER_PORT, self::SDK_VERSION, $accountType, $config->get('account_sid'), $type, $resource, $sig); } } diff --git a/src/Messenger.php b/src/Messenger.php index 12537c28147dd3f7fe9778972c291ca27832995a..e11edc876d9e28ff8780648ea392f95651d1cc4f 100644 --- a/src/Messenger.php +++ b/src/Messenger.php @@ -39,9 +39,9 @@ class Messenger /** * Send a message. * - * @param string|array $to + * @param string|array $to * @param string|array|\Overtrue\EasySms\Contracts\MessageInterface $message - * @param array $gateways + * @param array $gateways * * @return array */ @@ -117,6 +117,7 @@ class Messenger $setting = []; } + $formatted[$gateway] = $setting; $globalSetting = $config->get("gateways.{$gateway}", []); if (is_string($gateway) && !empty($globalSetting) && is_array($setting)) { diff --git a/tests/Gateways/AlidayuGatewayTest.php b/tests/Gateways/AlidayuGatewayTest.php index dd3ab2353c9d32aa424475b90124a5b164ce947d..a218460cb85090d37383041b6a877359b683af20 100644 --- a/tests/Gateways/AlidayuGatewayTest.php +++ b/tests/Gateways/AlidayuGatewayTest.php @@ -27,7 +27,7 @@ class AlidayuGatewayTest extends TestCase ]; $gateway = \Mockery::mock(AlidayuGateway::class.'[post]', [$config])->shouldAllowMockingProtectedMethods(); - $params = [ + $expected = [ 'method' => 'alibaba.aliqin.fc.sms.num.send', 'format' => 'json', 'v' => '2.0', @@ -39,15 +39,28 @@ class AlidayuGatewayTest extends TestCase 'rec_num' => strval(18888888888), 'sms_param' => json_encode(['code' => '123456', 'time' => '15']), ]; - $gateway->shouldReceive('post')->with('https://eco.taobao.com/router/rest', \Mockery::subset($params)) - ->andReturn([ - 'success_response' => 'mock-result', - ], [ - 'error_response' => ['sub_msg' => 'mock-msg', 'code' => 100], - ]) - ->twice(); + $gateway->shouldReceive('post') + ->with('https://eco.taobao.com/router/rest', \Mockery::on(function ($params) use ($expected) { + if (empty($params['timestamp']) || empty($params['sign'])) { + return false; + } + + unset($params['timestamp'], $params['sign']); + + ksort($params); + ksort($expected); + + return $params == $expected; + })) + ->andReturn([ + 'success_response' => 'mock-result', + ], [ + 'error_response' => ['sub_msg' => 'mock-msg', 'code' => 100], + ]) + ->twice(); $message = new Message([ + 'template' => 'mock-template-code', 'data' => ['code' => '123456', 'time' => '15'], ]); $config = new Config($config); diff --git a/tests/Gateways/SendcloudGatewayTest.php b/tests/Gateways/SendcloudGatewayTest.php new file mode 100644 index 0000000000000000000000000000000000000000..cd98b2d10a00c1d32b2784b78273efdf02743b11 --- /dev/null +++ b/tests/Gateways/SendcloudGatewayTest.php @@ -0,0 +1,86 @@ + + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Overtrue\EasySms\Tests\Gateways; + +use Overtrue\EasySms\Exceptions\GatewayErrorException; +use Overtrue\EasySms\Gateways\SendcloudGateway; +use Overtrue\EasySms\Message; +use Overtrue\EasySms\Support\Config; +use Overtrue\EasySms\Tests\TestCase; + +class SendcloudGatewayTest extends TestCase +{ + public function testSend() + { + $config = [ + 'sms_user' => 'mock-user', + 'sms_key' => 'mock-key', + ]; + $gateway = \Mockery::mock(SendcloudGateway::class.'[post]', [$config])->shouldAllowMockingProtectedMethods(); + + $expected = [ + 'smsUser' => 'mock-user', + 'templateId' => 'mock-tpl-id', + 'phone' => 18188888888, + 'vars' => json_encode(['%code%' => 1234]), + ]; + $gateway->shouldReceive('post') + ->with(sprintf(SendcloudGateway::ENDPOINT_TEMPLATE, 'send'), \Mockery::on(function ($params) use ($expected, $config) { + $expected['timestamp'] = $params['timestamp']; + ksort($expected); + $signString = []; + foreach ($expected as $key => $value) { + $signString[] = "{$key}={$value}"; + } + $signString = join('&', $signString); + + $expectedSignature = md5("{$config['sms_key']}&{$signString}&{$config['sms_key']}"); + + return $params['smsUser'] == $expected['smsUser'] + && $params['templateId'] == $expected['templateId'] + && $params['phone'] == $expected['phone'] + && $params['vars'] == $expected['vars'] + && $params['timestamp'] >= time() + && $params['signature'] == $expectedSignature + ; + })) + ->andReturn([ + 'message' => '操作成功', + 'result' => true, + 'statusCode' => 200, + ], [ + 'message' => '手机号不存在', + 'result' => false, + 'statusCode' => 400, + ])->times(2); + + $message = new Message([ + 'content' => 'This is a huyi test message.', + 'template' => 'mock-tpl-id', + 'data' => [ + 'code' => 1234, + ], + ]); + + $config = new Config($config); + + $this->assertSame([ + 'message' => '操作成功', + 'result' => true, + 'statusCode' => 200, + ], $gateway->send(18188888888, $message, $config)); + + $this->expectException(GatewayErrorException::class); + $this->expectExceptionCode(400); + $this->expectExceptionMessage('手机号不存在'); + + $gateway->send(18188888888, $message, $config); + } +}