This commit is contained in:
2025-10-20 10:02:41 +08:00
parent a4858d47fc
commit dc0a271adf
2805 changed files with 451240 additions and 0 deletions

1401
vendor/overtrue/wechat/CHANGELOG.md vendored Normal file

File diff suppressed because it is too large Load Diff

67
vendor/overtrue/wechat/CONTRIBUTING.md vendored Normal file
View File

@@ -0,0 +1,67 @@
# Contribute
## Introduction
First, thank you for considering contributing to wechat! It's people like you that make the open source community such a great community! 😊
We welcome any type of contribution, not only code. You can help with
- **QA**: file bug reports, the more details you can give the better (e.g. screenshots with the console open)
- **Marketing**: writing blog posts, howto's, printing stickers, ...
- **Community**: presenting the project at meetups, organizing a dedicated meetup for the local community, ...
- **Code**: take a look at the [open issues](issues). Even if you can't write code, commenting on them, showing that you care about a given issue matters. It helps us triage them.
- **Money**: we welcome financial contributions in full transparency on our [open collective](https://opencollective.com/wechat).
## Your First Contribution
Working on your first Pull Request? You can learn how from this *free* series, [How to Contribute to an Open Source Project on GitHub](https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github).
## Submitting code
Any code change should be submitted as a pull request. The description should explain what the code does and give steps to execute it. The pull request should also contain tests.
## Code review process
The bigger the pull request, the longer it will take to review and merge. Try to break down large pull requests in smaller chunks that are easier to review and merge.
It is also always helpful to have some context for your pull request. What was the purpose? Why does it matter to you?
## Financial contributions
We also welcome financial contributions in full transparency on our [open collective](https://opencollective.com/wechat).
Anyone can file an expense. If the expense makes sense for the development of the community, it will be "merged" in the ledger of our open collective by the core contributors and the person who filed the expense will be reimbursed.
## Questions
If you have any questions, create an [issue](issue) (protip: do a quick search first to see if someone else didn't ask the same question before!).
You can also reach us at hello@wechat.opencollective.com.
## Credits
### Contributors
Thank you to all the people who have already contributed to wechat!
<a href="graphs/contributors"><img src="https://opencollective.com/wechat/contributors.svg?width=890" /></a>
### Backers
Thank you to all our backers! [[Become a backer](https://opencollective.com/wechat#backer)]
<a href="https://opencollective.com/wechat#backers" target="_blank"><img src="https://opencollective.com/wechat/backers.svg?width=890"></a>
### Sponsors
Thank you to all our sponsors! (please ask your company to also support this open source project by [becoming a sponsor](https://opencollective.com/wechat#sponsor))
<a href="https://opencollective.com/wechat/sponsor/0/website" target="_blank"><img src="https://opencollective.com/wechat/sponsor/0/avatar.svg"></a>
<a href="https://opencollective.com/wechat/sponsor/1/website" target="_blank"><img src="https://opencollective.com/wechat/sponsor/1/avatar.svg"></a>
<a href="https://opencollective.com/wechat/sponsor/2/website" target="_blank"><img src="https://opencollective.com/wechat/sponsor/2/avatar.svg"></a>
<a href="https://opencollective.com/wechat/sponsor/3/website" target="_blank"><img src="https://opencollective.com/wechat/sponsor/3/avatar.svg"></a>
<a href="https://opencollective.com/wechat/sponsor/4/website" target="_blank"><img src="https://opencollective.com/wechat/sponsor/4/avatar.svg"></a>
<a href="https://opencollective.com/wechat/sponsor/5/website" target="_blank"><img src="https://opencollective.com/wechat/sponsor/5/avatar.svg"></a>
<a href="https://opencollective.com/wechat/sponsor/6/website" target="_blank"><img src="https://opencollective.com/wechat/sponsor/6/avatar.svg"></a>
<a href="https://opencollective.com/wechat/sponsor/7/website" target="_blank"><img src="https://opencollective.com/wechat/sponsor/7/avatar.svg"></a>
<a href="https://opencollective.com/wechat/sponsor/8/website" target="_blank"><img src="https://opencollective.com/wechat/sponsor/8/avatar.svg"></a>
<a href="https://opencollective.com/wechat/sponsor/9/website" target="_blank"><img src="https://opencollective.com/wechat/sponsor/9/avatar.svg"></a>
<!-- This `CONTRIBUTING.md` is based on @nayafia's template https://github.com/nayafia/contributing-template -->

22
vendor/overtrue/wechat/LICENSE vendored Normal file
View File

@@ -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.

92
vendor/overtrue/wechat/README.md vendored Normal file
View File

@@ -0,0 +1,92 @@
<img align="right" width="100" src="https://user-images.githubusercontent.com/1472352/49656357-1e874080-fa78-11e8-80ea-69e2103345cf.png" alt="EasyWeChat Logo"/>
<h1 align="left"><a href="https://www.easywechat.com">EasyWeChat</a></h1>
📦 It is probably the best SDK in the world for developing Wechat App.
[![Test Status](https://github.com/overtrue/wechat/workflows/Test/badge.svg)](https://github.com/overtrue/wechat/actions)
[![Lint Status](https://github.com/overtrue/wechat/workflows/Lint/badge.svg)](https://github.com/overtrue/wechat/actions)
[![Latest Stable Version](https://poser.pugx.org/overtrue/wechat/v/stable.svg)](https://packagist.org/packages/overtrue/wechat)
[![Latest Unstable Version](https://poser.pugx.org/overtrue/wechat/v/unstable.svg)](https://packagist.org/packages/overtrue/wechat)
[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/overtrue/wechat/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/overtrue/wechat/?branch=master)
[![Code Coverage](https://scrutinizer-ci.com/g/overtrue/wechat/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/overtrue/wechat/?branch=master)
[![Total Downloads](https://poser.pugx.org/overtrue/wechat/downloads)](https://packagist.org/packages/overtrue/wechat)
[![License](https://poser.pugx.org/overtrue/wechat/license)](https://packagist.org/packages/overtrue/wechat)
## Requirement
1. PHP >= 7.2
2. **[Composer](https://getcomposer.org/)**
3. openssl 拓展
4. fileinfo 拓展(素材管理模块需要用到)
## Installation
```shell
$ composer require "overtrue/wechat:^4.2" -vvv
```
## Usage
基本使用(以服务端为例):
```php
<?php
use EasyWeChat\Factory;
$options = [
'app_id' => 'wx3cf0f39249eb0exxx',
'secret' => 'f1c242f4f28f735d4687abb469072xxx',
'token' => 'easywechat',
'log' => [
'level' => 'debug',
'file' => '/tmp/easywechat.log',
],
// ...
];
$app = Factory::officialAccount($options);
$server = $app->server;
$user = $app->user;
$server->push(function($message) use ($user) {
$fromUser = $user->get($message['FromUserName']);
return "{$fromUser->nickname} 您好!欢迎关注 overtrue!";
});
$server->serve()->send();
```
更多请参考 [https://www.easywechat.com/](https://www.easywechat.com/)。
## Documentation
[官网](https://www.easywechat.com) · [教程](https://www.easywechat.com/tutorials) · [讨论](https://yike.io/) · [微信公众平台](https://mp.weixin.qq.com/wiki) · [WeChat Official](http://admin.wechat.com/wiki)
## Integration
[Laravel 5 拓展包: overtrue/laravel-wechat](https://github.com/overtrue/laravel-wechat)
## Contributors
This project exists thanks to all the people who contribute. [[Contribute](CONTRIBUTING.md)].
<a href="https://github.com/overtrue/wechat/graphs/contributors"><img src="https://opencollective.com/wechat/contributors.svg?width=890" /></a>
## PHP 扩展包开发
> 想知道如何从零开始构建 PHP 扩展包?
>
> 请关注我的实战课程,我会在此课程中分享一些扩展开发经验 —— [《PHP 扩展包实战教程 - 从入门到发布》](https://learnku.com/courses/creating-package)
## License
MIT
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fovertrue%2Fwechat.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2Fovertrue%2Fwechat?ref=badge_large)

62
vendor/overtrue/wechat/composer.json vendored Normal file
View File

@@ -0,0 +1,62 @@
{
"name": "overtrue/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 || ^7.0",
"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",
"check-style": "php-cs-fixer fix --using-cache=no --diff --config=.php_cs --dry-run --ansi",
"fix-style": "php-cs-fixer fix --using-cache=no --config=.php_cs --ansi",
"test": "vendor/bin/phpunit --colors=always"
}
}

View File

@@ -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,
];
}

View File

@@ -0,0 +1,107 @@
<?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
{
/**
* Text content security check.
*
* @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('wxa/msg_sec_check', $params);
}
/**
* Image security check.
*
* @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('wxa/img_sec_check', ['media' => $path]);
}
/**
* Media security check.
*
* @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('wxa/media_check_async', $params);
}
/**
* Image security check async.
*
* @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.
*
* @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);
}
}

View File

@@ -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);
};
}
}

View File

@@ -0,0 +1,198 @@
<?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 = '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
* @param array $openTagList
* @param string $url
*
* @return array|string
*
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
* @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, array $openTagList = [], string $url = null)
{
$config = array_merge(compact('debug', 'beta', 'jsApiList', 'openTagList'), $this->configSignature($url));
return $json ? json_encode($config) : $config;
}
/**
* Return jsapi config as a PHP array.
*
* @param array $apis
* @param bool $debug
* @param bool $beta
* @param array $openTagList
* @param string $url
*
* @return array
*
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
* @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, array $openTagList = [], string $url = null)
{
return $this->buildConfig($apis, $debug, $beta, false, $openTagList, $url);
}
/**
* Get js ticket.
*
* @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 int|null $timestamp
*
* @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
*/
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.
*
* @return $this
*/
public function setUrl(string $url)
{
$this->url = $url;
return $this;
}
/**
* Get current url.
*/
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');
}
}

View File

@@ -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);
};
}
}

View File

@@ -0,0 +1,192 @@
<?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
{
/**
* 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.
*
* @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('cgi-bin/media/upload', ['media' => $path], ['type' => $type]);
}
/**
* @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;
}
/**
* @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('cgi-bin/media/uploadvideo', [
'media_id' => $mediaId,
'title' => $title,
'description' => $description,
]);
}
/**
* Fetch item from WeChat server.
*
* @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('cgi-bin/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'));
}
/**
* @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('cgi-bin/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'));
}
}

View File

@@ -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);
};
}
}

View File

@@ -0,0 +1,115 @@
<?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
{
public const DAY = 86400;
public const SCENE_MAX_VALUE = 100000;
public const SCENE_QR_CARD = 'QR_CARD';
public const SCENE_QR_TEMPORARY = 'QR_SCENE';
public const SCENE_QR_TEMPORARY_STR = 'QR_STR_SCENE';
public const SCENE_QR_FOREVER = 'QR_LIMIT_SCENE';
public 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('cgi-bin/qrcode/create', $params);
}
}

View File

@@ -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);
};
}
}

View File

@@ -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\BasicService\Url;
use EasyWeChat\Kernel\BaseClient;
/**
* Class Client.
*
* @author overtrue <i@overtrue.me>
*/
class Client extends BaseClient
{
/**
* Shorten the 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);
}
}

View File

@@ -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);
};
}
}

53
vendor/overtrue/wechat/src/Factory.php vendored Normal file
View File

@@ -0,0 +1,53 @@
<?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
*
* @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);
}
}

View File

@@ -0,0 +1,255 @@
<?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;
}
public function getLastToken(): array
{
return $this->token;
}
/**
* @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);
}
/**
* @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) && $result = $cache->get($cacheKey)) {
return $result;
}
/** @var array $token */
$token = $this->requestToken($this->getCredentials(), true);
$this->setToken($token[$this->tokenKey], $token['expires_in'] ?? 7200);
$this->token = $token;
$this->app->events->dispatch(new Events\AccessTokenRefreshed($this));
return $token;
}
/**
* @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;
}
/**
* @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 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;
}
/**
* @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.
*
* @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.
*
* @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]];
}
/**
* @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.
*/
abstract protected function getCredentials(): array;
}

View File

@@ -0,0 +1,243 @@
<?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
*/
public function __construct(ServiceContainer $app, AccessTokenInterface $accessToken = null)
{
$this->app = $app;
$this->accessToken = $accessToken ?? $this->app['access_token'];
}
/**
* GET request.
*
* @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.
*
* @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.
*
* @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.
*
* @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]);
}
public function getAccessToken(): AccessTokenInterface
{
return $this->accessToken;
}
/**
* @return $this
*/
public function setAccessToken(AccessTokenInterface $accessToken)
{
$this->accessToken = $accessToken;
return $this;
}
/**
* @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'));
}
/**
* @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));
});
}
}

View File

@@ -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\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])) {
continue;
}
if (is_array($value) && !in_array($payload[$key], $value)) {
return true;
}
if (!is_array($value) && $payload[$key] !== $value) {
return true;
}
}
return false;
}
}

View File

@@ -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
{
}

View File

@@ -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\Contracts;
use Psr\Http\Message\RequestInterface;
/**
* Interface AuthorizerAccessToken.
*
* @author overtrue <i@overtrue.me>
*/
interface AccessTokenInterface
{
public function getToken(): array;
/**
* @return \EasyWeChat\Kernel\Contracts\AccessTokenInterface
*/
public function refresh(): self;
public function applyToRequest(RequestInterface $request, array $requestOptions = []): RequestInterface;
}

View File

@@ -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();
}

View File

@@ -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);
}

View File

@@ -0,0 +1,22 @@
<?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
{
public function getMediaId(): string;
}

View File

@@ -0,0 +1,26 @@
<?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
{
public function getType(): string;
public function transformForJsonRequest(): array;
public function transformToXml(): string;
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,198 @@
<?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 EasyWeChat\Kernel\Support\XML;
use Throwable;
use function EasyWeChat\Kernel\Support\str_random;
/**
* Class Encryptor.
*
* @author overtrue <i@overtrue.me>
*/
class Encryptor
{
public const ERROR_INVALID_SIGNATURE = -40001; // Signature verification failed
public const ERROR_PARSE_XML = -40002; // Parse XML failed
public const ERROR_CALC_SIGNATURE = -40003; // Calculating the signature failed
public const ERROR_INVALID_AES_KEY = -40004; // Invalid AESKey
public const ERROR_INVALID_APP_ID = -40005; // Check AppID failed
public const ERROR_ENCRYPT_AES = -40006; // AES EncryptionInterface failed
public const ERROR_DECRYPT_AES = -40007; // AES decryption failed
public const ERROR_INVALID_XML = -40008; // Invalid XML
public const ERROR_BASE64_ENCODE = -40009; // Base64 encoding failed
public const ERROR_BASE64_DECODE = -40010; // Base64 decoding failed
public const ERROR_XML_BUILD = -40011; // XML build failed
public 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.
*/
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.
*/
public function getToken(): string
{
return $this->token;
}
/**
* Encrypt the message and return XML.
*
* @param string $xml
* @param string $nonce
* @param int $timestamp
*
* @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
*
* @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.
*/
public function signature(): string
{
$array = func_get_args();
sort($array, SORT_STRING);
return sha1(implode($array));
}
/**
* PKCS#7 pad.
*
* @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.
*/
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));
}
}

View File

@@ -0,0 +1,32 @@
<?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;
public function __construct(AccessToken $accessToken)
{
$this->accessToken = $accessToken;
}
}

View File

@@ -0,0 +1,32 @@
<?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;
public function __construct(ServiceContainer $app)
{
$this->app = $app;
}
}

View File

@@ -0,0 +1,32 @@
<?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;
public function __construct(ResponseInterface $response)
{
$this->response = $response;
}
}

View File

@@ -0,0 +1,32 @@
<?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;
public function __construct(Response $response)
{
$this->response = $response;
}
}

View File

@@ -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
{
}

View File

@@ -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
{
}

View File

@@ -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
{
}

View File

@@ -0,0 +1,51 @@
<?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 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();
}
}
}

View File

@@ -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
{
}

View File

@@ -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
{
}

View File

@@ -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
{
}

View File

@@ -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
{
}

View File

@@ -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'));
}
}

View File

@@ -0,0 +1,117 @@
<?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;
}
/**
* @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();
}
/**
* @return string
*/
protected function removeControlCharacters(string $content)
{
return \preg_replace('/[\x00-\x1F\x80-\x9F]/u', '', $content);
}
}

View File

@@ -0,0 +1,78 @@
<?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
{
/**
* @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;
}
/**
* @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);
}
}

View File

@@ -0,0 +1,580 @@
<?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\NullHandler;
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.
*/
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']));
}
/**
* Call a custom driver creator.
*
* @return mixed
*/
protected function callCustomCreator(array $config)
{
return $this->customCreators[$config['driver']]($this->app, $config);
}
/**
* 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'])
)]));
}
/**
* Create an aggregate log driver instance.
*
* @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.
*
* @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.
*
* @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.
*
* @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.
*
* @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.
*
* @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)
)
),
]);
}
protected function createNullDriver()
{
return new Monolog('EasyWeChat', [new NullHandler()]);
}
/**
* Prepare the handlers for usage by Monolog.
*
* @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.
*
* @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.
*
* @return string
*/
protected function parseChannel(array $config)
{
return $config['name'] ?? 'EasyWeChat';
}
/**
* Parse the string level into a Monolog constant.
*
* @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
*
* @return $this
*/
public function extend($driver, \Closure $callback)
{
$this->customCreators[$driver] = $callback->bindTo($this, $this);
return $this;
}
/**
* System is unusable.
*
* @param string $message
*
* @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
*
* @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
*
* @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
*
* @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
*
* @return mixed
*
* @throws \Exception
*/
public function warning($message, array $context = [])
{
return $this->driver()->warning($message, $context);
}
/**
* Normal but significant events.
*
* @param string $message
*
* @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
*
* @return mixed
*
* @throws \Exception
*/
public function info($message, array $context = [])
{
return $this->driver()->info($message, $context);
}
/**
* Detailed debug information.
*
* @param string $message
*
* @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
*
* @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);
}
}

View File

@@ -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',
];
}

View File

@@ -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.
*/
/**
* 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.
*/
public function __construct(string $cardId)
{
parent::__construct(['card_id' => $cardId]);
}
}

