Browse Source

first commit

master
wuliangbo 4 years ago
commit
9f80d5a2ac
  1. 8
      .idea/.gitignore
  2. 8
      .idea/base-wechat.iml
  3. 6
      .idea/misc.xml
  4. 8
      .idea/modules.xml
  5. 14
      .idea/php.xml
  6. 22
      LICENSE
  7. 1
      README.md
  8. 60
      composer.json
  9. 39
      src/BasicService/Application.php
  10. 123
      src/BasicService/ContentSecurity/Client.php
  11. 31
      src/BasicService/ContentSecurity/ServiceProvider.php
  12. 207
      src/BasicService/Jssdk/Client.php
  13. 33
      src/BasicService/Jssdk/ServiceProvider.php
  14. 212
      src/BasicService/Media/Client.php
  15. 44
      src/BasicService/Media/ServiceProvider.php
  16. 120
      src/BasicService/QrCode/Client.php
  17. 31
      src/BasicService/QrCode/ServiceProvider.php
  18. 47
      src/BasicService/Url/Client.php
  19. 31
      src/BasicService/Url/ServiceProvider.php
  20. 54
      src/Factory.php
  21. 277
      src/Kernel/AccessToken.php
  22. 271
      src/Kernel/BaseClient.php
  23. 64
      src/Kernel/Clauses/Clause.php
  24. 23
      src/Kernel/Config.php
  25. 40
      src/Kernel/Contracts/AccessTokenInterface.php
  26. 29
      src/Kernel/Contracts/Arrayable.php
  27. 25
      src/Kernel/Contracts/EventHandlerInterface.php
  28. 25
      src/Kernel/Contracts/MediaInterface.php
  29. 35
      src/Kernel/Contracts/MessageInterface.php
  30. 35
      src/Kernel/Decorators/FinallyResult.php
  31. 35
      src/Kernel/Decorators/TerminateResult.php
  32. 219
      src/Kernel/Encryptor.php
  33. 35
      src/Kernel/Events/AccessTokenRefreshed.php
  34. 35
      src/Kernel/Events/ApplicationInitialized.php
  35. 35
      src/Kernel/Events/HttpResponseCreated.php
  36. 35
      src/Kernel/Events/ServerGuardResponseCreated.php
  37. 21
      src/Kernel/Exceptions/BadRequestException.php
  38. 16
      src/Kernel/Exceptions/DecryptException.php
  39. 23
      src/Kernel/Exceptions/Exception.php
  40. 52
      src/Kernel/Exceptions/HttpException.php
  41. 21
      src/Kernel/Exceptions/InvalidArgumentException.php
  42. 21
      src/Kernel/Exceptions/InvalidConfigException.php
  43. 21
      src/Kernel/Exceptions/RuntimeException.php
  44. 21
      src/Kernel/Exceptions/UnboundServiceException.php
  45. 57
      src/Kernel/Helpers.php
  46. 121
      src/Kernel/Http/Response.php
  47. 86
      src/Kernel/Http/StreamResponse.php
  48. 608
      src/Kernel/Log/LogManager.php
  49. 58
      src/Kernel/Messages/Article.php
  50. 52
      src/Kernel/Messages/Card.php
  51. 40
      src/Kernel/Messages/DeviceEvent.php
  52. 50
      src/Kernel/Messages/DeviceText.php
  53. 25
      src/Kernel/Messages/File.php
  54. 27
      src/Kernel/Messages/Image.php
  55. 36
      src/Kernel/Messages/Link.php
  56. 38
      src/Kernel/Messages/Location.php
  57. 70
      src/Kernel/Messages/Media.php
  58. 208
      src/Kernel/Messages/Message.php
  59. 31
      src/Kernel/Messages/MiniProgramPage.php
  60. 73
      src/Kernel/Messages/Music.php
  61. 73
      src/Kernel/Messages/News.php
  62. 57
      src/Kernel/Messages/NewsItem.php
  63. 56
      src/Kernel/Messages/Raw.php
  64. 30
      src/Kernel/Messages/ShortVideo.php
  65. 44
      src/Kernel/Messages/TaskCard.php
  66. 54
      src/Kernel/Messages/Text.php
  67. 40
      src/Kernel/Messages/TextCard.php
  68. 56
      src/Kernel/Messages/Transfer.php
  69. 65
      src/Kernel/Messages/Video.php
  70. 37
      src/Kernel/Messages/Voice.php
  71. 39
      src/Kernel/Providers/ConfigServiceProvider.php
  72. 47
      src/Kernel/Providers/EventDispatcherServiceProvider.php
  73. 39
      src/Kernel/Providers/ExtensionServiceProvider.php
  74. 39
      src/Kernel/Providers/HttpClientServiceProvider.php
  75. 79
      src/Kernel/Providers/LogServiceProvider.php
  76. 39
      src/Kernel/Providers/RequestServiceProvider.php
  77. 375
      src/Kernel/ServerGuard.php
  78. 167
      src/Kernel/ServiceContainer.php
  79. 85
      src/Kernel/Support/AES.php
  80. 466
      src/Kernel/Support/Arr.php
  81. 66
      src/Kernel/Support/ArrayAccessible.php
  82. 421
      src/Kernel/Support/Collection.php
  83. 135
      src/Kernel/Support/File.php
  84. 131
      src/Kernel/Support/Helpers.php
  85. 193
      src/Kernel/Support/Str.php
  86. 167
      src/Kernel/Support/XML.php
  87. 251
      src/Kernel/Traits/HasAttributes.php
  88. 231
      src/Kernel/Traits/HasHttpRequests.php
  89. 105
      src/Kernel/Traits/InteractsWithCache.php
  90. 285
      src/Kernel/Traits/Observable.php
  91. 93
      src/Kernel/Traits/ResponseCastable.php
  92. 173
      src/MicroMerchant/Application.php
  93. 126
      src/MicroMerchant/Base/Client.php
  94. 33
      src/MicroMerchant/Base/ServiceProvider.php
  95. 93
      src/MicroMerchant/Certficates/Client.php
  96. 33
      src/MicroMerchant/Certficates/ServiceProvider.php
  97. 256
      src/MicroMerchant/Kernel/BaseClient.php
  98. 23
      src/MicroMerchant/Kernel/Exceptions/EncryptException.php
  99. 23
      src/MicroMerchant/Kernel/Exceptions/InvalidExtensionException.php
  100. 23
      src/MicroMerchant/Kernel/Exceptions/InvalidSignException.php
  101. Some files were not shown because too many files have changed in this diff Show More

8
.idea/.gitignore vendored

@ -0,0 +1,8 @@ @@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
# Editor-based HTTP Client requests
/httpRequests/

8
.idea/base-wechat.iml

@ -0,0 +1,8 @@ @@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

6
.idea/misc.xml

@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptSettings">
<option name="languageLevel" value="ES6" />
</component>
</project>

8
.idea/modules.xml

@ -0,0 +1,8 @@ @@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/base-wechat.iml" filepath="$PROJECT_DIR$/.idea/base-wechat.iml" />
</modules>
</component>
</project>

14
.idea/php.xml

@ -0,0 +1,14 @@ @@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="PhpCSFixer">
<phpcsfixer_settings>
<PhpCSFixerConfiguration tool_path="$PROJECT_DIR$/vendor/bin/php-cs-fixer" />
</phpcsfixer_settings>
</component>
<component name="PhpProjectSharedConfiguration" php_language_level="7.2" />
<component name="PhpUnit">
<phpunit_settings>
<PhpUnitSettings custom_loader_path="$PROJECT_DIR$/vendor/autoload.php" />
</phpunit_settings>
</component>
</project>

22
LICENSE

@ -0,0 +1,22 @@ @@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) overtrue <i@overtrue.me>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

1
README.md

