├── .gitignore ├── composer.json ├── readme.md └── src ├── Apis ├── Api.php ├── ApiRequest.php ├── MiniApi.php ├── Mp │ ├── AccessTokenApiRequest.php │ ├── ImageMessageRequest.php │ ├── MaterialAddMaterialApiRequest.php │ ├── MaterialAddNewsApiRequest.php │ ├── MaterialBatchgetMaterialApiRequest.php │ ├── MaterialDelMaterialApiRequest.php │ ├── MaterialGetMaterialApiRequest.php │ ├── MaterialGetMaterialCountApiRequest.php │ ├── MaterialUpdateNewsApiRequest.php │ ├── MediaGetApiRequest.php │ ├── MediaGetJssdkApiRequest.php │ ├── MediaUploadApiRequest.php │ ├── MediaUploadimgApiRequest.php │ ├── MenuCreateApiRequest.php │ ├── MenuDeleteApiRequest.php │ ├── MenuGetApiRequest.php │ ├── MessageCustomSend.php │ ├── MusicMessageRequest.php │ ├── NewsMessageRequest.php │ ├── QrcodeCreateApiRequest.php │ ├── TemplateApiRequest.php │ ├── TextMessageRequest.php │ ├── TicketGetTicketApiRequest.php │ ├── UserGetApiRequest.php │ ├── UserInfoApiRequest.php │ ├── VideoMessageRequest.php │ └── VoiceMessageRequest.php ├── MpApi.php ├── ThirdApi.php ├── Web │ ├── SnsAuthApiRequest.php │ ├── SnsOauthAccessTokenApiRequest.php │ ├── SnsOauthRefreshTokenApiRequest.php │ └── SnsUserInfoApiRequest.php └── WebApi.php ├── Cache ├── Cache.php ├── File.php └── Store.php ├── Exception ├── ApiException.php ├── ParamsException.php ├── TokenException.php └── WeixinException.php ├── Helper └── Menu.php ├── Message ├── Responser.php ├── Result.php └── SHA1.php ├── Request └── Request.php ├── Test ├── AccessTokenTest.php ├── MpApiTest.php └── test_config.php ├── Token ├── AccessTokenCreator.php ├── OAuthTokenCreator.php └── TokenInterface.php ├── Weixin.php ├── WeixinJsSignaturePack.php └── config.php /.gitignore: -------------------------------------------------------------------------------- 1 | /nbproject 2 | *.cache 3 | /demo 4 | /.idea 5 | vendor 6 | composer.lock 7 | /Test/test_config.php -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vrobin/weixin", 3 | "type": "library", 4 | "description": "Weixin SDK", 5 | "keywords": [ 6 | "weixin" 7 | ], 8 | "homepage": "https://github.com/health901/Weixin", 9 | "license": "MIT", 10 | "version": "v3.0.5", 11 | "authors": [ 12 | { 13 | "name": "health901" 14 | } 15 | ], 16 | "require": { 17 | "php": ">=7.0", 18 | "ext-json": "*", 19 | "ext-curl": "*", 20 | "ext-fileinfo": "*" 21 | }, 22 | "autoload": { 23 | "psr-4": { 24 | "VRobin\\Weixin\\": "src/" 25 | } 26 | }, 27 | "require-dev": { 28 | "phpunit/phpunit": "^9.5" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | Weixin SDK 2 | ====== 3 | 微信 公众平台 SDK (订阅号/服务号) 4 | 5 | * 事件驱动 6 | * 单例模式 7 | * IDE友好 (自动提示,补全) 8 | 9 | 10 | ##订阅号 11 | * 用户消息响应接口 12 | 13 | 14 | ##服务号 15 | * 用户消息响应接口 16 | * 客服消息接口 17 | * 自定义菜单接口 18 | * 用户管理接口 19 | * 二维码接口 20 | 21 | 22 | ##文档 23 | 见demo 24 | 25 | ##测试账号 26 | ![QR](http://mmbiz.qpic.cn/mmbiz/7YOXna1VLtwmgqEf41BuBUmTJHmnAuMotiatfAtvcR4FfvIYuwDkKedefkWicTEdsERmJXuuAHu8qNmdb9HB31mw/0) 27 | 28 | ##.htaccess文件说明 29 | `SetEnv WEIXIN_NO_SIGNATURE 1` : 取消该行注释,接收消息时将不验证签名,用于调试. 30 | -------------------------------------------------------------------------------- /src/Apis/Api.php: -------------------------------------------------------------------------------- 1 | appid = $appid; 29 | $this->secret = $secret; 30 | } 31 | 32 | protected function getTokenCreator($type = null) 33 | { 34 | $type = $type ? $type : $this->tokenType; 35 | $class = Weixin::$config['token'][$type]; 36 | return new $class($this->appid, $this->secret); 37 | } 38 | 39 | 40 | /** 41 | * @param ApiRequest $api 42 | * @return string 43 | * @throws TokenException 44 | * @throws ApiException 45 | */ 46 | public function sendRequest(ApiRequest $api) 47 | { 48 | $api->apiUrl = $this->apiUrl; 49 | if ($api->isNeedToken()) { 50 | $this->accessToken = $this->getTokenCreator($api->getTokenType())->getToken(); 51 | if (!$this->accessToken) { 52 | throw new TokenException("Cannot get accessToken"); 53 | } 54 | $api->setAccessToken($this->accessToken); 55 | } 56 | return $this->request($api->getApi(), $api->getData(), $api->getMethod(), $api->returnRaw()); 57 | } 58 | 59 | /** 60 | * 别名 61 | * @param ApiRequest $api 62 | * @return string 63 | * @throws ApiException 64 | * @throws TokenException 65 | */ 66 | public function call(ApiRequest $api) 67 | { 68 | return $this->sendRequest($api); 69 | } 70 | 71 | /** 72 | * 73 | * @param string $url 74 | * @param array $data 75 | * @param string $method 76 | * @param bool $raw 77 | * @return mixed 78 | * @throws ApiException 79 | */ 80 | public function request(string $url, $data = array(), $method = 'get', $raw = false) 81 | { 82 | 83 | if ($method == 'post') { 84 | $result = $this->post($url, $data); 85 | } else { 86 | $result = $this->get($url, $data); 87 | } 88 | $data = json_decode($result, true); 89 | if (isset($data['errcode']) && $data['errcode']) { 90 | throw new ApiException($data['errcode'] . ':' . $data['errmsg'], $data['errcode']); 91 | } 92 | return $raw ? $result : $data; 93 | } 94 | 95 | protected function get($api, $data) 96 | { 97 | return Request::get($api, $data); 98 | } 99 | 100 | protected function post($api, $data) 101 | { 102 | return Request::post($api, $data); 103 | } 104 | } -------------------------------------------------------------------------------- /src/Apis/ApiRequest.php: -------------------------------------------------------------------------------- 1 | data[$name] = $value; 32 | } 33 | 34 | protected function postJson() 35 | { 36 | return $this->method == 'GET' ? false : ($this->data ? $this->postJson : false); 37 | } 38 | 39 | public function getData() 40 | { 41 | return $this->postJson() ? json_encode($this->data, JSON_UNESCAPED_UNICODE) : $this->data; 42 | } 43 | 44 | public function isNeedToken() 45 | { 46 | return $this->needToken; 47 | } 48 | 49 | public function getTokenType() 50 | { 51 | return $this->tokenType; 52 | } 53 | 54 | public function setAccessToken($token) 55 | { 56 | $this->accessToken = $token; 57 | } 58 | 59 | public function returnRaw() 60 | { 61 | return $this->returnRaw; 62 | } 63 | 64 | public function getApi() 65 | { 66 | return $this->apiUrl(); 67 | } 68 | 69 | public function getMethod() 70 | { 71 | return strtolower($this->method); 72 | } 73 | 74 | protected function apiUrl() 75 | { 76 | $url = $this->apiUrl . $this->api; 77 | if ($this->accessToken) { 78 | $this->queryData['access_token'] = $this->accessToken; 79 | } 80 | if ($this->queryData) { 81 | $url .= '?' . http_build_query($this->queryData); 82 | } 83 | return $url; 84 | } 85 | } -------------------------------------------------------------------------------- /src/Apis/MiniApi.php: -------------------------------------------------------------------------------- 1 | data['grant_type'] = 'client_credential'; 17 | } 18 | 19 | public function setAppid($appid) 20 | { 21 | $this->data['appid'] = $appid; 22 | } 23 | 24 | public function setSecret($secret) 25 | { 26 | $this->data['secret'] = $secret; 27 | } 28 | } -------------------------------------------------------------------------------- /src/Apis/Mp/ImageMessageRequest.php: -------------------------------------------------------------------------------- 1 | data['image']['media_id'] = $media_id; 14 | } 15 | } -------------------------------------------------------------------------------- /src/Apis/Mp/MaterialAddMaterialApiRequest.php: -------------------------------------------------------------------------------- 1 | data['type'] = $type; 21 | } 22 | 23 | public function setMediaFile($file) 24 | { 25 | $this->data['@media'] = $file; 26 | } 27 | } -------------------------------------------------------------------------------- /src/Apis/Mp/MaterialAddNewsApiRequest.php: -------------------------------------------------------------------------------- 1 | data['articles'] = $articles; 24 | } 25 | } -------------------------------------------------------------------------------- /src/Apis/Mp/MaterialBatchgetMaterialApiRequest.php: -------------------------------------------------------------------------------- 1 | data['offset'] = 0; 24 | } 25 | 26 | /** 27 | * @param string $type 素材的类型,图片(image)、视频(video)、语音 (voice)、图文(news) 28 | */ 29 | public function setType($type) 30 | { 31 | $this->data['type'] = $type; 32 | } 33 | 34 | public function setOffset($offset) 35 | { 36 | $this->data['offset'] = $offset; 37 | } 38 | 39 | public function setCount($count) 40 | { 41 | $this->data['count'] = $count; 42 | } 43 | } -------------------------------------------------------------------------------- /src/Apis/Mp/MaterialDelMaterialApiRequest.php: -------------------------------------------------------------------------------- 1 | data['media_id'] = $media_id; 18 | } 19 | } -------------------------------------------------------------------------------- /src/Apis/Mp/MaterialGetMaterialApiRequest.php: -------------------------------------------------------------------------------- 1 | data['media_id'] = $media_id; 23 | } 24 | } -------------------------------------------------------------------------------- /src/Apis/Mp/MaterialGetMaterialCountApiRequest.php: -------------------------------------------------------------------------------- 1 | data['index'] = $index; 23 | } 24 | 25 | public function setMediaId($media_id) 26 | { 27 | $this->data['media_id'] = $media_id; 28 | } 29 | 30 | 31 | public function setArticles($articles) 32 | { 33 | $this->data['articles'] = $articles; 34 | } 35 | } -------------------------------------------------------------------------------- /src/Apis/Mp/MediaGetApiRequest.php: -------------------------------------------------------------------------------- 1 | data['media_id'] = $media_id; 22 | } 23 | } -------------------------------------------------------------------------------- /src/Apis/Mp/MediaGetJssdkApiRequest.php: -------------------------------------------------------------------------------- 1 | data['media_id'] = $media_id; 22 | } 23 | } -------------------------------------------------------------------------------- /src/Apis/Mp/MediaUploadApiRequest.php: -------------------------------------------------------------------------------- 1 | queryData['type'] = $type; 28 | } 29 | 30 | public function setMediaFile($file) 31 | { 32 | $this->data['@media'] = $file; 33 | } 34 | } -------------------------------------------------------------------------------- /src/Apis/Mp/MediaUploadimgApiRequest.php: -------------------------------------------------------------------------------- 1 | data['@media'] = $file; 25 | } 26 | } -------------------------------------------------------------------------------- /src/Apis/Mp/MenuCreateApiRequest.php: -------------------------------------------------------------------------------- 1 | data = $data->toArray(); 20 | } else { 21 | $this->data = $data; 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /src/Apis/Mp/MenuDeleteApiRequest.php: -------------------------------------------------------------------------------- 1 | data['msgtype'] = $this->msgtype; 18 | } 19 | 20 | public function setToUser($to) 21 | { 22 | $this->data['toUser'] = $to; 23 | } 24 | } -------------------------------------------------------------------------------- /src/Apis/Mp/MusicMessageRequest.php: -------------------------------------------------------------------------------- 1 | data['music'] = array( 23 | 'thumb_media_id' => $media_id, 24 | 'title' => $title, 25 | 'description' => $desc, 26 | 'musicurl' => $url, 27 | 'hqmusicurl' => $hqurl 28 | ); 29 | } 30 | } -------------------------------------------------------------------------------- /src/Apis/Mp/NewsMessageRequest.php: -------------------------------------------------------------------------------- 1 | '','description'=>'','picurl'=>'','url'=>'') 16 | * @return void 17 | * @see http://mp.weixin.qq.com/wiki/index.php?title=%E5%8F%91%E9%80%81%E5%AE%A2%E6%9C%8D%E6%B6%88%E6%81%AF#.E5.8F.91.E9.80.81.E5.9B.BE.E6.96.87.E6.B6.88.E6.81.AF 18 | */ 19 | public function setNews($articles) 20 | { 21 | $data['news']['articles'] = $articles; 22 | } 23 | } -------------------------------------------------------------------------------- /src/Apis/Mp/QrcodeCreateApiRequest.php: -------------------------------------------------------------------------------- 1 | data['expire_seconds'] = intval($sec); 29 | $this->forever = false; 30 | } 31 | 32 | /** 33 | * 设置场景值ID 34 | * @param $scene_id 35 | * @throws ParamsException 36 | */ 37 | public function setSceneId($scene_id) 38 | { 39 | if ($this->scene != 'id') { 40 | throw new ParamsException('Can not set scene_id and scene_str both'); 41 | } 42 | $this->data['action_info']['scene']['scene_id'] = $scene_id; 43 | $this->scene = 'id'; 44 | } 45 | 46 | /** 47 | * 设置场景值ID 48 | * @param $scene_str 49 | * @throws ParamsException 50 | */ 51 | public function setSceneStr($scene_str) 52 | { 53 | if ($this->scene != 'str') { 54 | throw new ParamsException('Can not set scene_id and scene_str both'); 55 | } 56 | $this->data['action_info']['scene']['scene_str'] = $scene_str; 57 | $this->scene = 'str'; 58 | } 59 | 60 | public function getData() 61 | { 62 | $action_name = $this->forever ? 'QR_LIMIT_SCENE' : 'QR_SCENE'; 63 | if ($this->scene == 'str') { 64 | $action_name = str_replace('_SCENE', '_STR_SCENE', $action_name); 65 | } 66 | $this->data['action_name'] = $action_name; 67 | return parent::getData(); 68 | } 69 | } -------------------------------------------------------------------------------- /src/Apis/Mp/TemplateApiRequest.php: -------------------------------------------------------------------------------- 1 | data['toUser'] = $to; 23 | } 24 | 25 | 26 | public function setTemplateId($id) 27 | { 28 | $this->data['template_id'] = $id; 29 | } 30 | 31 | public function setTemplateData($data) 32 | { 33 | $this->data['data'] = $data; 34 | } 35 | 36 | public function setUrl($url) 37 | { 38 | $this->data['url'] = $url; 39 | } 40 | 41 | public function setMiniProgram($appid, $pagepath) 42 | { 43 | $data['miniprogram'] = [ 44 | "appid" => $appid, 45 | "pagepath" => $pagepath 46 | ]; 47 | } 48 | } -------------------------------------------------------------------------------- /src/Apis/Mp/TextMessageRequest.php: -------------------------------------------------------------------------------- 1 | data['text']['content'] = $content; 14 | } 15 | } -------------------------------------------------------------------------------- /src/Apis/Mp/TicketGetTicketApiRequest.php: -------------------------------------------------------------------------------- 1 | data['type'] = 'jsapi'; 16 | } 17 | } -------------------------------------------------------------------------------- /src/Apis/Mp/UserGetApiRequest.php: -------------------------------------------------------------------------------- 1 | data['next_openid'] = $openid; 19 | } 20 | } -------------------------------------------------------------------------------- /src/Apis/Mp/UserInfoApiRequest.php: -------------------------------------------------------------------------------- 1 | data['openid'] = $openid; 16 | } 17 | } -------------------------------------------------------------------------------- /src/Apis/Mp/VideoMessageRequest.php: -------------------------------------------------------------------------------- 1 | $media_id, 24 | 'title' => $title, 25 | 'description' => $desc 26 | ); 27 | } 28 | } -------------------------------------------------------------------------------- /src/Apis/Mp/VoiceMessageRequest.php: -------------------------------------------------------------------------------- 1 | data['voice']['media_id'] = $media_id; 14 | } 15 | } -------------------------------------------------------------------------------- /src/Apis/MpApi.php: -------------------------------------------------------------------------------- 1 | data['openid'] = $openid; 21 | } 22 | 23 | } -------------------------------------------------------------------------------- /src/Apis/Web/SnsOauthAccessTokenApiRequest.php: -------------------------------------------------------------------------------- 1 | data['grant_type'] = 'authorization_code'; 17 | } 18 | 19 | public function setAppid($appid) 20 | { 21 | $this->data['appid'] = $appid; 22 | } 23 | 24 | public function setSecret($secret) 25 | { 26 | $this->data['secret'] = $secret; 27 | } 28 | 29 | public function setCode($code) 30 | { 31 | $this->data['code'] = $code; 32 | } 33 | } -------------------------------------------------------------------------------- /src/Apis/Web/SnsOauthRefreshTokenApiRequest.php: -------------------------------------------------------------------------------- 1 | data['grant_type'] = 'refresh_token'; 17 | } 18 | 19 | public function setAppid($appid) 20 | { 21 | $this->data['appid'] = $appid; 22 | } 23 | 24 | public function setRefreshToken($token) 25 | { 26 | $this->data['refresh_token'] = $token; 27 | } 28 | } -------------------------------------------------------------------------------- /src/Apis/Web/SnsUserInfoApiRequest.php: -------------------------------------------------------------------------------- 1 | data['openid'] = $openid; 16 | } 17 | 18 | public function setLanguage($language) 19 | { 20 | $this->data['lang'] = $language; 21 | } 22 | } -------------------------------------------------------------------------------- /src/Apis/WebApi.php: -------------------------------------------------------------------------------- 1 | set($key, $value, $ttl); 27 | } 28 | 29 | /** 30 | * @param $key 31 | * @param $value 32 | * @return mixed 33 | * @throws WeixinException 34 | */ 35 | public static function get($key, $value = "") 36 | { 37 | if (!self::$instance) { 38 | self::getInstance(); 39 | } 40 | return self::$instance->get($key, $value); 41 | } 42 | 43 | public static function clear(){ 44 | if (!self::$instance) { 45 | self::getInstance(); 46 | } 47 | return self::$instance->clear(); 48 | } 49 | 50 | /** 51 | * @return mixed 52 | * @throws WeixinException 53 | */ 54 | protected static function getInstance() 55 | { 56 | $store = self::$config['store'] ?? ''; 57 | if (!$store) { 58 | throw new WeixinException("store is not set"); 59 | } 60 | 61 | $storeInstance = new $store; 62 | $storeInstance->config(self::$config['config']); 63 | self::$instance = $storeInstance; 64 | return $storeInstance; 65 | } 66 | 67 | /** 68 | * @param $store 69 | * @param array $config 70 | * @throws WeixinException 71 | */ 72 | public static function setStore($store, $config = []) 73 | { 74 | self::$config = [ 75 | 'store' => $store, 76 | 'config' => $config 77 | ]; 78 | self::getInstance(); 79 | } 80 | } -------------------------------------------------------------------------------- /src/Cache/File.php: -------------------------------------------------------------------------------- 1 | getCacheFile(); 21 | if (file_exists($cachefile)) { 22 | $_cache = file_get_contents($cachefile); 23 | if ($_cache && $cache = unserialize($_cache)) { 24 | if ($cache['expire'] < time()) { 25 | return $cache['cache'][$key]; 26 | } 27 | } 28 | } 29 | return false; 30 | } 31 | 32 | public function set($key, $value, $ttl) 33 | { 34 | $cacheFile = $this->getCacheFile(); 35 | $cache = array($key => $value); 36 | $expire = time() + $ttl; 37 | file_put_contents($cacheFile, serialize(['cache' => $cache, 'expire' => $expire])); 38 | } 39 | 40 | public function clear() 41 | { 42 | $cacheFile = $this->getCacheFile(); 43 | if ($cacheFile) { 44 | unlink($cacheFile); 45 | } 46 | } 47 | 48 | public function config($config) 49 | { 50 | $this->cacheDir = $config['cacheDir'] ?? ''; 51 | $this->cacheFile = $config['cacheFile'] ?? 'weixin.cache'; 52 | } 53 | 54 | protected function getCacheFile() 55 | { 56 | return $this->cacheDir . '/' . $this->cacheFile; 57 | } 58 | 59 | 60 | } -------------------------------------------------------------------------------- /src/Cache/Store.php: -------------------------------------------------------------------------------- 1 | 8 | */ 9 | Class Menu 10 | { 11 | 12 | protected $buttons = array(); 13 | 14 | /** 15 | * 创建一级菜单 16 | * 17 | * @param string $name 菜单标题,不超过16个字节,子菜单不超过40个字节 18 | * @param string $type 菜单的响应动作类型,目前有click、view两种类型 19 | * @param string $value 若为click类型,则为key的值,若为view类型,则为url链接 20 | * @return boolean 返回是否创建成功 21 | */ 22 | public function addButton($name, $type = NULL, $value = NULL) 23 | { 24 | if (sizeof($this->buttons) == 3) { 25 | return FALSE; 26 | } 27 | $this->buttons[] = $this->button($name,$type,$value); 28 | } 29 | 30 | /** 31 | * 创建二级菜单 32 | * 33 | * @param int $index 父按钮序号 1~3 之间 34 | * @param string $name 菜单标题,不超过16个字节,子菜单不超过40个字节 35 | * @param string $type 菜单的响应动作类型,目前有click、view两种类型 36 | * @param string $value 若为click类型,则为key的值,若为view类型,则为url链接 37 | * @return boolean 返回是否创建成功 38 | */ 39 | public function addSubButton($index, $name, $type, $value) 40 | { 41 | $index--; 42 | if (!isset($this->buttons[$index])) { 43 | return FALSE; 44 | } 45 | if (sizeof($this->buttons[$index]) == 5) { 46 | return FALSE; 47 | } 48 | $this->buttons[$index]['sub'][] = $this->button($name,$type,$value); 49 | return TRUE; 50 | } 51 | 52 | protected function button($name,$type, $value){ 53 | $button['type'] = $type; 54 | switch ($type){ 55 | case 'view': 56 | $button['url'] = $value; 57 | break; 58 | case 'miniprogram': 59 | $button['url'] = $value['url']; 60 | $button['appid'] = $value['appid']; 61 | $button['pagepath'] = $value['pagepath']; 62 | break; 63 | case 'media_id': 64 | $button['media_id'] = $value; 65 | break; 66 | default: 67 | $button['key'] = $value; 68 | break; 69 | } 70 | $button['name'] = $name; 71 | return $button; 72 | } 73 | /** 74 | * 导出数组 75 | * 76 | * @return array 77 | */ 78 | public function toArray() 79 | { 80 | $buttons = array(); 81 | foreach ($this->buttons as $button) { 82 | if (isset($button['sub'])) { 83 | $buttons[] = array('name' => $button['name'], 'sub_button' => $button['sub']); 84 | } else { 85 | $buttons[] = $button; 86 | } 87 | } 88 | return array('button' => $buttons); 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /src/Message/Responser.php: -------------------------------------------------------------------------------- 1 | token = $token; 107 | $this->aseKey = $key; 108 | $this->appid = $appid; 109 | } 110 | 111 | /** 112 | * 监听用户消息 113 | */ 114 | public function listen() 115 | { 116 | 117 | if ($_SERVER['REQUEST_METHOD'] != 'POST') { 118 | $check = $this->checkSignature(); 119 | if (FALSE === $check) { 120 | return; 121 | } else { 122 | echo $check; 123 | } 124 | } else { 125 | $data = file_get_contents("php://input"); 126 | if (!$data) { 127 | return; 128 | } 129 | $type = $this->parseData($data)->getMsgType(); 130 | 131 | if ($this->data->MsgType == self::TYPE_EVENT) { 132 | for ($i = 0; $i <= sizeof($type); $i++) { 133 | $_type = implode('.', array_slice($type, 0, sizeof($type) - $i)); 134 | if (isset($this->callbacks[$_type])) { 135 | foreach ($this->callbacks[$_type] as $callback) { 136 | call_user_func($callback, $this->data); 137 | } 138 | break; 139 | } 140 | } 141 | } else if (isset($this->callbacks[$type])) { 142 | foreach ($this->callbacks[$type] as $callback) { 143 | call_user_func($callback, $this->data); 144 | } 145 | } elseif (isset($this->callbacks[self::TYPE_UNDEFINED])) { 146 | foreach ($this->callbacks[self::TYPE_UNDEFINED] as $callback) { 147 | call_user_func($callback, $this->data); 148 | } 149 | } 150 | } 151 | } 152 | 153 | /** 154 | * 对不同类型的用户消息设置对应的处理过程(回调函数) 155 | * 若设置了$type为undefind的回调函数,则所有未指定回调函数的消息类型将调用此函数 156 | * 接收到的用户消息(WeixinResult)将作为唯一参数传递给回调函数 157 | * @see http://mp.weixin.qq.com/wiki/index.php?title=%E6%8E%A5%E6%94%B6%E6%99%AE%E9%80%9A%E6%B6%88%E6%81%AF 158 | * 159 | * @param string|array $type 消息类型,事件类型可以使用数组来描述,元素依次为 事件,事件名,事件值 160 | * @param callback $callback 回调函数 161 | * @return self 162 | */ 163 | public function setCallback($type, $callback) 164 | { 165 | if (is_array($type)) { 166 | $this->callbacks[implode('.', $type)][] = $callback; 167 | } else { 168 | $this->callbacks[strtolower($type)][] = $callback; 169 | } 170 | return $this; 171 | } 172 | 173 | /** 174 | * 批量接口,使用方式参考setCallback 175 | * @param array $callbacks 176 | * @return self 177 | */ 178 | public function setCallbacks($callbacks = array()) 179 | { 180 | foreach ($callbacks as $callback) { 181 | $this->setCallback($callback['type'], $callback['callback']); 182 | } 183 | return $this; 184 | } 185 | 186 | /** 187 | * 获取发送者ID 188 | * 189 | * @return string 发送消息的用户OpenID 190 | */ 191 | public function getSender() 192 | { 193 | return $this->sender; 194 | } 195 | 196 | /** 197 | * 回复文本消息 198 | * 199 | * @param string $content 消息内容 200 | * @return void 201 | */ 202 | public function responseText($content) 203 | { 204 | $this->xml .= ""; 205 | $this->xml .= ""; 206 | $this->response(); 207 | } 208 | 209 | /** 210 | * 回复图片消息 211 | * 212 | * @param string $mediaid 通过上传多媒体文件,得到的id 213 | * @return array 214 | */ 215 | public function responseImage($mediaid) 216 | { 217 | $this->xml .= ""; 218 | $this->xml .= ""; 219 | $this->response(); 220 | } 221 | 222 | /** 223 | * 回复语音消息 224 | * 225 | * @param string $mediaid 通过上传多媒体文件,得到的id 226 | * @return array 227 | */ 228 | public function responseVoice($mediaid) 229 | { 230 | $this->xml .= ""; 231 | $this->xml .= ""; 232 | $this->response(); 233 | } 234 | 235 | /** 236 | * 回复视频消息 237 | * 238 | * @param string $mediaid 通过上传多媒体文件,得到的id 239 | * @param string $title 视频消息的标题 240 | * @param string $desc 视频消息的描述 241 | * @return array 242 | */ 243 | public function responseVideo($mediaid, $title = '', $desc = '') 244 | { 245 | $this->xml .= ""; 246 | $this->xml .= ""; 247 | $this->response(); 248 | } 249 | 250 | /** 251 | * 回复音乐消息 252 | * 253 | * @param string $mediaid 缩略图的媒体id,通过上传多媒体文件,得到的id 254 | * @param string $title 音乐标题 255 | * @param string $desc 音乐描述 256 | * @param string $url 音乐链接 257 | * @param string $hqurl 高质量音乐链接,WIFI环境优先使用该链接播放音乐 258 | * @return array 259 | */ 260 | public function responseMusic($mediaid, $title = '', $desc = '', $url = '', $hqurl = '') 261 | { 262 | $this->xml .= ""; 263 | $this->xml .= "<![CDATA[{$title}]]>"; 264 | $this->response(); 265 | } 266 | 267 | /** 268 | * 回复图文消息 269 | * 图文消息个数,限制为10条以内 270 | * 271 | * @param array $articles 多个article构成的数组,article格式为array('title'=>'','description'=>'','picurl'=>'','url'=>'') 272 | * @return array 273 | */ 274 | public function responseNews($articles) 275 | { 276 | $count = sizeof($articles); 277 | $this->xml .= ""; 278 | $this->xml .= "{$count}"; 279 | $this->xml .= ""; 280 | foreach ($articles as $article) { 281 | $title = isset($article['title']) ? $article['title'] : ''; 282 | $desc = isset($article['description']) ? $article['description'] : ''; 283 | $picurl = isset($article['picurl']) ? $article['picurl'] : ''; 284 | $url = isset($article['url']) ? $article['url'] : ''; 285 | $this->xml .= "<![CDATA[{$title}]]>"; 286 | } 287 | $this->xml .= ""; 288 | $this->response(); 289 | 290 | } 291 | 292 | protected function response() 293 | { 294 | if ($this->responseLock) 295 | return; 296 | $this->responseLock = TRUE; 297 | $t = time(); 298 | $xml = "data->FromUserName}]]>"; 299 | $xml .= "data->ToUserName}]]>"; 300 | $xml .= "{$t}"; 301 | $xml = "" . $xml . $this->xml . ""; 302 | echo $this->aseKey ? $this->encrypt($xml) : $xml; 303 | } 304 | 305 | protected function checkSignature() 306 | { 307 | if (empty($_GET['signature']) || empty($_GET['timestamp']) || empty($_GET['nonce'])) { 308 | return FALSE; 309 | } 310 | $signature = $_GET["signature"]; 311 | $timestamp = $_GET["timestamp"]; 312 | $nonce = $_GET["nonce"]; 313 | 314 | $tmpArr = array($this->token, $timestamp, $nonce); 315 | sort($tmpArr, SORT_STRING); 316 | $tmpStr = sha1(implode($tmpArr)); 317 | 318 | if ($tmpStr == $signature) { 319 | if (isset($_GET['echostr'])) { 320 | return $_GET['echostr']; 321 | } 322 | return true; 323 | } else { 324 | return false; 325 | } 326 | } 327 | 328 | /** 329 | * 330 | * @param string $data 331 | * @return self 332 | */ 333 | protected function parseData($data) 334 | { 335 | $this->data = new Result($data, $this->aseKey); 336 | $this->sender = $this->data->FromUserName; 337 | return $this; 338 | } 339 | 340 | protected function getMsgType() 341 | { 342 | if ($this->data->MsgType == self::TYPE_EVENT) { 343 | $type = array($this->data->MsgType); 344 | $type[] = strtolower($this->data->Event); 345 | if ($this->data->EventKey) { 346 | $type[] = strtolower($this->data->EventKey); 347 | } 348 | return $type; 349 | } else { 350 | return strtolower($this->data->MsgType); 351 | } 352 | } 353 | 354 | protected function encrypt($text) 355 | { 356 | //获得16位随机字符串,填充到明文之前 357 | $random = $this->getRandomStr(); 358 | $text = $random . pack("N", strlen($text)) . $text . $this->appid; 359 | //AES加密 360 | $iv = str_repeat("\0", 16); 361 | $key = $this->key . '='; 362 | $data = openssl_encrypt($text, 'AES-256-CBC', base64_decode($key), OPENSSL_RAW_DATA, $iv); 363 | $encryptData = base64_encode($data); 364 | //生成签名 365 | $timeStamp = time(); 366 | $sha1 = new SHA1; 367 | $nonce = $this->getRandomStr(10); 368 | $signature = $sha1->getSHA1($this->token, $timeStamp, $nonce, $encryptData); 369 | //拼装XML 370 | $format = " 371 | 372 | 373 | %s 374 | 375 | "; 376 | return sprintf($format, $encryptData, $signature, $timeStamp, $nonce); 377 | } 378 | 379 | /** 380 | * 随机生成16位字符串 381 | * @param int $len 382 | * @return string 生成的字符串 383 | */ 384 | protected function getRandomStr($len = 16) 385 | { 386 | 387 | $str = ""; 388 | $str_pol = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz"; 389 | $max = strlen($str_pol) - 1; 390 | for ($i = 0; $i < $len; $i++) { 391 | $str .= $str_pol[mt_rand(0, $max)]; 392 | } 393 | return $str; 394 | } 395 | } -------------------------------------------------------------------------------- /src/Message/Result.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | /** 10 | * @property-read Object $xml SimpleXML格式消息 11 | * @property-read string $MsgType 消息类型 12 | * @property-read string $ToUserName 开发者微信号 13 | * @property-read string $FromUserName 发送方帐号 14 | * @property-read string $CreateTime 创建时间 15 | * @property-read string $Content 文本消息内容 16 | * @property-read string $Event 事件类型 17 | * @property-read string $EventKey 事件KEY值 18 | * @property-read string $Ticket 二维码的ticket,可用来换取二维码图片 19 | * @property-read string $MediaId 图片消息媒体id,可以调用多媒体文件下载接口拉取数据 20 | * @property-read string $ThumbMediaId 视频消息缩略图的媒体id,可以调用多媒体文件下载接口拉取数据 21 | * @property-read string $Location_X 地理位置纬度 22 | * @property-read string $Location_Y 地理位置经度 23 | * @property-read string $Scale 地图缩放大小 24 | * @property-read string $Label 地理位置信息 25 | * @property-read string $PicUrl 图片链接 26 | * @property-read string $Description 消息描述 27 | * @property-read string $Title 消息标题 28 | * @property-read string $Url 消息链接 29 | * @property-read string $Format 语音格式,如amr,speex等 30 | * @property-read string $MsgId 消息id 31 | */ 32 | class Result 33 | { 34 | 35 | protected $xml; 36 | protected $key; 37 | 38 | public function __construct($xml, $key = '') 39 | { 40 | $xml = simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA); 41 | $this->xml = $this->toObject($xml); 42 | $this->key = base64_decode($key . '='); 43 | if ($this->Encrypt) { 44 | $this->aesDecode(); 45 | } 46 | } 47 | 48 | public function __get($name) 49 | { 50 | if (property_exists($this->xml, $name)) { 51 | return $this->xml->$name; 52 | } 53 | 54 | $method = 'get' . $name; 55 | if (method_exists($this, $method)) { 56 | return $this->$method(); 57 | } 58 | } 59 | 60 | /** 61 | * @return Object 62 | */ 63 | public function getXml() 64 | { 65 | return $this->xml; 66 | } 67 | 68 | protected function aesDecode() 69 | { 70 | $iv = substr($this->key, 0, 16); 71 | $decrypted = openssl_decrypt($this->Encrypt, 'AES-256-CBC', substr($this->key, 0, 32), OPENSSL_ZERO_PADDING, $iv); 72 | $result = $this->decode($decrypted); 73 | if (strlen($result) < 16) 74 | return ""; 75 | $content = substr($result, 16, strlen($result)); 76 | $len_list = unpack("N", substr($content, 0, 4)); 77 | $xml_len = $len_list[1]; 78 | $xml_content = substr($content, 4, $xml_len); 79 | $xml = simplexml_load_string($xml_content, 'SimpleXMLElement', LIBXML_NOCDATA); 80 | $this->xml = $this->toObject($xml); 81 | } 82 | 83 | /** 84 | * 对解密后的明文进行补位删除 85 | * @param decrypted 解密后的明文 86 | * @return 删除填充补位后的明文 87 | */ 88 | protected function decode($text) 89 | { 90 | 91 | $pad = ord(substr($text, -1)); 92 | if ($pad < 1 || $pad > 32) { 93 | $pad = 0; 94 | } 95 | return substr($text, 0, (strlen($text) - $pad)); 96 | } 97 | 98 | protected function toObject($sxml) 99 | { 100 | $array = json_decode(json_encode($sxml), 1); 101 | $arrayNoEmpty = $this->removeEmpty($array); 102 | return json_decode(json_encode($arrayNoEmpty)); 103 | } 104 | 105 | protected function removeEmpty($array) 106 | { 107 | foreach ($array as $k => $v) { 108 | if (is_array($v)) { 109 | if (empty($v)) { 110 | $array[$k] = ''; 111 | } else { 112 | $array[$k] = $this->removeEmpty($v); 113 | } 114 | } 115 | 116 | } 117 | return $array; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/Message/SHA1.php: -------------------------------------------------------------------------------- 1 | $v) { 54 | if (stripos($k, '@') === 0) { 55 | $file = new CURLFile(realpath($v), mime_content_type($v)); 56 | $params[substr($k, 1)] = $file; 57 | unset($params[$k]); 58 | } 59 | } 60 | 61 | } 62 | curl_setopt($ch,CURLOPT_URL, $url); 63 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE); 64 | curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE); 65 | curl_setopt($ch, CURLOPT_POST, TRUE); 66 | curl_setopt($ch, CURLOPT_POSTFIELDS, $params); 67 | curl_setopt_array($ch, $option); 68 | 69 | $result = curl_exec($ch); 70 | curl_close($ch); 71 | 72 | return $result ? $result : false; 73 | } 74 | 75 | 76 | } -------------------------------------------------------------------------------- /src/Test/AccessTokenTest.php: -------------------------------------------------------------------------------- 1 | getToken(); 22 | $this->assertIsString($token, 'token 成功获取'); 23 | $token = $creator->getToken(); 24 | $this->assertIsString($token, 'token 缓存成功获取'); 25 | } 26 | } -------------------------------------------------------------------------------- /src/Test/MpApiTest.php: -------------------------------------------------------------------------------- 1 | appid = $appid; 25 | $this->secret = $secret; 26 | } 27 | 28 | /** 29 | * @return mixed 30 | * @throws ApiException 31 | * @throws TokenException 32 | * @throws WeixinException 33 | */ 34 | public function getToken(): string 35 | { 36 | $tokenKey = 'acccessToken_' . $this->appid; 37 | $cache = Cache::get($tokenKey, ''); 38 | if ($cache && $cache['appid'] == $this->appid) { 39 | return $cache['acccessToken']; 40 | } 41 | $data = $this->request(); 42 | $ttl = $data['expires_in'] - 200; 43 | $accessToken = array('acccessToken' => $data['access_token'], 'appid' => $this->appid); 44 | Cache::set($tokenKey, $accessToken, $ttl); 45 | return $data['access_token']; 46 | } 47 | 48 | /** 49 | * @return string 50 | * @throws ApiException 51 | * @throws TokenException 52 | */ 53 | protected function request() 54 | { 55 | $api = new AccessTokenApiRequest(); 56 | $api->setAppid($this->appid); 57 | $api->setSecret($this->secret); 58 | 59 | $sender = new Api(); 60 | return $sender->sendRequest($api); 61 | } 62 | 63 | } -------------------------------------------------------------------------------- /src/Token/OAuthTokenCreator.php: -------------------------------------------------------------------------------- 1 | appid = $appid; 31 | $this->secret = $secret; 32 | } 33 | 34 | public function __get($key) 35 | { 36 | return $this->result[$key] ?? null; 37 | } 38 | 39 | public function setCode($code) 40 | { 41 | $this->code = $code; 42 | } 43 | 44 | /** 45 | * @return mixed 46 | * @throws ApiException 47 | * @throws TokenException 48 | */ 49 | public function getToken() 50 | { 51 | $result = $this->request(); 52 | $this->result = $result; 53 | return $result; 54 | } 55 | 56 | /** 57 | * @return string 58 | * @throws ApiException 59 | * @throws TokenException 60 | */ 61 | protected function request() 62 | { 63 | $api = new SnsOauthAccessTokenApiRequest(); 64 | $api->setAppid($this->appid); 65 | $api->setSecret($this->secret); 66 | $api->setCode($this->code); 67 | 68 | $sender = new Api(); 69 | return $sender->sendRequest($api); 70 | } 71 | } -------------------------------------------------------------------------------- /src/Token/TokenInterface.php: -------------------------------------------------------------------------------- 1 | 15 | * 16 | */ 17 | class Weixin 18 | { 19 | 20 | public static $config; 21 | 22 | public static function config($config = null) 23 | { 24 | if (!$config) { 25 | $config = require(__DIR__ . '/config.php'); 26 | } 27 | self::$config = $config; 28 | return new self(); 29 | } 30 | 31 | public static function __callStatic($name, $arguments) 32 | { 33 | return (new self())->$name($arguments); 34 | } 35 | 36 | /** 37 | * 返回公众号回复响应器 38 | * @param $token 39 | * @param string $aesKey 消息加密秘钥 40 | * @return Responser 41 | */ 42 | public static function responser($token, $aesKey = '') 43 | { 44 | return new Responser($token, $aesKey); 45 | } 46 | 47 | /** 48 | * 返回公众号接口 49 | * @param string $appid 50 | * @param string $secret 51 | * @return MpApi 52 | * @throws Exception\WeixinException 53 | */ 54 | public function mp($appid = '', $secret = '') 55 | { 56 | return new MpApi($appid, $secret); 57 | } 58 | 59 | /** 60 | * 返回网页接口 61 | * @param string $appid 62 | * @param string $secret 63 | * @return WebApi 64 | * @throws Exception\WeixinException 65 | */ 66 | public function web(string $appid = '', string $secret = '') 67 | { 68 | return new WebApi($appid, $secret); 69 | } 70 | 71 | /** 72 | * @param string $appid 73 | * @param string $secret 74 | * @return MiniApi 75 | * @throws Exception\WeixinException 76 | */ 77 | public function mini($appid = '', $secret = '') 78 | { 79 | return new MiniApi($appid, $secret); 80 | } 81 | 82 | /** 83 | * @param string $appid 84 | * @param string $secret 85 | * @return ThirdApi 86 | * @throws Exception\WeixinException 87 | */ 88 | public function third($appid = '', $secret = '') 89 | { 90 | return new ThirdApi($appid, $secret); 91 | } 92 | 93 | /** 94 | * js 签名生成 95 | * @param string $appid 96 | * @param string $secret 97 | * @return WeixinJsSignaturePack 98 | * @throws Exception\WeixinException 99 | */ 100 | public static function jsSignaturePack($appid = '', $secret = '') 101 | { 102 | return new WeixinJsSignaturePack($appid, $secret); 103 | } 104 | } -------------------------------------------------------------------------------- /src/WeixinJsSignaturePack.php: -------------------------------------------------------------------------------- 1 | createTicket(); 17 | $params['timestamp'] = time(); 18 | $params['noncestr'] = rand(1000, 9999); 19 | ksort($params); 20 | $a = array(); 21 | foreach ($params as $k => $v) { 22 | $a[] = $k . '=' . $v; 23 | } 24 | $params['signature'] = sha1(implode("&", $a)); 25 | $params['appid'] = $this->appid; 26 | return $params; 27 | } 28 | 29 | protected function createTicket() 30 | { 31 | $ticketKey = 'jsapi_ticket_' . $this->appid; 32 | $cache = Cache::get($ticketKey); 33 | if ($cache) { 34 | return $cache; 35 | } 36 | $data = $this->call(new TicketGetTicketApiRequest()); 37 | $ticket = $data['ticket']; 38 | $ttl = $data['expires_in'] - 200; 39 | Cache::set($ticketKey, $ticket, $ttl); 40 | return $ticket; 41 | } 42 | } -------------------------------------------------------------------------------- /src/config.php: -------------------------------------------------------------------------------- 1 | [ 9 | 'access_token' => AccessTokenCreator::class, 10 | 'oauth_token' => OAuthTokenCreator::class 11 | ], 12 | 'cache' => [ 13 | 'class' => File::class, 14 | 'config' => [ 15 | 'cacheDir' => __DIR__, 16 | 'cacheFile' => 'weixin.cache' 17 | ] 18 | ] 19 | ]; --------------------------------------------------------------------------------