View File

@@ -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',
];
}

View File

@@ -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')),
];
}
}

View File

@@ -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';
}

View File

@@ -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';
}

View File

@@ -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',
];
}

View File

@@ -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',
];
}

View File

@@ -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\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 $type
*/
public function __construct(string $mediaId, $type = null, array $attributes = [])
{
parent::__construct(array_merge(['media_id' => $mediaId], $attributes));
!empty($type) && $this->setType($type);
}
/**
* @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'),
],
];
}
}

View File

@@ -0,0 +1,187 @@
<?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;
public const TEXT = 2;
public const IMAGE = 4;
public const VOICE = 8;
public const VIDEO = 16;
public const SHORT_VIDEO = 32;
public const LOCATION = 64;
public const LINK = 128;
public const DEVICE_EVENT = 256;
public const DEVICE_TEXT = 512;
public const FILE = 1024;
public const TEXT_CARD = 2048;
public const TRANSFER = 4096;
public const EVENT = 1048576;
public const MINIPROGRAM_PAGE = 2097152;
public const MINIPROGRAM_NOTICE = 4194304;
public 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 | self::MINIPROGRAM_NOTICE;
/**
* @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.
*/
public function __construct(array $attributes = [])
{
$this->setAttributes($attributes);
}
/**
* Return type name message.
*/
public function getType(): string
{
return $this->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;
}
/**
* @return array
*/
public function transformForJsonRequestWithoutType(array $appends = [])
{
return $this->transformForJsonRequest($appends, false);
}
/**
* @param bool $withType
*
* @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;
}
public function transformToXml(array $appends = [], bool $returnAsArray = false): string
{
$data = array_merge(['MsgType' => $this->getType()], $this->toXmlArray(), $appends);
return $returnAsArray ? $data : XML::build($data);
}
/**
* @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__));
}
}

View File

@@ -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',
];
}

View File

@@ -0,0 +1,22 @@
<?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 MiniprogramNotice extends Message
{
protected $type = 'miniprogram_notice';
protected $properties = [
'appid',
'title',
];
}

View File

@@ -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;
}
}

View File

@@ -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 News.
*
* @author overtrue <i@overtrue.me>
*/
class News extends Message
{
/**
* @var string
*/
protected $type = 'news';
/**
* @var array
*/
protected $properties = [
'items',
];
/**
* News constructor.
*/
public function __construct(array $items = [])
{
parent::__construct(compact('items'));
}
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,
];
}
}

