├── .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 | 
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 .= "";
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 .= " ";
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 | ];
--------------------------------------------------------------------------------