Save
This commit is contained in:
Vendored
+22
@@ -0,0 +1,22 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014-present rap2hpoutre
|
||||
|
||||
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.
|
||||
|
||||
Vendored
+341
@@ -0,0 +1,341 @@
|
||||
EasyHttp 是一个轻量级、语义化、对IDE友好的HTTP客户端,支持常见的HTTP请求、异步请求和并发请求,让你可以快速地使用 HTTP 请求与其他 Web 应用进行通信。
|
||||
|
||||
> EasyHttp并不强制依赖于cURL,如果没有安装cURL,EasyHttp会自动选择使用PHP流处理,或者你也可以提供自己的发送HTTP请求的处理方式。
|
||||
|
||||
如果您觉得EasyHttp对您有用的话,别忘了给点个赞哦^_^ !
|
||||
|
||||
github:[github.com/yzh52521/easyhttp](https://github.com/yzh52521/easyhttp "github.com/yzh52521/easyhttp")
|
||||
|
||||
gitee:[gitee.com/yzh52521/easyhttp](https://gitee.com/yzh52521/easyhttp "gitee.com/yzh52521/easyhttp")
|
||||
|
||||
本包是基于 [ gouguoyin/easyhttp ](https://gitee.com/gouguoyin/easyhttp "gitee.com/gouguoyin/easyhttp") 进行扩展开发,主要实现了以下扩展:
|
||||
|
||||
1. 增加 retry() 重试机制。
|
||||
2. 增加 debug 日志调试功能。
|
||||
3. 增加 withHost 指定服务端base_url
|
||||
|
||||
|
||||
# 安装说明
|
||||
|
||||
#### 环境依赖
|
||||
|
||||
- PHP >= 7.2.5
|
||||
- 如果使用PHP流处理,allow_url_fopen 必须在php.ini中启用。
|
||||
- 如果使用cURL处理,cURL >= 7.19.4,并且编译了OpenSSL 与 zlib。
|
||||
|
||||
#### 一键安装
|
||||
|
||||
composer require yzh52521/easyhttp
|
||||
|
||||
## 发起请求
|
||||
|
||||
#### 同步请求
|
||||
|
||||
###### 常规请求
|
||||
|
||||
```php
|
||||
$response = Http::get('http://httpbin.org/get');
|
||||
|
||||
$response = Http::get('http://httpbin.org/get?name=yzh52521');
|
||||
|
||||
$response = Http::get('http://httpbin.org/get?name=yzh52521', ['age' => 18]);
|
||||
|
||||
$response = Http::post('http://httpbin.org/post');
|
||||
|
||||
$response = Http::post('http://httpbin.org/post', ['name' => 'yzh52521']);
|
||||
|
||||
$response = Http::patch(...);
|
||||
|
||||
$response = Http::put(...);
|
||||
|
||||
$response = Http::delete(...);
|
||||
|
||||
$response = Http::head(...);
|
||||
|
||||
$response = Http::options(...);
|
||||
```
|
||||
|
||||
###### 指定服务端base_url的请求
|
||||
|
||||
```php
|
||||
// 指定服务端base_url地址,最终请求地址为 https://serv.yzh52521.com/login
|
||||
$response = Http::withHost('https://serv.yzh52521.com')->post('/login');
|
||||
|
||||
```
|
||||
###### 发送 Content-Type 编码请求
|
||||
|
||||
```php
|
||||
// application/x-www-form-urlencoded(默认)
|
||||
$response = Http::asForm()->post(...);
|
||||
|
||||
// application/json
|
||||
$response = Http::asJson()->post(...);
|
||||
```
|
||||
|
||||
###### 发送 Multipart 表单请求
|
||||
|
||||
```php
|
||||
$response = Http::asMultipart(
|
||||
'file_input_name', file_get_contents('photo1.jpg'), 'photo2.jpg'
|
||||
)->post('http://test.com/attachments');
|
||||
|
||||
$response = Http::asMultipart(
|
||||
'file_input_name', fopen('photo1.jpg', 'r'), 'photo2.jpg'
|
||||
)->post(...);
|
||||
|
||||
$response = Http::attach(
|
||||
'file_input_name', file_get_contents('photo1.jpg'), 'photo2.jpg'
|
||||
)->post(...);
|
||||
|
||||
$response = Http::attach(
|
||||
'file_input_name', fopen('photo1.jpg', 'r'), 'photo2.jpg'
|
||||
)->post(...);
|
||||
```
|
||||
> 表单enctype属性需要设置成 multipart/form-data
|
||||
|
||||
###### 携带请求头的请求
|
||||
|
||||
```php
|
||||
$response = Http::withHeaders([
|
||||
'x-powered-by' => 'yzh52521'
|
||||
])->post(...);
|
||||
```
|
||||
|
||||
###### 携带重定向的请求
|
||||
|
||||
```php
|
||||
// 默认
|
||||
$response = Http::withRedirect(false)->post(...);
|
||||
|
||||
$response = Http::withRedirect([
|
||||
'max' => 5,
|
||||
'strict' => false,
|
||||
'referer' => true,
|
||||
'protocols' => ['http', 'https'],
|
||||
'track_redirects' => false
|
||||
])->post(...);
|
||||
```
|
||||
|
||||
###### 携带认证的请求
|
||||
|
||||
```php
|
||||
// Basic认证
|
||||
$response = Http::withBasicAuth('username', 'password')->post(...);
|
||||
|
||||
// Digest认证(需要被HTTP服务器支持)
|
||||
$response = Http::withDigestAuth('username', 'password')->post(...);
|
||||
```
|
||||
|
||||
###### 携带 User-Agent 的请求
|
||||
```php
|
||||
$response = Http::withUA('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3100.0 Safari/537.36')->post(...);
|
||||
```
|
||||
|
||||
###### 携带Token令牌的请求
|
||||
|
||||
```php
|
||||
$response = Http::withToken('token')->post(...);
|
||||
```
|
||||
|
||||
###### 携带认证文件的请求
|
||||
|
||||
```php
|
||||
$response = Http::withCert('/path/server.pem', 'password')->post(...);
|
||||
```
|
||||
|
||||
###### 携带SSL证书的请求
|
||||
|
||||
```php
|
||||
// 默认
|
||||
$response = Http::withVerify(false)->post(...);
|
||||
|
||||
$response = Http::withVerify('/path/to/cert.pem')->post(...);
|
||||
```
|
||||
|
||||
###### 携带COOKIE的请求
|
||||
|
||||
```php
|
||||
$response = Http::withCookies(array $cookies, string $domain)->post(...);
|
||||
```
|
||||
|
||||
###### 携带协议版本的请求
|
||||
|
||||
```php
|
||||
$response = Http::withVersion(1.1)->post(...);
|
||||
```
|
||||
|
||||
###### 携带代理的请求
|
||||
|
||||
```php
|
||||
$response = Http::withProxy('tcp://localhost:8125')->post(...);
|
||||
|
||||
$response = Http::withProxy([
|
||||
'http' => 'tcp://localhost:8125', // Use this proxy with "http"
|
||||
'https' => 'tcp://localhost:9124', // Use this proxy with "https",
|
||||
'no' => ['.com.cn', 'yzh52521.cn'] // Don't use a proxy with these
|
||||
])->post(...);
|
||||
```
|
||||
|
||||
###### 设置超时时间(单位秒)
|
||||
|
||||
```php
|
||||
$response = Http::timeout(60)->post(...);
|
||||
```
|
||||
|
||||
###### 设置延迟时间(单位秒)
|
||||
|
||||
```php
|
||||
$response = Http::delay(60)->post(...);
|
||||
```
|
||||
|
||||
###### 设置并发次数
|
||||
|
||||
```php
|
||||
$response = Http::concurrency(10)->promise(...);
|
||||
```
|
||||
|
||||
###### 重发请求,设置retry方法。重试次数/两次重试之间的时间间隔(毫秒):
|
||||
|
||||
```php
|
||||
$response = Http::retry(3, 100)->post(...);
|
||||
```
|
||||
|
||||
#### 异步请求
|
||||
|
||||
```php
|
||||
use yzh52521\EasyHttp\Response;
|
||||
use yzh52521\EasyHttp\RequestException;
|
||||
|
||||
Http::getAsync('http://easyhttp.yzh52521.cn/api/sleep3.json', ['token' => TOKEN], function (Response $response) {
|
||||
echo '异步请求成功,响应内容:' . $response->body() . PHP_EOL;
|
||||
}, function (RequestException $e) {
|
||||
echo '异步请求异常,错误码:' . $e->getCode() . ',错误信息:' . $e->getMessage() . PHP_EOL;
|
||||
});
|
||||
echo json_encode(['code' => 200, 'msg' => '请求成功'], JSON_UNESCAPED_UNICODE) . PHP_EOL;
|
||||
|
||||
//输出
|
||||
{"code":200,"msg":"请求成功"}
|
||||
异步请求成功,响应内容:{"code":200,"msg":"success","second":3}
|
||||
|
||||
Http::getAsync('http1://easyhttp.yzh52521.cn/api/sleep3.json', function (Response $response) {
|
||||
echo '异步请求成功,响应内容:' . $response->body() . PHP_EOL;
|
||||
}, function (RequestException $e) {
|
||||
echo '异步请求异常,错误信息:' . $e->getMessage() . PHP_EOL;
|
||||
});
|
||||
echo json_encode(['code' => 200, 'msg' => '请求成功'], JSON_UNESCAPED_UNICODE) . PHP_EOL;
|
||||
|
||||
//输出
|
||||
{"code":200,"msg":"请求成功"}
|
||||
异步请求异常,错误信息:cURL error 1: Protocol "http1" not supported or disabled in libcurl (see https://curl.haxx.se/libcurl/c/libcurl-errors.html)
|
||||
|
||||
Http::postAsync(...);
|
||||
|
||||
Http::patchAsync(...);
|
||||
|
||||
Http::putAsync(...);
|
||||
|
||||
Http::deleteAsync(...);
|
||||
|
||||
Http::headAsync(...);
|
||||
|
||||
Http::optionsAsync(...);
|
||||
```
|
||||
|
||||
#### 异步并发请求
|
||||
|
||||
```php
|
||||
use yzh52521\EasyHttp\Response;
|
||||
use yzh52521\EasyHttp\RequestException;
|
||||
|
||||
$promises = [
|
||||
Http::getAsync('http://easyhttp.yzh52521.cn/api/sleep3.json'),
|
||||
Http::getAsync('http1://easyhttp.yzh52521.cn/api/sleep1.json', ['name' => 'yzh52521']),
|
||||
Http::postAsync('http://easyhttp.yzh52521.cn/api/sleep2.json', ['name' => 'yzh52521']),
|
||||
];
|
||||
|
||||
Http::concurrency(10)->multiAsync($promises, function (Response $response, $index) {
|
||||
echo "发起第 $index 个异步请求,请求时长:" . $response->json()->second . '秒' . PHP_EOL;
|
||||
}, function (RequestException $e, $index) {
|
||||
echo "发起第 $index 个请求失败,失败原因:" . $e->getMessage() . PHP_EOL;
|
||||
});
|
||||
|
||||
//输出
|
||||
发起第 1 个请求失败,失败原因:cURL error 1: Protocol "http1" not supported or disabled in libcurl (see https://curl.haxx.se/libcurl/c/libcurl-errors.html)
|
||||
发起第 2 个异步请求,请求时长:2 秒
|
||||
发起第 0 个异步请求,请求时长:3 秒
|
||||
```
|
||||
> 如果未调用concurrency()方法,并发次数默认为$promises的元素个数,$promises数组里必须是异步请求
|
||||
|
||||
## 使用响应
|
||||
|
||||
发起请求后会返回一个 yzh52521\EasyHttp\Response $response的实例,该实例提供了以下方法来检查请求的响应:
|
||||
|
||||
```php
|
||||
$response->body() : string;
|
||||
$response->json() : object;
|
||||
$response->array() : array;
|
||||
$response->status() : int;
|
||||
$response->ok() : bool;
|
||||
$response->successful() : bool;
|
||||
$response->serverError() : bool;
|
||||
$response->clientError() : bool;
|
||||
$response->headers() : array;
|
||||
$response->header($header) : string;
|
||||
```
|
||||
|
||||
## 异常处理
|
||||
|
||||
请求在发生客户端或服务端错误时会抛出 yzh52521\EasyHttp\RequestException $e异常,该实例提供了以下方法来返回异常信息:
|
||||
|
||||
```php
|
||||
$e->getCode() : int;
|
||||
$e->getMessage() : string;
|
||||
$e->getFile() : string;
|
||||
$e->getLine() : int;
|
||||
$e->getTrace() : array;
|
||||
$e->getTraceAsString() : string;
|
||||
```
|
||||
## 调试日志
|
||||
|
||||
有时候难免要对 Http 的请求和响应包体进行记录以方便查找问题或做什么
|
||||
|
||||
```php
|
||||
//传递一个日志类 thinkphp \think\facade\Log laravel Illuminate\Support\Facades\Log
|
||||
Http::debug(Log::class)->post(...);
|
||||
```
|
||||
|
||||
|
||||
## 更新日志
|
||||
### 2022-05-11
|
||||
* 新增removeBodyFormat() 用于withOptions 指定body时,清除原由的bodyFromat
|
||||
### 2022-05-10
|
||||
* 新增发送原生请求的方法client()
|
||||
* 新增发送原生异步请求的方法clientASync()
|
||||
### 2021-09-03
|
||||
* 新增 debug() 调试日志
|
||||
* 新增 retry() 重试机制
|
||||
* 修复header重叠的bug
|
||||
### 2020-03-30
|
||||
* 修复部分情况下IDE不能智能提示的BUG
|
||||
* get()、getAsync()方法支持带参数的url
|
||||
* 新增withUA()方法
|
||||
* 新增withStream()方法
|
||||
* 新增asMultipart()方法,attach()的别名
|
||||
* 新增multiAsync()异步并发请求方法
|
||||
|
||||
### 2020-03-20
|
||||
* 新增异步请求getAsync()方法
|
||||
* 新增异步请求postAsync()方法
|
||||
* 新增异步请求patchAsync()方法
|
||||
* 新增异步请求putAsync()方法
|
||||
* 新增异步请求deleteAsync()方法
|
||||
* 新增异步请求headAsync()方法
|
||||
* 新增异步请求optionsAsync()方法
|
||||
|
||||
## Todo List
|
||||
- [x] 异步请求
|
||||
- [x] 并发请求
|
||||
- [x] 重试机制
|
||||
- [ ] 支持http2
|
||||
- [ ] 支持swoole
|
||||
# easyhttp
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "yzh52521/easyhttp",
|
||||
"description": "EasyHttp 是一个轻量级、语义化、对IDE友好的HTTP客户端,支持常见的HTTP请求、异步请求和并发请求,让你可以快速地使用 HTTP 请求与其他 Web 应用进行通信。",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"easyhttp",
|
||||
"EasyHttp",
|
||||
"php-http",
|
||||
"phphttp",
|
||||
"easy-http",
|
||||
"php",
|
||||
"http",
|
||||
"curl"
|
||||
],
|
||||
"homepage": "https://github.com/yzh52521/easyhttp",
|
||||
"authors": [
|
||||
{
|
||||
"name": "yzh52521",
|
||||
"email": "396751927@qq.com"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": "^7.2.5|^8.0",
|
||||
"guzzlehttp/guzzle": "^6.0|^7.0",
|
||||
"psr/log":"^1.0|^2.0|^3.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"yzh52521\\EasyHttp\\": "src/"
|
||||
}
|
||||
},
|
||||
"minimum-stability": "stable"
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace yzh52521\EasyHttp;
|
||||
|
||||
use Exception;
|
||||
|
||||
class ConnectionException extends Exception
|
||||
{
|
||||
//
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace yzh52521\EasyHttp;
|
||||
|
||||
class Facade
|
||||
{
|
||||
protected $facade;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->facade = new $this->facade;
|
||||
}
|
||||
|
||||
public function __call($name, $params) {
|
||||
return call_user_func_array([$this->facade, $name], $params);
|
||||
}
|
||||
|
||||
public static function __callStatic($name, $params) {
|
||||
return call_user_func_array([new static(), $name], $params);
|
||||
}
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace yzh52521\EasyHttp;
|
||||
|
||||
/**
|
||||
* @method static \yzh52521\EasyHttp\Request asJson()
|
||||
* @method static \yzh52521\EasyHttp\Request asForm()
|
||||
* @method static \yzh52521\EasyHttp\Request asMultipart(string $name, string $contents, string|null $filename = null, array $headers)
|
||||
* @method static \yzh52521\EasyHttp\Request attach(string $name, string $contents, string|null $filename = null, array $headers)
|
||||
*
|
||||
* @method static \yzh52521\EasyHttp\Request withRedirect(bool|array $redirect)
|
||||
* @method static \yzh52521\EasyHttp\Request withStream(bool $boolean)
|
||||
* @method static \yzh52521\EasyHttp\Request withVerify(bool|string $verify)
|
||||
* @method static \yzh52521\EasyHttp\Request withHost(string $host)
|
||||
* @method static \yzh52521\EasyHttp\Request withHeaders(array $headers)
|
||||
* @method static \yzh52521\EasyHttp\Request withBasicAuth(string $username, string $password)
|
||||
* @method static \yzh52521\EasyHttp\Request withDigestAuth(string $username, string $password)
|
||||
* @method static \yzh52521\EasyHttp\Request withUA(string $ua)
|
||||
* @method static \yzh52521\EasyHttp\Request withToken(string $token, string $type = 'Bearer')
|
||||
* @method static \yzh52521\EasyHttp\Request withCookies(array $cookies, string $domain)
|
||||
* @method static \yzh52521\EasyHttp\Request withProxy(string|array $proxy)
|
||||
* @method static \yzh52521\EasyHttp\Request withVersion(string $version)
|
||||
* @method static \yzh52521\EasyHttp\Request withOptions(array $options)
|
||||
*
|
||||
* @method static \yzh52521\EasyHttp\Request debug($class)
|
||||
* @method static \yzh52521\EasyHttp\Request retry(int $retries=1,int $sleep=0)
|
||||
* @method static \yzh52521\EasyHttp\Request delay(int $seconds)
|
||||
* @method static \yzh52521\EasyHttp\Request timeout(int $seconds)
|
||||
* @method static \yzh52521\EasyHttp\Request concurrency(int $times)
|
||||
* @method static \yzh52521\EasyHttp\Request client(string $method, string $url, array $options = [])
|
||||
* @method static \yzh52521\EasyHttp\Request clientAsync(string $method, string $url, array $options = [])
|
||||
* @method static \yzh52521\EasyHttp\Request removeBodyFormat()
|
||||
*
|
||||
* @method static \yzh52521\EasyHttp\Request get(string $url, array $query = [])
|
||||
* @method static \yzh52521\EasyHttp\Request post(string $url, array $data = [])
|
||||
* @method static \yzh52521\EasyHttp\Request patch(string $url, array $data = [])
|
||||
* @method static \yzh52521\EasyHttp\Request put(string $url, array $data = [])
|
||||
* @method static \yzh52521\EasyHttp\Request delete(string $url, array $data = [])
|
||||
* @method static \yzh52521\EasyHttp\Request head(string $url, array $data = [])
|
||||
* @method static \yzh52521\EasyHttp\Request options(string $url, array $data = [])
|
||||
*
|
||||
* @method static \yzh52521\EasyHttp\Request getAsync(string $url, array|null $query = null, callable $success = null, callable $fail = null)
|
||||
* @method static \yzh52521\EasyHttp\Request postAsync(string $url, array|null $data = null, callable $success = null, callable $fail = null)
|
||||
* @method static \yzh52521\EasyHttp\Request patchAsync(string $url, array|null $data = null, callable $success = null, callable $fail = null)
|
||||
* @method static \yzh52521\EasyHttp\Request putAsync(string $url, array|null $data = null, callable $success = null, callable $fail = null)
|
||||
* @method static \yzh52521\EasyHttp\Request deleteAsync(string $url, array|null $data = null, callable $success = null, callable $fail = null)
|
||||
* @method static \yzh52521\EasyHttp\Request headAsync(string $url, array|null $data = null, callable $success = null, callable $fail = null)
|
||||
* @method static \yzh52521\EasyHttp\Request optionsAsync(string $url, array|null $data = null, callable $success = null, callable $fail = null)
|
||||
* @method static \yzh52521\EasyHttp\Request multiAsync(array $promises, callable $success = null, callable $fail = null)
|
||||
* @method static \yzh52521\EasyHttp\Request wait()
|
||||
*/
|
||||
|
||||
class Http extends Facade
|
||||
{
|
||||
protected $facade = Request::class;
|
||||
}
|
||||
+273
@@ -0,0 +1,273 @@
|
||||
<?php
|
||||
|
||||
namespace yzh52521\EasyHttp;
|
||||
|
||||
use GuzzleHttp\Exception\RequestException;
|
||||
use GuzzleHttp\MessageFormatter;
|
||||
use GuzzleHttp\Promise;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Log\LogLevel;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Guzzle middleware which logs a request and its response.
|
||||
*/
|
||||
class Logger
|
||||
{
|
||||
/**
|
||||
* @var \Psr\Log\LoggerInterface|callable
|
||||
*/
|
||||
protected $logger;
|
||||
|
||||
/**
|
||||
* @var \GuzzleHttp\MessageFormatter|callable
|
||||
*/
|
||||
protected $formatter;
|
||||
|
||||
/**
|
||||
* @var string|callable Constant or callable that accepts a Response.
|
||||
*/
|
||||
protected $logLevel;
|
||||
|
||||
/**
|
||||
* @var boolean Whether or not to log requests as they are made.
|
||||
*/
|
||||
protected $logRequests;
|
||||
|
||||
/**
|
||||
* Creates a callable middleware for logging requests and responses.
|
||||
*
|
||||
* @param LoggerInterface|callable $logger
|
||||
* @param string|callable Constant or callable that accepts a Response.
|
||||
*/
|
||||
public function __construct($logger, $formatter = null)
|
||||
{
|
||||
// Use the setters to take care of type validation
|
||||
$this->setLogger($logger);
|
||||
$this->setFormatter($formatter ?: $this->getDefaultFormatter());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the default formatter;
|
||||
*
|
||||
* @return MessageFormatter
|
||||
*/
|
||||
protected function getDefaultFormatter()
|
||||
{
|
||||
return new MessageFormatter();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether requests should be logged before the response is received.
|
||||
*
|
||||
* @param boolean $logRequests
|
||||
*/
|
||||
public function setRequestLoggingEnabled($logRequests = true)
|
||||
{
|
||||
$this->logRequests = (bool) $logRequests;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the logger, which can be a PSR-3 logger or a callable that accepts
|
||||
* a log level, message, and array context.
|
||||
*
|
||||
* @param LoggerInterface|callable $logger
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function setLogger($logger)
|
||||
{
|
||||
if ($logger instanceof LoggerInterface || is_callable($logger)) {
|
||||
$this->logger = $logger;
|
||||
} else {
|
||||
throw new InvalidArgumentException(
|
||||
"Logger has to be a Psr\Log\LoggerInterface or callable"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the formatter, which can be a MessageFormatter or callable that
|
||||
* accepts a request, response, and a reason if an error has occurred.
|
||||
*
|
||||
* @param MessageFormatter|callable $formatter
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function setFormatter($formatter)
|
||||
{
|
||||
if ($formatter instanceof MessageFormatter || is_callable($formatter)) {
|
||||
$this->formatter = $formatter;
|
||||
} else {
|
||||
throw new InvalidArgumentException(
|
||||
"Formatter has to be a \GuzzleHttp\MessageFormatter or callable"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the log level to use, which can be either a string or a callable
|
||||
* that accepts a response (which could be null). A log level could also
|
||||
* be null, which indicates that the default log level should be used.
|
||||
*
|
||||
* @param string|callable|null
|
||||
*/
|
||||
public function setLogLevel($logLevel)
|
||||
{
|
||||
$this->logLevel = $logLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a request and/or a response.
|
||||
*
|
||||
* @param RequestInterface $request
|
||||
* @param ResponseInterface|null $response
|
||||
* @param $reason
|
||||
* @return mixed
|
||||
*/
|
||||
protected function log(
|
||||
RequestInterface $request,
|
||||
ResponseInterface $response = null,
|
||||
$reason = null
|
||||
) {
|
||||
if ($reason instanceof RequestException) {
|
||||
$response = $reason->getResponse();
|
||||
}
|
||||
|
||||
$level = $this->getLogLevel($response);
|
||||
$message = $this->getLogMessage($request, $response, $reason);
|
||||
$context = compact('request', 'response', 'reason');
|
||||
|
||||
// Make sure that the content of the body is available again.
|
||||
if ($response) {
|
||||
$response->getBody()->seek(0);;
|
||||
}
|
||||
|
||||
if (is_callable($this->logger)) {
|
||||
return call_user_func($this->logger, $level, $message, $context);
|
||||
}
|
||||
|
||||
$this->logger->log($level, $message, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a request and response as a log message.
|
||||
*
|
||||
* @param RequestInterface $request
|
||||
* @param ResponseInterface|null $response
|
||||
* @param mixed $reason
|
||||
*
|
||||
* @return string The formatted message.
|
||||
*/
|
||||
protected function getLogMessage(
|
||||
RequestInterface $request,
|
||||
ResponseInterface $response = null,
|
||||
$reason = null
|
||||
) {
|
||||
if ($this->formatter instanceof MessageFormatter) {
|
||||
return $this->formatter->format(
|
||||
$request,
|
||||
$response,
|
||||
$reason
|
||||
);
|
||||
}
|
||||
|
||||
return call_user_func($this->formatter, $request, $response, $reason);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a log level for a given response.
|
||||
*
|
||||
* @param ResponseInterface $response The response being logged.
|
||||
*
|
||||
* @return string LogLevel
|
||||
*/
|
||||
protected function getLogLevel(ResponseInterface $response = null)
|
||||
{
|
||||
if ( ! $this->logLevel) {
|
||||
return $this->getDefaultLogLevel($response);
|
||||
}
|
||||
|
||||
if (is_callable($this->logLevel)) {
|
||||
return call_user_func($this->logLevel, $response);
|
||||
}
|
||||
|
||||
return (string) $this->logLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the default log level for a response.
|
||||
*
|
||||
* @param ResponseInterface $response
|
||||
*
|
||||
* @return string LogLevel
|
||||
*/
|
||||
protected function getDefaultLogLevel(ResponseInterface $response = null) {
|
||||
if ($response && $response->getStatusCode() >= 300) {
|
||||
return LogLevel::NOTICE;
|
||||
}
|
||||
|
||||
return LogLevel::INFO;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a function which is handled when a request was successful.
|
||||
*
|
||||
* @param RequestInterface $request
|
||||
*
|
||||
* @return \Closure
|
||||
*/
|
||||
protected function onSuccess(RequestInterface $request)
|
||||
{
|
||||
return function ($response) use ($request) {
|
||||
$this->log($request, $response);
|
||||
return $response;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a function which is handled when a request was rejected.
|
||||
*
|
||||
* @param RequestInterface $request
|
||||
*
|
||||
* @return \Closure
|
||||
*/
|
||||
protected function onFailure(RequestInterface $request)
|
||||
{
|
||||
return function ($reason) use ($request) {
|
||||
|
||||
// Only log a rejected request if it hasn't already been logged.
|
||||
if ( ! $this->logRequests) {
|
||||
$this->log($request, null, $reason);
|
||||
}
|
||||
|
||||
return Promise\rejection_for($reason);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the middleware is handled by the client.
|
||||
*
|
||||
* @param callable $handler
|
||||
*
|
||||
* @return \Closure
|
||||
*/
|
||||
public function __invoke(callable $handler)
|
||||
{
|
||||
return function ($request, array $options) use ($handler) {
|
||||
|
||||
// Only log requests if explicitly set to do so
|
||||
if ($this->logRequests) {
|
||||
$this->log($request);
|
||||
}
|
||||
|
||||
return $handler($request, $options)->then(
|
||||
$this->onSuccess($request),
|
||||
$this->onFailure($request)
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
+615
@@ -0,0 +1,615 @@
|
||||
<?php
|
||||
|
||||
namespace yzh52521\EasyHttp;
|
||||
|
||||
use GuzzleHttp\Handler\CurlHandler;
|
||||
use GuzzleHttp\HandlerStack;
|
||||
|
||||
use GuzzleHttp\Pool;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Promise;
|
||||
use GuzzleHttp\Cookie\CookieJar;
|
||||
use GuzzleHttp\Exception\ConnectException;
|
||||
|
||||
/**
|
||||
* @method \yzh52521\EasyHttp\Response body()
|
||||
* @method \yzh52521\EasyHttp\Response array()
|
||||
* @method \yzh52521\EasyHttp\Response json()
|
||||
* @method \yzh52521\EasyHttp\Response headers()
|
||||
* @method \yzh52521\EasyHttp\Response header( string $header )
|
||||
* @method \yzh52521\EasyHttp\Response status()
|
||||
* @method \yzh52521\EasyHttp\Response successful()
|
||||
* @method \yzh52521\EasyHttp\Response ok()
|
||||
* @method \yzh52521\EasyHttp\Response redirect()
|
||||
* @method \yzh52521\EasyHttp\Response clientError()
|
||||
* @method \yzh52521\EasyHttp\Response serverError()
|
||||
*/
|
||||
class Request
|
||||
{
|
||||
/**
|
||||
* \GuzzleHttp\Client单例
|
||||
* @var array
|
||||
*/
|
||||
private static $instances = [];
|
||||
|
||||
/**
|
||||
* \GuzzleHttp\Client;
|
||||
* @var Client
|
||||
*/
|
||||
protected $client;
|
||||
|
||||
/**
|
||||
* Body格式
|
||||
* @var string
|
||||
*/
|
||||
protected $bodyFormat;
|
||||
|
||||
protected $isRemoveBodyFormat = false;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $options = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $promises = [];
|
||||
|
||||
/**
|
||||
* 并发次数
|
||||
* @var
|
||||
*/
|
||||
protected $concurrency;
|
||||
|
||||
/**
|
||||
*
|
||||
* @var HandlerStack
|
||||
*/
|
||||
protected $handlerStack;
|
||||
|
||||
|
||||
/**
|
||||
* Request constructor.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->client = $this->getInstance();
|
||||
|
||||
$this->bodyFormat = 'form_params';
|
||||
$this->options = [
|
||||
'http_errors' => false,
|
||||
];
|
||||
if (!$this->handlerStack instanceof HandlerStack) {
|
||||
$this->handlerStack = HandlerStack::create( new CurlHandler() );
|
||||
}
|
||||
$this->options['handler'] = $this->handlerStack;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request destructor.
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
if (!empty( $this->promises )) {
|
||||
Promise\settle( $this->promises )->wait();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单例
|
||||
* @return mixed
|
||||
*/
|
||||
public function getInstance()
|
||||
{
|
||||
$name = get_called_class();
|
||||
|
||||
if (!isset( self::$instances[$name] )) {
|
||||
self::$instances[$name] = new Client();
|
||||
}
|
||||
|
||||
return self::$instances[$name];
|
||||
}
|
||||
|
||||
public function asForm()
|
||||
{
|
||||
$this->bodyFormat = 'form_params';
|
||||
$this->withHeaders( ['Content-Type' => 'application/x-www-form-urlencoded'] );
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function asJson()
|
||||
{
|
||||
$this->bodyFormat = 'json';
|
||||
$this->withHeaders( ['Content-Type' => 'application/json'] );
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function asMultipart(string $name,string $contents,string $filename = null,array $headers = [])
|
||||
{
|
||||
$this->bodyFormat = 'multipart';
|
||||
|
||||
$this->options = array_filter( [
|
||||
'name' => $name,
|
||||
'contents' => $contents,
|
||||
'headers' => $headers,
|
||||
'filename' => $filename,
|
||||
] );
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function withHost(string $host)
|
||||
{
|
||||
$this->options['base_uri'] = $host;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function withOptions(array $options)
|
||||
{
|
||||
unset( $this->options[$this->bodyFormat],$this->options['body'] );
|
||||
|
||||
$this->options = array_merge_recursive( $this->options,$options );
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function withCert(string $path,string $password)
|
||||
{
|
||||
$this->options['cert'] = [$path,$password];
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function withHeaders(array $headers)
|
||||
{
|
||||
$this->options = array_merge_recursive( $this->options,[
|
||||
'headers' => $headers,
|
||||
] );
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function withBasicAuth(string $username,string $password)
|
||||
{
|
||||
$this->options['auth'] = [$username,$password];
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function withDigestAuth(string $username,string $password)
|
||||
{
|
||||
$this->options['auth'] = [$username,$password,'digest'];
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function withUA(string $ua)
|
||||
{
|
||||
$this->options['headers']['User-Agent'] = trim( $ua );
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function withToken(string $token,string $type = 'Bearer')
|
||||
{
|
||||
$this->options['headers']['Authorization'] = trim( $type.' '.$token );
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function withCookies(array $cookies,string $domain)
|
||||
{
|
||||
$this->options = array_merge_recursive( $this->options,[
|
||||
'cookies' => CookieJar::fromArray( $cookies,$domain ),
|
||||
] );
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function withProxy($proxy)
|
||||
{
|
||||
$this->options['proxy'] = $proxy;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function withVersion($version)
|
||||
{
|
||||
$this->options['version'] = $version;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function withRedirect($redirect = false)
|
||||
{
|
||||
$this->options['allow_redirects'] = $redirect;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function withVerify($verify = false)
|
||||
{
|
||||
$this->options['verify'] = $verify;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function withStream($boolean = false)
|
||||
{
|
||||
$this->options['stream'] = $boolean;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function concurrency(int $times)
|
||||
{
|
||||
$this->concurrency = $times;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function retry(int $retries = 1,int $sleep = 0)
|
||||
{
|
||||
$this->handlerStack->push( ( new Retry() )->handle( $retries,$sleep ) );
|
||||
$this->options['handler'] = $this->handlerStack;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function delay(int $seconds)
|
||||
{
|
||||
$this->options['delay'] = $seconds * 1000;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function timeout(int $seconds)
|
||||
{
|
||||
$this->options['timeout'] = $seconds * 1000;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeBodyFormat()
|
||||
{
|
||||
$this->isRemoveBodyFormat = true;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function debug($class)
|
||||
{
|
||||
$logger = new Logger( function ($level,$message,array $context) use ($class) {
|
||||
$class::log( $level,$message );
|
||||
},function ($request,$response,$reason) {
|
||||
$requestBody = $request->getBody();
|
||||
$requestBody->rewind();
|
||||
|
||||
//请求头
|
||||
$requestHeaders = [];
|
||||
|
||||
foreach ( (array)$request->getHeaders() as $k => $vs ) {
|
||||
foreach ( $vs as $v ) {
|
||||
$requestHeaders[] = "$k: $v";
|
||||
}
|
||||
}
|
||||
|
||||
//响应头
|
||||
$responseHeaders = [];
|
||||
|
||||
foreach ( (array)$response->getHeaders() as $k => $vs ) {
|
||||
foreach ( $vs as $v ) {
|
||||
$responseHeaders[] = "$k: $v";
|
||||
}
|
||||
}
|
||||
|
||||
$uri = $request->getUri();
|
||||
$path = $uri->getPath();
|
||||
|
||||
if ($query = $uri->getQuery()) {
|
||||
$path .= '?'.$query;
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
"Request %s\n%s %s HTTP/%s\r\n%s\r\n\r\n%s\r\n--------------------\r\nHTTP/%s %s %s\r\n%s\r\n\r\n%s",
|
||||
$uri,
|
||||
$request->getMethod(),
|
||||
$path,
|
||||
$request->getProtocolVersion(),
|
||||
join( "\r\n",$requestHeaders ),
|
||||
$requestBody->getContents(),
|
||||
$response->getProtocolVersion(),
|
||||
$response->getStatusCode(),
|
||||
$response->getReasonPhrase(),
|
||||
join( "\r\n",$responseHeaders ),
|
||||
$response->getBody()->getContents()
|
||||
);
|
||||
} );
|
||||
$this->handlerStack->push( $logger );
|
||||
$this->options['handler'] = $this->handlerStack;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function attach(string $name,string $contents,string $filename = null,array $headers = [])
|
||||
{
|
||||
$this->options['multipart'] = array_filter( [
|
||||
'name' => $name,
|
||||
'contents' => $contents,
|
||||
'headers' => $headers,
|
||||
'filename' => $filename,
|
||||
] );
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function get(string $url,array $query = [])
|
||||
{
|
||||
$params= parse_url( $url,PHP_URL_QUERY );
|
||||
|
||||
parse_str( $params?:'',$result );
|
||||
|
||||
$this->options['query'] = array_merge( $result,$query );
|
||||
|
||||
return $this->request( 'GET',$url,$query );
|
||||
}
|
||||
|
||||
public function post(string $url,array $data = [])
|
||||
{
|
||||
$this->options[$this->bodyFormat] = $data;
|
||||
|
||||
return $this->request( 'POST',$url,$data );
|
||||
}
|
||||
|
||||
public function patch(string $url,array $data = [])
|
||||
{
|
||||
$this->options[$this->bodyFormat] = $data;
|
||||
|
||||
return $this->request( 'PATCH',$url,$data );
|
||||
}
|
||||
|
||||
public function put(string $url,array $data = [])
|
||||
{
|
||||
$this->options[$this->bodyFormat] = $data;
|
||||
|
||||
return $this->request( 'PUT',$url,$data );
|
||||
}
|
||||
|
||||
public function delete(string $url,array $data = [])
|
||||
{
|
||||
$this->options[$this->bodyFormat] = $data;
|
||||
|
||||
return $this->request( 'DELETE',$url,$data );
|
||||
}
|
||||
|
||||
public function head(string $url,array $data = [])
|
||||
{
|
||||
$this->options[$this->bodyFormat] = $data;
|
||||
|
||||
return $this->request( 'HEAD',$url,$data );
|
||||
}
|
||||
|
||||
public function options(string $url,array $data = [])
|
||||
{
|
||||
$this->options[$this->bodyFormat] = $data;
|
||||
|
||||
return $this->request( 'OPTIONS',$url,$data );
|
||||
}
|
||||
|
||||
public function getAsync(string $url,$query = null,callable $success = null,callable $fail = null)
|
||||
{
|
||||
is_callable( $query ) || $this->options['query'] = $query;
|
||||
|
||||
return $this->requestAsync( 'GET',$url,$query,$success,$fail );
|
||||
}
|
||||
|
||||
public function postAsync(string $url,$data = null,callable $success = null,callable $fail = null)
|
||||
{
|
||||
is_callable( $data ) || $this->options[$this->bodyFormat] = $data;
|
||||
|
||||
return $this->requestAsync( 'POST',$url,$data,$success,$fail );
|
||||
}
|
||||
|
||||
public function patchAsync(string $url,$data = null,callable $success = null,callable $fail = null)
|
||||
{
|
||||
is_callable( $data ) || $this->options[$this->bodyFormat] = $data;
|
||||
|
||||
return $this->requestAsync( 'PATCH',$url,$data,$success,$fail );
|
||||
}
|
||||
|
||||
public function putAsync(string $url,$data = null,callable $success = null,callable $fail = null)
|
||||
{
|
||||
is_callable( $data ) || $this->options[$this->bodyFormat] = $data;
|
||||
|
||||
return $this->requestAsync( 'PUT',$url,$data,$success,$fail );
|
||||
}
|
||||
|
||||
public function deleteAsync(string $url,$data = null,callable $success = null,callable $fail = null)
|
||||
{
|
||||
is_callable( $data ) || $this->options[$this->bodyFormat] = $data;
|
||||
|
||||
return $this->requestAsync( 'DELETE',$url,$data,$success,$fail );
|
||||
}
|
||||
|
||||
public function headAsync(string $url,$data = null,callable $success = null,callable $fail = null)
|
||||
{
|
||||
is_callable( $data ) || $this->options[$this->bodyFormat] = $data;
|
||||
|
||||
return $this->requestAsync( 'HEAD',$url,$data,$success,$fail );
|
||||
}
|
||||
|
||||
public function optionsAsync(string $url,$data = null,callable $success = null,callable $fail = null)
|
||||
{
|
||||
is_callable( $data ) || $this->options[$this->bodyFormat] = $data;
|
||||
|
||||
return $this->requestAsync( 'OPTIONS',$url,$data,$success,$fail );
|
||||
}
|
||||
|
||||
public function multiAsync(array $promises,callable $success = null,callable $fail = null)
|
||||
{
|
||||
$count = count( $promises );
|
||||
|
||||
$this->concurrency = $this->concurrency ?: $count;
|
||||
|
||||
$requests = function () use ($promises) {
|
||||
foreach ( $promises as $promise ) {
|
||||
yield function () use ($promise) {
|
||||
return $promise;
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
$fulfilled = function ($response,$index) use ($success) {
|
||||
if (!is_null( $success )) {
|
||||
$response = $this->response( $response );
|
||||
call_user_func_array( $success,[$response,$index] );
|
||||
}
|
||||
};
|
||||
|
||||
$rejected = function ($exception,$index) use ($fail) {
|
||||
if (!is_null( $fail )) {
|
||||
$exception = $this->exception( $exception );
|
||||
call_user_func_array( $fail,[$exception,$index] );
|
||||
}
|
||||
};
|
||||
|
||||
$pool = new Pool( $this->client,$requests(),[
|
||||
'concurrency' => $this->concurrency,
|
||||
'fulfilled' => $fulfilled,
|
||||
'rejected' => $rejected,
|
||||
] );
|
||||
|
||||
$pool->promise();
|
||||
|
||||
return $pool;
|
||||
}
|
||||
|
||||
protected function request(string $method,string $url,array $options = [])
|
||||
{
|
||||
isset( $this->options[$this->bodyFormat] ) && $this->options[$this->bodyFormat] = $options;
|
||||
if ($this->isRemoveBodyFormat) {
|
||||
unset( $this->options[$this->bodyFormat] );
|
||||
}
|
||||
try {
|
||||
$response = $this->client->request( $method,$url,$this->options );
|
||||
return $this->response( $response );
|
||||
} catch ( ConnectException $e ) {
|
||||
throw new ConnectionException( $e->getMessage(),0,$e );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 原生请求
|
||||
* @param string $method
|
||||
* @param string $url
|
||||
* @param array $options
|
||||
* @return Response
|
||||
* @throws ConnectionException
|
||||
* @throws \GuzzleHttp\Exception\GuzzleException
|
||||
*/
|
||||
public function client(string $method,string $url,array $options = [])
|
||||
{
|
||||
try {
|
||||
if (empty( $options )) {
|
||||
$options = $this->options;
|
||||
}
|
||||
$response = $this->client->request( $method,$url,$options );
|
||||
return $this->response( $response );
|
||||
} catch ( ConnectException $e ) {
|
||||
throw new ConnectionException( $e->getMessage(),0,$e );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 原生异常请求
|
||||
* @param string $method
|
||||
* @param string $url
|
||||
* @param array $options
|
||||
* @return Response
|
||||
* @throws ConnectionException
|
||||
*/
|
||||
public function clientAsync(string $method,string $url,array $options = [])
|
||||
{
|
||||
try {
|
||||
if (empty( $options )) {
|
||||
$options = $this->options;
|
||||
}
|
||||
$response = $this->client->requestAsync( $method,$url,$options );
|
||||
return $this->response( $response );
|
||||
} catch ( ConnectException $e ) {
|
||||
throw new ConnectionException( $e->getMessage(),0,$e );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected function requestAsync(
|
||||
string $method,
|
||||
string $url,
|
||||
$options = null,
|
||||
callable $success = null,
|
||||
callable $fail = null
|
||||
)
|
||||
{
|
||||
if (is_callable( $options )) {
|
||||
$successCallback = $options;
|
||||
$failCallback = $success;
|
||||
} else {
|
||||
$successCallback = $success;
|
||||
$failCallback = $fail;
|
||||
}
|
||||
|
||||
isset( $this->options[$this->bodyFormat] ) && $this->options[$this->bodyFormat] = $options;
|
||||
|
||||
if ($this->isRemoveBodyFormat) {
|
||||
unset( $this->options[$this->bodyFormat] );
|
||||
}
|
||||
|
||||
try {
|
||||
$promise = $this->client->requestAsync( $method,$url,$this->options );
|
||||
|
||||
$fulfilled = function ($response) use ($successCallback) {
|
||||
if (!is_null( $successCallback )) {
|
||||
$response = $this->response( $response );
|
||||
call_user_func_array( $successCallback,[$response] );
|
||||
}
|
||||
};
|
||||
|
||||
$rejected = function ($exception) use ($failCallback) {
|
||||
if (!is_null( $failCallback )) {
|
||||
$exception = $this->exception( $exception );
|
||||
call_user_func_array( $failCallback,[$exception] );
|
||||
}
|
||||
};
|
||||
|
||||
$promise->then( $fulfilled,$rejected );
|
||||
|
||||
$this->promises[] = $promise;
|
||||
|
||||
return $promise;
|
||||
} catch ( ConnectException $e ) {
|
||||
throw new ConnectionException( $e->getMessage(),0,$e );
|
||||
}
|
||||
}
|
||||
|
||||
public function wait()
|
||||
{
|
||||
if (!empty($this->promises)) {
|
||||
Promise\settle($this->promises)->wait();
|
||||
}
|
||||
$this->promises = [];
|
||||
}
|
||||
|
||||
protected function response($response)
|
||||
{
|
||||
return new Response( $response );
|
||||
}
|
||||
|
||||
protected function exception($exception)
|
||||
{
|
||||
return new RequestException( $exception );
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace yzh52521\EasyHttp;
|
||||
|
||||
class RequestException
|
||||
{
|
||||
public $exception;
|
||||
|
||||
public function __construct($exception)
|
||||
{
|
||||
$this->exception = $exception;
|
||||
}
|
||||
|
||||
public function getCode()
|
||||
{
|
||||
return $this->exception->getCode();
|
||||
}
|
||||
|
||||
public function getMessage()
|
||||
{
|
||||
return $this->exception->getMessage();
|
||||
}
|
||||
|
||||
public function getFile()
|
||||
{
|
||||
return $this->exception->getFile();
|
||||
}
|
||||
|
||||
public function getLine()
|
||||
{
|
||||
return $this->exception->getLine();
|
||||
}
|
||||
|
||||
public function getTrace()
|
||||
{
|
||||
return $this->exception->getTrace();
|
||||
}
|
||||
|
||||
public function getTraceAsString()
|
||||
{
|
||||
return $this->exception->getTraceAsString();
|
||||
}
|
||||
}
|
||||
+212
@@ -0,0 +1,212 @@
|
||||
<?php
|
||||
|
||||
namespace yzh52521\EasyHttp;
|
||||
|
||||
use ArrayAccess;
|
||||
use LogicException;
|
||||
|
||||
class Response implements ArrayAccess
|
||||
{
|
||||
protected $response;
|
||||
|
||||
/**
|
||||
* The decoded JSON response.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $decoded;
|
||||
|
||||
public function __construct($response)
|
||||
{
|
||||
$this->response = $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the body of the response.
|
||||
* @return string
|
||||
*/
|
||||
public function body()
|
||||
{
|
||||
return (string)$this->response->getBody();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Array decoded body of the response.
|
||||
* @return array|mixed
|
||||
*/
|
||||
public function array()
|
||||
{
|
||||
if (!$this->decoded) {
|
||||
$this->decoded = json_decode( (string)$this->response->getBody(),true );
|
||||
}
|
||||
|
||||
return $this->decoded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the JSON decoded body of the response.
|
||||
* @return object|mixed
|
||||
*/
|
||||
public function json()
|
||||
{
|
||||
if (!$this->decoded) {
|
||||
$this->decoded = json_decode( (string)$this->response->getBody() );
|
||||
}
|
||||
|
||||
return $this->decoded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a header from the response.
|
||||
* @param string $header
|
||||
* @return mixed
|
||||
*/
|
||||
public function header(string $header)
|
||||
{
|
||||
return $this->response->getHeaderLine( $header );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the headers from the response.
|
||||
* @return mixed
|
||||
*/
|
||||
public function headers()
|
||||
{
|
||||
return $this->mapWithKeys( $this->response->getHeaders(),function ($v,$k) {
|
||||
return [$k => $v];
|
||||
} )->response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the status code of the response.
|
||||
* @return int
|
||||
*/
|
||||
public function status()
|
||||
{
|
||||
return (int)$this->response->getStatusCode();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the request was successful.
|
||||
* @return bool
|
||||
*/
|
||||
public function successful()
|
||||
{
|
||||
return $this->status() >= 200 && $this->status() < 300;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the response code was "OK".
|
||||
* @return bool
|
||||
*/
|
||||
public function ok()
|
||||
{
|
||||
return $this->status() === 200;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the response was a redirect.
|
||||
* @return bool
|
||||
*/
|
||||
public function redirect()
|
||||
{
|
||||
return $this->status() >= 300 && $this->status() < 400;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the response indicates a client error occurred.
|
||||
* @return bool
|
||||
*/
|
||||
public function clientError()
|
||||
{
|
||||
return $this->status() >= 400 && $this->status() < 500;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the response indicates a server error occurred.
|
||||
* @return bool
|
||||
*/
|
||||
public function serverError()
|
||||
{
|
||||
return $this->status() >= 500;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the given offset exists.
|
||||
*
|
||||
* @param string $offset
|
||||
* @return mixed
|
||||
*/
|
||||
#[\ReturnTypeWillChange]
|
||||
public function offsetExists($offset)
|
||||
{
|
||||
return array_key_exists( $offset,$this->json() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value for a given offset.
|
||||
*
|
||||
* @param string $offset
|
||||
* @return mixed
|
||||
*/
|
||||
#[\ReturnTypeWillChange]
|
||||
public function offsetGet($offset)
|
||||
{
|
||||
return $this->json()[$offset];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the value at the given offset.
|
||||
*
|
||||
* @param string $offset
|
||||
* @param mixed $value
|
||||
* @return void
|
||||
*
|
||||
* @throws \LogicException
|
||||
*/
|
||||
#[\ReturnTypeWillChange]
|
||||
public function offsetSet($offset,$value)
|
||||
{
|
||||
throw new LogicException( 'Response data may not be mutated using array access.' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Unset the value at the given offset.
|
||||
*
|
||||
* @param string $offset
|
||||
* @return void
|
||||
*
|
||||
* @throws \LogicException
|
||||
*/
|
||||
#[\ReturnTypeWillChange]
|
||||
public function offsetUnset($offset)
|
||||
{
|
||||
throw new LogicException( 'Response data may not be mutated using array access.' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the body of the response.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return $this->body();
|
||||
}
|
||||
|
||||
protected function mapWithKeys($items,callable $callback)
|
||||
{
|
||||
$result = [];
|
||||
|
||||
foreach ( $items as $key => $value ) {
|
||||
$assoc = $callback( $value,$key );
|
||||
|
||||
foreach ( $assoc as $mapKey => $mapValue ) {
|
||||
$result[$mapKey] = $mapValue;
|
||||
}
|
||||
}
|
||||
|
||||
return new static( $result );
|
||||
}
|
||||
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace yzh52521\EasyHttp;
|
||||
|
||||
use GuzzleHttp\Middleware;
|
||||
use GuzzleHttp\Psr7\Request;
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
use GuzzleHttp\Exception\ServerException;
|
||||
use GuzzleHttp\Exception\ConnectException;
|
||||
|
||||
class Retry
|
||||
{
|
||||
|
||||
public function handle($retries,$sleep)
|
||||
{
|
||||
return Middleware::retry($this->decider($retries), $this->delay($sleep));
|
||||
}
|
||||
|
||||
protected function decider(int $times)
|
||||
{
|
||||
return function (
|
||||
$retries,
|
||||
Request $request,
|
||||
Response $response = null,
|
||||
RequestException $exception = null
|
||||
) use ($times) {
|
||||
// 超过最大重试次数,不再重试
|
||||
if ($retries >= $times) {
|
||||
return false;
|
||||
}
|
||||
return $exception instanceof ConnectException || $exception instanceof ServerException || ($response && $response->getStatusCode() >= 500);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回一个匿名函数,该匿名函数返回下次重试的时间(毫秒)
|
||||
* @param int $retry_delay
|
||||
* @return \Closure
|
||||
*/
|
||||
protected function delay(int $retry_delay)
|
||||
{
|
||||
return function ($retries) use ($retry_delay) {
|
||||
return $retry_delay * $retries;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
build
|
||||
vendor
|
||||
.vscode
|
||||
.phpunit*
|
||||
composer.lock
|
||||
.idea
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
# webman-throttle
|
||||
|
||||
## 作用
|
||||
通过本中间件可限定用户在一段时间内的访问次数,可用于保护接口防爬防爆破的目的。
|
||||
|
||||
## 安装
|
||||
|
||||
```composer require yzh52521/webman-throttle```
|
||||
|
||||
|
||||
安装后会自动为项目生成 config/plugin/yzh52521/throttle/app.php (***注意:任何时候你都不应该修改最后一行的 hash注释***)配置文件,安装后组件不会自动启用,需要手动设置。
|
||||
|
||||
|
||||
## 开启
|
||||
|
||||
插件以中间件的方式进行工作,因此它的开启与其他中间件一样,例如在全局中间件中使用:
|
||||
|
||||
|
||||
```
|
||||
<?php
|
||||
//cat config/middleware.php
|
||||
|
||||
return [
|
||||
// 全局中间件
|
||||
'' => [
|
||||
// ... 这里省略其它中间件
|
||||
app\middleware\Throttle::class,
|
||||
]
|
||||
];
|
||||
|
||||
```
|
||||
|
||||
|
||||
## 配置说明
|
||||
```
|
||||
<?php
|
||||
// cat config/plugin/yzh52521/throttle/app.php
|
||||
// 中间件配置
|
||||
return [
|
||||
|
||||
// 缓存键前缀,防止键值与其他应用冲突
|
||||
'prefix' => 'throttle_',
|
||||
|
||||
// 缓存的键,true 表示使用来源ip (request->getRealIp(true))
|
||||
'key' => true,
|
||||
|
||||
// 要被限制的请求类型, eg: GET POST PUT DELETE HEAD
|
||||
'visit_method' => ['GET'],
|
||||
|
||||
// 设置访问频率,例如 '10/m' 指的是允许每分钟请求10次。值 null 表示不限制,
|
||||
// eg: null 10/m 20/h 300/d 200/300
|
||||
'visit_rate' => '100/m',
|
||||
|
||||
// 响应体中设置速率限制的头部信息,含义见:https://docs.github.com/en/rest/overview/resources-in-the-rest-api#rate-limiting
|
||||
'visit_enable_show_rate_limit' => true,
|
||||
|
||||
// 访问受限时返回的响应( type: null|callable )
|
||||
'visit_fail_response' => function (Throttle $throttle, Request $request, int $wait_seconds): Response {
|
||||
return response('Too many requests, try again after ' . $wait_seconds . ' seconds.', 429);
|
||||
},
|
||||
|
||||
/*
|
||||
* 设置节流算法,组件提供了四种算法:
|
||||
* - CounterFixed :计数固定窗口
|
||||
* - CounterSlider: 滑动窗口
|
||||
* - TokenBucket : 令牌桶算法
|
||||
* - LeakyBucket : 漏桶限流算法
|
||||
*/
|
||||
'driver_name' => CounterFixed::class,
|
||||
|
||||
// Psr-16通用缓存库规范: https://blog.csdn.net/maquealone/article/details/79651111
|
||||
// Cache驱动必须符合PSR-16缓存库规范,最低实现get/set俩个方法 (且需静态化实现)
|
||||
// static get(string $key, mixed $default=null)
|
||||
// static set(string $key, mixed $value, int $ttl=0);
|
||||
|
||||
//webman默认使用 symfony/cache作为cache组件(https://www.workerman.net/doc/webman/db/cache.html)
|
||||
'cache_drive' => support\Cache::class,
|
||||
|
||||
//使用ThinkCache
|
||||
//'cache_drive' => think\facade\Cache::class,
|
||||
|
||||
];
|
||||
```
|
||||
|
||||
当配置项满足以下条件任何一个时,不会限制访问频率:
|
||||
|
||||
|
||||
```key === false || key === null || visit_rate === null```
|
||||
|
||||
|
||||
|
||||
其中 key 用来设置缓存键的, 而 visit_rate 用来设置访问频率,单位可以是秒,分,时,天。例如:1/s, 10/m, 98/h, 100/d , 也可以是 100/600 (600 秒内最多 100 次请求)。
|
||||
|
||||
|
||||
### 灵活定制
|
||||
|
||||
###### 示例一:针对用户个体做限制,key的值可以设为函数,该函数返回新的缓存键值(需要Session支持),例如:
|
||||
|
||||
```
|
||||
'key' => function($throttle, $request) {
|
||||
return $request->session()->get('user_id');
|
||||
},
|
||||
```
|
||||
|
||||
###### 实例二:在回调函数里针对不同控制器和方法定制生成key,中间件会进行转换:
|
||||
|
||||
```
|
||||
'key' => function($throttle, $request) {
|
||||
return implode('/', [
|
||||
$request->controller,
|
||||
$request->action,
|
||||
$request->getRealIp($safe_mode=true)
|
||||
]);
|
||||
},
|
||||
|
||||
//'key' => 'controller/action/ip' //上述配置的快捷实现
|
||||
|
||||
|
||||
```
|
||||
|
||||
|
||||
###### 示例三:在闭包内修改本次访问频率或临时更换限流策略:(PS:此示例需要本中间件在路由中间件后启用,这样预设的替换功能才会生效。)
|
||||
|
||||
```
|
||||
'key' => function($throttle, $request) {
|
||||
$throttle->setRate('5/m'); // 设置频率
|
||||
$throttle->setDriverClass(CounterSlider::class);// 设置限流策略
|
||||
return true;
|
||||
},
|
||||
```
|
||||
|
||||
###### 示例四:在路由中独立配置
|
||||
|
||||
```
|
||||
|
||||
Route::any('/api/driver-ocr', [ app\api\controller\Ocr::class, 'driver'])->middleware([
|
||||
app\middleware\Throttle::class
|
||||
]);
|
||||
|
||||
|
||||
//正确使用向中间件传参例子 (Webman-framework >= 1.3.16)
|
||||
Route::group('/path', function() {
|
||||
//路由注册
|
||||
...
|
||||
|
||||
})->setParams(['visit_rate' => '20/m',
|
||||
...
|
||||
...
|
||||
])->middleware(\app\middleware\Throttle::class);
|
||||
```
|
||||
|
||||
## 注意:
|
||||
1、Webman-framework >= 1.3.16 支持路由向中间件传参 使用 $route->param() 方法
|
||||
2、禁止访问时,throttle 默认是抛出 HttpResponseException, 当前插件场景下将正常响应一个httpResponse(即不会主动:Throw Exception),特殊需求请在 "visit_fail_response" 匿名函数中配置
|
||||
|
||||
## 申明
|
||||
|
||||
本库提取于[think-throttle v1.3.x](https://github.com/top-think/think-throttle)
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "yzh52521/webman-throttle",
|
||||
"description": "Webman框架限流中间件",
|
||||
"type": "library",
|
||||
"authors": [
|
||||
{
|
||||
"name": "yzh52521",
|
||||
"email": "396751927@qq.com"
|
||||
}
|
||||
],
|
||||
"keywords": [ "webman", "middleware", "throttle", "php"],
|
||||
"license": "MIT",
|
||||
"require": {
|
||||
"php": ">=7.2",
|
||||
"workerman/webman-framework": "^1.2.1"
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"yzh52521\\middleware\\": "src"
|
||||
}
|
||||
}
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace yzh52521\middleware;
|
||||
|
||||
class Install
|
||||
{
|
||||
const WEBMAN_PLUGIN = true;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected static $pathRelation = array(
|
||||
'config/plugin/yzh52521/throttle' => 'config/plugin/yzh52521/throttle',
|
||||
);
|
||||
|
||||
/**
|
||||
* Install
|
||||
* @return void
|
||||
*/
|
||||
public static function install()
|
||||
{
|
||||
copy(__DIR__ . '/middleware.tpl', app_path() . '/middleware/Throttle.php');
|
||||
static::installByRelation();
|
||||
}
|
||||
|
||||
/**
|
||||
* Uninstall
|
||||
* @return void
|
||||
*/
|
||||
public static function uninstall()
|
||||
{
|
||||
if (is_file(app_path() . "/middleware/Throttle.php")) {
|
||||
unlink(app_path() . "/middleware/Throttle.php");
|
||||
}
|
||||
self::uninstallByRelation();
|
||||
}
|
||||
|
||||
/**
|
||||
* installByRelation
|
||||
* @return void
|
||||
*/
|
||||
public static function installByRelation()
|
||||
{
|
||||
foreach (static::$pathRelation as $source => $dest) {
|
||||
if ($pos = strrpos($dest, '/')) {
|
||||
$parent_dir = base_path() . '/' . substr($dest, 0, $pos);
|
||||
if (!is_dir($parent_dir)) {
|
||||
mkdir($parent_dir, 0777, true);
|
||||
}
|
||||
}
|
||||
//symlink(__DIR__ . "/$source", base_path()."/$dest");
|
||||
copy_dir(__DIR__ . "/$source", base_path() . "/$dest");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* uninstallByRelation
|
||||
* @return void
|
||||
*/
|
||||
public static function uninstallByRelation()
|
||||
{
|
||||
foreach (static::$pathRelation as $source => $dest) {
|
||||
$path = base_path() . "/$dest";
|
||||
if (!is_dir($path) && !is_file($path)) {
|
||||
continue;
|
||||
}
|
||||
/*if (is_link($path) {
|
||||
unlink($path);
|
||||
}*/
|
||||
remove_dir($path);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+267
@@ -0,0 +1,267 @@
|
||||
<?php
|
||||
/**
|
||||
*
|
||||
* 访问频率限制中间件
|
||||
* @link https://github.com/yzh52521/webman-throttle
|
||||
* @license http://www.apache.org/licenses/LICENSE-2.0
|
||||
* @copyright The PHP - Tools
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace yzh52521\middleware;
|
||||
|
||||
use Psr\SimpleCache\CacheInterface;
|
||||
use yzh52521\middleware\throttle\{CounterFixed, ThrottleAbstract};
|
||||
use Webman\Config;
|
||||
use support\{Container, Cache};
|
||||
use Webman\Http\{Request,Response};
|
||||
use function sprintf;
|
||||
|
||||
/**
|
||||
* 访问频率限制中间件
|
||||
* Class Throttle
|
||||
* @package app\middleware\Throttle
|
||||
*/
|
||||
class Throttle
|
||||
{
|
||||
/**
|
||||
* 默认配置参数
|
||||
* @var array
|
||||
*/
|
||||
public static $default_config = [
|
||||
'prefix' => 'throttle_', // 缓存键前缀,防止键与其他应用冲突
|
||||
'key' => true, // 节流规则 true为自动规则
|
||||
'visit_method' => ['GET', 'HEAD'], // 要被限制的请求类型
|
||||
'visit_rate' => null, // 节流频率 null 表示不限制 eg: 10/m 20/h 300/d
|
||||
'visit_enable_show_rate_limit' => true, // 在响应体中设置速率限制的头部信息
|
||||
'visit_fail_code' => 429, // 访问受限时返回的http状态码,当没有visit_fail_response时生效
|
||||
'visit_fail_text' => 'Too Many Requests', // 访问受限时访问的文本信息,当没有visit_fail_response时生效
|
||||
'visit_fail_response' => null, // 访问受限时的响应信息闭包回调
|
||||
'driver_name' => CounterFixed::class, // 限流算法驱动
|
||||
'cache_drive' => Cache::class // 缓存驱动
|
||||
];
|
||||
|
||||
public static $duration = [
|
||||
's' => 1,
|
||||
'm' => 60,
|
||||
'h' => 3600,
|
||||
'd' => 86400,
|
||||
];
|
||||
|
||||
/**
|
||||
* 缓存对象
|
||||
* @var CacheInterface
|
||||
*/
|
||||
protected $cache;
|
||||
|
||||
/**
|
||||
* 配置参数
|
||||
* @var array
|
||||
*/
|
||||
protected $config = [];
|
||||
|
||||
protected $key = null; // 解析后的标识
|
||||
protected $wait_seconds = 0; // 下次合法请求还有多少秒
|
||||
protected $now = 0; // 当前时间戳
|
||||
protected $max_requests = 0; // 规定时间内允许的最大请求次数
|
||||
protected $expire = 0; // 规定时间
|
||||
protected $remaining = 0; // 规定时间内还能请求的次数
|
||||
/**
|
||||
* @var ThrottleAbstract|null
|
||||
*/
|
||||
protected $driver_class = null;
|
||||
|
||||
/**
|
||||
* Throttle constructor.
|
||||
* @param Cache $cache
|
||||
* @param Config $config
|
||||
*/
|
||||
public function __construct(array $params = [])
|
||||
{
|
||||
$this->config = array_merge(static::$default_config, Config::get('plugin.yzh52521.throttle.app',[]), $params);
|
||||
$this->cache = Container::make($this->config['cache_drive'], []);
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求是否允许
|
||||
* @param Request $request
|
||||
* @return bool
|
||||
*/
|
||||
protected function allowRequest(Request $request): bool
|
||||
{
|
||||
// 若请求类型不在限制内
|
||||
if (!in_array($request->method(), $this->config['visit_method'])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$key = $this->getCacheKey($request);
|
||||
if (null === $key) {
|
||||
return true;
|
||||
}
|
||||
[$max_requests, $duration] = $this->parseRate($this->config['visit_rate']);
|
||||
|
||||
$micronow = microtime(true);
|
||||
$now = (int)$micronow;
|
||||
|
||||
$this->driver_class = Container::make($this->config['driver_name'], []);
|
||||
if (!$this->driver_class instanceof ThrottleAbstract) {
|
||||
throw new \TypeError('The throttle driver must extends ' . ThrottleAbstract::class);
|
||||
}
|
||||
$allow = $this->driver_class->allowRequest($key, $micronow, $max_requests, $duration, $this->cache);
|
||||
|
||||
if ($allow) {
|
||||
// 允许访问
|
||||
$this->now = $now;
|
||||
$this->expire = $duration;
|
||||
$this->max_requests = $max_requests;
|
||||
$this->remaining = $max_requests - $this->driver_class->getCurRequests();
|
||||
return true;
|
||||
}
|
||||
|
||||
$this->wait_seconds = $this->driver_class->getWaitSeconds();
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理限制访问
|
||||
* @param Request $request
|
||||
* @param array $params
|
||||
* @return Response
|
||||
* @exception
|
||||
*/
|
||||
public function handle(Request $request, callable $next, array $params = []): Response
|
||||
{
|
||||
|
||||
if ($params) {
|
||||
$this->config = array_merge($this->config, $params);
|
||||
}
|
||||
|
||||
$allow = $this->allowRequest($request);
|
||||
if (!$allow) {
|
||||
// 访问受限
|
||||
return $this->buildLimitException($this->wait_seconds, $request);
|
||||
}
|
||||
|
||||
$response = $next($request);
|
||||
|
||||
if ((200 <= $response->getStatusCode() || 300 > $response->getStatusCode()) && $this->config['visit_enable_show_rate_limit']) {
|
||||
// 将速率限制 headers 添加到响应中
|
||||
$response->withHeaders($this->getRateLimitHeaders());
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成缓存的 key
|
||||
* @param Request $request
|
||||
* @return null|string
|
||||
*/
|
||||
protected function getCacheKey(Request $request): ?string
|
||||
{
|
||||
$key = $this->config['key'];
|
||||
|
||||
if ($key instanceof \Closure) {
|
||||
$key = $key($this, $request);
|
||||
}
|
||||
|
||||
if ($key === null || $key === false || $this->config['visit_rate'] === null) {
|
||||
// 关闭当前限制
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($key === true) {
|
||||
$key = $request->getRealIp($safe_mode = true);
|
||||
} else {
|
||||
$key = str_replace(
|
||||
[' ', 'controller/action/ip'],
|
||||
['', $request->controller . '/' . $request->action . '/' . $request->getRealIp($safe_mode = true)],
|
||||
strtolower(trim($key))
|
||||
);
|
||||
}
|
||||
return md5($this->config['prefix'] . $key . $this->config['driver_name']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析频率配置项
|
||||
* @param string $rate
|
||||
* @return int[]
|
||||
*/
|
||||
protected function parseRate($rate): array
|
||||
{
|
||||
[$num, $period] = explode("/", $rate);
|
||||
$max_requests = (int)$num;
|
||||
$duration = static::$duration[$period] ?? (int)$period;
|
||||
return [$max_requests, $duration];
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置速率
|
||||
* @param string $rate '10/m' '20/300'
|
||||
* @return $this
|
||||
*/
|
||||
public function setRate(string $rate): self
|
||||
{
|
||||
$this->config['visit_rate'] = $rate;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置缓存驱动
|
||||
* @param CacheInterface $cache
|
||||
* @return $this
|
||||
*/
|
||||
public function setCache(CacheInterface $cache): self
|
||||
{
|
||||
$this->cache = $cache;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置限流算法类
|
||||
* @param string $class_name
|
||||
* @return $this
|
||||
*/
|
||||
public function setDriverClass(string $class_name): self
|
||||
{
|
||||
$this->config['driver_name'] = $class_name;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取速率限制头
|
||||
* @return array
|
||||
*/
|
||||
public function getRateLimitHeaders(): array
|
||||
{
|
||||
return [
|
||||
'X-Rate-Limit-Limit' => $this->max_requests,
|
||||
'X-Rate-Limit-Remaining' => max($this->remaining, 0),
|
||||
'X-Rate-Limit-Reset' => $this->now + $this->expire,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建 Response Exception
|
||||
* @param int $wait_seconds
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
*/
|
||||
public function buildLimitException(int $wait_seconds, Request $request): Response
|
||||
{
|
||||
$visitFail = $this->config['visit_fail_response'] ?? null;
|
||||
if ($visitFail instanceof \Closure) {
|
||||
$response = $visitFail($this, $request, $wait_seconds);
|
||||
if (!$response instanceof Response) {
|
||||
throw new \TypeError(sprintf('The closure must return %s instance', Response::class));
|
||||
}
|
||||
} else {
|
||||
$content = str_replace('__WAIT__', (string)$wait_seconds, $this->config['visit_fail_text']);
|
||||
$response = new Response($this->config['visit_fail_code'], [], $content);
|
||||
}
|
||||
if ($this->config['visit_enable_show_rate_limit']) {
|
||||
$response->withHeaders(['Retry-After' => $wait_seconds]);
|
||||
}
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | 节流设置
|
||||
// +----------------------------------------------------------------------
|
||||
use yzh52521\middleware\Throttle;
|
||||
use yzh52521\middleware\throttle\CounterFixed;
|
||||
// use yzh52521\middleware\throttle\CounterSlider;
|
||||
// use yzh52521\middleware\throttle\TokenBucket;
|
||||
// use yzh52521\middleware\throttle\LeakyBucket;
|
||||
use Webman\Http\{Request, Response};
|
||||
|
||||
return [
|
||||
'enable' => true,
|
||||
// 缓存键前缀,防止键值与其他应用冲突
|
||||
'prefix' => 'throttle_',
|
||||
|
||||
// 缓存的键,true 表示使用来源ip (request->getRealIp(true))
|
||||
'key' => true,
|
||||
|
||||
// 要被限制的请求类型, eg: GET POST PUT DELETE HEAD
|
||||
'visit_method' => ['GET'],
|
||||
|
||||
// 设置访问频率,例如 '10/m' 指的是允许每分钟请求10次。值 null 表示不限制,
|
||||
// eg: null 10/m 20/h 300/d 200/300
|
||||
'visit_rate' => '100/m',
|
||||
|
||||
// 响应体中设置速率限制的头部信息,含义见:https://docs.github.com/en/rest/overview/resources-in-the-rest-api#rate-limiting
|
||||
'visit_enable_show_rate_limit' => true,
|
||||
|
||||
// 访问受限时返回的响应( type: null|callable )
|
||||
'visit_fail_response' => function (Throttle $throttle, Request $request, int $wait_seconds): Response {
|
||||
return response('Too many requests, try again after ' . $wait_seconds . ' seconds.');
|
||||
},
|
||||
|
||||
/*
|
||||
* 设置节流算法,组件提供了四种算法:
|
||||
* - CounterFixed :计数固定窗口
|
||||
* - CounterSlider: 滑动窗口
|
||||
* - TokenBucket : 令牌桶算法
|
||||
* - LeakyBucket : 漏桶限流算法
|
||||
*/
|
||||
'driver_name' => CounterFixed::class,
|
||||
|
||||
// Psr-16通用缓存库规范: https://blog.csdn.net/maquealone/article/details/79651111
|
||||
// Cache驱动必须符合PSR-16缓存库规范,最低实现get/set俩个方法 (且需静态化实现)
|
||||
// static get(string $key, mixed $default=null)
|
||||
// static set(string $key, mixed $value, int $ttl=0);
|
||||
|
||||
//webman默认使用 symfony/cache作为cache组件(https://www.workerman.net/doc/webman/db/cache.html)
|
||||
'cache_drive' => support\Cache::class,
|
||||
|
||||
//使用ThinkCache
|
||||
//'cache_drive' => think\facade\Cache::class,
|
||||
];
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
/**
|
||||
* 节流设置
|
||||
* @copyright The PHP-Tools
|
||||
*/
|
||||
|
||||
namespace app\middleware;
|
||||
|
||||
use Webman\MiddlewareInterface;
|
||||
use Webman\Http\Response;
|
||||
use Webman\Http\Request;
|
||||
|
||||
/**
|
||||
* Class StaticFile
|
||||
* @package app\middleware
|
||||
*/
|
||||
class Throttle implements MiddlewareInterface
|
||||
{
|
||||
public function process(Request $request, callable $next):Response
|
||||
{
|
||||
if ( $route = $request->route ) {
|
||||
$params = $route->param();
|
||||
}
|
||||
return (new \yzh52521\middleware\Throttle())->handle($request, $next, $params??[]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace yzh52521\middleware\throttle;
|
||||
|
||||
|
||||
/**
|
||||
* 计数器固定窗口算法
|
||||
* Class CounterFixed
|
||||
* @package yzh52521\middleware\throttle
|
||||
*/
|
||||
class CounterFixed extends ThrottleAbstract
|
||||
{
|
||||
|
||||
public function allowRequest(string $key, float $micronow, int $max_requests, int $duration, $cache): bool
|
||||
{
|
||||
$cur_requests = (int)$cache::get($key, 0);
|
||||
$now = (int)$micronow;
|
||||
$wait_reset_seconds = $duration - $now % $duration; // 距离下次重置还有n秒时间
|
||||
$this->wait_seconds = $wait_reset_seconds % $duration + 1;
|
||||
$this->cur_requests = $cur_requests;
|
||||
|
||||
if ($cur_requests < $max_requests) { // 允许访问
|
||||
$cache::set($key, $this->cur_requests + 1, $wait_reset_seconds);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace yzh52521\middleware\throttle;
|
||||
|
||||
|
||||
/**
|
||||
* 计数器滑动窗口算法
|
||||
* Class CouterSlider
|
||||
* @package yzh52521\middleware\throttle
|
||||
*/
|
||||
class CounterSlider extends ThrottleAbstract
|
||||
{
|
||||
public function allowRequest(string $key, float $micronow, int $max_requests, int $duration, $cache): bool
|
||||
{
|
||||
$history = $cache::get($key, []);
|
||||
$now = (int)$micronow;
|
||||
// 移除过期的请求的记录
|
||||
$history = array_values(array_filter($history, function ($val) use ($now, $duration) {
|
||||
return $val >= $now - $duration;
|
||||
}));
|
||||
|
||||
$this->cur_requests = count($history);
|
||||
if ($this->cur_requests < $max_requests) {
|
||||
// 允许访问
|
||||
$history[] = $now;
|
||||
$cache::set($key, $history, $duration);
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($history) {
|
||||
$wait_seconds = $duration - ($now - $history[0]) + 1;
|
||||
$this->wait_seconds = max($wait_seconds, 0);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace yzh52521\middleware\throttle;
|
||||
|
||||
|
||||
/**
|
||||
* 漏桶算法
|
||||
* Class LeakyBucket
|
||||
* @package yzh52521\middleware\throttle
|
||||
*/
|
||||
class LeakyBucket extends ThrottleAbstract
|
||||
{
|
||||
|
||||
public function allowRequest(string $key, float $micronow, int $max_requests, int $duration, $cache): bool
|
||||
{
|
||||
if ($max_requests <= 0) return false;
|
||||
|
||||
$last_time = (float)$cache::get($key, 0); // 最近一次请求
|
||||
$rate = (float)$duration / $max_requests; // 平均 n 秒一个请求
|
||||
if ($micronow - $last_time < $rate) {
|
||||
$this->cur_requests = 1;
|
||||
$this->wait_seconds = ceil($rate - ($micronow - $last_time));
|
||||
return false;
|
||||
}
|
||||
|
||||
$cache::set($key, $micronow, $duration);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace yzh52521\middleware\throttle;
|
||||
|
||||
use Psr\SimpleCache\CacheInterface;
|
||||
|
||||
abstract class ThrottleAbstract
|
||||
{
|
||||
/** @var int */
|
||||
protected int $cur_requests = 0; // 当前已有的请求数
|
||||
/** @var int */
|
||||
protected int $wait_seconds = 0; // 距离下次合法请求还有多少秒
|
||||
|
||||
/**
|
||||
* 是否允许访问
|
||||
* @param string $key 缓存键
|
||||
* @param float $micronow 当前时间戳,可含毫秒
|
||||
* @param int $max_requests 允许最大请求数
|
||||
* @param int $duration 限流时长
|
||||
* @param CacheInterface $cache 缓存对象
|
||||
* @return bool
|
||||
*/
|
||||
abstract public function allowRequest(string $key, float $micronow, int $max_requests, int $duration, CacheInterface $cache): bool;
|
||||
|
||||
/**
|
||||
* 计算距离下次合法请求还有多少秒
|
||||
* @return int
|
||||
*/
|
||||
public function getWaitSeconds(): int
|
||||
{
|
||||
return $this->wait_seconds;
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前已有的请求数
|
||||
* @return int
|
||||
*/
|
||||
public function getCurRequests(): int
|
||||
{
|
||||
return $this->cur_requests;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace yzh52521\middleware\throttle;
|
||||
|
||||
|
||||
/**
|
||||
* 令牌桶算法
|
||||
* Class TokenBucket
|
||||
* @package yzh52521\middleware\throttle
|
||||
*/
|
||||
class TokenBucket extends ThrottleAbstract
|
||||
{
|
||||
public function allowRequest(string $key, float $micronow, int $max_requests, int $duration, $cache): bool
|
||||
{
|
||||
if ($max_requests <= 0 || $duration <= 0) return false;
|
||||
|
||||
$assist_key = $key . 'store_num'; // 辅助缓存
|
||||
$rate = (float)$max_requests / $duration; // 平均一秒生成 n 个 token
|
||||
|
||||
$last_time = $cache::get($key, null);
|
||||
$store_num = $cache::get($assist_key, null);
|
||||
|
||||
if ($last_time === null || $store_num === null) { // 首次访问
|
||||
$cache::set($key, $micronow, $duration);
|
||||
$cache::set($assist_key, $max_requests - 1, $duration);
|
||||
return true;
|
||||
}
|
||||
|
||||
$create_num = floor(($micronow - $last_time) * $rate); // 推算生成的 token 数
|
||||
$token_left = (int)min($max_requests, $store_num + $create_num); //当前剩余 tokens 数量
|
||||
|
||||
if ($token_left < 1) {
|
||||
$tmp = (int)ceil($duration / $max_requests);
|
||||
$this->wait_seconds = $tmp - ($micronow - $last_time) % $tmp;
|
||||
return false;
|
||||
}
|
||||
$this->cur_requests = $max_requests - $token_left;
|
||||
$cache::set($key, $micronow, $duration);
|
||||
$cache::set($assist_key, $token_left - 1, $duration);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user