View File

@@ -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'),
];
}
}

View File

@@ -0,0 +1,51 @@
<?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.
*/
public function __construct(string $content)
{
parent::__construct(['content' => strval($content)]);
}
/**
* @param bool $withType
*/
public function transformForJsonRequest(array $appends = [], $withType = true): array
{
return json_decode($this->content, true) ?? [];
}
public function __toString()
{
return $this->get('content') ?? '';
}
}

View File

@@ -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';
}

View File

@@ -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',
];
}

View File

@@ -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\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.
*/
public function __construct(string $content)
{
parent::__construct(compact('content'));
}
/**
* @return array
*/
public function toXmlArray()
{
return [
'Content' => $this->get('content'),
];
}
}

View File

@@ -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',
];
}

View File

@@ -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 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.
*/
public function __construct(string $account = null)
{
parent::__construct(compact('account'));
}
public function toXmlArray()
{
return empty($this->get('account')) ? [] : [
'TransInfo' => [
'KfAccount' => $this->get('account'),
],
];
}
}

View File

@@ -0,0 +1,62 @@
<?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.
*/
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'),
],
];
}
}

View File

@@ -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',
];
}

View File

@@ -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)
{
!isset($pimple['config']) && $pimple['config'] = function ($app) {
return new Config($app->getConfig());
};
}
}

