├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── composer.json ├── phpunit.xml ├── src ├── Yunpian.php └── helpers │ └── ConstDocHelper.php └── tests ├── YunpianTest.php ├── bootstrap.php ├── helpers └── ConstDocHelperTest.php └── yii.php /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | composer.lock 3 | .idea 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.4 5 | 6 | install: 7 | - composer global require "fxp/composer-asset-plugin:1.0.0" 8 | - composer install 9 | 10 | script: 11 | - ./vendor/bin/phpunit 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 BOB CHENGBIN 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 | Yunpian-sdk for YII2 2 | ==================== 3 | 4 | 云片还未出 PHP 的 SDK 支持,只提供了接口,现阶段正在使用 YII2 做开发,所以就把相应的组件共享出来,给需要的人用。 5 | 6 | [![Build Status](https://travis-ci.org/dcb9/yii2-yunpian.svg?branch=master)](https://travis-ci.org/dcb9/yii2-yunpian)[![Latest Stable Version](https://poser.pugx.org/dcb9/yii2-yunpian/v/stable.svg)](https://packagist.org/packages/dcb9/yii2-yunpian) [![Total Downloads](https://poser.pugx.org/dcb9/yii2-yunpian/downloads.svg)](https://packagist.org/packages/dcb9/yii2-yunpian) [![Latest Unstable Version](https://poser.pugx.org/dcb9/yii2-yunpian/v/unstable.svg)](https://packagist.org/packages/dcb9/yii2-yunpian) [![License](https://poser.pugx.org/dcb9/yii2-yunpian/license.svg)](https://packagist.org/packages/dcb9/yii2-yunpian) 7 | 8 | ## Install 9 | 10 | add `dcb9/yii2-yunpian` to composer.json 11 | 12 | ``` 13 | $ composer update 14 | ``` 15 | 16 | OR 17 | 18 | ``` 19 | $ composer require dcb9/yii2-yunpian 20 | ``` 21 | 22 | ## Configurtion 23 | 24 | ```php 25 | \# file app/config/main.php 26 | [ 30 | 'yunpian' => [ 31 | 'class' => 'dcb9\Yunpian\sdk\Yunpian', 32 | 'apiKey' => 'your yunpian apiKey', 33 | // 'useFileTransport' => false, // 如果该值为 true 则不会真正的发送短信,而是把内容写到文件里面,测试环境经常需要用到! 34 | ], 35 | ], 36 | ]; 37 | ``` 38 | 39 | ## Usage 40 | 41 | ```php 42 | $phone = '01234567890'; 43 | // $phone = ['01234567890']; # 可以为数组 44 | // $phone = '12345678900,01234567890'; # 还可以号码与号码之间用空格隔开 45 | $text ='sms content'; 46 | $sms = Yii::$app->yunpian; 47 | if($sms->sendSms($phone, $text)) 48 | { 49 | $responseBody = $sms->getBody(); 50 | # ["code"=>0, "msg"=>"OK", "result" => ["count" => 1, "fee" => 1, "sid" => 3995844410]] 51 | } elseif ($sms->hasError()) { 52 | $error = $sms->getLastError() 53 | # ["code" => 2, "msg" => "请求参数格式错误", "detail" => "参数 text 格式不正确,text短信内容头部需要加签名,如【云片网】"] 54 | } 55 | ``` 56 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dcb9/yii2-yunpian", 3 | "description": "yunpian sdk for yii2", 4 | "keywords": ["yii2", "yunpian", "sdk"], 5 | "homepage": "https://github.com/dcb9/yii2-yunpian", 6 | "type": "library", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "dcb9", 11 | "email": "bob@phpor.me", 12 | "homepage": "http://blog.phpor.me", 13 | "role": "Creator" 14 | } 15 | ], 16 | "support": { 17 | "issues": "https://github.com/dcb9/yii2-yunpian/issues", 18 | "source": "https://github.com/dcb9/yii2-yunpian" 19 | }, 20 | "require": { 21 | "php": ">=5.4.0", 22 | "yiisoft/yii2": "2.0.*", 23 | "guzzlehttp/guzzle": "~5.0" 24 | }, 25 | "require-dev": { 26 | "phpunit/phpunit": "3.7.*" 27 | }, 28 | "autoload": { 29 | "psr-4": { 30 | "dcb9\\Yunpian\\sdk\\": "src" 31 | } 32 | }, 33 | "extra": { 34 | "branch-alias": { 35 | "dev-master": "0.0.x-dev" 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | ./tests/ 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/Yunpian.php: -------------------------------------------------------------------------------- 1 | lastError; 201 | $this->lastError = null; 202 | 203 | return $lastError; 204 | } 205 | 206 | /** 207 | * @throws InvalidConfigException 208 | */ 209 | public function init() 210 | { 211 | if ($this->apiKey === null) { 212 | throw new InvalidConfigException('The apiKey property must be set.'); 213 | } 214 | $this->urlFormat = self::YUNPIAN_URL_DOMAIN 215 | . '/' 216 | . $this->version 217 | . '/%s/%s.' 218 | . $this->format 219 | . '/?apikey=' 220 | . $this->apiKey; 221 | } 222 | 223 | /** 224 | * @return string the file name for saving the message when [[useFileTransport]] is true. 225 | */ 226 | public function generateMessageFileName() 227 | { 228 | $time = microtime(true); 229 | 230 | return date('Ymd-His-', $time) . sprintf('%04d', (int)(($time - (int)$time) * 10000)) . '-' . sprintf('%04d', 231 | mt_rand(0, 10000)) . '.txt'; 232 | } 233 | 234 | /** 235 | * @param $mobile 236 | * @param $text 237 | * @return bool 238 | */ 239 | public function saveSms($mobile, $text) 240 | { 241 | $path = Yii::getAlias($this->fileTransportPath); 242 | if (!is_dir(($path))) { 243 | mkdir($path, 0777, true); 244 | } 245 | 246 | if ($this->fileTransportCallback !== null) { 247 | $file = $path . '/' . call_user_func($this->fileTransportCallback, $mobile, $text); 248 | } else { 249 | $file = $path . '/' . $this->generateMessageFileName(); 250 | } 251 | 252 | $mobile = is_array($mobile) ? implode(',', $mobile) : $mobile; 253 | $content = sprintf("mobile: %s\n\rtext:%s", $mobile, $text); 254 | file_put_contents($file, $content); 255 | 256 | return true; 257 | } 258 | 259 | public function hasError() 260 | { 261 | return $this->lastError != null; 262 | } 263 | 264 | protected function setError($msg, $code = null, $detail = '') 265 | { 266 | $this->lastError = compact('msg', 'code', 'detail'); 267 | Yii::error($msg, self::LOG_CATEGORY); 268 | } 269 | 270 | /** 271 | * 发送短信 272 | * 273 | * 群发时最多只能发 100 条,所以我们这里最多发 90 条,多出来的用回调解决 274 | * 275 | * @param string|array $mobile 276 | * @param string $text 277 | * @return array|boolean 278 | */ 279 | public function sendSms($mobile, $text) 280 | { 281 | if ($this->useFileTransport) { 282 | return $this->saveSms($mobile, $text); 283 | } 284 | 285 | if (!is_array($mobile)) { 286 | $mobile = explode(',', $mobile); 287 | } 288 | 289 | if (empty($mobile)) { 290 | $this->setError("手机号不得为空"); 291 | 292 | return false; 293 | } 294 | 295 | $mobileArr = array_chunk($mobile, 90); 296 | $mobile = array_pop($mobileArr); 297 | foreach ($mobileArr as $val) { 298 | self::sendSms($val, $text); 299 | } 300 | 301 | $mobile = array_filter($mobile, function ($val) { 302 | $isPhoneNumber = strlen($val) === 11; 303 | if ($isPhoneNumber) { 304 | return true; 305 | } else { 306 | $this->setError("Error phone number: " . $val); 307 | 308 | return false; 309 | } 310 | }); 311 | 312 | $body = ['mobile' => implode(',', $mobile), 'text' => $text,]; 313 | $url = sprintf($this->urlFormat, self::RESOURCE_SMS, self::FUNCTION_SEND); 314 | 315 | if (($body = $this->post($url, $body)) === false) { 316 | return false; 317 | } 318 | 319 | $code = $body['code']; 320 | if ($code != self::CODE_0) { 321 | $n = new ConstDocHelper(__CLASS__); 322 | if ($code < 0) { 323 | $const = 'CODE_N' . (-$code); 324 | } else { 325 | $const = 'CODE_' . $code; 326 | } 327 | $this->setError($n->getDocComment($const), $code, $body['detail']); 328 | 329 | return false; 330 | } 331 | $this->body = $body; 332 | 333 | return true; 334 | } 335 | 336 | protected $body; 337 | 338 | public function getBody() 339 | { 340 | return $this->body; 341 | } 342 | 343 | private function post($url, array $body) 344 | { 345 | 346 | $client = new Client(); 347 | $options = [ 348 | 'body' => [ 349 | 'apikey' => $this->apiKey, 350 | ] + $body, 351 | 'headers' => [ 352 | 'Accept' => 'text/plain;charset=utf-8;', 353 | 'Content-Type' => 'application/x-www-form-urlencoded;charset=utf-8;' 354 | ] 355 | ]; 356 | try { 357 | $response = $client->post($url, $options); 358 | } catch (RequestException $e) { 359 | $message = $e->getRequest() . "\n"; 360 | if ($e->hasResponse()) { 361 | $message .= $e->getResponse() . "\n"; 362 | } 363 | $this->setError($message); 364 | 365 | return false; 366 | } 367 | 368 | if ($response->getStatusCode() == 200) { 369 | 370 | switch ($this->format) { 371 | case 'json': 372 | $body = json_decode($response->getBody()->getContents(), true); 373 | break; 374 | default: 375 | return false; 376 | break; 377 | } 378 | 379 | return $body; 380 | } else { 381 | $this->setError("http request status code is not 200"); 382 | 383 | return false; 384 | } 385 | } 386 | } 387 | -------------------------------------------------------------------------------- /src/helpers/ConstDocHelper.php: -------------------------------------------------------------------------------- 1 | parse(new \ReflectionClass($clazz)); 14 | } 15 | 16 | /** Parses the class for constant DocComments. */ 17 | private function parse(\ReflectionClass $clazz) 18 | { 19 | $content = file_get_contents($clazz->getFileName()); 20 | $tokens = token_get_all($content); 21 | 22 | $doc = null; 23 | $isConst = false; 24 | foreach ($tokens as $token) { 25 | @ list($tokenType, $tokenValue) = $token; 26 | 27 | switch ($tokenType) { 28 | // ignored tokens 29 | case T_WHITESPACE: 30 | case T_COMMENT: 31 | break; 32 | 33 | case T_DOC_COMMENT: 34 | $doc = $tokenValue; 35 | break; 36 | 37 | case T_CONST: 38 | $isConst = true; 39 | break; 40 | 41 | case T_STRING: 42 | if ($isConst) { 43 | $this->docComments[$tokenValue] = self::clean($doc); 44 | } 45 | $doc = null; 46 | $isConst = false; 47 | break; 48 | 49 | // all other tokens reset the parser 50 | default: 51 | $doc = null; 52 | $isConst = false; 53 | break; 54 | } 55 | } 56 | 57 | } 58 | 59 | /** Returns an array of all constants to their DocComment. If no comment is present the comment is null. */ 60 | public function getDocComments() 61 | { 62 | return $this->docComments; 63 | } 64 | 65 | /** Returns the DocComment of a class constant. Null if the constant has no DocComment or the constant does not exist. */ 66 | public function getDocComment($constantName) 67 | { 68 | if (!isset($this->docComments)) { 69 | return null; 70 | } 71 | 72 | return $this->docComments[$constantName]; 73 | } 74 | 75 | /** Cleans the doc comment. Returns null if the doc comment is null. */ 76 | private static function clean($doc) 77 | { 78 | if ($doc === null) { 79 | return null; 80 | } 81 | 82 | $result = null; 83 | $lines = preg_split('/\r/', $doc); 84 | foreach ($lines as $line) { 85 | 86 | $line = preg_replace('/^\s*\* /', '', trim($line, "\n/* \t\x0B\0")); 87 | if ($line === '') { 88 | continue; 89 | } 90 | 91 | if ($result != null) { 92 | $result .= ' '; 93 | } 94 | $result .= $line; 95 | } 96 | 97 | return $result; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /tests/YunpianTest.php: -------------------------------------------------------------------------------- 1 | component = Yii::createObject([ 15 | 'class' => Yunpian::className(), 16 | 'apiKey' => 'not_exist_key', 17 | ]); 18 | } 19 | 20 | public function testFileTransport() 21 | { 22 | $component = $this->component; 23 | $component->useFileTransport = true; 24 | $component->fileTransportPath = __DIR__; 25 | $fileName = '20150907-190215-6418-4977.txt'; 26 | $component->fileTransportCallback = function ($mobile, $text) use ($fileName) { 27 | return $fileName; 28 | }; 29 | 30 | $this->assertTrue($this->component->sendSms('01234567890', 'test content')); 31 | 32 | $realFile = $component->fileTransportPath . '/' . $fileName; 33 | $this->assertTrue(file_exists($realFile)); 34 | unlink($realFile); 35 | } 36 | 37 | public function testLastError() 38 | { 39 | $sms = $this->component; 40 | $sms->useFileTransport = false; 41 | if (!$sms->sendSms('01234567890', 'test content') && $sms->hasError()) { 42 | $error = $sms->getLastError(); 43 | $this->assertTrue(isset($error['code'])); 44 | $this->assertTrue(isset($error['msg'])); 45 | $this->assertTrue(isset($error['detail'])); 46 | } 47 | } 48 | 49 | /** 50 | * @expectedException yii\base\InvalidConfigException 51 | */ 52 | public function testInvalidConfigException() 53 | { 54 | Yii::createObject([ 55 | 'class' => Yunpian::className(), 56 | ]); 57 | } 58 | } -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | constDocHelper = new ConstDocHelper(__CLASS__); 25 | } 26 | 27 | public function testGetDocComment() 28 | { 29 | $this->assertEquals('测试常量1', $this->constDocHelper->getDocComment('TEST_CONSTANT_1')); 30 | $this->assertEquals('测试常量2', $this->constDocHelper->getDocComment('TEST_CONSTANT_2')); 31 | } 32 | } -------------------------------------------------------------------------------- /tests/yii.php: -------------------------------------------------------------------------------- 1 | 'yunpian-tester', 5 | 'basePath' => dirname(__DIR__), 6 | 'bootstrap' => [], 7 | 'components' => [ 8 | ], 9 | 'params' => [], 10 | ]; 11 | --------------------------------------------------------------------------------