@ -0,0 +1 @@ @@ -0,0 +1 @@
基于 [overtrue/wechat](https://github.com/overtrue/wechat) 二次修改版 自用

60
composer.json

@ -0,0 +1,60 @@ @@ -0,0 +1,60 @@
{
"name": "wuearl/base-wechat",
"description": "微信SDK",
"keywords": [
"easywechat",
"wechat",
"weixin",
"weixin-sdk",
"sdk"
],
"license": "MIT",
"authors": [
{
"name": "overtrue",
"email": "anzhengchao@gmail.com"
}
],
"require": {
"php": ">=7.2",
"ext-fileinfo": "*",
"ext-openssl": "*",
"ext-simplexml": "*",
"easywechat-composer/easywechat-composer": "^1.1",
"guzzlehttp/guzzle": "^6.2",
"monolog/monolog": "^1.22 || ^2.0",
"overtrue/socialite": "~2.0",
"pimple/pimple": "^3.0",
"psr/simple-cache": "^1.0",
"symfony/cache": "^3.3 || ^4.3 || ^5.0",
"symfony/event-dispatcher": "^4.3 || ^5.0",
"symfony/http-foundation": "^2.7 || ^3.0 || ^4.0 || ^5.0",
"symfony/psr-http-message-bridge": "^0.3 || ^1.0 || ^2.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^2.15",
"mikey179/vfsstream": "^1.6",
"mockery/mockery": "^1.2.3",
"phpstan/phpstan": "^0.12.0",
"phpunit/phpunit": "^7.5"
},
"autoload": {
"psr-4": {
"EasyWeChat\\": "src/"
},
"files": [
"src/Kernel/Support/Helpers.php",
"src/Kernel/Helpers.php"
]
},
"autoload-dev": {
"psr-4": {
"EasyWeChat\\Tests\\": "tests/"
}
},
"scripts": {
"phpcs": "vendor/bin/php-cs-fixer fix",
"phpstan": "vendor/bin/phpstan analyse",
"test": "vendor/bin/phpunit"
}
}

39
src/BasicService/Application.php

@ -0,0 +1,39 @@ @@ -0,0 +1,39 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\BasicService;
use EasyWeChat\Kernel\ServiceContainer;
/**
* Class Application.
*
* @author overtrue <i@overtrue.me>
*
* @property \EasyWeChat\BasicService\Jssdk\Client $jssdk
* @property \EasyWeChat\BasicService\Media\Client $media
* @property \EasyWeChat\BasicService\QrCode\Client $qrcode
* @property \EasyWeChat\BasicService\Url\Client $url
* @property \EasyWeChat\BasicService\ContentSecurity\Client $content_security
*/
class Application extends ServiceContainer
{
/**
* @var array
*/
protected $providers = [
Jssdk\ServiceProvider::class,
QrCode\ServiceProvider::class,
Media\ServiceProvider::class,
Url\ServiceProvider::class,
ContentSecurity\ServiceProvider::class,
];
}

123
src/BasicService/ContentSecurity/Client.php

@ -0,0 +1,123 @@ @@ -0,0 +1,123 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\BasicService\ContentSecurity;
use EasyWeChat\Kernel\BaseClient;
use EasyWeChat\Kernel\Exceptions\InvalidArgumentException;
/**
* Class Client.
*
* @author tianyong90 <412039588@qq.com>
*/
class Client extends BaseClient
{
/**
* @var string
*/
protected $baseUri = 'https://api.weixin.qq.com/wxa/';
/**
* Text content security check.
*
* @param string $text
*
* @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string
*
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function checkText(string $text)
{
$params = [
'content' => $text,
];
return $this->httpPostJson('msg_sec_check', $params);
}
/**
* Image security check.
*
* @param string $path
*
* @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string
*
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function checkImage(string $path)
{
return $this->httpUpload('img_sec_check', ['media' => $path]);
}
/**
* Media security check.
*
* @param string $mediaUrl
* @param int $mediaType
*
* @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string
*
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
* @throws \GuzzleHttp\Exception\GuzzleException
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
*/
public function checkMediaAsync(string $mediaUrl, int $mediaType)
{
/*
* 1:音频;2:图片
*/
$mediaTypes = [1, 2];
if (!in_array($mediaType, $mediaTypes, true)) {
throw new InvalidArgumentException('media type must be 1 or 2');
}
$params = [
'media_url' => $mediaUrl,
'media_type' => $mediaType,
];
return $this->httpPostJson('media_check_async', $params);
}
/**
* Image security check async.
*
* @param string $mediaUrl
*
* @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string
*
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function checkImageAsync(string $mediaUrl)
{
return $this->checkMediaAsync($mediaUrl, 2);
}
/**
* Audio security check async.
*
* @param string $mediaUrl
*
* @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string
*
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function checkAudioAsync(string $mediaUrl)
{
return $this->checkMediaAsync($mediaUrl, 1);
}
}

31
src/BasicService/ContentSecurity/ServiceProvider.php

@ -0,0 +1,31 @@ @@ -0,0 +1,31 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\BasicService\ContentSecurity;
use Pimple\Container;
use Pimple\ServiceProviderInterface;
/**
* Class ServiceProvider.
*/
class ServiceProvider implements ServiceProviderInterface
{
/**
* {@inheritdoc}.
*/
public function register(Container $app)
{
$app['content_security'] = function ($app) {
return new Client($app);
};
}
}

207
src/BasicService/Jssdk/Client.php

@ -0,0 +1,207 @@ @@ -0,0 +1,207 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\BasicService\Jssdk;
use EasyWeChat\Kernel\BaseClient;
use EasyWeChat\Kernel\Exceptions\RuntimeException;
use EasyWeChat\Kernel\Support;
use EasyWeChat\Kernel\Traits\InteractsWithCache;
/**
* Class Client.
*
* @author overtrue <i@overtrue.me>
*/
class Client extends BaseClient
{
use InteractsWithCache;
/**
* @var string
*/
protected $ticketEndpoint = 'https://api.weixin.qq.com/cgi-bin/ticket/getticket';
/**
* Current URI.
*
* @var string
*/
protected $url;
/**
* Get config json for jsapi.
*
* @param array $jsApiList
* @param bool $debug
* @param bool $beta
* @param bool $json
*
* @return array|string
*
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
* @throws \Psr\SimpleCache\InvalidArgumentException
* @throws \EasyWeChat\Kernel\Exceptions\RuntimeException
*/
public function buildConfig(array $jsApiList, bool $debug = false, bool $beta = false, bool $json = true)
{
$config = array_merge(compact('debug', 'beta', 'jsApiList'), $this->configSignature());
return $json ? json_encode($config) : $config;
}
/**
* Return jsapi config as a PHP array.
*
* @param array $apis
* @param bool $debug
* @param bool $beta
*
* @return array
*
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
* @throws \Psr\SimpleCache\InvalidArgumentException
* @throws \EasyWeChat\Kernel\Exceptions\RuntimeException
*/
public function getConfigArray(array $apis, bool $debug = false, bool $beta = false)
{
return $this->buildConfig($apis, $debug, $beta, false);
}
/**
* Get js ticket.
*
* @param bool $refresh
* @param string $type
*
* @return array
*
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
* @throws \EasyWeChat\Kernel\Exceptions\RuntimeException
* @throws \GuzzleHttp\Exception\GuzzleException
* @throws \Psr\SimpleCache\InvalidArgumentException
*/
public function getTicket(bool $refresh = false, string $type = 'jsapi'): array
{
$cacheKey = sprintf('easywechat.basic_service.jssdk.ticket.%s.%s', $type, $this->getAppId());
if (!$refresh && $this->getCache()->has($cacheKey)) {
return $this->getCache()->get($cacheKey);
}
/** @var array<string, mixed> $result */
$result = $this->castResponseToType(
$this->requestRaw($this->ticketEndpoint, 'GET', ['query' => ['type' => $type]]),
'array'
);
$this->getCache()->set($cacheKey, $result, $result['expires_in'] - 500);
if (!$this->getCache()->has($cacheKey)) {
throw new RuntimeException('Failed to cache jssdk ticket.');
}
return $result;
}
/**
* Build signature.
*
* @param string|null $url
* @param string|null $nonce
* @param int|null $timestamp
*
* @return array
*
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
* @throws \EasyWeChat\Kernel\Exceptions\RuntimeException
* @throws \Psr\SimpleCache\InvalidArgumentException
*/
protected function configSignature(string $url = null, string $nonce = null, $timestamp = null): array
{
$url = $url ?: $this->getUrl();
$nonce = $nonce ?: Support\Str::quickRandom(10);
$timestamp = $timestamp ?: time();
return [
'appId' => $this->getAppId(),
'nonceStr' => $nonce,
'timestamp' => $timestamp,
'url' => $url,
'signature' => $this->getTicketSignature($this->getTicket()['ticket'], $nonce, $timestamp, $url),
];
}
/**
* Sign the params.
*
* @param string $ticket
* @param string $nonce
* @param int $timestamp
* @param string $url
*
* @return string
*/
public function getTicketSignature($ticket, $nonce, $timestamp, $url): string
{
return sha1(sprintf('jsapi_ticket=%s&noncestr=%s&timestamp=%s&url=%s', $ticket, $nonce, $timestamp, $url));
}
/**
* @return string
*/
public function dictionaryOrderSignature()
{
$params = func_get_args();
sort($params, SORT_STRING);
return sha1(implode('', $params));
}
/**
* Set current url.
*
* @param string $url
*
* @return $this
*/
public function setUrl(string $url)
{
$this->url = $url;
return $this;
}
/**
* Get current url.
*
* @return string
*/
public function getUrl(): string
{
if ($this->url) {
return $this->url;
}
return Support\current_url();
}
/**
* @return string
*/
protected function getAppId()
{
return $this->app['config']->get('app_id');
}
}

33
src/BasicService/Jssdk/ServiceProvider.php

@ -0,0 +1,33 @@ @@ -0,0 +1,33 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\BasicService\Jssdk;
use Pimple\Container;
use Pimple\ServiceProviderInterface;
/**
* Class ServiceProvider.
*
* @author overtrue <i@overtrue.me>
*/
class ServiceProvider implements ServiceProviderInterface
{
/**
* {@inheritdoc}.
*/
public function register(Container $app)
{
$app['jssdk'] = function ($app) {
return new Client($app);
};
}
}

212
src/BasicService/Media/Client.php

@ -0,0 +1,212 @@ @@ -0,0 +1,212 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\BasicService\Media;
use EasyWeChat\Kernel\BaseClient;
use EasyWeChat\Kernel\Exceptions\InvalidArgumentException;
use EasyWeChat\Kernel\Http\StreamResponse;
/**
* Class Client.
*
* @author overtrue <i@overtrue.me>
*/
class Client extends BaseClient
{
/**
* @var string
*/
protected $baseUri = 'https://api.weixin.qq.com/cgi-bin/';
/**
* Allow media type.
*
* @var array
*/
protected $allowTypes = ['image', 'voice', 'video', 'thumb'];
/**
* Upload image.
*
* @param string $path
*
* @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string
*
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function uploadImage($path)
{
return $this->upload('image', $path);
}
/**
* Upload video.
*
* @param string $path
*
* @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string
*
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function uploadVideo($path)
{
return $this->upload('video', $path);
}
/**
* @param string $path
*
* @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string
*
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function uploadVoice($path)
{
return $this->upload('voice', $path);
}
/**
* @param string $path
*
* @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string
*
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function uploadThumb($path)
{
return $this->upload('thumb', $path);
}
/**
* Upload temporary material.
*
* @param string $type
* @param string $path
*
* @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string
*
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function upload(string $type, string $path)
{
if (!file_exists($path) || !is_readable($path)) {
throw new InvalidArgumentException(sprintf("File does not exist, or the file is unreadable: '%s'", $path));
}
if (!in_array($type, $this->allowTypes, true)) {
throw new InvalidArgumentException(sprintf("Unsupported media type: '%s'", $type));
}
return $this->httpUpload('media/upload', ['media' => $path], ['type' => $type]);
}
/**
* @param string $path
* @param string $title
* @param string $description
*
* @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string
*
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function uploadVideoForBroadcasting(string $path, string $title, string $description)
{
$response = $this->uploadVideo($path);
/** @var array $arrayResponse */
$arrayResponse = $this->detectAndCastResponseToType($response, 'array');
if (!empty($arrayResponse['media_id'])) {
return $this->createVideoForBroadcasting($arrayResponse['media_id'], $title, $description);
}
return $response;
}
/**
* @param string $mediaId
* @param string $title
* @param string $description
*
* @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string
*
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function createVideoForBroadcasting(string $mediaId, string $title, string $description)
{
return $this->httpPostJson('media/uploadvideo', [
'media_id' => $mediaId,
'title' => $title,
'description' => $description,
]);
}
/**
* Fetch item from WeChat server.
*
* @param string $mediaId
*
* @return \EasyWeChat\Kernel\Http\StreamResponse|\Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string
*
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function get(string $mediaId)
{
$response = $this->requestRaw('media/get', 'GET', [
'query' => [
'media_id' => $mediaId,
],
]);
if (false !== stripos($response->getHeaderLine('Content-disposition'), 'attachment')) {
return StreamResponse::buildFromPsrResponse($response);
}
return $this->castResponseToType($response, $this->app['config']->get('response_type'));
}
/**
* @param string $mediaId
*
* @return array|\EasyWeChat\Kernel\Http\Response|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string
*
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function getJssdkMedia(string $mediaId)
{
$response = $this->requestRaw('media/get/jssdk', 'GET', [
'query' => [
'media_id' => $mediaId,
],
]);
if (false !== stripos($response->getHeaderLine('Content-disposition'), 'attachment')) {
return StreamResponse::buildFromPsrResponse($response);
}
return $this->castResponseToType($response, $this->app['config']->get('response_type'));
}
}

44
src/BasicService/Media/ServiceProvider.php

@ -0,0 +1,44 @@ @@ -0,0 +1,44 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
/**
* ServiceProvider.php.
*
* This file is part of the wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\BasicService\Media;
use Pimple\Container;
use Pimple\ServiceProviderInterface;
/**
* Class ServiceProvider.
*
* @author overtrue <i@overtrue.me>
*/
class ServiceProvider implements ServiceProviderInterface
{
/**
* {@inheritdoc}.
*/
public function register(Container $app)
{
$app['media'] = function ($app) {
return new Client($app);
};
}
}

120
src/BasicService/QrCode/Client.php

@ -0,0 +1,120 @@ @@ -0,0 +1,120 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\BasicService\QrCode;
use EasyWeChat\Kernel\BaseClient;
/**
* Class Client.
*
* @author overtrue <i@overtrue.me>
*/
class Client extends BaseClient
{
/**
* @var string
*/
protected $baseUri = 'https://api.weixin.qq.com/cgi-bin/';
const DAY = 86400;
const SCENE_MAX_VALUE = 100000;
const SCENE_QR_CARD = 'QR_CARD';
const SCENE_QR_TEMPORARY = 'QR_SCENE';
const SCENE_QR_TEMPORARY_STR = 'QR_STR_SCENE';
const SCENE_QR_FOREVER = 'QR_LIMIT_SCENE';
const SCENE_QR_FOREVER_STR = 'QR_LIMIT_STR_SCENE';
/**
* Create forever QR code.
*
* @param string|int $sceneValue
*
* @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string
*/
public function forever($sceneValue)
{
if (is_int($sceneValue) && $sceneValue > 0 && $sceneValue < self::SCENE_MAX_VALUE) {
$type = self::SCENE_QR_FOREVER;
$sceneKey = 'scene_id';
} else {
$type = self::SCENE_QR_FOREVER_STR;
$sceneKey = 'scene_str';
}
$scene = [$sceneKey => $sceneValue];
return $this->create($type, $scene, false);
}
/**
* Create temporary QR code.
*
* @param string|int $sceneValue
* @param int|null $expireSeconds
*
* @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string
*/
public function temporary($sceneValue, $expireSeconds = null)
{
if (is_int($sceneValue) && $sceneValue > 0) {
$type = self::SCENE_QR_TEMPORARY;
$sceneKey = 'scene_id';
} else {
$type = self::SCENE_QR_TEMPORARY_STR;
$sceneKey = 'scene_str';
}
$scene = [$sceneKey => $sceneValue];
return $this->create($type, $scene, true, $expireSeconds);
}
/**
* Return url for ticket.
* Detail: https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1443433542 .
*
* @param string $ticket
*
* @return string
*/
public function url($ticket)
{
return sprintf('https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=%s', urlencode($ticket));
}
/**
* Create a QrCode.
*
* @param string $actionName
* @param array $actionInfo
* @param bool $temporary
* @param int $expireSeconds
*
* @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string
*
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
* @throws \GuzzleHttp\Exception\GuzzleException
*/
protected function create($actionName, $actionInfo, $temporary = true, $expireSeconds = null)
{
null !== $expireSeconds || $expireSeconds = 7 * self::DAY;
$params = [
'action_name' => $actionName,
'action_info' => ['scene' => $actionInfo],
];
if ($temporary) {
$params['expire_seconds'] = min($expireSeconds, 30 * self::DAY);
}
return $this->httpPostJson('qrcode/create', $params);
}
}

31
src/BasicService/QrCode/ServiceProvider.php

@ -0,0 +1,31 @@ @@ -0,0 +1,31 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\BasicService\QrCode;
use Pimple\Container;
use Pimple\ServiceProviderInterface;
/**
* Class ServiceProvider.
*/
class ServiceProvider implements ServiceProviderInterface
{
/**
* {@inheritdoc}.
*/
public function register(Container $app)
{
$app['qrcode'] = function ($app) {
return new Client($app);
};
}
}

47
src/BasicService/Url/Client.php

@ -0,0 +1,47 @@ @@ -0,0 +1,47 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\BasicService\Url;
use EasyWeChat\Kernel\BaseClient;
/**
* Class Client.
*
* @author overtrue <i@overtrue.me>
*/
class Client extends BaseClient
{
/**
* @var string
*/
protected $baseUri = 'https://api.weixin.qq.com/';
/**
* Shorten the url.
*
* @param string $url
*
* @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string
*
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function shorten(string $url)
{
$params = [
'action' => 'long2short',
'long_url' => $url,
];
return $this->httpPostJson('cgi-bin/shorturl', $params);
}
}

31
src/BasicService/Url/ServiceProvider.php

@ -0,0 +1,31 @@ @@ -0,0 +1,31 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\BasicService\Url;
use Pimple\Container;
use Pimple\ServiceProviderInterface;
/**
* Class ServiceProvider.
*/
class ServiceProvider implements ServiceProviderInterface
{
/**
* {@inheritdoc}.
*/
public function register(Container $app)
{
$app['url'] = function ($app) {
return new Client($app);
};
}
}

54
src/Factory.php

@ -0,0 +1,54 @@ @@ -0,0 +1,54 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat;
/**
* Class Factory.
*
* @method static \EasyWeChat\Payment\Application payment(array $config)
* @method static \EasyWeChat\MiniProgram\Application miniProgram(array $config)
* @method static \EasyWeChat\OpenPlatform\Application openPlatform(array $config)
* @method static \EasyWeChat\OfficialAccount\Application officialAccount(array $config)
* @method static \EasyWeChat\BasicService\Application basicService(array $config)
* @method static \EasyWeChat\Work\Application work(array $config)
* @method static \EasyWeChat\OpenWork\Application openWork(array $config)
* @method static \EasyWeChat\MicroMerchant\Application microMerchant(array $config)
*/
class Factory
{
/**
* @param string $name
* @param array $config
*
* @return \EasyWeChat\Kernel\ServiceContainer
*/
public static function make($name, array $config)
{
$namespace = Kernel\Support\Str::studly($name);
$application = "\\EasyWeChat\\{$namespace}\\Application";
return new $application($config);
}
/**
* Dynamically pass methods to the application.
*
* @param string $name
* @param array $arguments
*
* @return mixed
*/
public static function __callStatic($name, $arguments)
{
return self::make($name, ...$arguments);
}
}

277
src/Kernel/AccessToken.php

@ -0,0 +1,277 @@ @@ -0,0 +1,277 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\Kernel;
use EasyWeChat\Kernel\Contracts\AccessTokenInterface;
use EasyWeChat\Kernel\Exceptions\HttpException;
use EasyWeChat\Kernel\Exceptions\InvalidArgumentException;
use EasyWeChat\Kernel\Exceptions\RuntimeException;
use EasyWeChat\Kernel\Traits\HasHttpRequests;
use EasyWeChat\Kernel\Traits\InteractsWithCache;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* Class AccessToken.
*
* @author overtrue <i@overtrue.me>
*/
abstract class AccessToken implements AccessTokenInterface
{
use HasHttpRequests;
use InteractsWithCache;
/**
* @var \EasyWeChat\Kernel\ServiceContainer
*/
protected $app;
/**
* @var string
*/
protected $requestMethod = 'GET';
/**
* @var string
*/
protected $endpointToGetToken;
/**
* @var string
*/
protected $queryName;
/**
* @var array
*/
protected $token;
/**
* @var string
*/
protected $tokenKey = 'access_token';
/**
* @var string
*/
protected $cachePrefix = 'easywechat.kernel.access_token.';
/**
* AccessToken constructor.
*
* @param \EasyWeChat\Kernel\ServiceContainer $app
*/
public function __construct(ServiceContainer $app)
{
$this->app = $app;
}
/**
* @return array
*
* @throws \EasyWeChat\Kernel\Exceptions\HttpException
* @throws \Psr\SimpleCache\InvalidArgumentException
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
* @throws \EasyWeChat\Kernel\Exceptions\RuntimeException
*/
public function getRefreshedToken(): array
{
return $this->getToken(true);
}
/**
* @param bool $refresh
*
* @return array
*
* @throws \EasyWeChat\Kernel\Exceptions\HttpException
* @throws \Psr\SimpleCache\InvalidArgumentException
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
* @throws \EasyWeChat\Kernel\Exceptions\RuntimeException
*/
public function getToken(bool $refresh = false): array
{
$cacheKey = $this->getCacheKey();
$cache = $this->getCache();
if (!$refresh && $cache->has($cacheKey)) {
return $cache->get($cacheKey);
}
/** @var array $token */
$token = $this->requestToken($this->getCredentials(), true);
$this->setToken($token[$this->tokenKey], $token['expires_in'] ?? 7200);
$this->app->events->dispatch(new Events\AccessTokenRefreshed($this));
return $token;
}
/**
* @param string $token
* @param int $lifetime
*
* @return \EasyWeChat\Kernel\Contracts\AccessTokenInterface
*
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
* @throws \EasyWeChat\Kernel\Exceptions\RuntimeException
* @throws \Psr\SimpleCache\InvalidArgumentException
*/
public function setToken(string $token, int $lifetime = 7200): AccessTokenInterface
{
$this->getCache()->set($this->getCacheKey(), [
$this->tokenKey => $token,
'expires_in' => $lifetime,
], $lifetime);
if (!$this->getCache()->has($this->getCacheKey())) {
throw new RuntimeException('Failed to cache access token.');
}
return $this;
}
/**
* @return \EasyWeChat\Kernel\Contracts\AccessTokenInterface
*
* @throws \EasyWeChat\Kernel\Exceptions\HttpException
* @throws \Psr\SimpleCache\InvalidArgumentException
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
* @throws \EasyWeChat\Kernel\Exceptions\RuntimeException
*/
public function refresh(): AccessTokenInterface
{
$this->getToken(true);
return $this;
}
/**
* @param array $credentials
* @param bool $toArray
*
* @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string
*
* @throws \EasyWeChat\Kernel\Exceptions\HttpException
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
*/
public function requestToken(array $credentials, $toArray = false)
{
$response = $this->sendRequest($credentials);
$result = json_decode($response->getBody()->getContents(), true);
$formatted = $this->castResponseToType($response, $this->app['config']->get('response_type'));
if (empty($result[$this->tokenKey])) {
throw new HttpException('Request access_token fail: '.json_encode($result, JSON_UNESCAPED_UNICODE), $response, $formatted);
}
return $toArray ? $result : $formatted;
}
/**
* @param \Psr\Http\Message\RequestInterface $request
* @param array $requestOptions
*
* @return \Psr\Http\Message\RequestInterface
*
* @throws \EasyWeChat\Kernel\Exceptions\HttpException
* @throws \Psr\SimpleCache\InvalidArgumentException
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
* @throws \EasyWeChat\Kernel\Exceptions\RuntimeException
*/
public function applyToRequest(RequestInterface $request, array $requestOptions = []): RequestInterface
{
parse_str($request->getUri()->getQuery(), $query);
$query = http_build_query(array_merge($this->getQuery(), $query));
return $request->withUri($request->getUri()->withQuery($query));
}
/**
* Send http request.
*
* @param array $credentials
*
* @return ResponseInterface
*
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
* @throws \GuzzleHttp\Exception\GuzzleException
*/
protected function sendRequest(array $credentials): ResponseInterface
{
$options = [
('GET' === $this->requestMethod) ? 'query' : 'json' => $credentials,
];
return $this->setHttpClient($this->app['http_client'])->request($this->getEndpoint(), $this->requestMethod, $options);
}
/**
* @return string
*/
protected function getCacheKey()
{
return $this->cachePrefix.md5(json_encode($this->getCredentials()));
}
/**
* The request query will be used to add to the request.
*
* @return array
*
* @throws \EasyWeChat\Kernel\Exceptions\HttpException
* @throws \Psr\SimpleCache\InvalidArgumentException
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
* @throws \EasyWeChat\Kernel\Exceptions\RuntimeException
*/
protected function getQuery(): array
{
return [$this->queryName ?? $this->tokenKey => $this->getToken()[$this->tokenKey]];
}
/**
* @return string
*
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
*/
public function getEndpoint(): string
{
if (empty($this->endpointToGetToken)) {
throw new InvalidArgumentException('No endpoint for access token request.');
}
return $this->endpointToGetToken;
}
/**
* @return string
*/
public function getTokenKey()
{
return $this->tokenKey;
}
/**
* Credential for get token.
*
* @return array
*/
abstract protected function getCredentials(): array;
}

271
src/Kernel/BaseClient.php

@ -0,0 +1,271 @@ @@ -0,0 +1,271 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\Kernel;
use EasyWeChat\Kernel\Contracts\AccessTokenInterface;
use EasyWeChat\Kernel\Http\Response;
use EasyWeChat\Kernel\Traits\HasHttpRequests;
use GuzzleHttp\MessageFormatter;
use GuzzleHttp\Middleware;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Log\LogLevel;
/**
* Class BaseClient.
*
* @author overtrue <i@overtrue.me>
*/
class BaseClient
{
use HasHttpRequests { request as performRequest; }
/**
* @var \EasyWeChat\Kernel\ServiceContainer
*/
protected $app;
/**
* @var \EasyWeChat\Kernel\Contracts\AccessTokenInterface
*/
protected $accessToken;
/**
* @var string
*/
protected $baseUri;
/**
* BaseClient constructor.
*
* @param \EasyWeChat\Kernel\ServiceContainer $app
* @param \EasyWeChat\Kernel\Contracts\AccessTokenInterface|null $accessToken
*/
public function __construct(ServiceContainer $app, AccessTokenInterface $accessToken = null)
{
$this->app = $app;
$this->accessToken = $accessToken ?? $this->app['access_token'];
}
/**
* GET request.
*
* @param string $url
* @param array $query
*
* @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string
*
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function httpGet(string $url, array $query = [])
{
return $this->request($url, 'GET', ['query' => $query]);
}
/**
* POST request.
*
* @param string $url
* @param array $data
*
* @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string
*
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function httpPost(string $url, array $data = [])
{
return $this->request($url, 'POST', ['form_params' => $data]);
}
/**
* JSON request.
*
* @param string $url
* @param array $data
* @param array $query
*
* @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string
*
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function httpPostJson(string $url, array $data = [], array $query = [])
{
return $this->request($url, 'POST', ['query' => $query, 'json' => $data]);
}
/**
* Upload file.
*
* @param string $url
* @param array $files
* @param array $form
* @param array $query
*
* @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string
*
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function httpUpload(string $url, array $files = [], array $form = [], array $query = [])
{
$multipart = [];
foreach ($files as $name => $path) {
$multipart[] = [
'name' => $name,
'contents' => fopen($path, 'r'),
];
}
foreach ($form as $name => $contents) {
$multipart[] = compact('name', 'contents');
}
return $this->request($url, 'POST', ['query' => $query, 'multipart' => $multipart, 'connect_timeout' => 30, 'timeout' => 30, 'read_timeout' => 30]);
}
/**
* @return AccessTokenInterface
*/
public function getAccessToken(): AccessTokenInterface
{
return $this->accessToken;
}
/**
* @param \EasyWeChat\Kernel\Contracts\AccessTokenInterface $accessToken
*
* @return $this
*/
public function setAccessToken(AccessTokenInterface $accessToken)
{
$this->accessToken = $accessToken;
return $this;
}
/**
* @param string $url
* @param string $method
* @param array $options
* @param bool $returnRaw
*
* @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string
*
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function request(string $url, string $method = 'GET', array $options = [], $returnRaw = false)
{
if (empty($this->middlewares)) {
$this->registerHttpMiddlewares();
}
$response = $this->performRequest($url, $method, $options);
$this->app->events->dispatch(new Events\HttpResponseCreated($response));
return $returnRaw ? $response : $this->castResponseToType($response, $this->app->config->get('response_type'));
}
/**
* @param string $url
* @param string $method
* @param array $options
*
* @return \EasyWeChat\Kernel\Http\Response
*
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function requestRaw(string $url, string $method = 'GET', array $options = [])
{
return Response::buildFromPsrResponse($this->request($url, $method, $options, true));
}
/**
* Register Guzzle middlewares.
*/
protected function registerHttpMiddlewares()
{
// retry
$this->pushMiddleware($this->retryMiddleware(), 'retry');
// access token
$this->pushMiddleware($this->accessTokenMiddleware(), 'access_token');
// log
$this->pushMiddleware($this->logMiddleware(), 'log');
}
/**
* Attache access token to request query.
*
* @return \Closure
*/
protected function accessTokenMiddleware()
{
return function (callable $handler) {
return function (RequestInterface $request, array $options) use ($handler) {
if ($this->accessToken) {
$request = $this->accessToken->applyToRequest($request, $options);
}
return $handler($request, $options);
};
};
}
/**
* Log the request.
*
* @return \Closure
*/
protected function logMiddleware()
{
$formatter = new MessageFormatter($this->app['config']['http.log_template'] ?? MessageFormatter::DEBUG);
return Middleware::log($this->app['logger'], $formatter, LogLevel::DEBUG);
}
/**
* Return retry middleware.
*
* @return \Closure
*/
protected function retryMiddleware()
{
return Middleware::retry(function (
$retries,
RequestInterface $request,
ResponseInterface $response = null
) {
// Limit the number of retries to 2
if ($retries < $this->app->config->get('http.max_retries', 1) && $response && $body = $response->getBody()) {
// Retry on server errors
$response = json_decode($body, true);
if (!empty($response['errcode']) && in_array(abs($response['errcode']), [40001, 40014, 42001], true)) {
$this->accessToken->refresh();
$this->app['logger']->debug('Retrying with refreshed access token.');
return true;
}
}
return false;
}, function () {
return abs($this->app->config->get('http.retry_delay', 500));
});
}
}

64
src/Kernel/Clauses/Clause.php

@ -0,0 +1,64 @@ @@ -0,0 +1,64 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\Kernel\Clauses;
/**
* Class Clause.
*
* @author mingyoung <mingyoungcheung@gmail.com>
*/
class Clause
{
/**
* @var array
*/
protected $clauses = [
'where' => [],
];
/**
* @param mixed ...$args
*
* @return $this
*/
public function where(...$args)
{
array_push($this->clauses['where'], $args);
return $this;
}
/**
* @param mixed $payload
*
* @return bool
*/
public function intercepted($payload)
{
return (bool) $this->interceptWhereClause($payload);
}
/**
* @param mixed $payload
*
* @return bool
*/
protected function interceptWhereClause($payload)
{
foreach ($this->clauses['where'] as $item) {
list($key, $value) = $item;
if (isset($payload[$key]) && $payload[$key] !== $value) {
return true;
}
}
}
}

23
src/Kernel/Config.php

@ -0,0 +1,23 @@ @@ -0,0 +1,23 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\Kernel;
use EasyWeChat\Kernel\Support\Collection;
/**
* Class Config.
*
* @author overtrue <i@overtrue.me>
*/
class Config extends Collection
{
}

40
src/Kernel/Contracts/AccessTokenInterface.php

@ -0,0 +1,40 @@ @@ -0,0 +1,40 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\Kernel\Contracts;
use Psr\Http\Message\RequestInterface;
/**
* Interface AuthorizerAccessToken.
*
* @author overtrue <i@overtrue.me>
*/
interface AccessTokenInterface
{
/**
* @return array
*/
public function getToken(): array;
/**
* @return \EasyWeChat\Kernel\Contracts\AccessTokenInterface
*/
public function refresh(): self;
/**
* @param \Psr\Http\Message\RequestInterface $request
* @param array $requestOptions
*
* @return \Psr\Http\Message\RequestInterface
*/
public function applyToRequest(RequestInterface $request, array $requestOptions = []): RequestInterface;
}

29
src/Kernel/Contracts/Arrayable.php

@ -0,0 +1,29 @@ @@ -0,0 +1,29 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\Kernel\Contracts;
use ArrayAccess;
/**
* Interface Arrayable.
*
* @author overtrue <i@overtrue.me>
*/
interface Arrayable extends ArrayAccess
{
/**
* Get the instance as an array.
*
* @return array
*/
public function toArray();
}

25
src/Kernel/Contracts/EventHandlerInterface.php

@ -0,0 +1,25 @@ @@ -0,0 +1,25 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\Kernel\Contracts;
/**
* Interface EventHandlerInterface.
*
* @author mingyoung <mingyoungcheung@gmail.com>
*/
interface EventHandlerInterface
{
/**
* @param mixed $payload
*/
public function handle($payload = null);
}

25
src/Kernel/Contracts/MediaInterface.php

@ -0,0 +1,25 @@ @@ -0,0 +1,25 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\Kernel\Contracts;
/**
* Interface MediaInterface.
*
* @author overtrue <i@overtrue.me>
*/
interface MediaInterface extends MessageInterface
{
/**
* @return string
*/
public function getMediaId(): string;
}

35
src/Kernel/Contracts/MessageInterface.php

@ -0,0 +1,35 @@ @@ -0,0 +1,35 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\Kernel\Contracts;
/**
* Interface MessageInterface.
*
* @author overtrue <i@overtrue.me>
*/
interface MessageInterface
{
/**
* @return string
*/
public function getType(): string;
/**
* @return array
*/
public function transformForJsonRequest(): array;
/**
* @return string
*/
public function transformToXml(): string;
}

35
src/Kernel/Decorators/FinallyResult.php

@ -0,0 +1,35 @@ @@ -0,0 +1,35 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\Kernel\Decorators;
/**
* Class FinallyResult.
*
* @author overtrue <i@overtrue.me>
*/
class FinallyResult
{
/**
* @var mixed
*/
public $content;
/**
* FinallyResult constructor.
*
* @param mixed $content
*/
public function __construct($content)
{
$this->content = $content;
}
}

35
src/Kernel/Decorators/TerminateResult.php

@ -0,0 +1,35 @@ @@ -0,0 +1,35 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\Kernel\Decorators;
/**
* Class TerminateResult.
*
* @author overtrue <i@overtrue.me>
*/
class TerminateResult
{
/**
* @var mixed
*/
public $content;
/**
* FinallyResult constructor.
*
* @param mixed $content
*/
public function __construct($content)
{
$this->content = $content;
}
}

219
src/Kernel/Encryptor.php

@ -0,0 +1,219 @@ @@ -0,0 +1,219 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\Kernel;
use EasyWeChat\Kernel\Exceptions\RuntimeException;
use EasyWeChat\Kernel\Support\AES;
use function EasyWeChat\Kernel\Support\str_random;
use EasyWeChat\Kernel\Support\XML;
use Throwable;
/**
* Class Encryptor.
*
* @author overtrue <i@overtrue.me>
*/
class Encryptor
{
const ERROR_INVALID_SIGNATURE = -40001; // Signature verification failed
const ERROR_PARSE_XML = -40002; // Parse XML failed
const ERROR_CALC_SIGNATURE = -40003; // Calculating the signature failed
const ERROR_INVALID_AES_KEY = -40004; // Invalid AESKey
const ERROR_INVALID_APP_ID = -40005; // Check AppID failed
const ERROR_ENCRYPT_AES = -40006; // AES EncryptionInterface failed
const ERROR_DECRYPT_AES = -40007; // AES decryption failed
const ERROR_INVALID_XML = -40008; // Invalid XML
const ERROR_BASE64_ENCODE = -40009; // Base64 encoding failed
const ERROR_BASE64_DECODE = -40010; // Base64 decoding failed
const ERROR_XML_BUILD = -40011; // XML build failed
const ILLEGAL_BUFFER = -41003; // Illegal buffer
/**
* App id.
*
* @var string
*/
protected $appId;
/**
* App token.
*
* @var string
*/
protected $token;
/**
* @var string
*/
protected $aesKey;
/**
* Block size.
*
* @var int
*/
protected $blockSize = 32;
/**
* Constructor.
*
* @param string $appId
* @param string|null $token
* @param string|null $aesKey
*/
public function __construct(string $appId, string $token = null, string $aesKey = null)
{
$this->appId = $appId;
$this->token = $token;
$this->aesKey = base64_decode($aesKey.'=', true);
}
/**
* Get the app token.
*
* @return string
*/
public function getToken(): string
{
return $this->token;
}
/**
* Encrypt the message and return XML.
*
* @param string $xml
* @param string $nonce
* @param int $timestamp
*
* @return string
*
* @throws \EasyWeChat\Kernel\Exceptions\RuntimeException
*/
public function encrypt($xml, $nonce = null, $timestamp = null): string
{
try {
$xml = $this->pkcs7Pad(str_random(16).pack('N', strlen($xml)).$xml.$this->appId, $this->blockSize);
$encrypted = base64_encode(AES::encrypt(
$xml,
$this->aesKey,
substr($this->aesKey, 0, 16),
OPENSSL_NO_PADDING
));
// @codeCoverageIgnoreStart
} catch (Throwable $e) {
throw new RuntimeException($e->getMessage(), self::ERROR_ENCRYPT_AES);
}
// @codeCoverageIgnoreEnd
!is_null($nonce) || $nonce = substr($this->appId, 0, 10);
!is_null($timestamp) || $timestamp = time();
$response = [
'Encrypt' => $encrypted,
'MsgSignature' => $this->signature($this->token, $timestamp, $nonce, $encrypted),
'TimeStamp' => $timestamp,
'Nonce' => $nonce,
];
//生成响应xml
return XML::build($response);
}
/**
* Decrypt message.
*
* @param string $content
* @param string $msgSignature
* @param string $nonce
* @param string $timestamp
*
* @return string
*
* @throws \EasyWeChat\Kernel\Exceptions\RuntimeException
*/
public function decrypt($content, $msgSignature, $nonce, $timestamp): string
{
$signature = $this->signature($this->token, $timestamp, $nonce, $content);
if ($signature !== $msgSignature) {
throw new RuntimeException('Invalid Signature.', self::ERROR_INVALID_SIGNATURE);
}
$decrypted = AES::decrypt(
base64_decode($content, true),
$this->aesKey,
substr($this->aesKey, 0, 16),
OPENSSL_NO_PADDING
);
$result = $this->pkcs7Unpad($decrypted);
$content = substr($result, 16, strlen($result));
$contentLen = unpack('N', substr($content, 0, 4))[1];
if (trim(substr($content, $contentLen + 4)) !== $this->appId) {
throw new RuntimeException('Invalid appId.', self::ERROR_INVALID_APP_ID);
}
return substr($content, 4, $contentLen);
}
/**
* Get SHA1.
*
* @return string
*/
public function signature(): string
{
$array = func_get_args();
sort($array, SORT_STRING);
return sha1(implode($array));
}
/**
* PKCS#7 pad.
*
* @param string $text
* @param int $blockSize
*
* @return string
*
* @throws \EasyWeChat\Kernel\Exceptions\RuntimeException
*/
public function pkcs7Pad(string $text, int $blockSize): string
{
if ($blockSize > 256) {
throw new RuntimeException('$blockSize may not be more than 256');
}
$padding = $blockSize - (strlen($text) % $blockSize);
$pattern = chr($padding);
return $text.str_repeat($pattern, $padding);
}
/**
* PKCS#7 unpad.
*
* @param string $text
*
* @return string
*/
public function pkcs7Unpad(string $text): string
{
$pad = ord(substr($text, -1));
if ($pad < 1 || $pad > $this->blockSize) {
$pad = 0;
}
return substr($text, 0, (strlen($text) - $pad));
}
}

35
src/Kernel/Events/AccessTokenRefreshed.php

@ -0,0 +1,35 @@ @@ -0,0 +1,35 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\Kernel\Events;
use EasyWeChat\Kernel\AccessToken;
/**
* Class AccessTokenRefreshed.
*
* @author mingyoung <mingyoungcheung@gmail.com>
*/
class AccessTokenRefreshed
{
/**
* @var \EasyWeChat\Kernel\AccessToken
*/
public $accessToken;
/**
* @param \EasyWeChat\Kernel\AccessToken $accessToken
*/
public function __construct(AccessToken $accessToken)
{
$this->accessToken = $accessToken;
}
}

35
src/Kernel/Events/ApplicationInitialized.php

@ -0,0 +1,35 @@ @@ -0,0 +1,35 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\Kernel\Events;
use EasyWeChat\Kernel\ServiceContainer;
/**
* Class ApplicationInitialized.
*
* @author mingyoung <mingyoungcheung@gmail.com>
*/
class ApplicationInitialized
{
/**
* @var \EasyWeChat\Kernel\ServiceContainer
*/
public $app;
/**
* @param \EasyWeChat\Kernel\ServiceContainer $app
*/
public function __construct(ServiceContainer $app)
{
$this->app = $app;
}
}

35
src/Kernel/Events/HttpResponseCreated.php

@ -0,0 +1,35 @@ @@ -0,0 +1,35 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\Kernel\Events;
use Psr\Http\Message\ResponseInterface;
/**
* Class HttpResponseCreated.
*
* @author mingyoung <mingyoungcheung@gmail.com>
*/
class HttpResponseCreated
{
/**
* @var \Psr\Http\Message\ResponseInterface
*/
public $response;
/**
* @param \Psr\Http\Message\ResponseInterface $response
*/
public function __construct(ResponseInterface $response)
{
$this->response = $response;
}
}

35
src/Kernel/Events/ServerGuardResponseCreated.php

@ -0,0 +1,35 @@ @@ -0,0 +1,35 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\Kernel\Events;
use Symfony\Component\HttpFoundation\Response;
/**
* Class ServerGuardResponseCreated.
*
* @author mingyoung <mingyoungcheung@gmail.com>
*/
class ServerGuardResponseCreated
{
/**
* @var \Symfony\Component\HttpFoundation\Response
*/
public $response;
/**
* @param \Symfony\Component\HttpFoundation\Response $response
*/
public function __construct(Response $response)
{
$this->response = $response;
}
}

21
src/Kernel/Exceptions/BadRequestException.php

@ -0,0 +1,21 @@ @@ -0,0 +1,21 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\Kernel\Exceptions;
/**
* Class BadRequestException.
*
* @author overtrue <i@overtrue.me>
*/
class BadRequestException extends Exception
{
}

16
src/Kernel/Exceptions/DecryptException.php

@ -0,0 +1,16 @@ @@ -0,0 +1,16 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\Kernel\Exceptions;
class DecryptException extends Exception
{
}

23
src/Kernel/Exceptions/Exception.php

@ -0,0 +1,23 @@ @@ -0,0 +1,23 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\Kernel\Exceptions;
use Exception as BaseException;
/**
* Class Exception.
*
* @author overtrue <i@overtrue.me>
*/
class Exception extends BaseException
{
}

52
src/Kernel/Exceptions/HttpException.php

@ -0,0 +1,52 @@ @@ -0,0 +1,52 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\Kernel\Exceptions;
use Psr\Http\Message\ResponseInterface;
/**
* Class HttpException.
*
* @author overtrue <i@overtrue.me>
*/
class HttpException extends Exception
{
/**
* @var \Psr\Http\Message\ResponseInterface|null
*/
public $response;
/**
* @var \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string|null
*/
public $formattedResponse;
/**
* HttpException constructor.
*
* @param string $message
* @param \Psr\Http\Message\ResponseInterface|null $response
* @param null $formattedResponse
* @param int|null $code
*/
public function __construct($message, ResponseInterface $response = null, $formattedResponse = null, $code = null)
{
parent::__construct($message, $code);
$this->response = $response;
$this->formattedResponse = $formattedResponse;
if ($response) {
$response->getBody()->rewind();
}
}
}

21
src/Kernel/Exceptions/InvalidArgumentException.php

@ -0,0 +1,21 @@ @@ -0,0 +1,21 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\Kernel\Exceptions;
/**
* Class InvalidArgumentException.
*
* @author overtrue <i@overtrue.me>
*/
class InvalidArgumentException extends Exception
{
}

21
src/Kernel/Exceptions/InvalidConfigException.php

@ -0,0 +1,21 @@ @@ -0,0 +1,21 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\Kernel\Exceptions;
/**
* Class InvalidConfigException.
*
* @author overtrue <i@overtrue.me>
*/
class InvalidConfigException extends Exception
{
}

21
src/Kernel/Exceptions/RuntimeException.php

@ -0,0 +1,21 @@ @@ -0,0 +1,21 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\Kernel\Exceptions;
/**
* Class RuntimeException.
*
* @author overtrue <i@overtrue.me>
*/
class RuntimeException extends Exception
{
}

21
src/Kernel/Exceptions/UnboundServiceException.php

@ -0,0 +1,21 @@ @@ -0,0 +1,21 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\Kernel\Exceptions;
/**
* Class InvalidConfigException.
*
* @author overtrue <i@overtrue.me>
*/
class UnboundServiceException extends Exception
{
}

57
src/Kernel/Helpers.php

@ -0,0 +1,57 @@ @@ -0,0 +1,57 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\Kernel;
use EasyWeChat\Kernel\Contracts\Arrayable;
use EasyWeChat\Kernel\Exceptions\RuntimeException;
use EasyWeChat\Kernel\Support\Arr;
use EasyWeChat\Kernel\Support\Collection;
function data_get($data, $key, $default = null)
{
switch (true) {
case is_array($data):
return Arr::get($data, $key, $default);
case $data instanceof Collection:
return $data->get($key, $default);
case $data instanceof Arrayable:
return Arr::get($data->toArray(), $key, $default);
case $data instanceof \ArrayIterator:
return $data->getArrayCopy()[$key] ?? $default;
case $data instanceof \ArrayAccess:
return $data[$key] ?? $default;
case $data instanceof \IteratorAggregate && $data->getIterator() instanceof \ArrayIterator:
return $data->getIterator()->getArrayCopy()[$key] ?? $default;
case is_object($data):
return $data->{$key} ?? $default;
default:
throw new RuntimeException(sprintf('Can\'t access data with key "%s"', $key));
}
}
function data_to_array($data)
{
switch (true) {
case is_array($data):
return $data;
case $data instanceof Collection:
return $data->all();
case $data instanceof Arrayable:
return $data->toArray();
case $data instanceof \IteratorAggregate && $data->getIterator() instanceof \ArrayIterator:
return $data->getIterator()->getArrayCopy();
case $data instanceof \ArrayIterator:
return $data->getArrayCopy();
default:
throw new RuntimeException(sprintf('Can\'t transform data to array'));
}
}

121
src/Kernel/Http/Response.php

@ -0,0 +1,121 @@ @@ -0,0 +1,121 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\Kernel\Http;
use EasyWeChat\Kernel\Support\Collection;
use EasyWeChat\Kernel\Support\XML;
use GuzzleHttp\Psr7\Response as GuzzleResponse;
use Psr\Http\Message\ResponseInterface;
/**
* Class Response.
*
* @author overtrue <i@overtrue.me>
*/
class Response extends GuzzleResponse
{
/**
* @return string
*/
public function getBodyContents()
{
$this->getBody()->rewind();
$contents = $this->getBody()->getContents();
$this->getBody()->rewind();
return $contents;
}
/**
* @param \Psr\Http\Message\ResponseInterface $response
*
* @return \EasyWeChat\Kernel\Http\Response
*/
public static function buildFromPsrResponse(ResponseInterface $response)
{
return new static(
$response->getStatusCode(),
$response->getHeaders(),
$response->getBody(),
$response->getProtocolVersion(),
$response->getReasonPhrase()
);
}
/**
* Build to json.
*
* @return string
*/
public function toJson()
{
return json_encode($this->toArray());
}
/**
* Build to array.
*
* @return array
*/
public function toArray()
{
$content = $this->removeControlCharacters($this->getBodyContents());
if (false !== stripos($this->getHeaderLine('Content-Type'), 'xml') || 0 === stripos($content, '<xml')) {
return XML::parse($content);
}
$array = json_decode($content, true, 512, JSON_BIGINT_AS_STRING);
if (JSON_ERROR_NONE === json_last_error()) {
return (array) $array;
}
return [];
}
/**
* Get collection data.
*
* @return \EasyWeChat\Kernel\Support\Collection
*/
public function toCollection()
{
return new Collection($this->toArray());
}
/**
* @return object
*/
public function toObject()
{
return json_decode($this->toJson());
}
/**
* @return bool|string
*/
public function __toString()
{
return $this->getBodyContents();
}
/**
* @param string $content
*
* @return string
*/
protected function removeControlCharacters(string $content)
{
return \preg_replace('/[\x00-\x1F\x80-\x9F]/u', '', $content);
}
}

86
src/Kernel/Http/StreamResponse.php

@ -0,0 +1,86 @@ @@ -0,0 +1,86 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\Kernel\Http;
use EasyWeChat\Kernel\Exceptions\InvalidArgumentException;
use EasyWeChat\Kernel\Exceptions\RuntimeException;
use EasyWeChat\Kernel\Support\File;
/**
* Class StreamResponse.
*
* @author overtrue <i@overtrue.me>
*/
class StreamResponse extends Response
{
/**
* @param string $directory
* @param string $filename
* @param bool $appendSuffix
*
* @return bool|int
*
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
* @throws \EasyWeChat\Kernel\Exceptions\RuntimeException
*/
public function save(string $directory, string $filename = '', bool $appendSuffix = true)
{
$this->getBody()->rewind();
$directory = rtrim($directory, '/');
if (!is_dir($directory)) {
mkdir($directory, 0755, true); // @codeCoverageIgnore
}
if (!is_writable($directory)) {
throw new InvalidArgumentException(sprintf("'%s' is not writable.", $directory));
}
$contents = $this->getBody()->getContents();
if (empty($contents) || '{' === $contents[0]) {
throw new RuntimeException('Invalid media response content.');
}
if (empty($filename)) {
if (preg_match('/filename="(?<filename>.*?)"/', $this->getHeaderLine('Content-Disposition'), $match)) {
$filename = $match['filename'];
} else {
$filename = md5($contents);
}
}
if ($appendSuffix && empty(pathinfo($filename, PATHINFO_EXTENSION))) {
$filename .= File::getStreamExt($contents);
}
file_put_contents($directory.'/'.$filename, $contents);
return $filename;
}
/**
* @param string $directory
* @param string $filename
* @param bool $appendSuffix
*
* @return bool|int
*
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
* @throws \EasyWeChat\Kernel\Exceptions\RuntimeException
*/
public function saveAs(string $directory, string $filename, bool $appendSuffix = true)
{
return $this->save($directory, $filename, $appendSuffix);
}
}

608
src/Kernel/Log/LogManager.php

@ -0,0 +1,608 @@ @@ -0,0 +1,608 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\Kernel\Log;
use EasyWeChat\Kernel\ServiceContainer;
use InvalidArgumentException;
use Monolog\Formatter\LineFormatter;
use Monolog\Handler\ErrorLogHandler;
use Monolog\Handler\FormattableHandlerInterface;
use Monolog\Handler\HandlerInterface;
use Monolog\Handler\RotatingFileHandler;
use Monolog\Handler\SlackWebhookHandler;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\SyslogHandler;
use Monolog\Handler\WhatFailureGroupHandler;
use Monolog\Logger as Monolog;
use Psr\Log\LoggerInterface;
/**
* Class LogManager.
*
* @author overtrue <i@overtrue.me>
*/
class LogManager implements LoggerInterface
{
/**
* @var \EasyWeChat\Kernel\ServiceContainer
*/
protected $app;
/**
* The array of resolved channels.
*
* @var array
*/
protected $channels = [];
/**
* The registered custom driver creators.
*
* @var array
*/
protected $customCreators = [];
/**
* The Log levels.
*
* @var array
*/
protected $levels = [
'debug' => Monolog::DEBUG,
'info' => Monolog::INFO,
'notice' => Monolog::NOTICE,
'warning' => Monolog::WARNING,
'error' => Monolog::ERROR,
'critical' => Monolog::CRITICAL,
'alert' => Monolog::ALERT,
'emergency' => Monolog::EMERGENCY,
];
/**
* LogManager constructor.
*
* @param \EasyWeChat\Kernel\ServiceContainer $app
*/
public function __construct(ServiceContainer $app)
{
$this->app = $app;
}
/**
* Create a new, on-demand aggregate logger instance.
*
* @param array $channels
* @param string|null $channel
*
* @return \Psr\Log\LoggerInterface
*
* @throws \Exception
*/
public function stack(array $channels, $channel = null)
{
return $this->createStackDriver(compact('channels', 'channel'));
}
/**
* Get a log channel instance.
*
* @param string|null $channel
*
* @return mixed
*
* @throws \Exception
*/
public function channel($channel = null)
{
return $this->driver($channel);
}
/**
* Get a log driver instance.
*
* @param string|null $driver
*
* @return mixed
*
* @throws \Exception
*/
public function driver($driver = null)
{
return $this->get($driver ?? $this->getDefaultDriver());
}
/**
* Attempt to get the log from the local cache.
*
* @param string $name
*
* @return \Psr\Log\LoggerInterface
*
* @throws \Exception
*/
protected function get($name)
{
try {
return $this->channels[$name] ?? ($this->channels[$name] = $this->resolve($name));
} catch (\Throwable $e) {
$logger = $this->createEmergencyLogger();
$logger->emergency('Unable to create configured logger. Using emergency logger.', [
'exception' => $e,
]);
return $logger;
}
}
/**
* Resolve the given log instance by name.
*
* @param string $name
*
* @return \Psr\Log\LoggerInterface
*
* @throws InvalidArgumentException
*/
protected function resolve($name)
{
$config = $this->app['config']->get(\sprintf('log.channels.%s', $name));
if (is_null($config)) {
throw new InvalidArgumentException(\sprintf('Log [%s] is not defined.', $name));
}
if (isset($this->customCreators[$config['driver']])) {
return $this->callCustomCreator($config);
}
$driverMethod = 'create'.ucfirst($config['driver']).'Driver';
if (method_exists($this, $driverMethod)) {
return $this->{$driverMethod}($config);
}
throw new InvalidArgumentException(\sprintf('Driver [%s] is not supported.', $config['driver']));
}
/**
* Create an emergency log handler to avoid white screens of death.
*
* @return \Monolog\Logger
*
* @throws \Exception
*/
protected function createEmergencyLogger()
{
return new Monolog('EasyWeChat', $this->prepareHandlers([new StreamHandler(
\sys_get_temp_dir().'/easywechat/easywechat.log',
$this->level(['level' => 'debug'])
)]));
}
/**
* Call a custom driver creator.
*
* @param array $config
*
* @return mixed
*/
protected function callCustomCreator(array $config)
{
return $this->customCreators[$config['driver']]($this->app, $config);
}
/**
* Create an aggregate log driver instance.
*
* @param array $config
*
* @return \Monolog\Logger
*
* @throws \Exception
*/
protected function createStackDriver(array $config)
{
$handlers = [];
foreach ($config['channels'] ?? [] as $channel) {
$handlers = \array_merge($handlers, $this->channel($channel)->getHandlers());
}
if ($config['ignore_exceptions'] ?? false) {
$handlers = [new WhatFailureGroupHandler($handlers)];
}
return new Monolog($this->parseChannel($config), $handlers);
}
/**
* Create an instance of the single file log driver.
*
* @param array $config
*
* @return \Psr\Log\LoggerInterface
*
* @throws \Exception
*/
protected function createSingleDriver(array $config)
{
return new Monolog($this->parseChannel($config), [
$this->prepareHandler(new StreamHandler(
$config['path'],
$this->level($config),
$config['bubble'] ?? true,
$config['permission'] ?? null,
$config['locking'] ?? false
), $config),
]);
}
/**
* Create an instance of the daily file log driver.
*
* @param array $config
*
* @return \Psr\Log\LoggerInterface
*/
protected function createDailyDriver(array $config)
{
return new Monolog($this->parseChannel($config), [
$this->prepareHandler(new RotatingFileHandler(
$config['path'],
$config['days'] ?? 7,
$this->level($config),
$config['bubble'] ?? true,
$config['permission'] ?? null,
$config['locking'] ?? false
), $config),
]);
}
/**
* Create an instance of the Slack log driver.
*
* @param array $config
*
* @return \Psr\Log\LoggerInterface
*/
protected function createSlackDriver(array $config)
{
return new Monolog($this->parseChannel($config), [
$this->prepareHandler(new SlackWebhookHandler(
$config['url'],
$config['channel'] ?? null,
$config['username'] ?? 'EasyWeChat',
$config['attachment'] ?? true,
$config['emoji'] ?? ':boom:',
$config['short'] ?? false,
$config['context'] ?? true,
$this->level($config),
$config['bubble'] ?? true,
$config['exclude_fields'] ?? []
), $config),
]);
}
/**
* Create an instance of the syslog log driver.
*
* @param array $config
*
* @return \Psr\Log\LoggerInterface
*/
protected function createSyslogDriver(array $config)
{
return new Monolog($this->parseChannel($config), [
$this->prepareHandler(new SyslogHandler(
'EasyWeChat',
$config['facility'] ?? LOG_USER,
$this->level($config)
), $config),
]);
}
/**
* Create an instance of the "error log" log driver.
*
* @param array $config
*
* @return \Psr\Log\LoggerInterface
*/
protected function createErrorlogDriver(array $config)
{
return new Monolog($this->parseChannel($config), [
$this->prepareHandler(
new ErrorLogHandler(
$config['type'] ?? ErrorLogHandler::OPERATING_SYSTEM,
$this->level($config)
)
),
]);
}
/**
* Prepare the handlers for usage by Monolog.
*
* @param array $handlers
*
* @return array
*/
protected function prepareHandlers(array $handlers)
{
foreach ($handlers as $key => $handler) {
$handlers[$key] = $this->prepareHandler($handler);
}
return $handlers;
}
/**
* Prepare the handler for usage by Monolog.
*
* @param \Monolog\Handler\HandlerInterface $handler
*
* @return \Monolog\Handler\HandlerInterface
*/
protected function prepareHandler(HandlerInterface $handler, array $config = [])
{
if (!isset($config['formatter'])) {
if ($handler instanceof FormattableHandlerInterface) {
$handler->setFormatter($this->formatter());
}
}
return $handler;
}
/**
* Get a Monolog formatter instance.
*
* @return \Monolog\Formatter\FormatterInterface
*/
protected function formatter()
{
$formatter = new LineFormatter(null, null, true, true);
$formatter->includeStacktraces();
return $formatter;
}
/**
* Extract the log channel from the given configuration.
*
* @param array $config
*
* @return string
*/
protected function parseChannel(array $config)
{
return $config['name'] ?? 'EasyWeChat';
}
/**
* Parse the string level into a Monolog constant.
*
* @param array $config
*
* @return int
*
* @throws InvalidArgumentException
*/
protected function level(array $config)
{
$level = $config['level'] ?? 'debug';
if (isset($this->levels[$level])) {
return $this->levels[$level];
}
throw new InvalidArgumentException('Invalid log level.');
}
/**
* Get the default log driver name.
*
* @return string
*/
public function getDefaultDriver()
{
return $this->app['config']['log.default'];
}
/**
* Set the default log driver name.
*
* @param string $name
*/
public function setDefaultDriver($name)
{
$this->app['config']['log.default'] = $name;
}
/**
* Register a custom driver creator Closure.
*
* @param string $driver
* @param \Closure $callback
*
* @return $this
*/
public function extend($driver, \Closure $callback)
{
$this->customCreators[$driver] = $callback->bindTo($this, $this);
return $this;
}
/**
* System is unusable.
*
* @param string $message
* @param array $context
*
* @return mixed
*
* @throws \Exception
*/
public function emergency($message, array $context = [])
{
return $this->driver()->emergency($message, $context);
}
/**
* Action must be taken immediately.
*
* Example: Entire website down, database unavailable, etc. This should
* trigger the SMS alerts and wake you up.
*
* @param string $message
* @param array $context
*
* @return mixed
*
* @throws \Exception
*/
public function alert($message, array $context = [])
{
return $this->driver()->alert($message, $context);
}
/**
* Critical conditions.
*
* Example: Application component unavailable, unexpected exception.
*
* @param string $message
* @param array $context
*
* @return mixed
*
* @throws \Exception
*/
public function critical($message, array $context = [])
{
return $this->driver()->critical($message, $context);
}
/**
* Runtime errors that do not require immediate action but should typically
* be logged and monitored.
*
* @param string $message
* @param array $context
*
* @return mixed
*
* @throws \Exception
*/
public function error($message, array $context = [])
{
return $this->driver()->error($message, $context);
}
/**
* Exceptional occurrences that are not errors.
*
* Example: Use of deprecated APIs, poor use of an API, undesirable things
* that are not necessarily wrong.
*
* @param string $message
* @param array $context
*
* @return mixed
*
* @throws \Exception
*/
public function warning($message, array $context = [])
{
return $this->driver()->warning($message, $context);
}
/**
* Normal but significant events.
*
* @param string $message
* @param array $context
*
* @return mixed
*
* @throws \Exception
*/
public function notice($message, array $context = [])
{
return $this->driver()->notice($message, $context);
}
/**
* Interesting events.
*
* Example: User logs in, SQL logs.
*
* @param string $message
* @param array $context
*
* @return mixed
*
* @throws \Exception
*/
public function info($message, array $context = [])
{
return $this->driver()->info($message, $context);
}
/**
* Detailed debug information.
*
* @param string $message
* @param array $context
*
* @return mixed
*
* @throws \Exception
*/
public function debug($message, array $context = [])
{
return $this->driver()->debug($message, $context);
}
/**
* Logs with an arbitrary level.
*
* @param mixed $level
* @param string $message
* @param array $context
*
* @return mixed
*
* @throws \Exception
*/
public function log($level, $message, array $context = [])
{
return $this->driver()->log($level, $message, $context);
}
/**
* Dynamically call the default driver instance.
*
* @param string $method
* @param array $parameters
*
* @return mixed
*
* @throws \Exception
*/
public function __call($method, $parameters)
{
return $this->driver()->$method(...$parameters);
}
}

58
src/Kernel/Messages/Article.php

@ -0,0 +1,58 @@ @@ -0,0 +1,58 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\Kernel\Messages;
/**
* Class Article.
*/
class Article extends Message
{
/**
* @var string
*/
protected $type = 'mpnews';
/**
* Properties.
*
* @var array
*/
protected $properties = [
'thumb_media_id',
'author',
'title',
'content',
'digest',
'source_url',
'show_cover',
];
/**
* Aliases of attribute.
*
* @var array
*/
protected $jsonAliases = [
'content_source_url' => 'source_url',
'show_cover_pic' => 'show_cover',
];
/**
* @var array
*/
protected $required = [
'thumb_media_id',
'title',
'content',
'show_cover',
];
}

52
src/Kernel/Messages/Card.php

@ -0,0 +1,52 @@ @@ -0,0 +1,52 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
/**
* Card.php.
*
* @author overtrue <i@overtrue.me>
* @copyright 2015 overtrue <i@overtrue.me>
*
* @see https://github.com/overtrue
* @see http://overtrue.me
*/
namespace EasyWeChat\Kernel\Messages;
/**
* Class Card.
*/
class Card extends Message
{
/**
* Message type.
*
* @var string
*/
protected $type = 'wxcard';
/**
* Properties.
*
* @var array
*/
protected $properties = ['card_id'];
/**
* Media constructor.
*
* @param string $cardId
*/
public function __construct(string $cardId)
{
parent::__construct(['card_id' => $cardId]);
}
}

40
src/Kernel/Messages/DeviceEvent.php

@ -0,0 +1,40 @@ @@ -0,0 +1,40 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\Kernel\Messages;
/**
* Class DeviceEvent.
*
* @property string $media_id
*/
class DeviceEvent extends Message
{
/**
* Messages type.
*
* @var string
*/
protected $type = 'device_event';
/**
* Properties.
*
* @var array
*/
protected $properties = [
'device_type',
'device_id',
'content',
'session_id',
'open_id',
];
}

50
src/Kernel/Messages/DeviceText.php

@ -0,0 +1,50 @@ @@ -0,0 +1,50 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\Kernel\Messages;
/**
* Class DeviceText.
*
* @property string $content
*/
class DeviceText extends Message
{
/**
* Messages type.
*
* @var string
*/
protected $type = 'device_text';
/**
* Properties.
*
* @var array
*/
protected $properties = [
'device_type',
'device_id',
'content',
'session_id',
'open_id',
];
public function toXmlArray()
{
return [
'DeviceType' => $this->get('device_type'),
'DeviceID' => $this->get('device_id'),
'SessionID' => $this->get('session_id'),
'Content' => base64_encode($this->get('content')),
];
}
}

25
src/Kernel/Messages/File.php

@ -0,0 +1,25 @@ @@ -0,0 +1,25 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\Kernel\Messages;
/**
* Class Image.
*
* @property string $media_id
*/
class File extends Media
{
/**
* @var string
*/
protected $type = 'file';
}

27
src/Kernel/Messages/Image.php

@ -0,0 +1,27 @@ @@ -0,0 +1,27 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\Kernel\Messages;
/**
* Class Image.
*
* @property string $media_id
*/
class Image extends Media
{
/**
* Messages type.
*
* @var string
*/
protected $type = 'image';
}

36
src/Kernel/Messages/Link.php

@ -0,0 +1,36 @@ @@ -0,0 +1,36 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\Kernel\Messages;
/**
* Class Link.
*/
class Link extends Message
{
/**
* Messages type.
*
* @var string
*/
protected $type = 'link';
/**
* Properties.
*
* @var array
*/
protected $properties = [
'title',
'description',
'url',
];
}

38
src/Kernel/Messages/Location.php

@ -0,0 +1,38 @@ @@ -0,0 +1,38 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\Kernel\Messages;
/**
* Class Location.
*/
class Location extends Message
{
/**
* Messages type.
*
* @var string
*/
protected $type = 'location';
/**
* Properties.
*
* @var array
*/
protected $properties = [
'latitude',
'longitude',
'scale',
'label',
'precision',
];
}

70
src/Kernel/Messages/Media.php

@ -0,0 +1,70 @@ @@ -0,0 +1,70 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\Kernel\Messages;
use EasyWeChat\Kernel\Contracts\MediaInterface;
use EasyWeChat\Kernel\Support\Str;
/**
* Class Media.
*/
class Media extends Message implements MediaInterface
{
/**
* Properties.
*
* @var array
*/
protected $properties = ['media_id'];
/**
* @var array
*/
protected $required = [
'media_id',
];
/**
* MaterialClient constructor.
*
* @param string $mediaId
* @param string $type
* @param array $attributes
*/
public function __construct(string $mediaId, $type = null, array $attributes = [])
{
parent::__construct(array_merge(['media_id' => $mediaId], $attributes));
!empty($type) && $this->setType($type);
}
/**
* @return string
*
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
*/
public function getMediaId(): string
{
$this->checkRequiredAttributes();
return $this->get('media_id');
}
public function toXmlArray()
{
return [
Str::studly($this->getType()) => [
'MediaId' => $this->get('media_id'),
],
];
}
}

208
src/Kernel/Messages/Message.php

@ -0,0 +1,208 @@ @@ -0,0 +1,208 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\Kernel\Messages;
use EasyWeChat\Kernel\Contracts\MessageInterface;
use EasyWeChat\Kernel\Exceptions\RuntimeException;
use EasyWeChat\Kernel\Support\XML;
use EasyWeChat\Kernel\Traits\HasAttributes;
/**
* Class Messages.
*/
abstract class Message implements MessageInterface
{
use HasAttributes;
const TEXT = 2;
const IMAGE = 4;
const VOICE = 8;
const VIDEO = 16;
const SHORT_VIDEO = 32;
const LOCATION = 64;
const LINK = 128;
const DEVICE_EVENT = 256;
const DEVICE_TEXT = 512;
const FILE = 1024;
const TEXT_CARD = 2048;
const TRANSFER = 4096;
const EVENT = 1048576;
const MINIPROGRAM_PAGE = 2097152;
const ALL = self::TEXT | self::IMAGE | self::VOICE | self::VIDEO | self::SHORT_VIDEO | self::LOCATION | self::LINK
| self::DEVICE_EVENT | self::DEVICE_TEXT | self::FILE | self::TEXT_CARD | self::TRANSFER | self::EVENT | self::MINIPROGRAM_PAGE;
/**
* @var string
*/
protected $type;
/**
* @var int
*/
protected $id;
/**
* @var string
*/
protected $to;
/**
* @var string
*/
protected $from;
/**
* @var array
*/
protected $properties = [];
/**
* @var array
*/
protected $jsonAliases = [];
/**
* Message constructor.
*
* @param array $attributes
*/
public function __construct(array $attributes = [])
{
$this->setAttributes($attributes);
}
/**
* Return type name message.
*
* @return string
*/
public function getType(): string
{
return $this->type;
}
/**
* @param string $type
*/
public function setType(string $type)
{
$this->type = $type;
}
/**
* Magic getter.
*
* @param string $property
*
* @return mixed
*/
public function __get($property)
{
if (property_exists($this, $property)) {
return $this->$property;
}
return $this->getAttribute($property);
}
/**
* Magic setter.
*
* @param string $property
* @param mixed $value
*
* @return Message
*/
public function __set($property, $value)
{
if (property_exists($this, $property)) {
$this->$property = $value;
} else {
$this->setAttribute($property, $value);
}
return $this;
}
/**
* @param array $appends
*
* @return array
*/
public function transformForJsonRequestWithoutType(array $appends = [])
{
return $this->transformForJsonRequest($appends, false);
}
/**
* @param array $appends
* @param bool $withType
*
* @return array
*
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
*/
public function transformForJsonRequest(array $appends = [], $withType = true): array
{
if (!$withType) {
return $this->propertiesToArray([], $this->jsonAliases);
}
$messageType = $this->getType();
$data = array_merge(['msgtype' => $messageType], $appends);
$data[$messageType] = array_merge($data[$messageType] ?? [], $this->propertiesToArray([], $this->jsonAliases));
return $data;
}
/**
* @param array $appends
* @param bool $returnAsArray
*
* @return string
*/
public function transformToXml(array $appends = [], bool $returnAsArray = false): string
{
$data = array_merge(['MsgType' => $this->getType()], $this->toXmlArray(), $appends);
return $returnAsArray ? $data : XML::build($data);
}
/**
* @param array $data
* @param array $aliases
*
* @return array
*
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
*/
protected function propertiesToArray(array $data, array $aliases = []): array
{
$this->checkRequiredAttributes();
foreach ($this->attributes as $property => $value) {
if (is_null($value) && !$this->isRequired($property)) {
continue;
}
$alias = array_search($property, $aliases, true);
$data[$alias ?: $property] = $this->get($property);
}
return $data;
}
public function toXmlArray()
{
throw new RuntimeException(sprintf('Class "%s" cannot support transform to XML message.', __CLASS__));
}
}

31
src/Kernel/Messages/MiniProgramPage.php

@ -0,0 +1,31 @@ @@ -0,0 +1,31 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\Kernel\Messages;
/**
* Class MiniProgramPage.
*/
class MiniProgramPage extends Message
{
protected $type = 'miniprogrampage';
protected $properties = [
'title',
'appid',
'pagepath',
'thumb_media_id',
];
protected $required = [
'thumb_media_id', 'appid', 'pagepath',
];
}

73
src/Kernel/Messages/Music.php

@ -0,0 +1,73 @@ @@ -0,0 +1,73 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\Kernel\Messages;
/**
* Class Music.
*
* @property string $url
* @property string $hq_url
* @property string $title
* @property string $description
* @property string $thumb_media_id
* @property string $format
*/
class Music extends Message
{
/**
* Messages type.
*
* @var string
*/
protected $type = 'music';
/**
* Properties.
*
* @var array
*/
protected $properties = [
'title',
'description',
'url',
'hq_url',
'thumb_media_id',
'format',
];
/**
* Aliases of attribute.
*
* @var array
*/
protected $jsonAliases = [
'musicurl' => 'url',
'hqmusicurl' => 'hq_url',
];
public function toXmlArray()
{
$music = [
'Music' => [
'Title' => $this->get('title'),
'Description' => $this->get('description'),
'MusicUrl' => $this->get('url'),
'HQMusicUrl' => $this->get('hq_url'),
],
];
if ($thumbMediaId = $this->get('thumb_media_id')) {
$music['Music']['ThumbMediaId'] = $thumbMediaId;
}
return $music;
}
}

73
src/Kernel/Messages/News.php

@ -0,0 +1,73 @@ @@ -0,0 +1,73 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\Kernel\Messages;
/**
* Class News.
*
* @author overtrue <i@overtrue.me>
*/
class News extends Message
{
/**
* @var string
*/
protected $type = 'news';
/**
* @var array
*/
protected $properties = [
'items',
];
/**
* News constructor.
*
* @param array $items
*/
public function __construct(array $items = [])
{
parent::__construct(compact('items'));
}
/**
* @param array $data
* @param array $aliases
*
* @return array
*/
public function propertiesToArray(array $data, array $aliases = []): array
{
return ['articles' => array_map(function ($item) {
if ($item instanceof NewsItem) {
return $item->toJsonArray();
}
}, $this->get('items'))];
}
public function toXmlArray()
{
$items = [];
foreach ($this->get('items') as $item) {
if ($item instanceof NewsItem) {
$items[] = $item->toXmlArray();
}
}
return [
'ArticleCount' => count($items),
'Articles' => $items,
];
}
}

57
src/Kernel/Messages/NewsItem.php

@ -0,0 +1,57 @@ @@ -0,0 +1,57 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\Kernel\Messages;
/**
* Class NewsItem.
*/
class NewsItem extends Message
{
/**
* Messages type.
*
* @var string
*/
protected $type = 'news';
/**
* Properties.
*
* @var array
*/
protected $properties = [
'title',
'description',
'url',
'image',
];
public function toJsonArray()
{
return [
'title' => $this->get('title'),
'description' => $this->get('description'),
'url' => $this->get('url'),
'picurl' => $this->get('image'),
];
}
public function toXmlArray()
{
return [
'Title' => $this->get('title'),
'Description' => $this->get('description'),
'Url' => $this->get('url'),
'PicUrl' => $this->get('image'),
];
}
}

56
src/Kernel/Messages/Raw.php

@ -0,0 +1,56 @@ @@ -0,0 +1,56 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\Kernel\Messages;
/**
* Class Raw.
*/
class Raw extends Message
{
/**
* @var string
*/
protected $type = 'raw';
/**
* Properties.
*
* @var array
*/
protected $properties = ['content'];
/**
* Constructor.
*
* @param string $content
*/
public function __construct(string $content)
{
parent::__construct(['content' => strval($content)]);
}
/**
* @param array $appends
* @param bool $withType
*
* @return array
*/
public function transformForJsonRequest(array $appends = [], $withType = true): array
{
return json_decode($this->content, true) ?? [];
}
public function __toString()
{
return $this->get('content') ?? '';
}
}

30
src/Kernel/Messages/ShortVideo.php

@ -0,0 +1,30 @@ @@ -0,0 +1,30 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\Kernel\Messages;
/**
* Class ShortVideo.
*
* @property string $title
* @property string $media_id
* @property string $description
* @property string $thumb_media_id
*/
class ShortVideo extends Video
{
/**
* Messages type.
*
* @var string
*/
protected $type = 'shortvideo';
}

44
src/Kernel/Messages/TaskCard.php

@ -0,0 +1,44 @@ @@ -0,0 +1,44 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\Kernel\Messages;
/**
* Class TaskCard.
*
* @property string $title
* @property string $description
* @property string $url
* @property string $task_id
* @property array $btn
*/
class TaskCard extends Message
{
/**
* Messages type.
*
* @var string
*/
protected $type = 'taskcard';
/**
* Properties.
*
* @var array
*/
protected $properties = [
'title',
'description',
'url',
'task_id',
'btn',
];
}

54
src/Kernel/Messages/Text.php

@ -0,0 +1,54 @@ @@ -0,0 +1,54 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\Kernel\Messages;
/**
* Class Text.
*
* @property string $content
*/
class Text extends Message
{
/**
* Message type.
*
* @var string
*/
protected $type = 'text';
/**
* Properties.
*
* @var array
*/
protected $properties = ['content'];
/**
* Text constructor.
*
* @param string $content
*/
public function __construct(string $content)
{
parent::__construct(compact('content'));
}
/**
* @return array
*/
public function toXmlArray()
{
return [
'Content' => $this->get('content'),
];
}
}

40
src/Kernel/Messages/TextCard.php

@ -0,0 +1,40 @@ @@ -0,0 +1,40 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\Kernel\Messages;
/**
* Class Text.
*
* @property string $title
* @property string $description
* @property string $url
*/
class TextCard extends Message
{
/**
* Messages type.
*
* @var string
*/
protected $type = 'textcard';
/**
* Properties.
*
* @var array
*/
protected $properties = [
'title',
'description',
'url',
];
}

56
src/Kernel/Messages/Transfer.php

@ -0,0 +1,56 @@ @@ -0,0 +1,56 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\Kernel\Messages;
/**
* Class Transfer.
*
* @property string $to
* @property string $account
*/
class Transfer extends Message
{
/**
* Messages type.
*
* @var string
*/
protected $type = 'transfer_customer_service';
/**
* Properties.
*
* @var array
*/
protected $properties = [
'account',
];
/**
* Transfer constructor.
*
* @param string|null $account
*/
public function __construct(string $account = null)
{
parent::__construct(compact('account'));
}
public function toXmlArray()
{
return empty($this->get('account')) ? [] : [
'TransInfo' => [
'KfAccount' => $this->get('account'),
],
];
}
}

65
src/Kernel/Messages/Video.php

@ -0,0 +1,65 @@ @@ -0,0 +1,65 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\Kernel\Messages;
/**
* Class Video.
*
* @property string $video
* @property string $title
* @property string $media_id
* @property string $description
* @property string $thumb_media_id
*/
class Video extends Media
{
/**
* Messages type.
*
* @var string
*/
protected $type = 'video';
/**
* Properties.
*
* @var array
*/
protected $properties = [
'title',
'description',
'media_id',
'thumb_media_id',
];
/**
* Video constructor.
*
* @param string $mediaId
* @param array $attributes
*/
public function __construct(string $mediaId, array $attributes = [])
{
parent::__construct($mediaId, 'video', $attributes);
}
public function toXmlArray()
{
return [
'Video' => [
'MediaId' => $this->get('media_id'),
'Title' => $this->get('title'),
'Description' => $this->get('description'),
],
];
}
}

37
src/Kernel/Messages/Voice.php

@ -0,0 +1,37 @@ @@ -0,0 +1,37 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\Kernel\Messages;
/**
* Class Voice.
*
* @property string $media_id
*/
class Voice extends Media
{
/**
* Messages type.
*
* @var string
*/
protected $type = 'voice';
/**
* Properties.
*
* @var array
*/
protected $properties = [
'media_id',
'recognition',
];
}

39
src/Kernel/Providers/ConfigServiceProvider.php

@ -0,0 +1,39 @@ @@ -0,0 +1,39 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\Kernel\Providers;
use EasyWeChat\Kernel\Config;
use Pimple\Container;
use Pimple\ServiceProviderInterface;
/**
* Class ConfigServiceProvider.
*
* @author overtrue <i@overtrue.me>
*/
class ConfigServiceProvider implements ServiceProviderInterface
{
/**
* Registers services on the given container.
*
* This method should only be used to configure services and parameters.
* It should not get services.
*
* @param Container $pimple A container instance
*/
public function register(Container $pimple)
{
$pimple['config'] = function ($app) {
return new Config($app->getConfig());
};
}
}

47
src/Kernel/Providers/EventDispatcherServiceProvider.php

@ -0,0 +1,47 @@ @@ -0,0 +1,47 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\Kernel\Providers;
use Pimple\Container;
use Pimple\ServiceProviderInterface;
use Symfony\Component\EventDispatcher\EventDispatcher;
/**
* Class EventDispatcherServiceProvider.
*
* @author mingyoung <mingyoungcheung@gmail.com>
*/
class EventDispatcherServiceProvider implements ServiceProviderInterface
{
/**
* Registers services on the given container.
*
* This method should only be used to configure services and parameters.
* It should not get services.
*
* @param Container $pimple A container instance
*/
public function register(Container $pimple)
{
$pimple['events'] = function ($app) {
$dispatcher = new EventDispatcher();
foreach ($app->config->get('events.listen', []) as $event => $listeners) {
foreach ($listeners as $listener) {
$dispatcher->addListener($event, $listener);
}
}
return $dispatcher;
};
}
}

39
src/Kernel/Providers/ExtensionServiceProvider.php

@ -0,0 +1,39 @@ @@ -0,0 +1,39 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\Kernel\Providers;
use EasyWeChatComposer\Extension;
use Pimple\Container;
use Pimple\ServiceProviderInterface;
/**
* Class ExtensionServiceProvider.
*
* @author overtrue <i@overtrue.me>
*/
class ExtensionServiceProvider implements ServiceProviderInterface
{
/**
* Registers services on the given container.
*
* This method should only be used to configure services and parameters.
* It should not get services.
*
* @param Container $pimple A container instance
*/
public function register(Container $pimple)
{
$pimple['extension'] = function ($app) {
return new Extension($app);
};
}
}

39
src/Kernel/Providers/HttpClientServiceProvider.php

@ -0,0 +1,39 @@ @@ -0,0 +1,39 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\Kernel\Providers;
use GuzzleHttp\Client;
use Pimple\Container;
use Pimple\ServiceProviderInterface;
/**
* Class HttpClientServiceProvider.
*
* @author overtrue <i@overtrue.me>
*/
class HttpClientServiceProvider implements ServiceProviderInterface
{
/**
* Registers services on the given container.
*
* This method should only be used to configure services and parameters.
* It should not get services.
*
* @param Container $pimple A container instance
*/
public function register(Container $pimple)
{
$pimple['http_client'] = function ($app) {
return new Client($app['config']->get('http', []));
};
}
}

79
src/Kernel/Providers/LogServiceProvider.php

@ -0,0 +1,79 @@ @@ -0,0 +1,79 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\Kernel\Providers;
use EasyWeChat\Kernel\Log\LogManager;
use Pimple\Container;
use Pimple\ServiceProviderInterface;
/**
* Class LoggingServiceProvider.
*
* @author overtrue <i@overtrue.me>
*/
class LogServiceProvider implements ServiceProviderInterface
{
/**
* Registers services on the given container.
*
* This method should only be used to configure services and parameters.
* It should not get services.
*
* @param Container $pimple A container instance
*/
public function register(Container $pimple)
{
$pimple['logger'] = $pimple['log'] = function ($app) {
$config = $this->formatLogConfig($app);
if (!empty($config)) {
$app->rebind('config', $app['config']->merge($config));
}
return new LogManager($app);
};
}
public function formatLogConfig($app)
{
if (!empty($app['config']->get('log.channels'))) {
return $app['config']->get('log');
}
if (empty($app['config']->get('log'))) {
return [
'log' => [
'default' => 'errorlog',
'channels' => [
'errorlog' => [
'driver' => 'errorlog',
'level' => 'debug',
],
],
],
];
}
return [
'log' => [
'default' => 'single',
'channels' => [
'single' => [
'driver' => 'single',
'path' => $app['config']->get('log.file') ?: \sys_get_temp_dir().'/logs/easywechat.log',
'level' => $app['config']->get('log.level', 'debug'),
],
],
],
];
}
}

39
src/Kernel/Providers/RequestServiceProvider.php

@ -0,0 +1,39 @@ @@ -0,0 +1,39 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\Kernel\Providers;
use Pimple\Container;
use Pimple\ServiceProviderInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Class RequestServiceProvider.
*
* @author overtrue <i@overtrue.me>
*/
class RequestServiceProvider implements ServiceProviderInterface
{
/**
* Registers services on the given container.
*
* This method should only be used to configure services and parameters.
* It should not get services.
*
* @param Container $pimple A container instance
*/
public function register(Container $pimple)
{
$pimple['request'] = function () {
return Request::createFromGlobals();
};
}
}

375
src/Kernel/ServerGuard.php

@ -0,0 +1,375 @@ @@ -0,0 +1,375 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\Kernel;
use EasyWeChat\Kernel\Contracts\MessageInterface;
use EasyWeChat\Kernel\Exceptions\BadRequestException;
use EasyWeChat\Kernel\Exceptions\InvalidArgumentException;
use EasyWeChat\Kernel\Messages\Message;
use EasyWeChat\Kernel\Messages\News;
use EasyWeChat\Kernel\Messages\NewsItem;
use EasyWeChat\Kernel\Messages\Raw as RawMessage;
use EasyWeChat\Kernel\Messages\Text;
use EasyWeChat\Kernel\Support\XML;
use EasyWeChat\Kernel\Traits\Observable;
use EasyWeChat\Kernel\Traits\ResponseCastable;
use Symfony\Component\HttpFoundation\Response;
/**
* Class ServerGuard.
*
* 1. url 里的 signature 只是将 token+nonce+timestamp 得到的签名,只是用于验证当前请求的,在公众号环境下一直有
* 2. 企业号消息发送时是没有的,因为固定为完全模式,所以 url 里不会存在 signature, 只有 msg_signature 用于解密消息的
*
* @author overtrue <i@overtrue.me>
*/
class ServerGuard
{
use Observable;
use ResponseCastable;
/**
* @var bool
*/
protected $alwaysValidate = false;
/**
* Empty string.
*/
const SUCCESS_EMPTY_RESPONSE = 'success';
/**
* @var array
*/
const MESSAGE_TYPE_MAPPING = [
'text' => Message::TEXT,
'image' => Message::IMAGE,
'voice' => Message::VOICE,
'video' => Message::VIDEO,
'shortvideo' => Message::SHORT_VIDEO,
'location' => Message::LOCATION,
'link' => Message::LINK,
'device_event' => Message::DEVICE_EVENT,
'device_text' => Message::DEVICE_TEXT,
'event' => Message::EVENT,
'file' => Message::FILE,
'miniprogrampage' => Message::MINIPROGRAM_PAGE,
];
/**
* @var \EasyWeChat\Kernel\ServiceContainer
*/
protected $app;
/**
* Constructor.
*
* @codeCoverageIgnore
*
* @param \EasyWeChat\Kernel\ServiceContainer $app
*/
public function __construct(ServiceContainer $app)
{
$this->app = $app;
foreach ($this->app->extension->observers() as $observer) {
call_user_func_array([$this, 'push'], $observer);
}
}
/**
* Handle and return response.
*
* @return Response
*
* @throws BadRequestException
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
*/
public function serve(): Response
{
$this->app['logger']->debug('Request received:', [
'method' => $this->app['request']->getMethod(),
'uri' => $this->app['request']->getUri(),
'content-type' => $this->app['request']->getContentType(),
'content' => $this->app['request']->getContent(),
]);
$response = $this->validate()->resolve();
$this->app['logger']->debug('Server response created:', ['content' => $response->getContent()]);
return $response;
}
/**
* @return $this
*
* @throws \EasyWeChat\Kernel\Exceptions\BadRequestException
*/
public function validate()
{
if (!$this->alwaysValidate && !$this->isSafeMode()) {
return $this;
}
if ($this->app['request']->get('signature') !== $this->signature([
$this->getToken(),
$this->app['request']->get('timestamp'),
$this->app['request']->get('nonce'),
])) {
throw new BadRequestException('Invalid request signature.', 400);
}
return $this;
}
/**
* Force validate request.
*
* @return $this
*/
public function forceValidate()
{
$this->alwaysValidate = true;
return $this;
}
/**
* Get request message.
*
* @return array|\EasyWeChat\Kernel\Support\Collection|object|string
*
* @throws BadRequestException
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
*/
public function getMessage()
{
$message = $this->parseMessage($this->app['request']->getContent(false));
if (!is_array($message) || empty($message)) {
throw new BadRequestException('No message received.');
}
if ($this->isSafeMode() && !empty($message['Encrypt'])) {
$message = $this->decryptMessage($message);
// Handle JSON format.
$dataSet = json_decode($message, true);
if ($dataSet && (JSON_ERROR_NONE === json_last_error())) {
return $dataSet;
}
$message = XML::parse($message);
}
return $this->detectAndCastResponseToType($message, $this->app->config->get('response_type'));
}
/**
* Resolve server request and return the response.
*
* @return \Symfony\Component\HttpFoundation\Response
*
* @throws \EasyWeChat\Kernel\Exceptions\BadRequestException
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
*/
protected function resolve(): Response
{
$result = $this->handleRequest();
if ($this->shouldReturnRawResponse()) {
$response = new Response($result['response']);
} else {
$response = new Response(
$this->buildResponse($result['to'], $result['from'], $result['response']),
200,
['Content-Type' => 'application/xml']
);
}
$this->app->events->dispatch(new Events\ServerGuardResponseCreated($response));
return $response;
}
/**
* @return string|null
*/
protected function getToken()
{
return $this->app['config']['token'];
}
/**
* @param string $to
* @param string $from
* @param \EasyWeChat\Kernel\Contracts\MessageInterface|string|int $message
*
* @return string
*
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
*/
public function buildResponse(string $to, string $from, $message)
{
if (empty($message) || self::SUCCESS_EMPTY_RESPONSE === $message) {
return self::SUCCESS_EMPTY_RESPONSE;
}
if ($message instanceof RawMessage) {
return $message->get('content', self::SUCCESS_EMPTY_RESPONSE);
}
if (is_string($message) || is_numeric($message)) {
$message = new Text((string) $message);
}
if (is_array($message) && reset($message) instanceof NewsItem) {
$message = new News($message);
}
if (!($message instanceof Message)) {
throw new InvalidArgumentException(sprintf('Invalid Messages type "%s".', gettype($message)));
}
return $this->buildReply($to, $from, $message);
}
/**
* Handle request.
*
* @return array
*
* @throws \EasyWeChat\Kernel\Exceptions\BadRequestException
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
*/
protected function handleRequest(): array
{
$castedMessage = $this->getMessage();
$messageArray = $this->detectAndCastResponseToType($castedMessage, 'array');
$response = $this->dispatch(self::MESSAGE_TYPE_MAPPING[$messageArray['MsgType'] ?? $messageArray['msg_type'] ?? 'text'], $castedMessage);
return [
'to' => $messageArray['FromUserName'] ?? '',
'from' => $messageArray['ToUserName'] ?? '',
'response' => $response,
];
}
/**
* Build reply XML.
*
* @param string $to
* @param string $from
* @param \EasyWeChat\Kernel\Contracts\MessageInterface $message
*
* @return string
*/
protected function buildReply(string $to, string $from, MessageInterface $message): string
{
$prepends = [
'ToUserName' => $to,
'FromUserName' => $from,
'CreateTime' => time(),
'MsgType' => $message->getType(),
];
$response = $message->transformToXml($prepends);
if ($this->isSafeMode()) {
$this->app['logger']->debug('Messages safe mode is enabled.');
$response = $this->app['encryptor']->encrypt($response);
}
return $response;
}
/**
* @param array $params
*
* @return string
*/
protected function signature(array $params)
{
sort($params, SORT_STRING);
return sha1(implode($params));
}
/**
* Parse message array from raw php input.
*
* @param string $content
*
* @return array
*
* @throws \EasyWeChat\Kernel\Exceptions\BadRequestException
*/
protected function parseMessage($content)
{
try {
if (0 === stripos($content, '<')) {
$content = XML::parse($content);
} else {
// Handle JSON format.
$dataSet = json_decode($content, true);
if ($dataSet && (JSON_ERROR_NONE === json_last_error())) {
$content = $dataSet;
}
}
return (array) $content;
} catch (\Exception $e) {
throw new BadRequestException(sprintf('Invalid message content:(%s) %s', $e->getCode(), $e->getMessage()), $e->getCode());
}
}
/**
* Check the request message safe mode.
*
* @return bool
*/
protected function isSafeMode(): bool
{
return $this->app['request']->get('signature') && 'aes' === $this->app['request']->get('encrypt_type');
}
/**
* @return bool
*/
protected function shouldReturnRawResponse(): bool
{
return false;
}
/**
* @param array $message
*
* @return mixed
*/
protected function decryptMessage(array $message)
{
return $message = $this->app['encryptor']->decrypt(
$message['Encrypt'],
$this->app['request']->get('msg_signature'),
$this->app['request']->get('nonce'),
$this->app['request']->get('timestamp')
);
}
}

167
src/Kernel/ServiceContainer.php

@ -0,0 +1,167 @@ @@ -0,0 +1,167 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\Kernel;
use EasyWeChat\Kernel\Providers\ConfigServiceProvider;
use EasyWeChat\Kernel\Providers\EventDispatcherServiceProvider;
use EasyWeChat\Kernel\Providers\ExtensionServiceProvider;
use EasyWeChat\Kernel\Providers\HttpClientServiceProvider;
use EasyWeChat\Kernel\Providers\LogServiceProvider;
use EasyWeChat\Kernel\Providers\RequestServiceProvider;
use EasyWeChatComposer\Traits\WithAggregator;
use Pimple\Container;
/**
* Class ServiceContainer.
*
* @author overtrue <i@overtrue.me>
*
* @property \EasyWeChat\Kernel\Config $config
* @property \Symfony\Component\HttpFoundation\Request $request
* @property \GuzzleHttp\Client $http_client
* @property \Monolog\Logger $logger
* @property \Symfony\Component\EventDispatcher\EventDispatcher $events
*/
class ServiceContainer extends Container
{
use WithAggregator;
/**
* @var string
*/
protected $id;
/**
* @var array
*/
protected $providers = [];
/**
* @var array
*/
protected $defaultConfig = [];
/**
* @var array
*/
protected $userConfig = [];
/**
* Constructor.
*
* @param array $config
* @param array $prepends
* @param string|null $id
*/
public function __construct(array $config = [], array $prepends = [], string $id = null)
{
$this->registerProviders($this->getProviders());
parent::__construct($prepends);
$this->userConfig = $config;
$this->id = $id;
$this->aggregate();
$this->events->dispatch(new Events\ApplicationInitialized($this));
}
/**
* @return string
*/
public function getId()
{
return $this->id ?? $this->id = md5(json_encode($this->userConfig));
}
/**
* @return array
*/
public function getConfig()
{
$base = [
// http://docs.guzzlephp.org/en/stable/request-options.html
'http' => [
'timeout' => 30.0,
'base_uri' => 'https://api.weixin.qq.com/',
],
];
return array_replace_recursive($base, $this->defaultConfig, $this->userConfig);
}
/**
* Return all providers.
*
* @return array
*/
public function getProviders()
{
return array_merge([
ConfigServiceProvider::class,
LogServiceProvider::class,
RequestServiceProvider::class,
HttpClientServiceProvider::class,
ExtensionServiceProvider::class,
EventDispatcherServiceProvider::class,
], $this->providers);
}
/**
* @param string $id
* @param mixed $value
*/
public function rebind($id, $value)
{
$this->offsetUnset($id);
$this->offsetSet($id, $value);
}
/**
* Magic get access.
*
* @param string $id
*
* @return mixed
*/
public function __get($id)
{
if ($this->shouldDelegate($id)) {
return $this->delegateTo($id);
}
return $this->offsetGet($id);
}
/**
* Magic set access.
*
* @param string $id
* @param mixed $value
*/
public function __set($id, $value)
{
$this->offsetSet($id, $value);
}
/**
* @param array $providers
*/
public function registerProviders(array $providers)
{
foreach ($providers as $provider) {
parent::register(new $provider());
}
}
}

85
src/Kernel/Support/AES.php

@ -0,0 +1,85 @@ @@ -0,0 +1,85 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\Kernel\Support;
/**
* Class AES.
*
* @author overtrue <i@overtrue.me>
*/
class AES
{
/**
* @param string $text
* @param string $key
* @param string $iv
* @param int $option
*
* @return string
*/
public static function encrypt(string $text, string $key, string $iv, int $option = OPENSSL_RAW_DATA): string
{
self::validateKey($key);
self::validateIv($iv);
return openssl_encrypt($text, self::getMode($key), $key, $option, $iv);
}
/**
* @param string $cipherText
* @param string $key
* @param string $iv
* @param int $option
* @param string|null $method
*
* @return string
*/
public static function decrypt(string $cipherText, string $key, string $iv, int $option = OPENSSL_RAW_DATA, $method = null): string
{
self::validateKey($key);
self::validateIv($iv);
return openssl_decrypt($cipherText, $method ?: self::getMode($key), $key, $option, $iv);
}
/**
* @param string $key
*
* @return string
*/
public static function getMode($key)
{
return 'aes-'.(8 * strlen($key)).'-cbc';
}
/**
* @param string $key
*/
public static function validateKey(string $key)
{
if (!in_array(strlen($key), [16, 24, 32], true)) {
throw new \InvalidArgumentException(sprintf('Key length must be 16, 24, or 32 bytes; got key len (%s).', strlen($key)));
}
}
/**
* @param string $iv
*
* @throws \InvalidArgumentException
*/
public static function validateIv(string $iv)
{
if (!empty($iv) && 16 !== strlen($iv)) {
throw new \InvalidArgumentException('IV length must be 16 bytes.');
}
}
}

466
src/Kernel/Support/Arr.php

@ -0,0 +1,466 @@ @@ -0,0 +1,466 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\Kernel\Support;
/**
* Array helper from Illuminate\Support\Arr.
*/
class Arr
{
/**
* Add an element to an array using "dot" notation if it doesn't exist.
*
* @param array $array
* @param string $key
* @param mixed $value
*
* @return array
*/
public static function add(array $array, $key, $value)
{
if (is_null(static::get($array, $key))) {
static::set($array, $key, $value);
}
return $array;
}
/**
* Cross join the given arrays, returning all possible permutations.
*
* @param array ...$arrays
*
* @return array
*/
public static function crossJoin(...$arrays)
{
$results = [[]];
foreach ($arrays as $index => $array) {
$append = [];
foreach ($results as $product) {
foreach ($array as $item) {
$product[$index] = $item;
$append[] = $product;
}
}
$results = $append;
}
return $results;
}
/**
* Divide an array into two arrays. One with keys and the other with values.
*
* @param array $array
*
* @return array
*/
public static function divide(array $array)
{
return [array_keys($array), array_values($array)];
}
/**
* Flatten a multi-dimensional associative array with dots.
*
* @param array $array
* @param string $prepend
*
* @return array
*/
public static function dot(array $array, $prepend = '')
{
$results = [];
foreach ($array as $key => $value) {
if (is_array($value) && !empty($value)) {
$results = array_merge($results, static::dot($value, $prepend.$key.'.'));
} else {
$results[$prepend.$key] = $value;
}
}
return $results;
}
/**
* Get all of the given array except for a specified array of items.
*
* @param array $array
* @param array|string $keys
*
* @return array
*/
public static function except(array $array, $keys)
{
static::forget($array, $keys);
return $array;
}
/**
* Determine if the given key exists in the provided array.
*
* @param array $array
* @param string|int $key
*
* @return bool
*/
public static function exists(array $array, $key)
{
return array_key_exists($key, $array);
}
/**
* Return the first element in an array passing a given truth test.
*
* @param array $array
* @param callable|null $callback
* @param mixed $default
*
* @return mixed
*/
public static function first(array $array, callable $callback = null, $default = null)
{
if (is_null($callback)) {
if (empty($array)) {
return $default;
}
foreach ($array as $item) {
return $item;
}
}
foreach ($array as $key => $value) {
if (call_user_func($callback, $value, $key)) {
return $value;
}
}
return $default;
}
/**
* Return the last element in an array passing a given truth test.
*
* @param array $array
* @param callable|null $callback
* @param mixed $default
*
* @return mixed
*/
public static function last(array $array, callable $callback = null, $default = null)
{
if (is_null($callback)) {
return empty($array) ? $default : end($array);
}
return static::first(array_reverse($array, true), $callback, $default);
}
/**
* Flatten a multi-dimensional array into a single level.
*
* @param array $array
* @param int $depth
*
* @return array
*/
public static function flatten(array $array, $depth = INF)
{
return array_reduce($array, function ($result, $item) use ($depth) {
$item = $item instanceof Collection ? $item->all() : $item;
if (!is_array($item)) {
return array_merge($result, [$item]);
} elseif (1 === $depth) {
return array_merge($result, array_values($item));
}
return array_merge($result, static::flatten($item, $depth - 1));
}, []);
}
/**
* Remove one or many array items from a given array using "dot" notation.
*
* @param array $array
* @param array|string $keys
*/
public static function forget(array &$array, $keys)
{
$original = &$array;
$keys = (array) $keys;
if (0 === count($keys)) {
return;
}
foreach ($keys as $key) {
// if the exact key exists in the top-level, remove it
if (static::exists($array, $key)) {
unset($array[$key]);
continue;
}
$parts = explode('.', $key);
// clean up before each pass
$array = &$original;
while (count($parts) > 1) {
$part = array_shift($parts);
if (isset($array[$part]) && is_array($array[$part])) {
$array = &$array[$part];
} else {
continue 2;
}
}
unset($array[array_shift($parts)]);
}
}
/**
* Get an item from an array using "dot" notation.
*
* @param array $array
* @param string $key
* @param mixed $default
*
* @return mixed
*/
public static function get(array $array, $key, $default = null)
{
if (is_null($key)) {
return $array;
}
if (static::exists($array, $key)) {
return $array[$key];
}
foreach (explode('.', $key) as $segment) {
if (static::exists($array, $segment)) {
$array = $array[$segment];
} else {
return $default;
}
}
return $array;
}
/**
* Check if an item or items exist in an array using "dot" notation.
*
* @param array $array
* @param string|array $keys
*
* @return bool
*/
public static function has(array $array, $keys)
{
if (is_null($keys)) {
return false;
}
$keys = (array) $keys;
if (empty($array)) {
return false;
}
if ($keys === []) {
return false;
}
foreach ($keys as $key) {
$subKeyArray = $array;
if (static::exists($array, $key)) {
continue;
}
foreach (explode('.', $key) as $segment) {
if (static::exists($subKeyArray, $segment)) {
$subKeyArray = $subKeyArray[$segment];
} else {
return false;
}
}
}
return true;
}
/**
* Determines if an array is associative.
*
* An array is "associative" if it doesn't have sequential numerical keys beginning with zero.
*
* @param array $array
*
* @return bool
*/
public static function isAssoc(array $array)
{
$keys = array_keys($array);
return array_keys($keys) !== $keys;
}
/**
* Get a subset of the items from the given array.
*
* @param array $array
* @param array|string $keys
*
* @return array
*/
public static function only(array $array, $keys)
{
return array_intersect_key($array, array_flip((array) $keys));
}
/**
* Push an item onto the beginning of an array.
*
* @param array $array
* @param mixed $value
* @param mixed $key
*
* @return array
*/
public static function prepend(array $array, $value, $key = null)
{
if (is_null($key)) {
array_unshift($array, $value);
} else {
$array = [$key => $value] + $array;
}
return $array;
}
/**
* Get a value from the array, and remove it.
*
* @param array $array
* @param string $key
* @param mixed $default
*
* @return mixed
*/
public static function pull(array &$array, $key, $default = null)
{
$value = static::get($array, $key, $default);
static::forget($array, $key);
return $value;
}
/**
* Get a 1 value from an array.
*
* @param array $array
* @param int|null $amount
*
* @return mixed
*
* @throws \InvalidArgumentException
*/
public static function random(array $array, int $amount = null)
{
if (is_null($amount)) {
return $array[array_rand($array)];
}
$keys = array_rand($array, $amount);
$results = [];
foreach ((array) $keys as $key) {
$results[] = $array[$key];
}
return $results;
}
/**
* Set an array item to a given value using "dot" notation.
*
* If no key is given to the method, the entire array will be replaced.
*
* @param array $array
* @param string $key
* @param mixed $value
*
* @return array
*/
public static function set(array &$array, string $key, $value)
{
$keys = explode('.', $key);
while (count($keys) > 1) {
$key = array_shift($keys);
// If the key doesn't exist at this depth, we will just create an empty array
// to hold the next value, allowing us to create the arrays to hold final
// values at the correct depth. Then we'll keep digging into the array.
if (!isset($array[$key]) || !is_array($array[$key])) {
$array[$key] = [];
}
$array = &$array[$key];
}
$array[array_shift($keys)] = $value;
return $array;
}
/**
* Filter the array using the given callback.
*
* @param array $array
* @param callable $callback
*
* @return array
*/
public static function where(array $array, callable $callback)
{
return array_filter($array, $callback, ARRAY_FILTER_USE_BOTH);
}
/**
* If the given value is not an array, wrap it in one.
*
* @param mixed $value
*
* @return array
*/
public static function wrap($value)
{
return !is_array($value) ? [$value] : $value;
}
}

66
src/Kernel/Support/ArrayAccessible.php

@ -0,0 +1,66 @@ @@ -0,0 +1,66 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\Kernel\Support;
use ArrayAccess;
use ArrayIterator;
use EasyWeChat\Kernel\Contracts\Arrayable;
use IteratorAggregate;
/**
* Class ArrayAccessible.
*
* @author overtrue <i@overtrue.me>
*/
class ArrayAccessible implements ArrayAccess, IteratorAggregate, Arrayable
{
private $array;
public function __construct(array $array = [])
{
$this->array = $array;
}
public function offsetExists($offset)
{
return array_key_exists($offset, $this->array);
}
public function offsetGet($offset)
{
return $this->array[$offset];
}
public function offsetSet($offset, $value)
{
if (null === $offset) {
$this->array[] = $value;
} else {
$this->array[$offset] = $value;
}
}
public function offsetUnset($offset)
{
unset($this->array[$offset]);
}
public function getIterator()
{
return new ArrayIterator($this->array);
}
public function toArray()
{
return $this->array;
}
}

421
src/Kernel/Support/Collection.php

@ -0,0 +1,421 @@ @@ -0,0 +1,421 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\Kernel\Support;
use ArrayAccess;
use ArrayIterator;
use Countable;
use EasyWeChat\Kernel\Contracts\Arrayable;
use IteratorAggregate;
use JsonSerializable;
use Serializable;
/**
* Class Collection.
*/
class Collection implements ArrayAccess, Countable, IteratorAggregate, JsonSerializable, Serializable, Arrayable
{
/**
* The collection data.
*
* @var array
*/
protected $items = [];
/**
* set data.
*
* @param array $items
*/
public function __construct(array $items = [])
{
foreach ($items as $key => $value) {
$this->set($key, $value);
}
}
/**
* Return all items.
*
* @return array
*/
public function all()
{
return $this->items;
}
/**
* Return specific items.
*
* @param array $keys
*
* @return \EasyWeChat\Kernel\Support\Collection
*/
public function only(array $keys)
{
$return = [];
foreach ($keys as $key) {
$value = $this->get($key);
if (!is_null($value)) {
$return[$key] = $value;
}
}
return new static($return);
}
/**
* Get all items except for those with the specified keys.
*
* @param mixed $keys
*
* @return static
*/
public function except($keys)
{
$keys = is_array($keys) ? $keys : func_get_args();
return new static(Arr::except($this->items, $keys));
}
/**
* Merge data.
*
* @param Collection|array $items
*
* @return \EasyWeChat\Kernel\Support\Collection
*/
public function merge($items)
{
$clone = new static($this->all());
foreach ($items as $key => $value) {
$clone->set($key, $value);
}
return $clone;
}
/**
* To determine Whether the specified element exists.
*
* @param string $key
*
* @return bool
*/
public function has($key)
{
return !is_null(Arr::get($this->items, $key));
}
/**
* Retrieve the first item.
*
* @return mixed
*/
public function first()
{
return reset($this->items);
}
/**
* Retrieve the last item.
*
* @return bool
*/
public function last()
{
$end = end($this->items);
reset($this->items);
return $end;
}
/**
* add the item value.
*
* @param string $key
* @param mixed $value
*/
public function add($key, $value)
{
Arr::set($this->items, $key, $value);
}
/**
* Set the item value.
*
* @param string $key
* @param mixed $value
*/
public function set($key, $value)
{
Arr::set($this->items, $key, $value);
}
/**
* Retrieve item from Collection.
*
* @param string $key
* @param mixed $default
*
* @return mixed
*/
public function get($key, $default = null)
{
return Arr::get($this->items, $key, $default);
}
/**
* Remove item form Collection.
*
* @param string $key
*/
public function forget($key)
{
Arr::forget($this->items, $key);
}
/**
* Build to array.
*
* @return array
*/
public function toArray()
{
return $this->all();
}
/**
* Build to json.
*
* @param int $option
*
* @return string
*/
public function toJson($option = JSON_UNESCAPED_UNICODE)
{
return json_encode($this->all(), $option);
}
/**
* To string.
*
* @return string
*/
public function __toString()
{
return $this->toJson();
}
/**
* (PHP 5 &gt;= 5.4.0)<br/>
* Specify data which should be serialized to JSON.
*
* @see http://php.net/manual/en/jsonserializable.jsonserialize.php
*
* @return mixed data which can be serialized by <b>json_encode</b>,
* which is a value of any type other than a resource
*/
public function jsonSerialize()
{
return $this->items;
}
/**
* (PHP 5 &gt;= 5.1.0)<br/>
* String representation of object.
*
* @see http://php.net/manual/en/serializable.serialize.php
*
* @return string the string representation of the object or null
*/
public function serialize()
{
return serialize($this->items);
}
/**
* (PHP 5 &gt;= 5.0.0)<br/>
* Retrieve an external iterator.
*
* @see http://php.net/manual/en/iteratoraggregate.getiterator.php
*
* @return \ArrayIterator An instance of an object implementing <b>Iterator</b> or
* <b>Traversable</b>
*/
public function getIterator()
{
return new ArrayIterator($this->items);
}
/**
* (PHP 5 &gt;= 5.1.0)<br/>
* Count elements of an object.
*
* @see http://php.net/manual/en/countable.count.php
*
* @return int the custom count as an integer.
* </p>
* <p>
* The return value is cast to an integer
*/
public function count()
{
return count($this->items);
}
/**
* (PHP 5 &gt;= 5.1.0)<br/>
* Constructs the object.
*
* @see http://php.net/manual/en/serializable.unserialize.php
*
* @param string $serialized <p>
* The string representation of the object.
* </p>
*
* @return mixed|void
*/
public function unserialize($serialized)
{
return $this->items = unserialize($serialized);
}
/**
* Get a data by key.
*
* @param string $key
*
* @return mixed
*/
public function __get($key)
{
return $this->get($key);
}
/**
* Assigns a value to the specified data.
*
* @param string $key
* @param mixed $value
*/
public function __set($key, $value)
{
$this->set($key, $value);
}
/**
* Whether or not an data exists by key.
*
* @param string $key
*
* @return bool
*/
public function __isset($key)
{
return $this->has($key);
}
/**
* Unset an data by key.
*
* @param string $key
*/
public function __unset($key)
{
$this->forget($key);
}
/**
* var_export.
*
* @return array
*/
public function __set_state()
{
return $this->all();
}
/**
* (PHP 5 &gt;= 5.0.0)<br/>
* Whether a offset exists.
*
* @see http://php.net/manual/en/arrayaccess.offsetexists.php
*
* @param mixed $offset <p>
* An offset to check for.
* </p>
*
* @return bool true on success or false on failure.
* The return value will be casted to boolean if non-boolean was returned
*/
public function offsetExists($offset)
{
return $this->has($offset);
}
/**
* (PHP 5 &gt;= 5.0.0)<br/>
* Offset to unset.
*
* @see http://php.net/manual/en/arrayaccess.offsetunset.php
*
* @param mixed $offset <p>
* The offset to unset.
* </p>
*/
public function offsetUnset($offset)
{
if ($this->offsetExists($offset)) {
$this->forget($offset);
}
}
/**
* (PHP 5 &gt;= 5.0.0)<br/>
* Offset to retrieve.
*
* @see http://php.net/manual/en/arrayaccess.offsetget.php
*
* @param mixed $offset <p>
* The offset to retrieve.
* </p>
*
* @return mixed Can return all value types
*/
public function offsetGet($offset)
{
return $this->offsetExists($offset) ? $this->get($offset) : null;
}
/**
* (PHP 5 &gt;= 5.0.0)<br/>
* Offset to set.
*
* @see http://php.net/manual/en/arrayaccess.offsetset.php
*
* @param mixed $offset <p>
* The offset to assign the value to.
* </p>
* @param mixed $value <p>
* The value to set.
* </p>
*/
public function offsetSet($offset, $value)
{
$this->set($offset, $value);
}
}

135
src/Kernel/Support/File.php

@ -0,0 +1,135 @@ @@ -0,0 +1,135 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\Kernel\Support;
use finfo;
/**
* Class File.
*/
class File
{
/**
* MIME mapping.
*
* @var array
*/
protected static $extensionMap = [
'audio/wav' => '.wav',
'audio/x-ms-wma' => '.wma',
'video/x-ms-wmv' => '.wmv',
'video/mp4' => '.mp4',
'audio/mpeg' => '.mp3',
'audio/amr' => '.amr',
'application/vnd.rn-realmedia' => '.rm',
'audio/mid' => '.mid',
'image/bmp' => '.bmp',
'image/gif' => '.gif',
'image/png' => '.png',
'image/tiff' => '.tiff',
'image/jpeg' => '.jpg',
'application/pdf' => '.pdf',
// 列举更多的文件 mime, 企业号是支持的,公众平台这边之后万一也更新了呢
'application/msword' => '.doc',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => '.docx',
'application/vnd.openxmlformats-officedocument.wordprocessingml.template' => '.dotx',
'application/vnd.ms-word.document.macroEnabled.12' => '.docm',
'application/vnd.ms-word.template.macroEnabled.12' => '.dotm',
'application/vnd.ms-excel' => '.xls',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => '.xlsx',
'application/vnd.openxmlformats-officedocument.spreadsheetml.template' => '.xltx',
'application/vnd.ms-excel.sheet.macroEnabled.12' => '.xlsm',
'application/vnd.ms-excel.template.macroEnabled.12' => '.xltm',
'application/vnd.ms-excel.addin.macroEnabled.12' => '.xlam',
'application/vnd.ms-excel.sheet.binary.macroEnabled.12' => '.xlsb',
'application/vnd.ms-powerpoint' => '.ppt',
'application/vnd.openxmlformats-officedocument.presentationml.presentation' => '.pptx',
'application/vnd.openxmlformats-officedocument.presentationml.template' => '.potx',
'application/vnd.openxmlformats-officedocument.presentationml.slideshow' => '.ppsx',
'application/vnd.ms-powerpoint.addin.macroEnabled.12' => '.ppam',
];
/**
* File header signatures.
*
* @var array
*/
protected static $signatures = [
'ffd8ff' => '.jpg',
'424d' => '.bmp',
'47494638' => '.gif',
'2f55736572732f6f7665' => '.png',
'89504e47' => '.png',
'494433' => '.mp3',
'fffb' => '.mp3',
'fff3' => '.mp3',
'3026b2758e66cf11' => '.wma',
'52494646' => '.wav',
'57415645' => '.wav',
'41564920' => '.avi',
'000001ba' => '.mpg',
'000001b3' => '.mpg',
'2321414d52' => '.amr',
'25504446' => '.pdf',
];
/**
* Return steam extension.
*
* @param string $stream
*
* @return string|false
*/
public static function getStreamExt($stream)
{
$ext = self::getExtBySignature($stream);
try {
if (empty($ext) && is_readable($stream)) {
$stream = file_get_contents($stream);
}
} catch (\Exception $e) {
}
$fileInfo = new finfo(FILEINFO_MIME);
$mime = strstr($fileInfo->buffer($stream), ';', true);
return isset(self::$extensionMap[$mime]) ? self::$extensionMap[$mime] : $ext;
}
/**
* Get file extension by file header signature.
*
* @param string $stream
*
* @return string
*/
public static function getExtBySignature($stream)
{
$prefix = strval(bin2hex(mb_strcut($stream, 0, 10)));
foreach (self::$signatures as $signature => $extension) {
if (0 === strpos($prefix, strval($signature))) {
return $extension;
}
}
return '';
}
}

131
src/Kernel/Support/Helpers.php

@ -0,0 +1,131 @@ @@ -0,0 +1,131 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\Kernel\Support;
/*
* helpers.
*
* @author overtrue <i@overtrue.me>
*/
/**
* Generate a signature.
*
* @param array $attributes
* @param string $key
* @param string $encryptMethod
*
* @return string
*/
function generate_sign(array $attributes, $key, $encryptMethod = 'md5')
{
ksort($attributes);
$attributes['key'] = $key;
return strtoupper(call_user_func_array($encryptMethod, [urldecode(http_build_query($attributes))]));
}
/**
* @param string $signType
* @param string $secretKey
*
* @return \Closure|string
*/
function get_encrypt_method(string $signType, string $secretKey = '')
{
if ('HMAC-SHA256' === $signType) {
return function ($str) use ($secretKey) {
return hash_hmac('sha256', $str, $secretKey);
};
}
return 'md5';
}
/**
* Get client ip.
*
* @return string
*/
function get_client_ip()
{
if (!empty($_SERVER['REMOTE_ADDR'])) {
$ip = $_SERVER['REMOTE_ADDR'];
} else {
// for php-cli(phpunit etc.)
$ip = defined('PHPUNIT_RUNNING') ? '127.0.0.1' : gethostbyname(gethostname());
}
return filter_var($ip, FILTER_VALIDATE_IP) ?: '127.0.0.1';
}
/**
* Get current server ip.
*
* @return string
*/
function get_server_ip()
{
if (!empty($_SERVER['SERVER_ADDR'])) {
$ip = $_SERVER['SERVER_ADDR'];
} elseif (!empty($_SERVER['SERVER_NAME'])) {
$ip = gethostbyname($_SERVER['SERVER_NAME']);
} else {
// for php-cli(phpunit etc.)
$ip = defined('PHPUNIT_RUNNING') ? '127.0.0.1' : gethostbyname(gethostname());
}
return filter_var($ip, FILTER_VALIDATE_IP) ?: '127.0.0.1';
}
/**
* Return current url.
*
* @return string
*/
function current_url()
{
$protocol = 'http://';
if ((!empty($_SERVER['HTTPS']) && 'off' !== $_SERVER['HTTPS']) || ($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? 'http') === 'https') {
$protocol = 'https://';
}
return $protocol.$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI'];
}
/**
* Return random string.
*
* @param string $length
*
* @return string
*/
function str_random($length)
{
return Str::random($length);
}
/**
* @param string $content
* @param string $publicKey
*
* @return string
*/
function rsa_public_encrypt($content, $publicKey)
{
$encrypted = '';
openssl_public_encrypt($content, $encrypted, openssl_pkey_get_public($publicKey), OPENSSL_PKCS1_OAEP_PADDING);
return base64_encode($encrypted);
}

193
src/Kernel/Support/Str.php

@ -0,0 +1,193 @@ @@ -0,0 +1,193 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\Kernel\Support;
use EasyWeChat\Kernel\Exceptions\RuntimeException;
/**
* Class Str.
*/
class Str
{
/**
* The cache of snake-cased words.
*
* @var array
*/
protected static $snakeCache = [];
/**
* The cache of camel-cased words.
*
* @var array
*/
protected static $camelCache = [];
/**
* The cache of studly-cased words.
*
* @var array
*/
protected static $studlyCache = [];
/**
* Convert a value to camel case.
*
* @param string $value
*
* @return string
*/
public static function camel($value)
{
if (isset(static::$camelCache[$value])) {
return static::$camelCache[$value];
}
return static::$camelCache[$value] = lcfirst(static::studly($value));
}
/**
* Generate a more truly "random" alpha-numeric string.
*
* @param int $length
*
* @return string
*
* @throws \EasyWeChat\Kernel\Exceptions\RuntimeException
*/
public static function random($length = 16)
{
$string = '';
while (($len = strlen($string)) < $length) {
$size = $length - $len;
$bytes = static::randomBytes($size);
$string .= substr(str_replace(['/', '+', '='], '', base64_encode($bytes)), 0, $size);
}
return $string;
}
/**
* Generate a more truly "random" bytes.
*
* @param int $length
*
* @return string
*
* @throws RuntimeException
*
* @codeCoverageIgnore
*
* @throws \Exception
*/
public static function randomBytes($length = 16)
{
if (function_exists('random_bytes')) {
$bytes = random_bytes($length);
} elseif (function_exists('openssl_random_pseudo_bytes')) {
$bytes = openssl_random_pseudo_bytes($length, $strong);
if (false === $bytes || false === $strong) {
throw new RuntimeException('Unable to generate random string.');
}
} else {
throw new RuntimeException('OpenSSL extension is required for PHP 5 users.');
}
return $bytes;
}
/**
* Generate a "random" alpha-numeric string.
*
* Should not be considered sufficient for cryptography, etc.
*
* @param int $length
*
* @return string
*/
public static function quickRandom($length = 16)
{
$pool = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
return substr(str_shuffle(str_repeat($pool, $length)), 0, $length);
}
/**
* Convert the given string to upper-case.
*
* @param string $value
*
* @return string
*/
public static function upper($value)
{
return mb_strtoupper($value);
}
/**
* Convert the given string to title case.
*
* @param string $value
*
* @return string
*/
public static function title($value)
{
return mb_convert_case($value, MB_CASE_TITLE, 'UTF-8');
}
/**
* Convert a string to snake case.
*
* @param string $value
* @param string $delimiter
*
* @return string
*/
public static function snake($value, $delimiter = '_')
{
$key = $value.$delimiter;
if (isset(static::$snakeCache[$key])) {
return static::$snakeCache[$key];
}
if (!ctype_lower($value)) {
$value = strtolower(preg_replace('/(.)(?=[A-Z])/', '$1'.$delimiter, $value));
}
return static::$snakeCache[$key] = trim($value, '_');
}
/**
* Convert a value to studly caps case.
*
* @param string $value
*
* @return string
*/
public static function studly($value)
{
$key = $value;
if (isset(static::$studlyCache[$key])) {
return static::$studlyCache[$key];
}
$value = ucwords(str_replace(['-', '_'], ' ', $value));
return static::$studlyCache[$key] = str_replace(' ', '', $value);
}
}

167
src/Kernel/Support/XML.php

@ -0,0 +1,167 @@ @@ -0,0 +1,167 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\Kernel\Support;
use SimpleXMLElement;
/**
* Class XML.
*/
class XML
{
/**
* XML to array.
*
* @param string $xml XML string
*
* @return array
*/
public static function parse($xml)
{
$backup = libxml_disable_entity_loader(true);
$result = self::normalize(simplexml_load_string(self::sanitize($xml), 'SimpleXMLElement', LIBXML_COMPACT | LIBXML_NOCDATA | LIBXML_NOBLANKS));
libxml_disable_entity_loader($backup);
return $result;
}
/**
* XML encode.
*
* @param mixed $data
* @param string $root
* @param string $item
* @param string $attr
* @param string $id
*
* @return string
*/
public static function build(
$data,
$root = 'xml',
$item = 'item',
$attr = '',
$id = 'id'
) {
if (is_array($attr)) {
$_attr = [];
foreach ($attr as $key => $value) {
$_attr[] = "{$key}=\"{$value}\"";
}
$attr = implode(' ', $_attr);
}
$attr = trim($attr);
$attr = empty($attr) ? '' : " {$attr}";
$xml = "<{$root}{$attr}>";
$xml .= self::data2Xml($data, $item, $id);
$xml .= "</{$root}>";
return $xml;
}
/**
* Build CDATA.
*
* @param string $string
*
* @return string
*/
public static function cdata($string)
{
return sprintf('<![CDATA[%s]]>', $string);
}
/**
* Object to array.
*
*
* @param SimpleXMLElement $obj
*
* @return array
*/
protected static function normalize($obj)
{
$result = null;
if (is_object($obj)) {
$obj = (array) $obj;
}
if (is_array($obj)) {
foreach ($obj as $key => $value) {
$res = self::normalize($value);
if (('@attributes' === $key) && ($key)) {
$result = $res; // @codeCoverageIgnore
} else {
$result[$key] = $res;
}
}
} else {
$result = $obj;
}
return $result;
}
/**
* Array to XML.
*
* @param array $data
* @param string $item
* @param string $id
*
* @return string
*/
protected static function data2Xml($data, $item = 'item', $id = 'id')
{
$xml = $attr = '';
foreach ($data as $key => $val) {
if (is_numeric($key)) {
$id && $attr = " {$id}=\"{$key}\"";
$key = $item;
}
$xml .= "<{$key}{$attr}>";
if ((is_array($val) || is_object($val))) {
$xml .= self::data2Xml((array) $val, $item, $id);
} else {
$xml .= is_numeric($val) ? $val : self::cdata($val);
}
$xml .= "</{$key}>";
}
return $xml;
}
/**
* Delete invalid characters in XML.
*
* @see https://www.w3.org/TR/2008/REC-xml-20081126/#charsets - XML charset range
* @see http://php.net/manual/en/regexp.reference.escape.php - escape in UTF-8 mode
*
* @param string $xml
*
* @return string
*/
public static function sanitize($xml)
{
return preg_replace('/[^\x{9}\x{A}\x{D}\x{20}-\x{D7FF}\x{E000}-\x{FFFD}\x{10000}-\x{10FFFF}]+/u', '', $xml);
}
}

251
src/Kernel/Traits/HasAttributes.php

@ -0,0 +1,251 @@ @@ -0,0 +1,251 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\Kernel\Traits;
use EasyWeChat\Kernel\Exceptions\InvalidArgumentException;
use EasyWeChat\Kernel\Support\Arr;
use EasyWeChat\Kernel\Support\Str;
/**
* Trait Attributes.
*/
trait HasAttributes
{
/**
* @var array
*/
protected $attributes = [];
/**
* @var bool
*/
protected $snakeable = true;
/**
* Set Attributes.
*
* @param array $attributes
*
* @return $this
*/
public function setAttributes(array $attributes = [])
{
$this->attributes = $attributes;
return $this;
}
/**
* Set attribute.
*
* @param string $attribute
* @param string $value
*
* @return $this
*/
public function setAttribute($attribute, $value)
{
Arr::set($this->attributes, $attribute, $value);
return $this;
}
/**
* Get attribute.
*
* @param string $attribute
* @param mixed $default
*
* @return mixed
*/
public function getAttribute($attribute, $default = null)
{
return Arr::get($this->attributes, $attribute, $default);
}
/**
* @param string $attribute
*
* @return bool
*/
public function isRequired($attribute)
{
return in_array($attribute, $this->getRequired(), true);
}
/**
* @return array|mixed
*/
public function getRequired()
{
return property_exists($this, 'required') ? $this->required : [];
}
/**
* Set attribute.
*
* @param string $attribute
* @param mixed $value
*
* @return $this
*/
public function with($attribute, $value)
{
$this->snakeable && $attribute = Str::snake($attribute);
$this->setAttribute($attribute, $value);
return $this;
}
/**
* Override parent set() method.
*
* @param string $attribute
* @param mixed $value
*
* @return $this
*/
public function set($attribute, $value)
{
$this->setAttribute($attribute, $value);
return $this;
}
/**
* Override parent get() method.
*
* @param string $attribute
* @param mixed $default
*
* @return mixed
*/
public function get($attribute, $default = null)
{
return $this->getAttribute($attribute, $default);
}
/**
* @param string $key
*
* @return bool
*/
public function has(string $key)
{
return Arr::has($this->attributes, $key);
}
/**
* @param array $attributes
*
* @return $this
*/
public function merge(array $attributes)
{
$this->attributes = array_merge($this->attributes, $attributes);
return $this;
}
/**
* @param array|string $keys
*
* @return array
*/
public function only($keys)
{
return Arr::only($this->attributes, $keys);
}
/**
* Return all items.
*
* @return array
*
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
*/
public function all()
{
$this->checkRequiredAttributes();
return $this->attributes;
}
/**
* Magic call.
*
* @param string $method
* @param array $args
*
* @return $this
*/
public function __call($method, $args)
{
if (0 === stripos($method, 'with')) {
return $this->with(substr($method, 4), array_shift($args));
}
throw new \BadMethodCallException(sprintf('Method "%s" does not exists.', $method));
}
/**
* Magic get.
*
* @param string $property
*
* @return mixed
*/
public function __get($property)
{
return $this->get($property);
}
/**
* Magic set.
*
* @param string $property
* @param mixed $value
*
* @return $this
*/
public function __set($property, $value)
{
return $this->with($property, $value);
}
/**
* Whether or not an data exists by key.
*
* @param string $key
*
* @return bool
*/
public function __isset($key)
{
return isset($this->attributes[$key]);
}
/**
* Check required attributes.
*
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
*/
protected function checkRequiredAttributes()
{
foreach ($this->getRequired() as $attribute) {
if (is_null($this->get($attribute))) {
throw new InvalidArgumentException(sprintf('"%s" cannot be empty.', $attribute));
}
}
}
}

231
src/Kernel/Traits/HasHttpRequests.php

@ -0,0 +1,231 @@ @@ -0,0 +1,231 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\Kernel\Traits;
use GuzzleHttp\Client;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\HandlerStack;
use Psr\Http\Message\ResponseInterface;
/**
* Trait HasHttpRequests.
*
* @author overtrue <i@overtrue.me>
*/
trait HasHttpRequests
{
use ResponseCastable;
/**
* @var \GuzzleHttp\ClientInterface
*/
protected $httpClient;
/**
* @var array
*/
protected $middlewares = [];
/**
* @var \GuzzleHttp\HandlerStack
*/
protected $handlerStack;
/**
* @var array
*/
protected static $defaults = [
'curl' => [
CURLOPT_IPRESOLVE => CURL_IPRESOLVE_V4,
],
];
/**
* Set guzzle default settings.
*
* @param array $defaults
*/
public static function setDefaultOptions($defaults = [])
{
self::$defaults = $defaults;
}
/**
* Return current guzzle default settings.
*
* @return array
*/
public static function getDefaultOptions(): array
{
return self::$defaults;
}
/**
* Set GuzzleHttp\Client.
*
* @param \GuzzleHttp\ClientInterface $httpClient
*
* @return $this
*/
public function setHttpClient(ClientInterface $httpClient)
{
$this->httpClient = $httpClient;
return $this;
}
/**
* Return GuzzleHttp\ClientInterface instance.
*
* @return ClientInterface
*/
public function getHttpClient(): ClientInterface
{
if (!($this->httpClient instanceof ClientInterface)) {
if (property_exists($this, 'app') && $this->app['http_client']) {
$this->httpClient = $this->app['http_client'];
} else {
$this->httpClient = new Client(['handler' => HandlerStack::create($this->getGuzzleHandler())]);
}
}
return $this->httpClient;
}
/**
* Add a middleware.
*
* @param callable $middleware
* @param string $name
*
* @return $this
*/
public function pushMiddleware(callable $middleware, string $name = null)
{
if (!is_null($name)) {
$this->middlewares[$name] = $middleware;
} else {
array_push($this->middlewares, $middleware);
}
return $this;
}
/**
* Return all middlewares.
*
* @return array
*/
public function getMiddlewares(): array
{
return $this->middlewares;
}
/**
* Make a request.
*
* @param string $url
* @param string $method
* @param array $options
*
* @return \Psr\Http\Message\ResponseInterface
*
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function request($url, $method = 'GET', $options = []): ResponseInterface
{
$method = strtoupper($method);
$options = array_merge(self::$defaults, $options, ['handler' => $this->getHandlerStack()]);
$options = $this->fixJsonIssue($options);
if (property_exists($this, 'baseUri') && !is_null($this->baseUri)) {
$options['base_uri'] = $this->baseUri;
}
$response = $this->getHttpClient()->request($method, $url, $options);
$response->getBody()->rewind();
return $response;
}
/**
* @param \GuzzleHttp\HandlerStack $handlerStack
*
* @return $this
*/
public function setHandlerStack(HandlerStack $handlerStack)
{
$this->handlerStack = $handlerStack;
return $this;
}
/**
* Build a handler stack.
*
* @return \GuzzleHttp\HandlerStack
*/
public function getHandlerStack(): HandlerStack
{
if ($this->handlerStack) {
return $this->handlerStack;
}
$this->handlerStack = HandlerStack::create($this->getGuzzleHandler());
foreach ($this->middlewares as $name => $middleware) {
$this->handlerStack->push($middleware, $name);
}
return $this->handlerStack;
}
/**
* @param array $options
*
* @return array
*/
protected function fixJsonIssue(array $options): array
{
if (isset($options['json']) && is_array($options['json'])) {
$options['headers'] = array_merge($options['headers'] ?? [], ['Content-Type' => 'application/json']);
if (empty($options['json'])) {
$options['body'] = \GuzzleHttp\json_encode($options['json'], JSON_FORCE_OBJECT);
} else {
$options['body'] = \GuzzleHttp\json_encode($options['json'], JSON_UNESCAPED_UNICODE);
}
unset($options['json']);
}
return $options;
}
/**
* Get guzzle handler.
*
* @return callable
*/
protected function getGuzzleHandler()
{
if (property_exists($this, 'app') && isset($this->app['guzzle_handler'])) {
return is_string($handler = $this->app->raw('guzzle_handler'))
? new $handler()
: $handler;
}
return \GuzzleHttp\choose_handler();
}
}

105
src/Kernel/Traits/InteractsWithCache.php

@ -0,0 +1,105 @@ @@ -0,0 +1,105 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\Kernel\Traits;
use EasyWeChat\Kernel\Exceptions\InvalidArgumentException;
use EasyWeChat\Kernel\ServiceContainer;
use Psr\Cache\CacheItemPoolInterface;
use Psr\SimpleCache\CacheInterface as SimpleCacheInterface;
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
use Symfony\Component\Cache\Psr16Cache;
use Symfony\Component\Cache\Simple\FilesystemCache;
/**
* Trait InteractsWithCache.
*
* @author overtrue <i@overtrue.me>
*/
trait InteractsWithCache
{
/**
* @var \Psr\SimpleCache\CacheInterface
*/
protected $cache;
/**
* Get cache instance.
*
* @return \Psr\SimpleCache\CacheInterface
*
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
*/
public function getCache()
{
if ($this->cache) {
return $this->cache;
}
if (property_exists($this, 'app') && $this->app instanceof ServiceContainer && isset($this->app['cache'])) {
$this->setCache($this->app['cache']);
// Fix PHPStan error
assert($this->cache instanceof \Psr\SimpleCache\CacheInterface);
return $this->cache;
}
return $this->cache = $this->createDefaultCache();
}
/**
* Set cache instance.
*
* @param \Psr\SimpleCache\CacheInterface|\Psr\Cache\CacheItemPoolInterface $cache
*
* @return $this
*
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
*/
public function setCache($cache)
{
if (empty(\array_intersect([SimpleCacheInterface::class, CacheItemPoolInterface::class], \class_implements($cache)))) {
throw new InvalidArgumentException(\sprintf('The cache instance must implements %s or %s interface.', SimpleCacheInterface::class, CacheItemPoolInterface::class));
}
if ($cache instanceof CacheItemPoolInterface) {
if (!$this->isSymfony43OrHigher()) {
throw new InvalidArgumentException(sprintf('The cache instance must implements %s', SimpleCacheInterface::class));
}
$cache = new Psr16Cache($cache);
}
$this->cache = $cache;
return $this;
}
/**
* @return \Psr\SimpleCache\CacheInterface
*/
protected function createDefaultCache()
{
if ($this->isSymfony43OrHigher()) {
return new Psr16Cache(new FilesystemAdapter('easywechat', 1500));
}
return new FilesystemCache();
}
/**
* @return bool
*/
protected function isSymfony43OrHigher(): bool
{
return \class_exists('Symfony\Component\Cache\Psr16Cache');
}
}

285
src/Kernel/Traits/Observable.php

@ -0,0 +1,285 @@ @@ -0,0 +1,285 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\Kernel\Traits;
use EasyWeChat\Kernel\Clauses\Clause;
use EasyWeChat\Kernel\Contracts\EventHandlerInterface;
use EasyWeChat\Kernel\Decorators\FinallyResult;
use EasyWeChat\Kernel\Decorators\TerminateResult;
use EasyWeChat\Kernel\Exceptions\InvalidArgumentException;
use EasyWeChat\Kernel\ServiceContainer;
/**
* Trait Observable.
*
* @author overtrue <i@overtrue.me>
*/
trait Observable
{
/**
* @var array
*/
protected $handlers = [];
/**
* @var array
*/
protected $clauses = [];
/**
* @param \Closure|EventHandlerInterface|callable|string $handler
* @param \Closure|EventHandlerInterface|callable|string $condition
*
* @return \EasyWeChat\Kernel\Clauses\Clause
*
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
* @throws \ReflectionException
*/
public function push($handler, $condition = '*')
{
list($handler, $condition) = $this->resolveHandlerAndCondition($handler, $condition);
if (!isset($this->handlers[$condition])) {
$this->handlers[$condition] = [];
}
array_push($this->handlers[$condition], $handler);
return $this->newClause($handler);
}
/**
* @param array $handlers
*
* @return $this
*/
public function setHandlers(array $handlers = [])
{
$this->handlers = $handlers;
return $this;
}
/**
* @param \Closure|EventHandlerInterface|string $handler
* @param \Closure|EventHandlerInterface|string $condition
*
* @return \EasyWeChat\Kernel\Clauses\Clause
*
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
* @throws \ReflectionException
*/
public function unshift($handler, $condition = '*')
{
list($handler, $condition) = $this->resolveHandlerAndCondition($handler, $condition);
if (!isset($this->handlers[$condition])) {
$this->handlers[$condition] = [];
}
array_unshift($this->handlers[$condition], $handler);
return $this->newClause($handler);
}
/**
* @param string $condition
* @param \Closure|EventHandlerInterface|string $handler
*
* @return \EasyWeChat\Kernel\Clauses\Clause
*
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
* @throws \ReflectionException
*/
public function observe($condition, $handler)
{
return $this->push($handler, $condition);
}
/**
* @param string $condition
* @param \Closure|EventHandlerInterface|string $handler
*
* @return \EasyWeChat\Kernel\Clauses\Clause
*
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
* @throws \ReflectionException
*/
public function on($condition, $handler)
{
return $this->push($handler, $condition);
}
/**
* @param string|int $event
* @param mixed ...$payload
*
* @return mixed|null
*/
public function dispatch($event, $payload)
{
return $this->notify($event, $payload);
}
/**
* @param string|int $event
* @param mixed ...$payload
*
* @return mixed|null
*/
public function notify($event, $payload)
{
$result = null;
foreach ($this->handlers as $condition => $handlers) {
if ('*' === $condition || ($condition & $event) === $event) {
foreach ($handlers as $handler) {
if ($clause = $this->clauses[$this->getHandlerHash($handler)] ?? null) {
if ($clause->intercepted($payload)) {
continue;
}
}
$response = $this->callHandler($handler, $payload);
switch (true) {
case $response instanceof TerminateResult:
return $response->content;
case true === $response:
continue 2;
case false === $response:
break 2;
case !empty($response) && !($result instanceof FinallyResult):
$result = $response;
}
}
}
}
return $result instanceof FinallyResult ? $result->content : $result;
}
/**
* @return array
*/
public function getHandlers()
{
return $this->handlers;
}
/**
* @param mixed $handler
*
* @return \EasyWeChat\Kernel\Clauses\Clause
*/
protected function newClause($handler): Clause
{
return $this->clauses[$this->getHandlerHash($handler)] = new Clause();
}
/**
* @param mixed $handler
*
* @return string
*/
protected function getHandlerHash($handler)
{
if (is_string($handler)) {
return $handler;
}
if (is_array($handler)) {
return is_string($handler[0])
? $handler[0].'::'.$handler[1]
: get_class($handler[0]).$handler[1];
}
return spl_object_hash($handler);
}
/**
* @param callable $handler
* @param mixed $payload
*
* @return mixed
*/
protected function callHandler(callable $handler, $payload)
{
try {
return call_user_func_array($handler, [$payload]);
} catch (\Exception $e) {
if (property_exists($this, 'app') && $this->app instanceof ServiceContainer) {
$this->app['logger']->error($e->getCode().': '.$e->getMessage(), [
'code' => $e->getCode(),
'message' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
]);
}
}
}
/**
* @param mixed $handler
*
* @return \Closure
*
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
* @throws \ReflectionException
*/
protected function makeClosure($handler)
{
if (is_callable($handler)) {
return $handler;
}
if (is_string($handler) && '*' !== $handler) {
if (!class_exists($handler)) {
throw new InvalidArgumentException(sprintf('Class "%s" not exists.', $handler));
}
if (!in_array(EventHandlerInterface::class, (new \ReflectionClass($handler))->getInterfaceNames(), true)) {
throw new InvalidArgumentException(sprintf('Class "%s" not an instance of "%s".', $handler, EventHandlerInterface::class));
}
return function ($payload) use ($handler) {
return (new $handler($this->app ?? null))->handle($payload);
};
}
if ($handler instanceof EventHandlerInterface) {
return function () use ($handler) {
return $handler->handle(...func_get_args());
};
}
throw new InvalidArgumentException('No valid handler is found in arguments.');
}
/**
* @param mixed $handler
* @param mixed $condition
*
* @return array
*
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
* @throws \ReflectionException
*/
protected function resolveHandlerAndCondition($handler, $condition): array
{
if (is_int($handler) || (is_string($handler) && !class_exists($handler))) {
list($handler, $condition) = [$condition, $handler];
}
return [$this->makeClosure($handler), $condition];
}
}

93
src/Kernel/Traits/ResponseCastable.php

@ -0,0 +1,93 @@ @@ -0,0 +1,93 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\Kernel\Traits;
use EasyWeChat\Kernel\Contracts\Arrayable;
use EasyWeChat\Kernel\Exceptions\InvalidArgumentException;
use EasyWeChat\Kernel\Exceptions\InvalidConfigException;
use EasyWeChat\Kernel\Http\Response;
use EasyWeChat\Kernel\Support\Collection;
use Psr\Http\Message\ResponseInterface;
/**
* Trait ResponseCastable.
*
* @author overtrue <i@overtrue.me>
*/
trait ResponseCastable
{
/**
* @param \Psr\Http\Message\ResponseInterface $response
* @param string|null $type
*
* @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string
*
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
*/
protected function castResponseToType(ResponseInterface $response, $type = null)
{
$response = Response::buildFromPsrResponse($response);
$response->getBody()->rewind();
switch ($type ?? 'array') {
case 'collection':
return $response->toCollection();
case 'array':
return $response->toArray();
case 'object':
return $response->toObject();
case 'raw':
return $response;
default:
if (!is_subclass_of($type, Arrayable::class)) {
throw new InvalidConfigException(sprintf('Config key "response_type" classname must be an instanceof %s', Arrayable::class));
}
return new $type($response);
}
}
/**
* @param mixed $response
* @param string|null $type
*
* @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string
*
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
*/
protected function detectAndCastResponseToType($response, $type = null)
{
switch (true) {
case $response instanceof ResponseInterface:
$response = Response::buildFromPsrResponse($response);
break;
case $response instanceof Arrayable:
$response = new Response(200, [], json_encode($response->toArray()));
break;
case ($response instanceof Collection) || is_array($response) || is_object($response):
$response = new Response(200, [], json_encode($response));
break;
case is_scalar($response):
$response = new Response(200, [], (string) $response);
break;
default:
throw new InvalidArgumentException(sprintf('Unsupported response type "%s"', gettype($response)));
}
return $this->castResponseToType($response, $type);
}
}

173
src/MicroMerchant/Application.php

@ -0,0 +1,173 @@ @@ -0,0 +1,173 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\MicroMerchant;
use EasyWeChat\Kernel\Exceptions\InvalidArgumentException;
use EasyWeChat\Kernel\ServiceContainer;
use EasyWeChat\Kernel\Support;
use EasyWeChat\MicroMerchant\Kernel\Exceptions\InvalidSignException;
/**
* Class Application.
*
* @author liuml <liumenglei0211@gmail.com>
*
* @property \EasyWeChat\MicroMerchant\Certficates\Client $certficates
* @property \EasyWeChat\MicroMerchant\Material\Client $material
* @property \EasyWeChat\MicroMerchant\MerchantConfig\Client $merchantConfig
* @property \EasyWeChat\MicroMerchant\Withdraw\Client $withdraw
* @property \EasyWeChat\MicroMerchant\Media\Client $media
*
* @method mixed submitApplication(array $params)
* @method mixed getStatus(string $applymentId, string $businessCode = '')
* @method mixed upgrade(array $params)
* @method mixed getUpgradeStatus(string $subMchId = '')
*/
class Application extends ServiceContainer
{
/**
* @var array
*/
protected $providers = [
// Base services
Base\ServiceProvider::class,
Certficates\ServiceProvider::class,
MerchantConfig\ServiceProvider::class,
Material\ServiceProvider::class,
Withdraw\ServiceProvider::class,
Media\ServiceProvider::class,
];
/**
* @var array
*/
protected $defaultConfig = [
'http' => [
'base_uri' => 'https://api.mch.weixin.qq.com/',
],
'log' => [
'default' => 'dev', // 默认使用的 channel,生产环境可以改为下面的 prod
'channels' => [
// 测试环境
'dev' => [
'driver' => 'single',
'path' => '/tmp/easywechat.log',
'level' => 'debug',
],
// 生产环境
'prod' => [
'driver' => 'daily',
'path' => '/tmp/easywechat.log',
'level' => 'info',
],
],
],
];
/**
* @return string
*
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
*/
public function getKey()
{
$key = $this['config']->key;
if (empty($key)) {
throw new InvalidArgumentException('config key connot be empty.');
}
if (32 !== strlen($key)) {
throw new InvalidArgumentException(sprintf("'%s' should be 32 chars length.", $key));
}
return $key;
}
/**
* set sub-mch-id and appid.
*
* @param string $subMchId Identification Number of Small and Micro Businessmen Reported by Service Providers
* @param string $appId Public Account ID of Service Provider
*
* @return $this
*/
public function setSubMchId(string $subMchId, string $appId = '')
{
$this['config']->set('sub_mch_id', $subMchId);
if ($appId) {
$this['config']->set('appid', $appId);
}
return $this;
}
/**
* setCertificate.
*
* @param string $certificate
* @param string $serialNo
*
* @return $this
*/
public function setCertificate(string $certificate, string $serialNo)
{
$this['config']->set('certificate', $certificate);
$this['config']->set('serial_no', $serialNo);
return $this;
}
/**
* Returning true indicates that the verification is successful,
* returning false indicates that the signature field does not exist or is empty,
* and if the signature verification is wrong, the InvalidSignException will be thrown directly.
*
* @param array $data
*
* @return bool
*
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
* @throws \EasyWeChat\MicroMerchant\Kernel\Exceptions\InvalidSignException
*/
public function verifySignature(array $data)
{
if (!isset($data['sign']) || empty($data['sign'])) {
return false;
}
$sign = $data['sign'];
unset($data['sign']);
$signType = strlen($sign) > 32 ? 'HMAC-SHA256' : 'MD5';
$secretKey = $this->getKey();
$encryptMethod = Support\get_encrypt_method($signType, $secretKey);
if (Support\generate_sign($data, $secretKey, $encryptMethod) === $sign) {
return true;
}
throw new InvalidSignException('return value signature verification error');
}
/**
* @param string $name
* @param array $arguments
*
* @return mixed
*/
public function __call($name, $arguments)
{
return call_user_func_array([$this['base'], $name], $arguments);
}
}

126
src/MicroMerchant/Base/Client.php

@ -0,0 +1,126 @@ @@ -0,0 +1,126 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\MicroMerchant\Base;
use EasyWeChat\MicroMerchant\Kernel\BaseClient;
/**
* Class Client.
*
* @author liuml <liumenglei0211@163.com>
* @DateTime 2019-05-30 14:19
*/
class Client extends BaseClient
{
/**
* apply to settle in to become a small micro merchant.
*
* @param array $params
*
* @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string
*
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
* @throws \EasyWeChat\MicroMerchant\Kernel\Exceptions\EncryptException
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function submitApplication(array $params)
{
$params = $this->processParams(array_merge($params, [
'version' => '3.0',
'cert_sn' => '',
'sign_type' => 'HMAC-SHA256',
'nonce_str' => uniqid('micro'),
]));
return $this->safeRequest('applyment/micro/submit', $params);
}
/**
* query application status.
*
* @param string $applymentId
* @param string $businessCode
*
* @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string
*
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function getStatus(string $applymentId, string $businessCode = '')
{
if (!empty($applymentId)) {
$params = [
'applyment_id' => $applymentId,
];
} else {
$params = [
'business_code' => $businessCode,
];
}
$params = array_merge($params, [
'version' => '1.0',
'sign_type' => 'HMAC-SHA256',
'nonce_str' => uniqid('micro'),
]);
return $this->safeRequest('applyment/micro/getstate', $params);
}
/**
* merchant upgrade api.
*
* @param array $params
*
* @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string
*
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
* @throws \EasyWeChat\MicroMerchant\Kernel\Exceptions\EncryptException
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function upgrade(array $params)
{
$params['sub_mch_id'] = $params['sub_mch_id'] ?? $this->app['config']->sub_mch_id;
$params = $this->processParams(array_merge($params, [
'version' => '1.0',
'cert_sn' => '',
'sign_type' => 'HMAC-SHA256',
'nonce_str' => uniqid('micro'),
]));
return $this->safeRequest('applyment/micro/submitupgrade', $params);
}
/**
* get upgrade status.
*
* @param string $subMchId
*
* @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string
*
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function getUpgradeStatus(string $subMchId = '')
{
return $this->safeRequest('applyment/micro/getupgradestate', [
'version' => '1.0',
'sign_type' => 'HMAC-SHA256',
'sub_mch_id' => $subMchId ?: $this->app['config']->sub_mch_id,
'nonce_str' => uniqid('micro'),
]);
}
}

33
src/MicroMerchant/Base/ServiceProvider.php

@ -0,0 +1,33 @@ @@ -0,0 +1,33 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\MicroMerchant\Base;
use Pimple\Container;
use Pimple\ServiceProviderInterface;
/**
* Class ServiceProvider.
*
* @author overtrue <i@overtrue.me>
*/
class ServiceProvider implements ServiceProviderInterface
{
/**
* {@inheritdoc}.
*/
public function register(Container $app)
{
$app['base'] = function ($app) {
return new Client($app);
};
}
}

93
src/MicroMerchant/Certficates/Client.php

@ -0,0 +1,93 @@ @@ -0,0 +1,93 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\MicroMerchant\Certficates;
use EasyWeChat\Kernel\Exceptions\InvalidArgumentException;
use EasyWeChat\MicroMerchant\Kernel\BaseClient;
use EasyWeChat\MicroMerchant\Kernel\Exceptions\InvalidExtensionException;
/**
* Class Client.
*
* @author liuml <liumenglei0211@163.com>
* @DateTime 2019-05-30 14:19
*/
class Client extends BaseClient
{
/**
* get certficates.
*
* @param bool $returnRaw
*
* @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string
*
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
* @throws \EasyWeChat\MicroMerchant\Kernel\Exceptions\InvalidExtensionException
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function get(bool $returnRaw = false)
{
$params = [
'sign_type' => 'HMAC-SHA256',
'nonce_str' => uniqid('micro'),
];
if (true === $returnRaw) {
return $this->requestRaw('risk/getcertficates', $params);
}
/** @var array $response */
$response = $this->requestArray('risk/getcertficates', $params);
if ('SUCCESS' !== $response['return_code']) {
throw new InvalidArgumentException(sprintf('Failed to get certificate. return_code_msg: "%s" .', $response['return_code'].'('.$response['return_msg'].')'));
}
if ('SUCCESS' !== $response['result_code']) {
throw new InvalidArgumentException(sprintf('Failed to get certificate. result_err_code_desc: "%s" .', $response['result_code'].'('.$response['err_code'].'['.$response['err_code_desc'].'])'));
}
$certificates = \GuzzleHttp\json_decode($response['certificates'], true)['data'][0];
$ciphertext = $this->decrypt($certificates['encrypt_certificate']);
unset($certificates['encrypt_certificate']);
$certificates['certificates'] = $ciphertext;
return $certificates;
}
/**
* decrypt ciphertext.
*
* @param array $encryptCertificate
*
* @return string
*
* @throws \EasyWeChat\MicroMerchant\Kernel\Exceptions\InvalidExtensionException
*/
public function decrypt(array $encryptCertificate)
{
if (false === extension_loaded('sodium')) {
throw new InvalidExtensionException('sodium extension is not installed,Reference link https://php.net/manual/zh/book.sodium.php');
}
if (false === sodium_crypto_aead_aes256gcm_is_available()) {
throw new InvalidExtensionException('aes256gcm is not currently supported');
}
// sodium_crypto_aead_aes256gcm_decrypt function needs to open libsodium extension.
// https://www.php.net/manual/zh/function.sodium-crypto-aead-aes256gcm-decrypt.php
return sodium_crypto_aead_aes256gcm_decrypt(
base64_decode($encryptCertificate['ciphertext'], true),
$encryptCertificate['associated_data'],
$encryptCertificate['nonce'],
$this->app['config']->apiv3_key
);
}
}

33
src/MicroMerchant/Certficates/ServiceProvider.php

@ -0,0 +1,33 @@ @@ -0,0 +1,33 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\MicroMerchant\Certficates;
use Pimple\Container;
use Pimple\ServiceProviderInterface;
/**
* Class ServiceProvider.
*
* @author overtrue <i@overtrue.me>
*/
class ServiceProvider implements ServiceProviderInterface
{
/**
* {@inheritdoc}.
*/
public function register(Container $app)
{
$app['certficates'] = function ($app) {
return new Client($app);
};
}
}

256
src/MicroMerchant/Kernel/BaseClient.php

@ -0,0 +1,256 @@ @@ -0,0 +1,256 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\MicroMerchant\Kernel;
use EasyWeChat\Kernel\Exceptions\InvalidArgumentException;
use EasyWeChat\Kernel\Support;
use EasyWeChat\MicroMerchant\Application;
use EasyWeChat\MicroMerchant\Kernel\Exceptions\EncryptException;
use EasyWeChat\Payment\Kernel\BaseClient as PaymentBaseClient;
/**
* Class BaseClient.
*
* @author liuml <liumenglei0211@163.com>
* @DateTime 2019-07-10 12:06
*/
class BaseClient extends PaymentBaseClient
{
/**
* @var string
*/
protected $certificates;
/**
* BaseClient constructor.
*
* @param \EasyWeChat\MicroMerchant\Application $app
*/
public function __construct(Application $app)
{
$this->app = $app;
$this->setHttpClient($this->app['http_client']);
}
/**
* Extra request params.
*
* @return array
*/
protected function prepends()
{
return [];
}
/**
* httpUpload.
*
* @param string $url
* @param array $files
* @param array $form
* @param array $query
* @param bool $returnResponse
*
* @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string
*
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
* @throws \EasyWeChat\MicroMerchant\Kernel\Exceptions\InvalidSignException
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function httpUpload(string $url, array $files = [], array $form = [], array $query = [], $returnResponse = false)
{
$multipart = [];
foreach ($files as $name => $path) {
$multipart[] = [
'name' => $name,
'contents' => fopen($path, 'r'),
];
}
$base = [
'mch_id' => $this->app['config']['mch_id'],
];
$form = array_merge($base, $form);
$form['sign'] = $this->getSign($form);
foreach ($form as $name => $contents) {
$multipart[] = compact('name', 'contents');
}
$options = [
'query' => $query,
'multipart' => $multipart,
'connect_timeout' => 30,
'timeout' => 30,
'read_timeout' => 30,
'cert' => $this->app['config']->get('cert_path'),
'ssl_key' => $this->app['config']->get('key_path'),
];
$this->pushMiddleware($this->logMiddleware(), 'log');
$response = $this->performRequest($url, 'POST', $options);
$result = $returnResponse ? $response : $this->castResponseToType($response, $this->app->config->get('response_type'));
// auto verify signature
if ($returnResponse || 'array' !== ($this->app->config->get('response_type') ?? 'array')) {
$this->app->verifySignature($this->castResponseToType($response, 'array'));
} else {
$this->app->verifySignature($result);
}
return $result;
}
/**
* request.
*
* @param string $endpoint
* @param array $params
* @param string $method
* @param array $options
* @param bool $returnResponse
*
* @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string
*
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
* @throws \EasyWeChat\MicroMerchant\Kernel\Exceptions\InvalidSignException
* @throws \GuzzleHttp\Exception\GuzzleException
*/
protected function request(string $endpoint, array $params = [], $method = 'post', array $options = [], $returnResponse = false)
{
$base = [
'mch_id' => $this->app['config']['mch_id'],
];
$params = array_merge($base, $this->prepends(), $params);
$params['sign'] = $this->getSign($params);
$options = array_merge([
'body' => Support\XML::build($params),
], $options);
$this->pushMiddleware($this->logMiddleware(), 'log');
$response = $this->performRequest($endpoint, $method, $options);
$result = $returnResponse ? $response : $this->castResponseToType($response, $this->app->config->get('response_type'));
// auto verify signature
if ($returnResponse || 'array' !== ($this->app->config->get('response_type') ?? 'array')) {
$this->app->verifySignature($this->castResponseToType($response, 'array'));
} else {
$this->app->verifySignature($result);
}
return $result;
}
/**
* processing parameters contain fields that require sensitive information encryption.
*
* @param array $params
*
* @return array
*
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
* @throws \EasyWeChat\MicroMerchant\Kernel\Exceptions\EncryptException
*/
protected function processParams(array $params)
{
$serial_no = $this->app['config']->get('serial_no');
if (null === $serial_no) {
throw new InvalidArgumentException('config serial_no connot be empty.');
}
$params['cert_sn'] = $serial_no;
$sensitive_fields = $this->getSensitiveFieldsName();
foreach ($params as $k => $v) {
if (in_array($k, $sensitive_fields, true)) {
$params[$k] = $this->encryptSensitiveInformation($v);
}
}
return $params;
}
/**
* To id card, mobile phone number and other fields sensitive information encryption.
*
* @param string $string
*
* @return string
*
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
* @throws \EasyWeChat\MicroMerchant\Kernel\Exceptions\EncryptException
*/
protected function encryptSensitiveInformation(string $string)
{
$certificates = $this->app['config']->get('certificate');
if (null === $certificates) {
throw new InvalidArgumentException('config certificate connot be empty.');
}
$encrypted = '';
$publicKeyResource = openssl_get_publickey($certificates);
$f = openssl_public_encrypt($string, $encrypted, $publicKeyResource);
openssl_free_key($publicKeyResource);
if ($f) {
return base64_encode($encrypted);
}
throw new EncryptException('Encryption of sensitive information failed');
}
/**
* get sensitive fields name.
*
* @return array
*/
protected function getSensitiveFieldsName()
{
return [
'id_card_name',
'id_card_number',
'account_name',
'account_number',
'contact',
'contact_phone',
'contact_email',
'legal_person',
'mobile_phone',
'email',
];
}
/**
* getSign.
*
* @param array $params
*
* @return string
*
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
*/
protected function getSign(array $params)
{
$params = array_filter($params);
$key = $this->app->getKey();
$encryptMethod = Support\get_encrypt_method(Support\Arr::get($params, 'sign_type', 'MD5'), $key);
return Support\generate_sign($params, $key, $encryptMethod);
}
}

23
src/MicroMerchant/Kernel/Exceptions/EncryptException.php

@ -0,0 +1,23 @@ @@ -0,0 +1,23 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\MicroMerchant\Kernel\Exceptions;
use EasyWeChat\Kernel\Exceptions\Exception;
/**
* Class EncryptException.
*
* @author liuml <liumenglei0211@163.com>
*/
class EncryptException extends Exception
{
}

23
src/MicroMerchant/Kernel/Exceptions/InvalidExtensionException.php

@ -0,0 +1,23 @@ @@ -0,0 +1,23 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\MicroMerchant\Kernel\Exceptions;
use EasyWeChat\Kernel\Exceptions\Exception;
/**
* Class InvalidExtensionException.
*
* @author liuml <liumenglei0211@163.com>
*/
class InvalidExtensionException extends Exception
{
}

23
src/MicroMerchant/Kernel/Exceptions/InvalidSignException.php

@ -0,0 +1,23 @@ @@ -0,0 +1,23 @@
<?php
/*
* This file is part of the overtrue/wechat.
*
* (c) overtrue <i@overtrue.me>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace EasyWeChat\MicroMerchant\Kernel\Exceptions;
use EasyWeChat\Kernel\Exceptions\Exception;
/**
* Class InvalidSignException.
*
* @author liuml <liumenglei0211@163.com>
*/
class InvalidSignException extends Exception
{
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save