View File

@@ -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)
{
!isset($pimple['events']) && $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;
};
}
}

View File

@@ -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)
{
!isset($pimple['extension']) && $pimple['extension'] = function ($app) {
return new Extension($app);
};
}
}

View File

@@ -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)
{
!isset($pimple['http_client']) && $pimple['http_client'] = function ($app) {
return new Client($app['config']->get('http', []));
};
}
}

View File

@@ -0,0 +1,82 @@
<?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)
{
!isset($pimple['log']) && $pimple['log'] = function ($app) {
$config = $this->formatLogConfig($app);
if (!empty($config)) {
$app->rebind('config', $app['config']->merge($config));
}
return new LogManager($app);
};
!isset($pimple['logger']) && $pimple['logger'] = $pimple['log'];
}
public function formatLogConfig($app)
{
if (!empty($app['config']->get('log.channels'))) {
return [
'log' => $app['config']->get('log'),
];
}
if (empty($app['config']->get('log'))) {
return [
'log' => [
'default' => 'null',
'channels' => [
'null' => [
'driver' => 'null',
],
],
],
];
}
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'),
],
],
],
];
}
}

View File

@@ -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)
{
!isset($pimple['request']) && $pimple['request'] = function () {
return Request::createFromGlobals();
};
}
}

View File

