├── .gitignore ├── README.md ├── composer.json └── src ├── Bridge ├── CacheTrait.php ├── Http.php ├── Serializer.php ├── Util.php └── XmlResponse.php ├── Event ├── Event.php ├── Event │ ├── Image.php │ ├── Link.php │ ├── Location.php │ ├── MenuClick.php │ ├── MenuView.php │ ├── ScanSubscribe.php │ ├── ScanSubscribed.php │ ├── ShortVideo.php │ ├── Subscribe.php │ ├── Text.php │ ├── Unsubscribe.php │ ├── UserLocation.php │ └── Voice.php ├── EventHandler.php ├── EventHandlerInterface.php ├── EventListener.php └── EventListenerInterface.php ├── Menu ├── Button.php ├── ButtonCollection.php ├── ButtonCollectionInterface.php ├── ButtonInterface.php ├── Create.php ├── Delete.php └── Query.php ├── Message ├── Entity.php ├── Entity │ ├── Article.php │ ├── ArticleItem.php │ ├── Image.php │ ├── Music.php │ ├── Text.php │ ├── Video.php │ └── Voice.php └── Template │ ├── Sender.php │ ├── Template.php │ └── TemplateInterface.php ├── OAuth ├── AbstractClient.php ├── AccessToken.php ├── Client.php ├── Qrcode.php └── StateManager.php ├── Payment ├── Address │ └── ConfigGenerator.php ├── Coupon │ ├── Cash.php │ └── Transfers.php ├── Jsapi │ ├── ConfigGenerator.php │ ├── PayChoose.php │ └── PayRequest.php ├── Notify.php ├── Qrcode │ ├── Forever.php │ ├── ForeverCallback.php │ └── Temporary.php ├── Query.php └── Unifiedorder.php ├── User ├── Group.php ├── Remark.php └── User.php └── Wechat ├── AccessToken.php ├── Jsapi.php ├── Jsapi └── Ticket.php ├── Qrcode.php ├── Qrcode └── Ticket.php ├── ServerIp.php └── ShortUrl.php /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### 简介 2 | 3 | 微信公众平台第三方 SDK 开发包,优雅、健壮,可扩展,遵循 [PSR](http://www.php-fig.org/) 开发规范。 4 | ### 安装 5 | ``` 6 | composer require itxiao6/wechat 7 | ``` 8 | ### 文档 9 | 文档请移步 项目 [WIKI](https://github.com/itxiao6/wechat/wiki) ,使用前建议先看文档! 10 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "itxiao6/wechat", 3 | "description": "MinKernel.Wechat", 4 | "keywords": ["wechat", "weixin", "微信", "微信 SDK", "微信支付", "微信开发", "自定义菜单"], 5 | "homepage": "http://github.com/itxiao6/wechat", 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "itxiao6", 10 | "email": "itxiao6@qq.com" 11 | } 12 | ], 13 | "require": { 14 | "php": ">=5.5.9", 15 | "guzzlehttp/guzzle": "^6.3|^7.0", 16 | "doctrine/cache": "^1.6", 17 | "doctrine/collections": "^1.3", 18 | "symfony/serializer": "^5.0", 19 | "symfony/options-resolver": "^5.4 || ^6.0", 20 | "symfony/property-access": "^5.4 || ^6.0", 21 | "symfony/http-foundation": "^5.4 || ^6.0" 22 | }, 23 | "require-dev": { 24 | "monolog/monolog": "^1.17" 25 | }, 26 | "autoload": { 27 | "psr-4": { "Itxiao6\\Wechat\\": "./src" } 28 | }, 29 | "minimum-stability": "dev" 30 | } 31 | -------------------------------------------------------------------------------- /src/Bridge/CacheTrait.php: -------------------------------------------------------------------------------- 1 | cache = $cache; 18 | } 19 | 20 | /** 21 | * 获取缓存驱动 22 | */ 23 | public function getCache() 24 | { 25 | return $this->cache; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Bridge/Http.php: -------------------------------------------------------------------------------- 1 | uri = $uri; 49 | $this->method = strtoupper($method); 50 | } 51 | 52 | /** 53 | * Create Client Factory 54 | */ 55 | public static function request($method, $uri) 56 | { 57 | return new static($method, $uri); 58 | } 59 | 60 | /** 61 | * Request Query 62 | */ 63 | public function withQuery(array $query) 64 | { 65 | $this->query = array_merge($this->query, $query); 66 | 67 | return $this; 68 | } 69 | 70 | /** 71 | * Request Json Body 72 | */ 73 | public function withBody(array $body) 74 | { 75 | $this->body = Serializer::jsonEncode($body); 76 | 77 | return $this; 78 | } 79 | 80 | /** 81 | * Request Xml Body 82 | */ 83 | public function withXmlBody(array $body) 84 | { 85 | $this->body = Serializer::xmlEncode($body); 86 | 87 | return $this; 88 | } 89 | 90 | /** 91 | * Query With AccessToken 92 | */ 93 | public function withAccessToken(AccessToken $accessToken) 94 | { 95 | $this->query['access_token'] = $accessToken->getTokenString(); 96 | 97 | return $this; 98 | } 99 | 100 | /** 101 | * Request SSL Cert 102 | */ 103 | public function withSSLCert($sslCert, $sslKey) 104 | { 105 | $this->sslCert = $sslCert; 106 | $this->sslKey = $sslKey; 107 | 108 | return $this; 109 | } 110 | 111 | /** 112 | * Send Request 113 | */ 114 | public function send($asArray = true) 115 | { 116 | $options = array(); 117 | 118 | // query 119 | if( !empty($this->query) ) { 120 | $options['query'] = $this->query; 121 | } 122 | 123 | // body 124 | if( !empty($this->body) ) { 125 | $options['body'] = $this->body; 126 | } 127 | 128 | // ssl cert 129 | if( $this->sslCert && $this->sslKey ) { 130 | $options['cert'] = $this->sslCert; 131 | $options['ssl_key'] = $this->sslKey; 132 | } 133 | 134 | $response = (new Client)->request($this->method, $this->uri, $options); 135 | $contents = $response->getBody()->getContents(); 136 | 137 | if( !$asArray ) { 138 | return $contents; 139 | } 140 | 141 | $array = Serializer::parse($contents); 142 | 143 | return new ArrayCollection($array); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/Bridge/Serializer.php: -------------------------------------------------------------------------------- 1 | defined('JSON_UNESCAPED_UNICODE') 17 | ? JSON_UNESCAPED_UNICODE 18 | : 0 19 | ); 20 | 21 | return (new JsonEncoder)->encode($data, 'json', array_replace($defaults, $context)); 22 | } 23 | 24 | /** 25 | * json decode 26 | */ 27 | public static function jsonDecode($data, array $context = array()) 28 | { 29 | $defaults = array( 30 | 'json_decode_associative' => true, 31 | 'json_decode_recursion_depth' => 512, 32 | 'json_decode_options' => 0, 33 | ); 34 | 35 | return (new JsonEncoder)->decode($data, 'json', array_replace($defaults, $context)); 36 | } 37 | 38 | /** 39 | * xml encode 40 | */ 41 | public static function xmlEncode($data, array $context = array()) 42 | { 43 | $defaults = array( 44 | 'xml_root_node_name' => 'xml', 45 | 'xml_format_output' => true, 46 | 'xml_version' => '1.0', 47 | 'xml_encoding' => 'utf-8', 48 | 'xml_standalone' => false, 49 | ); 50 | 51 | return (new XmlEncoder)->encode($data, 'xml', array_replace($defaults, $context)); 52 | } 53 | 54 | /** 55 | * xml decode 56 | */ 57 | public static function xmlDecode($data, array $context = array()) 58 | { 59 | return (new XmlEncoder)->decode($data, 'xml', $context); 60 | } 61 | 62 | /** 63 | * xml/json to array 64 | */ 65 | public static function parse($string) 66 | { 67 | if( static::isJSON($string) ) { 68 | $result = static::jsonDecode($string); 69 | } elseif( static::isXML($string) ) { 70 | $result = static::xmlDecode($string); 71 | } else { 72 | throw new \InvalidArgumentException(sprintf('Unable to parse: %s', (string) $string)); 73 | } 74 | 75 | return (array) $result; 76 | } 77 | 78 | /** 79 | * check is json string 80 | */ 81 | public static function isJSON($data) 82 | { 83 | return (@json_decode($data) !== null); 84 | } 85 | 86 | /** 87 | * check is xml string 88 | */ 89 | public static function isXML($data) 90 | { 91 | $xml = @simplexml_load_string($data); 92 | 93 | return ($xml instanceof \SimpleXmlElement); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Bridge/Util.php: -------------------------------------------------------------------------------- 1 | 'application/xml')); 17 | 18 | parent::__construct($content, $status, $headers); 19 | } 20 | } -------------------------------------------------------------------------------- /src/Event/Event.php: -------------------------------------------------------------------------------- 1 | getBody(); 17 | 18 | $body['ToUserName'] = $this['FromUserName']; 19 | $body['FromUserName'] = $this['ToUserName']; 20 | $body['MsgType'] = $entity->getType(); 21 | $body['CreateTime'] = time(); 22 | 23 | $response = new XmlResponse($body); 24 | $response->send(); 25 | } 26 | 27 | /** 28 | * check event options is valid 29 | */ 30 | abstract public function isValid(); 31 | } 32 | -------------------------------------------------------------------------------- /src/Event/Event/Image.php: -------------------------------------------------------------------------------- 1 | setRequest($request); 23 | } 24 | 25 | /** 26 | * set from request 27 | */ 28 | public function setRequest(Request $request) 29 | { 30 | $this->request = $request; 31 | } 32 | 33 | /** 34 | * get from rquest 35 | */ 36 | public function getRequest() 37 | { 38 | return $this->request; 39 | } 40 | 41 | /** 42 | * handle event via request 43 | */ 44 | public function handle(EventListenerInterface $listener) 45 | { 46 | if( !$listener->getListeners() ) { 47 | return; 48 | } 49 | 50 | $content = $this->request->getContent(); 51 | 52 | try { 53 | $options = Serializer::parse($content); 54 | } catch (\InvalidArgumentException $e) { 55 | $options = array(); 56 | } 57 | 58 | foreach( $listener->getListeners() as $namespace => $callable ) { 59 | $event = new $namespace($options); 60 | if( $event->isValid() ) { 61 | $listener->trigger($namespace, $event); 62 | break; 63 | } 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Event/EventHandlerInterface.php: -------------------------------------------------------------------------------- 1 | getListener($handler) ) { 18 | return call_user_func_array($listener, array($event)); 19 | } 20 | } 21 | 22 | /** 23 | * add listener 24 | */ 25 | public function addListener($handler, callable $callable) 26 | { 27 | if( !class_exists($handler) ) { 28 | throw new \InvalidArgumentException(sprintf('Invlaid Handler "%s"', $handler)); 29 | } 30 | 31 | if( !is_subclass_of($handler, Event::class) ) { 32 | throw new \InvalidArgumentException(sprintf( 33 | 'The Handler "%s" must be extends "%s"', $handler, Event::class)); 34 | } 35 | 36 | $this->listeners[$handler] = $callable; 37 | 38 | return $this; 39 | } 40 | 41 | /** 42 | * get listener 43 | */ 44 | public function getListener($handler) 45 | { 46 | if( $this->hasListener($handler) ) { 47 | return $this->listeners[$handler]; 48 | } 49 | } 50 | 51 | /** 52 | * has listener 53 | */ 54 | public function hasListener($handler) 55 | { 56 | return array_key_exists($handler, $this->listeners); 57 | } 58 | 59 | /** 60 | * remove listener 61 | */ 62 | public function removeListener($handler) 63 | { 64 | if( $this->hasListener($handler) ) { 65 | unset($this->listeners[$handler]); 66 | } 67 | } 68 | 69 | /** 70 | * get listeners 71 | */ 72 | public function getListeners() 73 | { 74 | return $this->listeners; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Event/EventListenerInterface.php: -------------------------------------------------------------------------------- 1 | value) 19 | */ 20 | protected $value = array(); 21 | 22 | /** 23 | * 菜单类型映射关系 24 | */ 25 | protected $mapping = array( 26 | 'view' => 'url', 27 | 'click' => 'key', 28 | 'scancode_push' => 'key', 29 | 'scancode_waitmsg' => 'key', 30 | 'pic_sysphoto' => 'key', 31 | 'pic_photo_or_album'=> 'key', 32 | 'pic_weixin' => 'key', 33 | 'location_select' => 'key', 34 | 'media_id' => 'media_id', 35 | 'view_limited' => 'media_id' 36 | ); 37 | 38 | /** 39 | * 构造方法 40 | */ 41 | public function __construct($name, $type, $value) 42 | { 43 | if( !array_key_exists($type, $this->mapping) ) { 44 | throw new \InvalidArgumentException(sprintf('Invalid Type: %s', $type)); 45 | } 46 | 47 | $this->name = $name; 48 | $this->type = $type; 49 | $this->value = array($this->mapping[$type] => $value); 50 | } 51 | 52 | /** 53 | * 菜单数据 54 | */ 55 | public function getData() 56 | { 57 | $data = array( 58 | 'name' => $this->name, 59 | 'type' => $this->type 60 | ); 61 | 62 | return array_merge($data, $this->value); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Menu/ButtonCollection.php: -------------------------------------------------------------------------------- 1 | name = $name; 28 | } 29 | 30 | /** 31 | * 添加子菜单 32 | */ 33 | public function addChild(ButtonInterface $button) 34 | { 35 | if( count($this->child) > (static::MAX_COUNT - 1) ) { 36 | throw new \InvalidArgumentException(sprintf( 37 | '子菜单不能超过 %d 个', static::MAX_COUNT 38 | )); 39 | } 40 | 41 | array_push($this->child, $button); 42 | } 43 | 44 | /** 45 | * 检测是否有子菜单 46 | */ 47 | public function hasChild() 48 | { 49 | return !empty($this->child); 50 | } 51 | 52 | /** 53 | * 获取子菜单 54 | */ 55 | public function getChild() 56 | { 57 | return $this->child; 58 | } 59 | 60 | /** 61 | * 获取菜单数据 62 | */ 63 | public function getData() 64 | { 65 | $data = array( 66 | 'name' => $this->name 67 | ); 68 | 69 | if( $this->hasChild() ) { 70 | foreach($this->child AS $k=>$v) { 71 | $data['sub_button'][] = $v->getData(); 72 | } 73 | } 74 | 75 | return $data; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Menu/ButtonCollectionInterface.php: -------------------------------------------------------------------------------- 1 | accessToken = $accessToken; 36 | } 37 | 38 | /** 39 | * 添加按钮 40 | */ 41 | public function add(ButtonInterface $button) 42 | { 43 | if( $button instanceof ButtonCollectionInterface ) { 44 | if( !$button->getChild() ) { 45 | throw new \InvalidArgumentException('一级菜单不能为空'); 46 | } 47 | } 48 | 49 | if( count($this->buttons) > (static::MAX_COUNT - 1) ) { 50 | throw new \InvalidArgumentException(sprintf( 51 | '一级菜单不能超过 %d 个', static::MAX_COUNT 52 | )); 53 | } 54 | 55 | $this->buttons[] = $button; 56 | } 57 | 58 | /** 59 | * 发布菜单 60 | */ 61 | public function doCreate() 62 | { 63 | $response = Http::request('POST', static::CREATE_URL) 64 | ->withAccessToken($this->accessToken) 65 | ->withBody($this->getRequestBody()) 66 | ->send(); 67 | 68 | if( $response['errcode'] != 0 ) { 69 | throw new \Exception($response['errmsg'], $response['errcode']); 70 | } 71 | 72 | return true; 73 | } 74 | 75 | /** 76 | * 获取数据 77 | */ 78 | public function getRequestBody() 79 | { 80 | $data = array(); 81 | 82 | foreach( $this->buttons AS $k=>$v ) { 83 | $data['button'][$k] = $v->getData(); 84 | } 85 | 86 | return $data; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Menu/Delete.php: -------------------------------------------------------------------------------- 1 | accessToken = $accessToken; 26 | } 27 | 28 | /** 29 | * 获取响应 30 | */ 31 | public function doDelete() 32 | { 33 | $response = Http::request('GET', static::DELETE_URL) 34 | ->withAccessToken($this->accessToken) 35 | ->send(); 36 | 37 | if( $response['errcode'] != 0 ) { 38 | throw new \Exception($response['errmsg'], $response['errcode']); 39 | } 40 | 41 | return true; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Menu/Query.php: -------------------------------------------------------------------------------- 1 | accessToken = $accessToken; 26 | } 27 | 28 | /** 29 | * 获取响应结果 30 | */ 31 | public function doQuery() 32 | { 33 | $response = Http::request('GET', static::QUERY_URL) 34 | ->withAccessToken($this->accessToken) 35 | ->send(); 36 | 37 | if( $response['errcode'] != 0 ) { 38 | throw new \Exception($response['errmsg'], $response['errcode']); 39 | } 40 | 41 | return $response; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Message/Entity.php: -------------------------------------------------------------------------------- 1 | items[] = $item; 20 | } 21 | 22 | /** 23 | * 消息内容 24 | */ 25 | public function getBody() 26 | { 27 | $body = array(); 28 | 29 | foreach( $this->items as $item ) { 30 | $body['item'][] = $item->getBody(); 31 | } 32 | 33 | $count = isset($body['item']) 34 | ? count($body['item']) 35 | : 0; 36 | 37 | return array('Articles' => $body, 'ArticleCount' => $count); 38 | } 39 | 40 | /** 41 | * 消息类型 42 | */ 43 | public function getType() 44 | { 45 | return 'news'; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Message/Entity/ArticleItem.php: -------------------------------------------------------------------------------- 1 | title = $title; 35 | } 36 | 37 | /** 38 | * 设置图文消息描述 39 | */ 40 | public function setDescription($description) 41 | { 42 | $this->description = $description; 43 | } 44 | 45 | /** 46 | * 设置图片链接 47 | */ 48 | public function setPicUrl($picUrl) 49 | { 50 | $this->picUrl = $picUrl; 51 | } 52 | 53 | /** 54 | * 设置点击图文消息跳转链接 55 | */ 56 | public function setUrl($url) 57 | { 58 | $this->url = $url; 59 | } 60 | 61 | /** 62 | * 消息内容 63 | */ 64 | public function getBody() 65 | { 66 | return array( 67 | 'Title' => $this->title, 68 | 'Description' => $this->description, 69 | 'PicUrl' => $this->picUrl, 70 | 'Url' => $this->url 71 | ); 72 | } 73 | 74 | /** 75 | * 消息类型 76 | */ 77 | public function getType() 78 | { 79 | return 'news'; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Message/Entity/Image.php: -------------------------------------------------------------------------------- 1 | mediaId = $mediaId; 20 | } 21 | 22 | /** 23 | * 消息内容 24 | */ 25 | public function getBody() 26 | { 27 | $body = array('MediaId'=>$this->mediaId); 28 | 29 | return array('Image'=>$body); 30 | } 31 | 32 | /** 33 | * 消息类型 34 | */ 35 | public function getType() 36 | { 37 | return 'image'; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Message/Entity/Music.php: -------------------------------------------------------------------------------- 1 | title = $title; 40 | } 41 | 42 | /** 43 | * 音乐描述 44 | */ 45 | public function setDescription($description) 46 | { 47 | $this->description = $description; 48 | } 49 | 50 | /** 51 | * 音乐链接 52 | */ 53 | public function setMusicUrl($musicUrl) 54 | { 55 | $this->musicUrl = $musicUrl; 56 | } 57 | 58 | /** 59 | * 高质量音乐链接 60 | */ 61 | public function setHQMusicUrl($HQMusicUrl) 62 | { 63 | $this->HQMusicUrl = $HQMusicUrl; 64 | } 65 | 66 | /** 67 | * 缩略图的媒体id 68 | */ 69 | public function setThumbMediaId($thumbMediaId) 70 | { 71 | $this->thumbMediaId = $thumbMediaId; 72 | } 73 | 74 | /** 75 | * 消息内容 76 | */ 77 | public function getBody() 78 | { 79 | $body = array( 80 | 'Title' => $this->title, 81 | 'Description' => $this->description, 82 | 'MusicUrl' => $this->musicUrl, 83 | 'HQMusicUrl' => $this->HQMusicUrl, 84 | 'ThumbMediaId' => $this->thumbMediaId 85 | ); 86 | 87 | return array('Music'=>$body); 88 | } 89 | 90 | /** 91 | * 消息类型 92 | */ 93 | public function getType() 94 | { 95 | return 'music'; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Message/Entity/Text.php: -------------------------------------------------------------------------------- 1 | content = $content; 20 | } 21 | 22 | /** 23 | * 消息内容 24 | */ 25 | public function getBody() 26 | { 27 | return array('Content'=>$this->content); 28 | } 29 | 30 | /** 31 | * 消息类型 32 | */ 33 | public function getType() 34 | { 35 | return 'text'; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Message/Entity/Video.php: -------------------------------------------------------------------------------- 1 | mediaId = $mediaId; 30 | } 31 | 32 | /** 33 | * 视频消息的标题 34 | */ 35 | public function setTitle($title) 36 | { 37 | $this->title = $title; 38 | } 39 | 40 | /** 41 | * 视频消息的描述 42 | */ 43 | public function setDescription($description) 44 | { 45 | $this->description = $description; 46 | } 47 | 48 | /** 49 | * 消息内容 50 | */ 51 | public function getBody() 52 | { 53 | $body = array( 54 | 'MediaId' => $this->mediaId, 55 | 'Title' => $this->title, 56 | 'Description' => $this->description 57 | ); 58 | 59 | return array('Video'=>$body); 60 | } 61 | 62 | /** 63 | * 消息类型 64 | */ 65 | public function getType() 66 | { 67 | return 'video'; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Message/Entity/Voice.php: -------------------------------------------------------------------------------- 1 | mediaId = $mediaId; 20 | } 21 | 22 | /** 23 | * 消息内容 24 | */ 25 | public function getBody() 26 | { 27 | $body = array('MediaId'=>$this->mediaId); 28 | 29 | return array('Voice'=>$body); 30 | } 31 | 32 | /** 33 | * 消息类型 34 | */ 35 | public function getType() 36 | { 37 | return 'voice'; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Message/Template/Sender.php: -------------------------------------------------------------------------------- 1 | accessToken = $accessToken; 27 | } 28 | 29 | /** 30 | * 发送模板消息 31 | */ 32 | public function send(TemplateInterface $template) 33 | { 34 | $response = Http::request('POST', static::SENDER) 35 | ->withAccessToken($this->accessToken) 36 | ->withBody($template->getRequestBody()) 37 | ->send(); 38 | 39 | if( $response['errcode'] != 0 ) { 40 | throw new \Exception($response['errmsg'], $response['errcode']); 41 | } 42 | 43 | return $response['msgid']; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Message/Template/Template.php: -------------------------------------------------------------------------------- 1 | id = $id; 33 | } 34 | 35 | /** 36 | * 获取模板 ID 37 | */ 38 | public function getId() 39 | { 40 | return $this->id; 41 | 42 | return $this; 43 | } 44 | 45 | /** 46 | * 设置链接 47 | */ 48 | public function setUrl($url) 49 | { 50 | $this->url = $url; 51 | 52 | return $this; 53 | } 54 | 55 | /** 56 | * 获取逻接 57 | */ 58 | public function getUrl() 59 | { 60 | return $this->url; 61 | } 62 | 63 | /** 64 | * 设置用户 Openid 65 | */ 66 | public function setOpenid($openid) 67 | { 68 | $this->openid = $openid; 69 | 70 | return $this; 71 | } 72 | 73 | /** 74 | * 获取用户 Openid 75 | */ 76 | public function getOpenid() 77 | { 78 | return $this->openid; 79 | } 80 | 81 | /** 82 | * 添加模板参数 83 | */ 84 | public function add($key, $value, $color = null) 85 | { 86 | $array = array('value' => $value); 87 | 88 | if( !is_null($color) ) { 89 | $array['color'] = $color; 90 | } 91 | 92 | $this->options[$key] = $array; 93 | 94 | return $this; 95 | } 96 | 97 | /** 98 | * 移除模板参数 99 | */ 100 | public function remove($key) 101 | { 102 | if( isset($this->options[$key]) ) { 103 | unset($this->options[$key]); 104 | } 105 | 106 | return $this; 107 | } 108 | 109 | /** 110 | * 获取请求内容 111 | */ 112 | public function getRequestBody() 113 | { 114 | return array( 115 | 'template_id' => $this->id, 116 | 'touser' => $this->openid, 117 | 'url' => $this->url, 118 | 'data' => $this->options 119 | ); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/Message/Template/TemplateInterface.php: -------------------------------------------------------------------------------- 1 | appid = $appid; 51 | $this->appsecret = $appsecret; 52 | $this->stateManager = new StateManager; 53 | } 54 | 55 | /** 56 | * 设置 scope 57 | */ 58 | public function setScope($scope) 59 | { 60 | $this->scope = $scope; 61 | } 62 | 63 | /** 64 | * 设置 state 65 | */ 66 | public function setState($state) 67 | { 68 | $this->state = $state; 69 | } 70 | 71 | /** 72 | * 设置 redirect uri 73 | */ 74 | public function setRedirectUri($redirectUri) 75 | { 76 | $this->redirectUri = $redirectUri; 77 | } 78 | 79 | /** 80 | * 获取授权 URL 81 | */ 82 | public function getAuthorizeUrl() 83 | { 84 | if (null === $this->state) { 85 | $this->state = Util::getRandomString(16); 86 | } 87 | 88 | $this->stateManager->setState($this->state); 89 | 90 | $query = array( 91 | 'appid' => $this->appid, 92 | 'redirect_uri' => $this->redirectUri ?: Util::getCurrentUrl(), 93 | 'response_type' => 'code', 94 | 'scope' => $this->resolveScope(), 95 | 'state' => $this->state 96 | ); 97 | 98 | return $this->resolveAuthorizeUrl().'?'.http_build_query($query); 99 | } 100 | 101 | /** 102 | * 通过 code 换取 AccessToken 103 | * @param $code 104 | * @param null $state 105 | * @return AccessToken 106 | * @throws \Exception 107 | */ 108 | public function getAccessToken($code, $state = null) 109 | { 110 | // if (null === $state && !isset($_GET['state'])) { 111 | // throw new \Exception('Invalid Request'); 112 | // } 113 | 114 | // http://www.twobotechnologies.com/blog/2014/02/importance-of-state-in-oauth2.html 115 | // $state = $state ?: $_GET['state']; 116 | // if (!$this->stateManager->isValid($state)) { 117 | // throw new \Exception(sprintf('Invalid Authentication State "%s"', $state)); 118 | // } 119 | 120 | $query = array( 121 | 'appid' => $this->appid, 122 | 'secret' => $this->appsecret, 123 | 'code' => $code, 124 | 'grant_type' => 'authorization_code' 125 | ); 126 | 127 | $response = Http::request('GET', static::ACCESS_TOKEN) 128 | ->withQuery($query) 129 | ->send(); 130 | 131 | // if( $response['errcode'] != 0 ) { 132 | // throw new \Exception($response['errmsg'], $response['errcode']); 133 | // } 134 | 135 | return new AccessToken($this->appid, $response->toArray()); 136 | } 137 | 138 | /** 139 | * 授权接口地址 140 | */ 141 | abstract public function resolveAuthorizeUrl(); 142 | 143 | /** 144 | * 授权作用域 145 | */ 146 | abstract public function resolveScope(); 147 | } 148 | -------------------------------------------------------------------------------- /src/OAuth/AccessToken.php: -------------------------------------------------------------------------------- 1 | appid = $appid; 36 | 37 | parent::__construct($options); 38 | } 39 | 40 | /** 41 | * 公众号 appid 42 | */ 43 | public function getAppid() 44 | { 45 | return $this->appid; 46 | } 47 | 48 | /** 49 | * 获取用户信息 50 | */ 51 | public function getUser($lang = 'zh_CN') 52 | { 53 | if( !$this->isValid() ) { 54 | $this->refresh(); 55 | } 56 | 57 | $query = array( 58 | 'access_token' => $this['access_token'], 59 | 'openid' => $this['openid'], 60 | 'lang' => $lang 61 | ); 62 | 63 | $response = Http::request('GET', static::USERINFO) 64 | ->withQuery($query) 65 | ->send(); 66 | 67 | if( $response['errcode'] != 0 ) { 68 | throw new \Exception($response['errmsg'], $response['errcode']); 69 | } 70 | 71 | return $response; 72 | } 73 | 74 | /** 75 | * 刷新用户 access_token 76 | */ 77 | public function refresh() 78 | { 79 | $query = array( 80 | 'appid' => $this->appid, 81 | 'grant_type' => 'refresh_token', 82 | 'refresh_token' => $this['refresh_token'] 83 | ); 84 | 85 | $response = Http::request('GET', static::REFRESH) 86 | ->withQuery($query) 87 | ->send(); 88 | 89 | if( $response['errcode'] != 0 ) { 90 | throw new \Exception($response['errmsg'], $response['errcode']); 91 | } 92 | 93 | // update new access_token from ArrayCollection 94 | parent::__construct($response->toArray()); 95 | 96 | return $this; 97 | } 98 | 99 | /** 100 | * 检测用户 access_token 是否有效 101 | */ 102 | public function isValid() 103 | { 104 | $query = array( 105 | 'access_token' => $this['access_token'], 106 | 'openid' => $this['openid'] 107 | ); 108 | 109 | $response = Http::request('GET', static::IS_VALID) 110 | ->withQuery($query) 111 | ->send(); 112 | 113 | return ($response['errmsg'] === 'ok'); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/OAuth/Client.php: -------------------------------------------------------------------------------- 1 | scope ?: 'snsapi_base'; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/OAuth/Qrcode.php: -------------------------------------------------------------------------------- 1 | scope ?: 'snsapi_login'; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/OAuth/StateManager.php: -------------------------------------------------------------------------------- 1 | namespace = $namespace; 15 | } 16 | 17 | public function setState($state) 18 | { 19 | if (!$this->isSessionStarted) { 20 | $this->startSession(); 21 | } 22 | 23 | $_SESSION[$this->namespace] = (string) $state; 24 | } 25 | 26 | public function getState() 27 | { 28 | if (!$this->isSessionStarted) { 29 | $this->startSession(); 30 | } 31 | 32 | return $this->hasState() 33 | ? (string) $_SESSION[$this->namespace] 34 | : null; 35 | } 36 | 37 | public function hasState() 38 | { 39 | if (!$this->isSessionStarted) { 40 | $this->startSession(); 41 | } 42 | 43 | return isset($_SESSION[$this->namespace]); 44 | } 45 | 46 | public function removeState() 47 | { 48 | if (!$this->isSessionStarted) { 49 | $this->startSession(); 50 | } 51 | 52 | if ($this->hasState()) { 53 | unset($_SESSION[$this->namespace]); 54 | } 55 | } 56 | 57 | public function isValid($state) 58 | { 59 | if (!$this->hasState()) { 60 | return false; 61 | } 62 | 63 | return ($state === $this->getState()); 64 | } 65 | 66 | private function startSession() 67 | { 68 | if (PHP_SESSION_NONE === session_status()) { 69 | session_start(); 70 | } 71 | 72 | $this->isSessionStarted = true; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Payment/Address/ConfigGenerator.php: -------------------------------------------------------------------------------- 1 | setAccessToken($accessToken); 22 | } 23 | 24 | /** 25 | * 设置用户 AccessToken 26 | */ 27 | public function setAccessToken(AccessToken $accessToken) 28 | { 29 | if( !$accessToken->isValid() ) { 30 | $accessToken->refresh(); 31 | } 32 | 33 | $this->accessToken = $accessToken; 34 | } 35 | 36 | /** 37 | * 获取配置 38 | * @param bool $asArray 39 | * @param null $url 40 | * @return array|bool|float|int|string 41 | */ 42 | public function getConfig($asArray = false,$url = null) 43 | { 44 | $options = array( 45 | 'appid' => $this->accessToken->getAppid(), 46 | 'url' => $url===null?Util::getCurrentUrl():$url, 47 | 'timestamp' => Util::getTimestamp(), 48 | 'noncestr' => Util::getRandomString(), 49 | 'accesstoken' => $this->accessToken['access_token'] 50 | ); 51 | 52 | // 按 ASCII 码排序 53 | ksort($options); 54 | 55 | $signature = http_build_query($options); 56 | $signature = urldecode($signature); 57 | $signature = sha1($signature); 58 | 59 | $config = array( 60 | 'appId' => $options['appid'], 61 | 'scope' => 'jsapi_address', 62 | 'signType' => 'sha1', 63 | 'addrSign' => $signature, 64 | 'timeStamp' => $options['timestamp'], 65 | 'nonceStr' => $options['noncestr'], 66 | ); 67 | 68 | return $asArray ? $config : Serializer::jsonEncode($config); 69 | } 70 | 71 | /** 72 | * 输出对象 73 | */ 74 | public function __toString() 75 | { 76 | return $this->getConfig(); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Payment/Coupon/Cash.php: -------------------------------------------------------------------------------- 1 | key = $key; 42 | 43 | $this->set('wxappid', $appid); 44 | $this->set('mch_id', $mchid); 45 | } 46 | 47 | /** 48 | * 调置 SSL 证书 49 | */ 50 | public function setSSLCert($sslCert, $sslKey) 51 | { 52 | if( !file_exists($sslCert) ) { 53 | throw new \InvalidArgumentException(sprintf('File "%s" Not Found', $sslCert)); 54 | } 55 | 56 | if( !file_exists($sslKey) ) { 57 | throw new \InvalidArgumentException(sprintf('File "%s" Not Found', $sslKey)); 58 | } 59 | 60 | $this->sslCert = $sslCert; 61 | $this->sslKey = $sslKey; 62 | } 63 | 64 | /** 65 | * 获取响应结果 66 | */ 67 | public function getResponse() 68 | { 69 | $options = $this->resolveOptions(); 70 | 71 | // 按 ASCII 码排序 72 | ksort($options); 73 | 74 | $signature = urldecode(http_build_query($options)); 75 | $signature = strtoupper(md5($signature.'&key='.$this->key)); 76 | 77 | $options['sign'] = $signature; 78 | 79 | $response = Http::request('POST', static::COUPON_CASH) 80 | ->withSSLCert($this->sslCert, $this->sslKey) 81 | ->withXmlBody($options) 82 | ->send(); 83 | 84 | if( $response['return_code'] === 'FAIL' ) { 85 | throw new \Exception($response['return_msg']); 86 | } 87 | 88 | if( $response['result_code'] === 'FAIL' ) { 89 | throw new \Exception($response['err_code_des']); 90 | } 91 | 92 | return $response; 93 | } 94 | 95 | /** 96 | * 合并和校验参数 97 | */ 98 | public function resolveOptions() 99 | { 100 | $defaults = array( 101 | 'nonce_str' => Util::getRandomString(), 102 | 'client_ip' => Util::getClientIp() 103 | ); 104 | 105 | $resolver = new OptionsResolver(); 106 | $resolver 107 | ->setDefined($this->required) 108 | ->setRequired($this->required) 109 | ->setDefaults($defaults); 110 | 111 | return $resolver->resolve($this->toArray()); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/Payment/Coupon/Transfers.php: -------------------------------------------------------------------------------- 1 | key = $key; 50 | 51 | $this->set('mch_appid', $mch_appid); 52 | $this->set('mchid', $mchid); 53 | } 54 | 55 | /** 56 | * 调置 SSL 证书 57 | */ 58 | public function setSSLCert($sslCert, $sslKey) 59 | { 60 | if( !file_exists($sslCert) ) { 61 | throw new \InvalidArgumentException(sprintf('File "%s" Not Found', $sslCert)); 62 | } 63 | 64 | if( !file_exists($sslKey) ) { 65 | throw new \InvalidArgumentException(sprintf('File "%s" Not Found', $sslKey)); 66 | } 67 | 68 | $this->sslCert = $sslCert; 69 | $this->sslKey = $sslKey; 70 | } 71 | 72 | /** 73 | * 获取响应结果 74 | */ 75 | public function getResponse() 76 | { 77 | $options = $this->resolveOptions(); 78 | // 按 ASCII 码排序 79 | ksort($options); 80 | 81 | 82 | $signature = urldecode(http_build_query($options)); 83 | $signature = strtoupper(md5($signature.'&key='.$this->key)); 84 | 85 | $options['sign'] = $signature; 86 | 87 | $response = Http::request('POST', static::TRANSFERS) 88 | ->withSSLCert($this->sslCert, $this->sslKey) 89 | ->withXmlBody($options) 90 | ->send(); 91 | 92 | if( $response['return_code'] === 'FAIL' ) { 93 | throw new \Exception($response['return_msg']); 94 | } 95 | 96 | if( $response['result_code'] === 'FAIL' ) { 97 | throw new \Exception($response['err_code_des']); 98 | } 99 | 100 | return $response; 101 | } 102 | 103 | /** 104 | * 合并和校验参数 105 | */ 106 | public function resolveOptions() 107 | { 108 | $defaults = array( 109 | 'nonce_str' => Util::getRandomString(), 110 | ); 111 | 112 | $resolver = new OptionsResolver(); 113 | $resolver 114 | ->setDefined($this->required) 115 | ->setRequired($this->required) 116 | ->setDefaults($defaults); 117 | 118 | return $resolver->resolve($this->toArray()); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/Payment/Jsapi/ConfigGenerator.php: -------------------------------------------------------------------------------- 1 | getResponse(); 18 | $key = $unifiedorder->getKey(); 19 | 20 | $config = array( 21 | 'appId' => $unifiedorder['appid'], 22 | 'timeStamp' => Util::getTimestamp(), 23 | 'nonceStr' => Util::getRandomString(), 24 | 'package' => 'prepay_id='.$res['prepay_id'], 25 | 'signType' => 'MD5' 26 | ); 27 | 28 | // 如果需要指定以上参数,可以通过 $defaults 变量传入 29 | $options = array_replace($config, $defaults); 30 | 31 | ksort($options); 32 | 33 | $queryString = urldecode(http_build_query($options)); 34 | $paySign = strtoupper(md5($queryString.'&key='.$key)); 35 | 36 | $options['paySign'] = $paySign; 37 | 38 | parent::__construct($options); 39 | } 40 | 41 | /** 42 | * 获取配置 43 | */ 44 | public function getConfig($asArray = false) 45 | { 46 | $config = $this->resolveConfig(); 47 | 48 | return $asArray ? $config : Serializer::jsonEncode($config); 49 | } 50 | 51 | /** 52 | * 输出对象 53 | */ 54 | public function __toString() 55 | { 56 | return $this->getConfig(); 57 | } 58 | 59 | /** 60 | * 分解配置 61 | */ 62 | abstract function resolveConfig(); 63 | } 64 | -------------------------------------------------------------------------------- /src/Payment/Jsapi/PayChoose.php: -------------------------------------------------------------------------------- 1 | $this['timeStamp'], 14 | 'nonceStr' => $this['nonceStr'], 15 | 'package' => $this['package'], 16 | 'signType' => $this['signType'], 17 | 'paySign' => $this['paySign'] 18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Payment/Jsapi/PayRequest.php: -------------------------------------------------------------------------------- 1 | toArray(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Payment/Notify.php: -------------------------------------------------------------------------------- 1 | getContent(); 29 | 30 | try { 31 | $options = Serializer::parse($content); 32 | } catch (\InvalidArgumentException $e) { 33 | $options = array(); 34 | } 35 | 36 | parent::__construct($options); 37 | } 38 | 39 | /** 40 | * 错误响应 41 | */ 42 | public function fail($message = null) 43 | { 44 | $options = array('return_code' => self::FAIL); 45 | 46 | if( !is_null($message) ) { 47 | $options['return_msg'] = $message; 48 | } 49 | 50 | $this->xmlResponse($options); 51 | } 52 | 53 | /** 54 | * 成功响应 55 | */ 56 | public function success($message = null) 57 | { 58 | $options = array('return_code' => self::SUCCESS); 59 | 60 | if( !is_null($message) ) { 61 | $options['return_msg'] = $message; 62 | } 63 | 64 | $this->xmlResponse($options); 65 | } 66 | 67 | /** 68 | * 响应 Xml 69 | */ 70 | protected function xmlResponse(array $options) 71 | { 72 | $response = new XmlResponse($options); 73 | $response->send(); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Payment/Qrcode/Forever.php: -------------------------------------------------------------------------------- 1 | key = $key; 27 | 28 | $this->set('appid', $appid); 29 | $this->set('mch_id', $mchid); 30 | } 31 | 32 | /** 33 | * 获取支付链接 34 | */ 35 | public function getPayurl($productId, array $defaults = array()) 36 | { 37 | $defaultOptions = array( 38 | 'appid' => $this['appid'], 39 | 'mch_id' => $this['mch_id'], 40 | 'time_stamp' => Util::getTimestamp(), 41 | 'nonce_str' => Util::getRandomString(), 42 | ); 43 | 44 | $options = array_replace($defaultOptions, $defaults); 45 | $options['product_id'] = $productId; 46 | 47 | // 按 ASCII 码排序 48 | ksort($options); 49 | 50 | $signature = urldecode(http_build_query($options)); 51 | $signature = strtoupper(md5($signature.'&key='.$this->key)); 52 | 53 | $options['sign'] = $signature; 54 | 55 | $query = http_build_query($options); 56 | 57 | return self::PAYMENT_URL.'?'.urlencode($query); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Payment/Qrcode/ForeverCallback.php: -------------------------------------------------------------------------------- 1 | getContent(); 31 | 32 | try { 33 | $options = Serializer::parse($content); 34 | } catch (\InvalidArgumentException $e) { 35 | $options = array(); 36 | } 37 | 38 | parent::__construct($options); 39 | } 40 | 41 | /** 42 | * 错误响应 43 | */ 44 | public function fail($message = null) 45 | { 46 | $options = array('return_code' => static::FAIL); 47 | 48 | if( !is_null($message) ) { 49 | $options['return_msg'] = $message; 50 | } 51 | 52 | $this->xmlResponse($options); 53 | } 54 | 55 | /** 56 | * 成功响应 57 | */ 58 | public function success(Unifiedorder $unifiedorder) 59 | { 60 | $unifiedorder->set('trade_type', 'NATIVE'); 61 | 62 | $response = $unifiedorder->getResponse(); 63 | 64 | $options = array( 65 | 'appid' => $unifiedorder['appid'], 66 | 'mch_id' => $unifiedorder['mch_id'], 67 | 'prepay_id' => $response['prepay_id'], 68 | 'nonce_str' => Util::getRandomString(), 69 | 'return_code' => static::SUCCESS, 70 | 'result_code' => static::SUCCESS 71 | ); 72 | 73 | // 按 ASCII 码排序 74 | ksort($options); 75 | 76 | $signature = urldecode(http_build_query($options)); 77 | $signature = strtoupper(md5($signature.'&key='.$unifiedorder->getKey())); 78 | 79 | $options['sign'] = $signature; 80 | 81 | $this->xmlResponse($options); 82 | } 83 | 84 | /** 85 | * 响应 Xml 86 | */ 87 | protected function xmlResponse(array $options) 88 | { 89 | $response = new XmlResponse($options); 90 | $response->send(); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Payment/Qrcode/Temporary.php: -------------------------------------------------------------------------------- 1 | set('trade_type', 'NATIVE'); 20 | 21 | $this->unifiedorder = $unifiedorder; 22 | } 23 | 24 | /** 25 | * 获取支付链接 26 | */ 27 | public function getPayurl() 28 | { 29 | $response = $this->unifiedorder->getResponse(); 30 | 31 | if( !$response->containsKey('code_url') ) { 32 | throw new \Exception('Invalid Unifiedorder Response'); 33 | } 34 | 35 | return $response['code_url']; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Payment/Query.php: -------------------------------------------------------------------------------- 1 | key = $key; 27 | 28 | $this->set('appid', $appid); 29 | $this->set('mch_id', $mchid); 30 | } 31 | 32 | /** 33 | * 根据 transaction_id 查询 34 | */ 35 | public function fromTransactionId($transactionId) 36 | { 37 | return $this->doQuery(array('transaction_id'=>$transactionId)); 38 | } 39 | 40 | /** 41 | * 根据 out_trade_no 查询 42 | */ 43 | public function fromOutTradeNo($outTradeNo) 44 | { 45 | return $this->doQuery(array('out_trade_no'=>$outTradeNo)); 46 | } 47 | 48 | /** 49 | * 查询订单 50 | */ 51 | public function doQuery(array $by) 52 | { 53 | $options = array_merge($this->toArray(), $by); 54 | $options['nonce_str'] = Util::getRandomString(); 55 | 56 | // 按 ASCII 码排序 57 | ksort($options); 58 | 59 | $signature = urldecode(http_build_query($options)); 60 | $signature = strtoupper(md5($signature.'&key='.$this->key)); 61 | 62 | $options['sign'] = $signature; 63 | 64 | $response = Http::request('POST', static::QUERY) 65 | ->withXmlBody($options) 66 | ->send(); 67 | 68 | if( $response['result_code'] === 'FAIL' ) { 69 | throw new \Exception($response['err_code_des']); 70 | } 71 | 72 | if( $response['return_code'] === 'FAIL' ) { 73 | throw new \Exception($response['return_msg']); 74 | } 75 | 76 | return $response; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Payment/Unifiedorder.php: -------------------------------------------------------------------------------- 1 | key = $key; 51 | 52 | $this->set('appid', $appid); 53 | $this->set('mch_id', $mchid); 54 | } 55 | 56 | /** 57 | * 获取商户 Key 58 | */ 59 | public function getKey() 60 | { 61 | return $this->key; 62 | } 63 | 64 | /** 65 | * 获取响应结果 66 | */ 67 | public function getResponse() 68 | { 69 | $options = $this->resolveOptions(); 70 | 71 | // 按 ASCII 码排序 72 | ksort($options); 73 | 74 | $signature = urldecode(http_build_query($options)); 75 | $signature = strtoupper(md5($signature.'&key='.$this->key)); 76 | 77 | $options['sign'] = $signature; 78 | 79 | $response = Http::request('POST', static::UNIFIEDORDER) 80 | ->withXmlBody($options) 81 | ->send(); 82 | 83 | if( $response['return_code'] === 'FAIL' ) { 84 | throw new \Exception($response['return_msg']); 85 | } 86 | 87 | if( $response['result_code'] === 'FAIL' ) { 88 | throw new \Exception($response['err_code_des']); 89 | } 90 | 91 | return $response; 92 | } 93 | 94 | /** 95 | * 合并和校验参数 96 | */ 97 | public function resolveOptions() 98 | { 99 | $normalizer = function($options, $value) { 100 | if( ($value === 'JSAPI') && !isset($options['openid']) ) { 101 | throw new \InvalidArgumentException(sprintf( 102 | '订单的 trade_type 为 “%s” 时,必需指定 “openid” 字段', $value)); 103 | } 104 | return $value; 105 | }; 106 | 107 | $defaults = array( 108 | 'trade_type' => current($this->tradeTypes), 109 | 'spbill_create_ip' => Util::getClientIp(), 110 | 'nonce_str' => Util::getRandomString(), 111 | ); 112 | 113 | $resolver = new OptionsResolver(); 114 | $resolver 115 | ->setDefined($this->defined) 116 | ->setRequired($this->required) 117 | ->setAllowedValues('trade_type', $this->tradeTypes) 118 | ->setNormalizer('trade_type', $normalizer) 119 | ->setDefaults($defaults); 120 | 121 | return $resolver->resolve($this->toArray()); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/User/Group.php: -------------------------------------------------------------------------------- 1 | accessToken = $accessToken; 30 | } 31 | 32 | /** 33 | * 查询全部分组 34 | */ 35 | public function query() 36 | { 37 | $response = Http::request('GET', static::SELECT) 38 | ->withAccessToken($this->accessToken) 39 | ->send(); 40 | 41 | if( $response['errcode'] != 0 ) { 42 | throw new \Exception($response['errmsg'], $response['errcode']); 43 | } 44 | 45 | return new ArrayCollection($response['groups']); 46 | } 47 | 48 | /** 49 | * 创建新分组 50 | */ 51 | public function create($name) 52 | { 53 | $body = array( 54 | 'group' => array('name'=>$name) 55 | ); 56 | 57 | $response = Http::request('POST', static::CREATE) 58 | ->withAccessToken($this->accessToken) 59 | ->withBody($body) 60 | ->send(); 61 | 62 | if( $response['errcode'] != 0 ) { 63 | throw new \Exception($response['errmsg'], $response['errcode']); 64 | } 65 | 66 | return new ArrayCollection($response['group']); 67 | } 68 | 69 | /** 70 | * 修改分组名称 71 | */ 72 | public function update($id, $newName) 73 | { 74 | $body = array( 75 | 'group' => array( 76 | 'id' => $id, 77 | 'name' => $newName 78 | ) 79 | ); 80 | 81 | $response = Http::request('POST', static::UPDAET) 82 | ->withAccessToken($this->accessToken) 83 | ->withBody($body) 84 | ->send(); 85 | 86 | if( $response['errcode'] != 0 ) { 87 | throw new \Exception($response['errmsg'], $response['errcode']); 88 | } 89 | 90 | return true; 91 | } 92 | 93 | /** 94 | * 删除分组 95 | */ 96 | public function delete($id) 97 | { 98 | $body = array( 99 | 'group' => array('id'=>$id) 100 | ); 101 | 102 | $response = Http::request('POST', static::DELETE) 103 | ->withAccessToken($this->accessToken) 104 | ->withBody($body) 105 | ->send(); 106 | 107 | if( $response['errcode'] != 0 ) { 108 | throw new \Exception($response['errmsg'], $response['errcode']); 109 | } 110 | 111 | return true; 112 | } 113 | 114 | /** 115 | * 查询指定用户所在分组 116 | */ 117 | public function queryUserGroup($openid) 118 | { 119 | $body = array('openid'=>$openid); 120 | 121 | $response = Http::request('POST', static::QUERY_USER_GROUP) 122 | ->withAccessToken($this->accessToken) 123 | ->withBody($body) 124 | ->send(); 125 | 126 | if( $response['errcode'] != 0 ) { 127 | throw new \Exception($response['errmsg'], $response['errcode']); 128 | } 129 | 130 | return $response['groupid']; 131 | } 132 | 133 | /** 134 | * 移动用户分组 135 | */ 136 | public function updateUserGroup($openid, $newId) 137 | { 138 | $key = is_array($openid) 139 | ? 'openid_list' 140 | : 'openid'; 141 | 142 | $api = is_array($openid) 143 | ? static::BETCH_UPDATE_USER_GROUP 144 | : static::UPDATE_USER_GROUP; 145 | 146 | $body = array($key=>$openid, 'to_groupid'=>$newId); 147 | 148 | $response = Http::request('POST', $api) 149 | ->withAccessToken($this->accessToken) 150 | ->withBody($body) 151 | ->send(); 152 | 153 | if( $response['errcode'] != 0 ) { 154 | throw new \Exception($response['errmsg'], $response['errcode']); 155 | } 156 | 157 | return true; 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/User/Remark.php: -------------------------------------------------------------------------------- 1 | accessToken = $accessToken; 26 | } 27 | 28 | /** 29 | * 设置/更新用户备注 30 | */ 31 | public function update($openid, $remark) 32 | { 33 | $body = array( 34 | 'openid' => $openid, 35 | 'remark' => $remark 36 | ); 37 | 38 | $response = Http::request('POST', static::REMARK) 39 | ->withAccessToken($this->accessToken) 40 | ->withBody($body) 41 | ->send(); 42 | 43 | if( $response['errcode'] != 0 ) { 44 | throw new \Exception($response['errmsg'], $response['errcode']); 45 | } 46 | 47 | return $response; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/User/User.php: -------------------------------------------------------------------------------- 1 | accessToken = $accessToken; 37 | } 38 | 39 | /** 40 | * 查询用户列表 41 | */ 42 | public function lists($nextOpenid = null) 43 | { 44 | $query = is_null($nextOpenid) 45 | ? array() 46 | : array('next_openid'=>$nextOpenid); 47 | 48 | $response = Http::request('GET', static::LISTS) 49 | ->withAccessToken($this->accessToken) 50 | ->withQuery($query) 51 | ->send(); 52 | 53 | if( $response['errcode'] != 0 ) { 54 | throw new \Exception($response['errmsg'], $response['errcode']); 55 | } 56 | 57 | return $response; 58 | } 59 | 60 | /** 61 | * 获取用户信息 62 | */ 63 | public function get($openid, $lang = 'zh_CN') 64 | { 65 | $query = array( 66 | 'openid' => $openid, 67 | 'lang' => $lang 68 | ); 69 | 70 | $response = Http::request('GET', static::USERINFO) 71 | ->withAccessToken($this->accessToken) 72 | ->withQuery($query) 73 | ->send(); 74 | 75 | if( $response['errcode'] != 0 ) { 76 | throw new \Exception($response['errmsg'], $response['errcode']); 77 | } 78 | 79 | return $response; 80 | } 81 | 82 | /** 83 | * 批量获取用户信息 84 | */ 85 | public function getBetch(array $openid, $lang = 'zh_CN') 86 | { 87 | $body = array(); 88 | 89 | foreach($openid as $key=>$value) { 90 | $body['user_list'][$key]['openid'] = $value; 91 | $body['user_list'][$key]['lang'] = $lang; 92 | } 93 | 94 | $response = Http::request('POST', static::BETCH) 95 | ->withAccessToken($this->accessToken) 96 | ->withBody($body) 97 | ->send(); 98 | 99 | if( $response['errcode'] != 0 ) { 100 | throw new \Exception($response['errmsg'], $response['errcode']); 101 | } 102 | 103 | return new ArrayCollection($response['user_info_list']); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/Wechat/AccessToken.php: -------------------------------------------------------------------------------- 1 | set('appid', $appid); 27 | $this->set('appsecret', $appsecret); 28 | } 29 | 30 | /** 31 | * 获取 AccessToken(调用缓存,返回 String) 32 | */ 33 | public function getTokenString() 34 | { 35 | $cacheId = $this->getCacheId(); 36 | 37 | if( $this->cache && $data = $this->cache->fetch($cacheId) ) { 38 | return $data['access_token']; 39 | } 40 | 41 | $response = $this->getTokenResponse(); 42 | 43 | if( $this->cache ) { 44 | $this->cache->save($cacheId, $response, $response['expires_in']); 45 | } 46 | 47 | return $response['access_token']; 48 | } 49 | 50 | /** 51 | * 获取 AccessToken(不缓存,返回原始数据) 52 | */ 53 | public function getTokenResponse() 54 | { 55 | $query = array( 56 | 'grant_type' => 'client_credential', 57 | 'appid' => $this['appid'], 58 | 'secret' => $this['appsecret'] 59 | ); 60 | 61 | $response = Http::request('GET', static::ACCESS_TOKEN) 62 | ->withQuery($query) 63 | ->send(); 64 | 65 | if( $response->containsKey('errcode') ) { 66 | throw new \Exception($response['errmsg'], $response['errcode']); 67 | } 68 | 69 | return $response; 70 | } 71 | 72 | /** 73 | * 从缓存中清除 74 | */ 75 | public function clearFromCache() 76 | { 77 | return $this->cache 78 | ? $this->cache->delete($this->getCacheId()) 79 | : false; 80 | } 81 | 82 | /** 83 | * 获取缓存 ID 84 | */ 85 | public function getCacheId() 86 | { 87 | return sprintf('%s_access_token', $this['appid']); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Wechat/Jsapi.php: -------------------------------------------------------------------------------- 1 | accessToken = $accessToken; 52 | } 53 | 54 | /** 55 | * 注入接口 56 | */ 57 | public function addApi($apis) 58 | { 59 | if( is_array($apis) ) { 60 | foreach( $apis AS $api ) { 61 | $this->addApi($api); 62 | } 63 | } 64 | 65 | $apiName = (string) $apis; 66 | 67 | if( !in_array($apiName, $this->apiValids, true) ) { 68 | throw new \InvalidArgumentException(sprintf('Invalid Api: %s', $apiName)); 69 | } 70 | 71 | array_push($this->api, $apiName); 72 | 73 | return $this; 74 | } 75 | 76 | /** 77 | * 启用调试模式 78 | */ 79 | public function enableDebug() 80 | { 81 | $this->debug = true; 82 | 83 | return $this; 84 | } 85 | 86 | /** 87 | * 获取配置文件 88 | * @param bool $asArray 89 | * @param null $url 90 | * @return array|bool|float|int|string 91 | */ 92 | public function getConfig($asArray = false,$url = null) 93 | { 94 | $ticket = new Ticket($this->accessToken); 95 | 96 | if( $this->cache ) { 97 | $ticket->setCache($this->cache); 98 | } 99 | 100 | $options = array( 101 | 'jsapi_ticket' => $ticket->getTicketString(), 102 | 'timestamp' => Util::getTimestamp(), 103 | 'url' => ($url===null)?Util::getCurrentUrl():$url, 104 | 'noncestr' => Util::getRandomString(), 105 | ); 106 | 107 | ksort($options); 108 | 109 | $signature = sha1(urldecode(http_build_query($options))); 110 | $configure = array( 111 | 'appId' => $this->accessToken['appid'], 112 | 'nonceStr' => $options['noncestr'], 113 | 'timestamp' => $options['timestamp'], 114 | 'signature' => $signature, 115 | 'jsApiList' => $this->api, 116 | 'debug' => (bool) $this->debug 117 | ); 118 | 119 | return $asArray ? $configure : Serializer::jsonEncode($configure); 120 | } 121 | 122 | /** 123 | * 输出对象 124 | */ 125 | public function __toString() 126 | { 127 | return $this->getConfig(); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/Wechat/Jsapi/Ticket.php: -------------------------------------------------------------------------------- 1 | accessToken = $accessToken; 32 | } 33 | 34 | /** 35 | * 获取 AccessToken 36 | */ 37 | public function getAccessToken() 38 | { 39 | return $this->accessToken; 40 | } 41 | 42 | /** 43 | * 获取 Jsapi 票据(调用缓存,返回 String) 44 | */ 45 | public function getTicketString() 46 | { 47 | $cacheId = $this->getCacheId(); 48 | 49 | if( $this->cache && $data = $this->cache->fetch($cacheId) ) { 50 | return $data['ticket']; 51 | } 52 | 53 | $response = $this->getTicketResponse(); 54 | 55 | if( $this->cache ) { 56 | $this->cache->save($cacheId, $response, $response['expires_in']); 57 | } 58 | 59 | return $response['ticket']; 60 | } 61 | 62 | /** 63 | * 获取 Jsapi 票据(不缓存,返回原始数据) 64 | */ 65 | public function getTicketResponse() 66 | { 67 | $response = Http::request('GET', static::JSAPI_TICKET) 68 | ->withAccessToken($this->accessToken) 69 | ->withQuery(array('type'=>'jsapi')) 70 | ->send(); 71 | 72 | if( $response['errcode'] != 0 ) { 73 | throw new \Exception($response['errmsg'], $response['errcode']); 74 | } 75 | 76 | return $response; 77 | } 78 | 79 | /** 80 | * 从缓存中清除 81 | */ 82 | public function clearFromCache() 83 | { 84 | return $this->cache 85 | ? $this->cache->delete($this->getCacheId()) 86 | : false; 87 | } 88 | 89 | /** 90 | * 获取缓存 ID 91 | */ 92 | public function getCacheId() 93 | { 94 | return sprintf('%s_jsapi_ticket', $this->accessToken['appid']); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/Wechat/Qrcode.php: -------------------------------------------------------------------------------- 1 | accessToken = $accessToken; 32 | } 33 | 34 | /** 35 | * 获取临时二维码 36 | */ 37 | public function getTemporary($scene, $expire = 2592000) 38 | { 39 | $ticket = new Ticket($this->accessToken, Ticket::QR_SCENE, $scene, $expire); 40 | 41 | if( $this->cache ) { 42 | $ticket->setCache($this->cache); 43 | } 44 | 45 | return $this->getResourceUrl($ticket); 46 | } 47 | 48 | /** 49 | * 获取永久二维码 50 | */ 51 | public function getForever($scene) 52 | { 53 | $type = is_int($scene) 54 | ? Ticket::QR_LIMIT_SCENE 55 | : Ticket::QR_LIMIT_STR_SCENE; 56 | 57 | $ticket = new Ticket($this->accessToken, $type, $scene); 58 | 59 | if( $this->cache ) { 60 | $ticket->setCache($this->cache); 61 | } 62 | 63 | return $this->getResourceUrl($ticket); 64 | } 65 | 66 | /** 67 | * 根据 Ticket 创建二维码资源链接 68 | */ 69 | public function getResourceUrl(Ticket $ticket) 70 | { 71 | $query = array('ticket' => $ticket->getTicketString()); 72 | 73 | return static::QRCODE_URL.'?'.http_build_query($query); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Wechat/Qrcode/Ticket.php: -------------------------------------------------------------------------------- 1 | 'integer', 60 | static::QR_LIMIT_SCENE => 'integer', 61 | static::QR_LIMIT_STR_SCENE => 'string' 62 | ); 63 | 64 | $type = strtoupper($type); 65 | 66 | if( !array_key_exists($type, $constraint) ) { 67 | throw new \InvalidArgumentException(sprintf('Invalid Qrcode Type: %s', $type)); 68 | } 69 | 70 | $callback = sprintf('is_%s', $constraint[$type]); 71 | 72 | if( !call_user_func($callback, $scene) ) { 73 | throw new \InvalidArgumentException(sprintf( 74 | 'parameter "scene" must be %s, %s given', $constraint[$type], gettype($scene))); 75 | } 76 | 77 | $this->type = $type; 78 | $this->scene = $scene; 79 | $this->sceneKey = (is_int($scene) ? 'scene_id' : 'scene_str'); 80 | $this->expire = $expire; 81 | $this->accessToken = $accessToken; 82 | } 83 | 84 | /** 85 | * 获取 Qrcode 票据(调用缓存,返回 String) 86 | */ 87 | public function getTicketString() 88 | { 89 | $cacheId = $this->getCacheId(); 90 | 91 | if( $this->cache && $data = $this->cache->fetch($cacheId) ) { 92 | return $data['ticket']; 93 | } 94 | 95 | $response = $this->getTicketResponse(); 96 | 97 | if( $this->cache ) { 98 | $this->cache->save($cacheId, $response, $response['expire_seconds'] ?: 0); 99 | } 100 | 101 | return $response['ticket']; 102 | } 103 | 104 | /** 105 | * 获取 Qrcode 票据(不缓存,返回原始数据) 106 | */ 107 | public function getTicketResponse() 108 | { 109 | $response = Http::request('POST', static::TICKET_URL) 110 | ->withAccessToken($this->accessToken) 111 | ->withBody($this->getRequestBody()) 112 | ->send(); 113 | 114 | if( $response['errcode'] != 0 ) { 115 | throw new \Exception($response['errmsg'], $response['errcode']); 116 | } 117 | 118 | return $response; 119 | } 120 | 121 | /** 122 | * 获取请求内容 123 | */ 124 | public function getRequestBody() 125 | { 126 | $options = array( 127 | 'action_name' => $this->type, 128 | 'action_info' => array( 129 | 'scene' => array($this->sceneKey=>$this->scene) 130 | ) 131 | ); 132 | 133 | if( $options['action_name'] === static::QR_SCENE ) { 134 | $options['expire_seconds'] = $this->expire; 135 | } 136 | 137 | return $options; 138 | } 139 | 140 | /** 141 | * 从缓存中清除 142 | */ 143 | public function clearFromCache() 144 | { 145 | return $this->cache 146 | ? $this->cache->delete($this->getCacheId()) 147 | : false; 148 | } 149 | 150 | /** 151 | * 获取缓存 ID 152 | */ 153 | public function getCacheId() 154 | { 155 | return implode('_', array($this->accessToken['appid'], $this->type, $this->sceneKey, $this->scene)); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/Wechat/ServerIp.php: -------------------------------------------------------------------------------- 1 | accessToken = $accessToken; 32 | } 33 | 34 | /** 35 | * 获取微信服务器 IP(默认缓存 1 天) 36 | */ 37 | public function getIps($cacheLifeTime = 86400) 38 | { 39 | $cacheId = $this->getCacheId(); 40 | 41 | if( $this->cache && $data = $this->cache->fetch($cacheId) ) { 42 | return $data['ip_list']; 43 | } 44 | 45 | $response = Http::request('GET', static::SERVER_IP) 46 | ->withAccessToken($this->accessToken) 47 | ->send(); 48 | 49 | if( $response->containsKey('errcode') ) { 50 | throw new \Exception($response['errmsg'], $response['errcode']); 51 | } 52 | 53 | if( $this->cache ) { 54 | $this->cache->save($cacheId, $response, $cacheLifeTime); 55 | } 56 | 57 | return $response['ip_list']; 58 | } 59 | 60 | /** 61 | * 从缓存中清除 62 | */ 63 | public function clearFromCache() 64 | { 65 | return $this->cache 66 | ? $this->cache->delete($this->getCacheId()) 67 | : false; 68 | } 69 | 70 | /** 71 | * 获取缓存 ID 72 | */ 73 | public function getCacheId() 74 | { 75 | return str_replace('\\', '_', strtolower(__CLASS__)); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Wechat/ShortUrl.php: -------------------------------------------------------------------------------- 1 | accessToken = $accessToken; 32 | } 33 | 34 | /** 35 | * 获取短链接 36 | */ 37 | public function toShort($longUrl, $cacheLifeTime = 86400) 38 | { 39 | $cacheId = md5($longUrl); 40 | 41 | if( $this->cache && $data = $this->cache->fetch($cacheId) ) { 42 | return $data; 43 | } 44 | 45 | $body = array( 46 | 'action' => 'long2short', 47 | 'long_url' => $longUrl 48 | ); 49 | 50 | $response = Http::request('POST', static::SHORT_URL) 51 | ->withAccessToken($this->accessToken) 52 | ->withBody($body) 53 | ->send(); 54 | 55 | if( $response['errcode'] != 0 ) { 56 | throw new \Exception($response['errmsg'], $response['errcode']); 57 | } 58 | 59 | if( $this->cache ) { 60 | $this->cache->save($cacheId, $response['short_url'], $cacheLifeTime); 61 | } 62 | 63 | return $response['short_url']; 64 | } 65 | } 66 | --------------------------------------------------------------------------------