@@ -0,0 +1,352 @@
<?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.
*/
public const SUCCESS_EMPTY_RESPONSE = 'success';
/**
* @var array
*/
public 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.
*
* @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.
*
* @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 \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.
*
* @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.
*/
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;
}
/**
* @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.
*/
protected function isSafeMode(): bool
{
return $this->app['request']->get('signature') && 'aes' === $this->app['request']->get('encrypt_type');
}
protected function shouldReturnRawResponse(): bool
{
return false;
}
/**
* @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')
);
}
}

View File

@@ -0,0 +1,164 @@
<?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.
*/
public function __construct(array $config = [], array $prepends = [], string $id = null)
{
$this->userConfig = $config;
parent::__construct($prepends);
$this->id = $id;
$this->registerProviders($this->getProviders());
$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);
}
public function registerProviders(array $providers)
{
foreach ($providers as $provider) {
parent::register(new $provider());
}
}
public function getClient () {
return new BaseClient($this);
}
}

View File

@@ -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;
/**
* Class AES.
*
* @author overtrue <i@overtrue.me>
*/
class AES
{
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|null $method
*/
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';
}
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)));
}
}
/**
* @throws \InvalidArgumentException
*/
public static function validateIv(string $iv)
{
if (!empty($iv) && 16 !== strlen($iv)) {
throw new \InvalidArgumentException('IV length must be 16 bytes.');
}
}
}

View File

@@ -0,0 +1,439 @@
<?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 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.
*
* @return array
*/
public static function divide(array $array)
{
return [array_keys($array), array_values($array)];
}
/**
* Flatten a multi-dimensional associative array with dots.
*
* @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|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 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 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 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 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|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 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 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.
*
* @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|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 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 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.
*
* @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 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.
*
* @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;
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,417 @@
<?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.
*/
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.
*
* @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 static function __set_state(array $properties)
{
return (new static($properties))->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);
}
}

View File

@@ -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 '';
}
}

View File

@@ -0,0 +1,127 @@
<?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 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))]));
}
/**
* @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);
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,166 @@
<?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);
}
}

View File

@@ -0,0 +1,245 @@
<?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.
*
* @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);
}
/**
* @return bool
*/
public function has(string $key)
{
return Arr::has($this->attributes, $key);
}
/**
* @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));
}
}
}
}

View File

@@ -0,0 +1,211 @@
<?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.
*/
public static function getDefaultOptions(): array
{
return self::$defaults;
}
/**
* Set GuzzleHttp\Client.
*
* @return $this
*/
public function setHttpClient(ClientInterface $httpClient)
{
$this->httpClient = $httpClient;
return $this;
}
/**
* Return GuzzleHttp\ClientInterface instance.
*/
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 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.
*/
public function getMiddlewares(): array
{
return $this->middlewares;
}
/**
* Make a request.
*
* @param string $url
* @param string $method
* @param array $options
*
* @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;
}
/**
* @return $this
*/
public function setHandlerStack(HandlerStack $handlerStack)
{
$this->handlerStack = $handlerStack;
return $this;
}
/**
* Build a handler stack.
*/
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;
}
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();
}
}

View File

@@ -0,0 +1,102 @@
<?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();
}
protected function isSymfony43OrHigher(): bool
{
return \class_exists('Symfony\Component\Cache\Psr16Cache');
}
}

View File

@@ -0,0 +1,278 @@
<?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);
}
/**
* @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
*/
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 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
*
* @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];
}
}

View File

@@ -0,0 +1,92 @@
<?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 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);
}
}

View File

@@ -0,0 +1,168 @@
<?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.
*
* @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.
*
* @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);
}
}

View File

@@ -0,0 +1,117 @@
<?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.
*
* @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.
*
* @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.
*
* @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.
*
* @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'),
]);
}
}

View File

@@ -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);
};
}
}

View File

@@ -0,0 +1,89 @@
<?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.
*
* @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.
*
* @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 installedReference 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
);
}
}

View File

@@ -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);
};
}
}

View File

@@ -0,0 +1,241 @@
<?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.
*/
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 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 $method
* @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.
*
* @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.
*
* @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.
*
* @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);
}
}

View File

@@ -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
{
}

View File

@@ -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
{
}

View File

@@ -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
{
}

View File

@@ -0,0 +1,69 @@
<?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\Material;
use EasyWeChat\MicroMerchant\Kernel\BaseClient;
/**
* Class Client.
*
* @author liuml <liumenglei0211@163.com>
* @DateTime 2019-05-30 14:19
*/
class Client extends BaseClient
{
/**
* update settlement card.
*
* @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 setSettlementCard(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/modifyarchives', $params);
}
/**
* update contact info.
*
* @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 updateContact(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/modifycontactinfo', $params);
}
}

View File

@@ -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\Material;
use Pimple\Container;
use Pimple\ServiceProviderInterface;
/**
* Class ServiceProvider.
*
* @author overtrue <i@overtrue.me>
*/
class ServiceProvider implements ServiceProviderInterface
{
/**
* {@inheritdoc}.
*/
public function register(Container $app)
{
$app['material'] = function ($app) {
return new Client($app);
};
}
}

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