Merge branch 'master' of https://github.com/hyperf/hyperf into metric

This commit is contained in:
reasno 2019-12-29 09:41:01 +08:00
commit 398afea1c9
404 changed files with 38668 additions and 428 deletions

View File

@ -5,13 +5,13 @@ sudo: required
matrix:
include:
- php: 7.2
env: SW_VERSION="4.4.12"
env: SW_VERSION="4.4.14"
- php: 7.3
env: SW_VERSION="4.4.12"
env: SW_VERSION="4.4.14"
- php: 7.4
env: SW_VERSION="4.4.12"
env: SW_VERSION="4.4.14"
- php: master
env: SW_VERSION="4.4.12"
env: SW_VERSION="4.4.14"
allow_failures:
- php: master

View File

@ -5,6 +5,6 @@ tar -xf swoole.tar.gz -C swoole --strip-components=1
rm swoole.tar.gz
cd swoole
phpize
./configure --enable-openssl --enable-mysqlnd
./configure --enable-openssl --enable-mysqlnd --enable-http2
make -j$(nproc)
make install

View File

@ -1,8 +1,48 @@
# v1.1.10 - TBD
# v1.1.13 - TBD
# v1.1.12 - 2019-12-26
## Added
- [#1177](https://github.com/hyperf/hyperf/pull/1177) Added protocol `jsonrpc-tcp-length-check` for `jsonrpc`.
## Fixed
- [#1175](https://github.com/hyperf/hyperf/pull/1175) Fixed `Hyperf\Utils\Collection::random` does not works when the number is null.
- [#1178](https://github.com/hyperf/hyperf/pull/1178) Fixed `Hyperf\Database\Query\Builder::chunkById` does not works when the collection item is array.
- [#1189](https://github.com/hyperf/hyperf/pull/1189) Fixed default operator does not works for `Hyperf\Utils\Collection::operatorForWhere`.
## Optimized
- [#1186](https://github.com/hyperf/hyperf/pull/1186) Automatically added default constructor's configuration, when you forgetton to set it.
# v1.1.11 - 2019-12-19
## Added
- [#849](https://github.com/hyperf/hyperf/pull/849) Added configuration of span tag for `tracer` component.
## Fixed
- [#1142](https://github.com/hyperf/hyperf/pull/1142) Fixed bug that Register::resolveConnection will return null.
- [#1144](https://github.com/hyperf/hyperf/pull/1144) Fixed rate-limit config does not works.
- [#1145](https://github.com/hyperf/hyperf/pull/1145) Fixed error return value for method `CoroutineMemoryDriver::delKey`.
- [#1153](https://github.com/hyperf/hyperf/pull/1153) Fixed validation rule `alpha_num` does not works.
# v1.1.10 - 2019-12-12
## Fixed
- [#1104](https://github.com/hyperf/hyperf/pull/1104) Fixed guzzle will be retried when the response has the correct status code 2xx.
- [#1105](https://github.com/hyperf/hyperf/pull/1105) Fixed Retry Component not restoring pipeline stack before retry attempts.
- [#1106](https://github.com/hyperf/hyperf/pull/1106) Fixed bug that sticky mode will affect the next request.
- [#1119](https://github.com/hyperf/hyperf/pull/1119) Fixed JSONRPC on TCP Server cannot response the expected error response when cannot unpack the data.
- [#1124](https://github.com/hyperf/hyperf/pull/1124) Fixed Session middleware does not store the current url correctly when the path of url end with a slash.
## Changed
- [#1108](https://github.com/hyperf/hyperf/pull/1108) Renamed `Hyperf\Tracer\Middleware\TraceMiddeware` to `Hyperf\Tracer\Middleware\TraceMiddleware`.
- [#1108](https://github.com/hyperf/hyperf/pull/1111) Upgrade the access level of methods and properties of `Hyperf\ServiceGovernance\Listener\ServiceRegisterListener` , for better override it.
# v1.1.9 - 2019-12-05

View File

@ -10,7 +10,7 @@
Hyperf 是基于 `Swoole 4.4+` 实现的高性能、高灵活性的 PHP 协程框架,内置协程服务器及大量常用的组件,性能较传统基于 `PHP-FPM` 的框架有质的提升,提供超高性能的同时,也保持着极其灵活的可扩展性,标准组件均基于 [PSR 标准](https://www.php-fig.org/psr) 实现,基于强大的依赖注入设计,保证了绝大部分组件或类都是 `可替换``可复用` 的。
框架组件库除了常见的协程版的 `MySQL 客户端`、`Redis 客户端`,还为您准备了协程版的 `Eloquent ORM`、`WebSocket 服务端及客户端`、`JSON RPC 服务端及客户端`、`GRPC 服务端及客户端`、`Zipkin/Jaeger (OpenTracing) 客户端`、`Guzzle HTTP 客户端`、`Elasticsearch 客户端`、`Consul 客户端`、`ETCD 客户端`、`AMQP 组件`、`Apollo 配置中心`、`阿里云 ACM 应用配置管理`、`ETCD 配置中心`、`基于令牌桶算法的限流器`、`通用连接池`、`熔断器`、`Swagger 文档生成`、`Swoole Tracker`、`Blade 、 Smarty 、 Twig 、 Plates 和 ThinkTemplate 视图引擎`、`Snowflake 全局ID生成器` 等组件,省去了自己实现对应协程版本的麻烦。
框架组件库除了常见的协程版的 `MySQL 客户端`、`Redis 客户端`,还为您准备了协程版的 `Eloquent ORM`、`WebSocket 服务端及客户端`、`JSON RPC 服务端及客户端`、`GRPC 服务端及客户端`、`Zipkin/Jaeger (OpenTracing) 客户端`、`Guzzle HTTP 客户端`、`Elasticsearch 客户端`、`Consul 客户端`、`ETCD 客户端`、`AMQP 组件`、`Apollo 配置中心`、`阿里云 ACM 应用配置管理`、`ETCD 配置中心`、`基于令牌桶算法的限流器`、`通用连接池`、`熔断器`、`Swagger 文档生成`、`Swoole Tracker`、`视图引擎`、`Snowflake 全局 ID 生成器` 等组件,省去了自己实现对应协程版本的麻烦。
Hyperf 还提供了 `基于 PSR-11 的依赖注入容器`、`注解`、`AOP 面向切面编程`、`基于 PSR-15 的中间件`、`自定义进程`、`基于 PSR-14 的事件管理器`、`Redis/RabbitMQ 消息队列`、`自动模型缓存`、`基于 PSR-16 的缓存`、`Crontab 秒级定时任务`、`Translation 国际化`、`Validation 验证器` 等非常便捷的功能,满足丰富的技术场景和业务场景,开箱即用。
@ -29,7 +29,7 @@ Hyperf 还提供了 `基于 PSR-11 的依赖注入容器`、`注解`、`AOP 面
# 生产可用
我们为组件进行了大量的单元测试以保证逻辑的正确,目前存在 `1120` 个单测共 `3369` 个断言条件,同时维护了高质量的文档,在 Hyperf 正式对外开放(2019年6月20日)之前,便已经过了严酷的生产环境的考验,我们才正式的对外开放该项目,现在已有很多的大型互联网企业都已将 Hyperf 部署到了自己的生产环境上并稳定运行。
我们为组件进行了大量的单元测试以保证逻辑的正确,目前存在 `1120` 个单测共 `3369` 个断言条件,同时维护了高质量的文档,在 Hyperf 正式对外开放(2019年6月20日)之前,便已经过了严酷的生产环境的考验,我们才正式的对外开放该项目,现在已有很多的大型互联网企业将 Hyperf 部署到了自己的生产环境上并稳定运行。
# 运行环境
@ -65,7 +65,7 @@ Hyperf 还提供了 `基于 PSR-11 的依赖注入容器`、`注解`、`AOP 面
<tr>
<td align="left" valign="middle">
<a href="https://1shanghu.com" target="_blank">
<img height="80px" src="https://github.com/hyperf/hyperf/blob/master/doc/zh/imgs/1shanghu.jpg">
<img height="80px" src="https://github.com/hyperf/hyperf/blob/master/doc/zh-cn/imgs/1shanghu.jpg">
</a>
</td>
</tr><tr></tr>

View File

@ -53,7 +53,7 @@ Support this project with your organization or company. Your logo will show up h
<tr>
<td align="left" valign="middle">
<a href="https://1shanghu.com" target="_blank">
<img height="80px" src="https://github.com/hyperf/hyperf/blob/master/doc/zh/imgs/1shanghu.jpg">
<img height="80px" src="https://github.com/hyperf/hyperf/blob/master/doc/zh-cn/imgs/1shanghu.jpg">
</a>
</td>
</tr><tr></tr>

51
bin/translate.php Executable file
View File

@ -0,0 +1,51 @@
<?php
/**
* @notice Install https://github.com/nauxliu/opencc4php extension before use this script.
* Run this PHP script to generate the zh-tw and zh-hk doucmentations via zh-cn.
*/
! defined('BASE_PATH') && define('BASE_PATH', dirname(__DIR__, 1));
require BASE_PATH . '/vendor/autoload.php';
use Symfony\Component\Finder\Finder;
$config = [
'zh-tw' => [
'targetDir' => BASE_PATH . '/doc/zh-tw/',
'rule' => 's2twp.json',
],
'zh-hk' => [
'targetDir' => BASE_PATH . '/doc/zh-hk/',
'rule' => 's2hk.json',
],
];
$finder = new Finder();
$finder->files()->in(BASE_PATH . '/doc/zh-cn');
foreach ($config as $key => $item) {
$od = opencc_open($item['rule']);
foreach ($finder as $fileInfo) {
$targetPath = $item['targetDir'] . $fileInfo->getRelativePath();
$isCreateDir = false;
if (! is_dir($targetPath)) {
mkdir($targetPath, 0777, true);
chmod($targetPath, 0777);
$isCreateDir = true;
}
if (! is_writable($targetPath)) {
echo sprintf('Target path %s is not writable.' . PHP_EOL, $targetPath);
}
if ($fileInfo->getExtension() === 'md') {
$translated = opencc_convert($fileInfo->getContents(), $od);
$translated = str_replace('](zh-cn/', '](' . $key . '/', $translated);
$targetTranslatedPath = $item['targetDir'] . $fileInfo->getRelativePathname();
@file_put_contents($targetTranslatedPath, $translated);
} else {
$targetTranslatedPath = $item['targetDir'] . $fileInfo->getRelativePathname();
@copy($fileInfo->getRealPath(), $targetTranslatedPath);
}
}
opencc_close($od);
}

View File

@ -237,6 +237,7 @@
"HyperfTest\\Pool\\": "src/pool/tests/",
"HyperfTest\\Process\\": "src/process/tests/",
"HyperfTest\\Protocol\\": "src/protocol/tests/",
"HyperfTest\\RateLimit\\": "src/rate-limit/tests/",
"HyperfTest\\Redis\\": "src/redis/tests/",
"HyperfTest\\Retry\\": "src/retry/tests/",
"HyperfTest\\Rpc\\": "src/rpc/tests/",

View File

@ -1,3 +1,5 @@
* 多语言/Translations
* [中文](zh/)
* [简体中文](zh-cn/)
* [繁體中文(臺)](zh-tw/)
* [繁體中文(港)](zh-hk/)
* [English](en/)

View File

@ -0,0 +1,97 @@
# 指南前言
为了帮助开发者更好的为 Hyperf 开发组件,共建生态,我们提供了本指南用于指导开发者进行组件开发,在阅读本指南前,需要您对 Hyperf 的文档进行了 **全面** 的阅读,特别是 [协程](zh/coroutine.md) 和 [依赖注入](zh/di.md) 章节,如果对 Hyperf 的基础组件缺少充分的理解,可能会导致开发时出现错误。
# 组件开发的目的
在传统的 PHP-FPM 架构下的开发,通常在我们需要借助第三方库来解决我们的需求时,都会通过 Composer 来直接引入一个对应的 `库(Library)`,但是在 Hyperf 下,由于 `持久化应用``协程` 这两个特性,导致了应用的生命周期和模式存在一些差异,所以并不是所有的 `库(Library)` 都能在 Hyperf 里直接使用,当然,一些设计优秀的 `库(Library)` 也是可以被直接使用的。通读本指南,便可知道如何甄别一些 `库(Library)` 是否能直接用于项目内,不能的话该进行如何的改动。
# 组件开发准备工作
这里所指的开发准备工作,除了 Hyperf 的基础运行条件外,这里关注的更多是如何更加便捷的组织代码的结构以便于组件的开发工作,注意以下方式可能会由于 *软连接无法跳转的问题* 而并不适用于 Windows for Docker 下的开发环境。
在代码组织上,我们建议在同一个目录下 Clone [hyperf-cloud/hyperf-skeleton](https://github.com/hyperf-cloud/hyperf-skeleton) 项目骨架和 [hyperf-cloud/hyperf](https://github.com/hyperf-cloud/hyperf) 项目组件库两个项目。进行下面的操作并呈以下结构:
```bash
// 安装 skeleton并配置完成
composer create-project hyperf/hyperf-skeleton
// 克隆 hyperf 组件库项目,这里记得要替换 hyperf-cloud 为您的 Github ID也就是克隆您所 Fork 的项目
git clone git@github.com:hyperf-cloud/hyperf.git
```
呈以下结构:
```
.
├── hyperf
│   ├── bin
│   └── src
└── hyperf-skeleton
├── app
├── bin
├── config
├── runtime
├── test
└── vendor
```
这样做的目的是为了让 `hyperf-skeleton` 项目可以直接通过 `path` 来源的形式,让 Composer 直接通过 `hyperf` 文件夹内的项目作为依赖项被加载到 `hyperf-skelton` 项目的 `vendor` 目录中,我们对 `hyperf-skelton` 内的 `composer.json` 文件增加一个 `repositories` 项,如下:
```json
{
"repositories": {
"hyperf": {
"type": "path",
"url": "../hyperf/src/*"
}
"packagist": {
"type": "composer",
"url": "https://mirrors.aliyun.com/composer"
}
}
}
```
然后再在 `hyperf-skeleton` 项目内删除 `composer.lock` 文件和 `vendor` 文件夹,再执行 `composer update` 让依赖重新更新,命令如下:
```bash
cd hyperf-skeleton
rm -rf composer.lock && rm -rf vendor && composer update
```
最终使 `hyperf-skeleton/vendor/hyperf` 文件夹内的项目文件夹全部通过 `软连接(softlink)` 连接到 `hyperf` 文件夹内。我们可以通过 `ls -l` 命令来验证 `软连接(softlink)` 是否已经建立成功:
```bash
cd vendor/hyperf/
ls -l
```
当我们看到类似下面这样的连接关系,即表明 `软连接(softlink)` 建立成功了:
```
cache -> ../../../hyperf/src/cache
command -> ../../../hyperf/src/command
config -> ../../../hyperf/src/config
contract -> ../../../hyperf/src/contract
database -> ../../../hyperf/src/database
db-connection -> ../../../hyperf/src/db-connection
devtool -> ../../../hyperf/src/devtool
di -> ../../../hyperf/src/di
dispatcher -> ../../../hyperf/src/dispatcher
event -> ../../../hyperf/src/event
exception-handler -> ../../../hyperf/src/exception-handler
framework -> ../../../hyperf/src/framework
guzzle -> ../../../hyperf/src/guzzle
http-message -> ../../../hyperf/src/http-message
http-server -> ../../../hyperf/src/http-server
logger -> ../../../hyperf/src/logger
memory -> ../../../hyperf/src/memory
paginator -> ../../../hyperf/src/paginator
pool -> ../../../hyperf/src/pool
process -> ../../../hyperf/src/process
redis -> ../../../hyperf/src/redis
server -> ../../../hyperf/src/server
testing -> ../../../hyperf/src/testing
utils -> ../../../hyperf/src/utils
```
此时,我们便可达到在 IDE 内直接对 `vendor/hyperf` 内的文件进行修改,而修改的却是 `hyperf` 内的代码的目的,这样最终我们便可直接对 `hyperf` 项目内进行 `commit`,然后向主干提交 `Pull Request(PR)` 了。

42
doc/en/controller.md Normal file
View File

@ -0,0 +1,42 @@
# Controller
通过控制器来处理 HTTP 请求,需要通过 `配置文件``注解` 的形式将路由与控制器方法进行绑定,具体请查阅 [路由](zh/route.md) 章节。
对于 `请求(Request)``响应(Response)`Hyperf 提供了 `Hyperf\HttpServer\Contract\RequestInterface``Hyperf\HttpServer\Contract\ResponseInterface` 方便您获取入参和返回数据,关于 [请求](zh/request.md) 与 [响应](zh/response.md) 的详细内容请查阅对应的章节。
## Create a Controller
```php
<?php
declare(strict_types=1);
namespace App\Controller;
use Hyperf\HttpServer\Contract\RequestInterface;
use Hyperf\HttpServer\Contract\ResponseInterface;
class IndexController
{
// 在参数上通过定义 RequestInterface 和 ResponseInterface 来获取相关对象,对象会被依赖注入容器自动注入
public function index(RequestInterface $request, ResponseInterface $response)
{
$target = $request->input('target', 'World');
return 'Hello ' . $target;
}
}
```
> 我们假设该 `Controller` 已经通过了配置文件的形式定义了路由为 `/`,当然您也可以使用注解路由
通过 `cURL` 调用该地址,即可看到返回的内容。
```bash
$ curl http://127.0.0.1:9501/\?target\=Hyperf
Hello Hyperf.
```
## 避免协程间数据混淆
在传统的 PHP-FPM 的框架里,会习惯提供一个 `AbstractController` 或其它命名的 `Controller 抽象父类`,然后定义的 `Controller` 需要基础它用于获取一些请求数据或进行一些返回操作,在 Hyperf 里是 **不能这样做** 的,因为在 Hyperf 内绝大部分的对象包括 `Controller` 都是以 `单例(Singleton)` 形式存在的,这也是为了更好的复用对象,而对于与请求相关的数据在协程下也是需要储存到 `协程上下文(Context)` 内的,所以在编写代码时请务必注意 **不要** 将单个请求相关的数据储存在类属性内,包括非静态属性。
当然如果非要通过类属性来储存请求数据的话,也不是没有办法的,我们可以注意到我们获取 `请求(Request)``响应(Response)` 对象时是通过注入 `Hyperf\HttpServer\Contract\RequestInterface``Hyperf\HttpServer\Contract\ResponseInterface` 来获取的,那对应的对象不也是个单例吗?这里是如何做到协程安全的呢?就 `RequestInterface` 来举例,对应的 `Hyperf\HttpServer\Request` 对象内部在获取 `PSR-7 请求对象` 时,都是从 `协程上下文(Context)` 获取的,所以实际使用的类仅仅是一个代理类,实际调用的都是从 `协程上下文(Context)` 中获取的。

178
doc/en/db/event.md Normal file
View File

@ -0,0 +1,178 @@
# 事件
模型事件实现于 [psr/event-dispatcher](https://github.com/php-fig/event-dispatcher) 接口。
## 自定义监听器
得益于 [hyperf/event](https://github.com/hyperf-cloud/event) 组件的支撑,用户可以很方便的对以下事件进行监听。
例如 `QueryExecuted` , `StatementPrepared` , `TransactionBeginning` , `TransactionCommitted` , `TransactionRolledBack`
接下来我们就实现一个记录SQL的监听器来说一下怎么使用。
首先我们定义好 `DbQueryExecutedListener` ,实现 `Hyperf\Event\Contract\ListenerInterface` 接口并对类定义 `Hyperf\Event\Annotation\Listener` 注解,这样 Hyperf 就会自动把该监听器注册到事件调度器中,无需任何手动配置,示例代码如下:
```php
<?php
declare(strict_types=1);
namespace App\Listeners;
use Hyperf\Database\Events\QueryExecuted;
use Hyperf\Event\Annotation\Listener;
use Hyperf\Event\Contract\ListenerInterface;
use Hyperf\Logger\LoggerFactory;
use Hyperf\Utils\Arr;
use Hyperf\Utils\Str;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
/**
* @Listener
*/
class DbQueryExecutedListener implements ListenerInterface
{
/**
* @var LoggerInterface
*/
private $logger;
public function __construct(LoggerFactory $loggerFactory)
{
$this->logger = $loggerFactory->get('sql');
}
public function listen(): array
{
return [
QueryExecuted::class,
];
}
/**
* @param QueryExecuted $event
*/
public function process(object $event)
{
if ($event instanceof QueryExecuted) {
$sql = $event->sql;
if (! Arr::isAssoc($event->bindings)) {
foreach ($event->bindings as $key => $value) {
$sql = Str::replaceFirst('?', "'{$value}'", $sql);
}
}
$this->logger->info(sprintf('[%s] %s', $event->time, $sql));
}
}
}
```
## 模型事件
模型事件与 `EloquentORM` 不太一致,`EloquentORM` 使用 `Observer` 监听模型事件。`Hyperf` 直接使用 `钩子函数` 来处理对应的事件。如果你还是喜欢 `Observer` 的方式,可以通过 `事件监听`,自己实现。当然,你也可以在 [issue#2](https://github.com/hyperf-cloud/hyperf/issues/2) 下面告诉我们。
### 钩子函数
| 事件名 | 触发实际 | 是否阻断 | 备注 |
|:------------:|:----------------:|:--------:|:-------------------------- --:|
| booting | 模型首次加载前 | 否 | 进程生命周期中只会触发一次 |
| booted | 模型首次加载后 | 否 | 进程生命周期中只会触发一次 |
| retrieved | 填充数据后 | 否 | 每当模型从DB或缓存查询出来后触发 |
| creating | 数据创建时 | 是 | |
| created | 数据创建后 | 否 | |
| updating | 数据更新时 | 是 | |
| updated | 数据更新后 | 否 | |
| saving | 数据创建或更新时 | 是 | |
| saved | 数据创建或更新后 | 否 | |
| restoring | 软删除数据回复时 | 是 | |
| restored | 软删除数据回复后 | 否 | |
| deleting | 数据删除时 | 是 | |
| deleted | 数据删除后 | 否 | |
| forceDeleted | 数据强制删除后 | 否 | |
针对某个模型的事件使用十分简单,只需要在模型中增加对应的方法即可。例如下方保存数据时,触发 `saving` 事件,主动覆写 `created_at` 字段。
```php
<?php
declare(strict_types=1);
namespace App\Models;
use Hyperf\Database\Model\Events\Saving;
/**
* @property $id
* @property $name
* @property $gender
* @property $created_at
* @property $updated_at
*/
class User extends Model
{
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'user';
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = ['id', 'name', 'gender', 'created_at', 'updated_at'];
protected $casts = ['id' => 'integer', 'gender' => 'integer'];
public function saving(Saving $event)
{
$this->setCreatedAt('2019-01-01');
}
}
```
### 事件监听
当你需要监听所有的模型事件时,可以很方便的自定义对应的 `Listener`,比如下方模型缓存的监听器,当模型修改和删除后,会删除对应缓存。
```php
<?php
declare(strict_types=1);
namespace Hyperf\ModelCache\Listener;
use Hyperf\Database\Model\Events\Deleted;
use Hyperf\Database\Model\Events\Event;
use Hyperf\Database\Model\Events\Saved;
use Hyperf\Event\Annotation\Listener;
use Hyperf\Event\Contract\ListenerInterface;
use Hyperf\ModelCache\CacheableInterface;
/**
* @Listener
*/
class DeleteCacheListener implements ListenerInterface
{
public function listen(): array
{
return [
Deleted::class,
Saved::class,
];
}
public function process(object $event)
{
if ($event instanceof Event) {
$model = $event->getModel();
if ($model instanceof CacheableInterface) {
$model->deleteCache();
}
}
}
}
```

128
doc/en/db/model-cache.md Normal file
View File

@ -0,0 +1,128 @@
# 模型缓存
在高频场景下我们会频繁的查询数据库虽然有主键加持但也会影响到数据库性能。这种kv查询方式我们可以很方便的使用 `模型缓存` 来减缓数据库压力。本模块实现了自动缓存,删除和修改模型时,自动删除缓存。累加、减操作时,直接操作缓存进行对应累加、减。
> 模型缓存暂支持 `Redis`存储,其他存储引擎会慢慢补充。
## Installation
```
composer require hyperf/model-cache
```
## 配置
模型缓存的配置在 `databases` 中。示例如下
| 配置 | 类型 | 默认值 | 备注 |
|:---------------:|:------:|:-----------------------------------------------------:|:-----------------------------------:|
| handler | string | Hyperf\DbConnection\Cache\Handler\RedisHandler::class | 无 |
| cache_key | string | `mc:%s:m:%s:%s:%s` | `mc:缓存前缀:m:表名:主键KEY:主键值` |
| prefix | string | db connection name | 缓存前缀 |
| ttl | int | 3600 | 超时时间 |
| empty_model_ttl | int | 60 | 查询不到数据时的超时时间 |
| load_script | bool | true | Redis引擎下 是否使用evalSha代替eval |
```php
<?php
return [
'default' => [
'driver' => env('DB_DRIVER', 'mysql'),
'host' => env('DB_HOST', 'localhost'),
'database' => env('DB_DATABASE', 'hyperf'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'charset' => env('DB_CHARSET', 'utf8'),
'collation' => env('DB_COLLATION', 'utf8_unicode_ci'),
'prefix' => env('DB_PREFIX', ''),
'pool' => [
'min_connections' => 1,
'max_connections' => 10,
'connect_timeout' => 10.0,
'wait_timeout' => 3.0,
'heartbeat' => -1,
'max_idle_time' => (float)env('DB_MAX_IDLE_TIME', 60),
],
'cache' => [
'handler' => \Hyperf\DbConnection\Cache\Handler\RedisHandler::class,
'cache_key' => 'mc:%s:m:%s:%s:%s',
'prefix' => 'default',
'ttl' => 3600 * 24,
'empty_model_ttl' => 3600,
'load_script' => true,
]
],
];
```
## 使用
模型缓存的使用十分简单只需要在对应Model中实现 `Hyperf\ModelCache\CacheableInterface` 接口,当然,框架已经提供了对应实现,只需要引入 `Hyperf\ModelCache\Cacheable` Trait 即可。
```php
<?php
declare(strict_types=1);
namespace App\Models;
use Hyperf\DbConnection\Model\Model;
use Hyperf\ModelCache\Cacheable;
use Hyperf\ModelCache\CacheableInterface;
/**
* @property $id
* @property $name
* @property $gender
* @property $created_at
* @property $updated_at
*/
class User extends Model implements CacheableInterface
{
use Cacheable;
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'user';
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = ['id', 'name', 'gender', 'created_at', 'updated_at'];
protected $casts = ['id' => 'integer', 'gender' => 'integer'];
}
// 查询单个缓存
$model = User::findFromCache($id);
// 批量查询缓存,返回 Hyperf\Database\Model\Collection
$models = User::findManyFromCache($ids);
```
对应Redis数据如下其中 `HF-DATA:DEFAULT` 作为占位符存在于 `HASH` 中,*所以用户不要使用 `HF-DATA` 作为数据库字段*。
```
127.0.0.1:6379> hgetall "mc:default:m:user:id:1"
1) "id"
2) "1"
3) "name"
4) "Hyperf"
5) "gender"
6) "1"
7) "created_at"
8) "2018-01-01 00:00:00"
9) "updated_at"
10) "2018-01-01 00:00:00"
11) "HF-DATA"
12) "DEFAULT"
```
另外一点就是,缓存更新机制,框架内实现了对应的 `Hyperf\ModelCache\Listener\DeleteCacheListener` 监听器,每当数据修改,会主动删除缓存。
如果用户不想由框架来删除缓存,可以主动覆写 `deleteCache` 方法,然后由自己实现对应监听即可。

428
doc/en/db/model.md Normal file
View File

@ -0,0 +1,428 @@
# 模型
模型组件衍生于 [Eloquent ORM](https://laravel.com/docs/5.8/eloquent),相关操作均可参考 Eloquent ORM 的文档。
## 创建模型
Hyperf 提供了创建模型的命令,您可以很方便的根据数据表创建对应模型。命令通过 `AST` 生成模型,所以当您增加了某些方法后,也可以使用脚本方便的重置模型。
```
$ php bin/hyperf.php db:model table_name
```
创建的模型如下
```php
<?php
declare(strict_types=1);
namespace App\Models;
use Hyperf\DbConnection\Model\Model;
/**
* @property $id
* @property $name
* @property $gender
* @property $created_at
* @property $updated_at
*/
class User extends Model
{
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'user';
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = ['id', 'name', 'gender', 'created_at', 'updated_at'];
/**
* The attributes that should be cast to native types.
*
* @var array
*/
protected $casts = ['id' => 'integer', 'gender' => 'integer'];
}
```
## 模型参数
| 参数 | 类型 | 默认值 | 备注 |
|:----------:|:------:|:-------:|:--------------------:|
| connection | string | default | 数据库连接 |
| table | string | 无 | 数据表名称 |
| primaryKey | string | id | 模型主键 |
| keyType | string | int | 主键类型 |
| fillable | array | [] | 允许被批量复制的属性 |
| casts | string | 无 | 数据格式化配置 |
| timestamps | bool | true | 是否自动维护时间戳 |
### 数据表名称
如果我们没有指定模型对应的 table它将使用类的复数形式「蛇形命名」来作为表名。因此在这种情况下Hyperf 将假设 User 模型存储的是 users 数据表中的数据。你可以通过在模型上定义 table 属性来指定自定义数据表:
```php
<?php
declare(strict_types=1);
namespace App\Models;
use Hyperf\DbConnection\Model\Model;
class User extends Model
{
protected $table = 'user';
}
```
### 主键
Hyperf 会假设每个数据表都有一个名为 id 的主键列。你可以定义一个受保护的 $primaryKey 属性来重写约定。
此外Hyperf 假设主键是一个自增的整数值,这意味着默认情况下主键会自动转换为 int 类型。如果您希望使用非递增或非数字的主键则需要设置公共的 $incrementing 属性设置为 false。如果你的主键不是一个整数你需要将模型上受保护的 $keyType 属性设置为 string。
### 时间戳
默认情况下Hyperf 预期你的数据表中存在 `created_at``updated_at` 。如果你不想让 Hyperf 自动管理这两个列, 请将模型中的 `$timestamps` 属性设置为 `false`
```php
<?php
declare(strict_types=1);
namespace App\Models;
use Hyperf\DbConnection\Model\Model;
class User extends Model
{
public $timestamps = false;
}
```
如果需要自定义时间戳的格式,在你的模型中设置 `$dateFormat` 属性。这个属性决定日期属性在数据库的存储方式,以及模型序列化为数组或者 JSON 的格式:
```php
<?php
declare(strict_types=1);
namespace App\Models;
use Hyperf\DbConnection\Model\Model;
class User extends Model
{
protected $dateFormat = 'U';
}
```
如果您需要不希望保持 `datetime` 格式的储存,或者希望对时间做进一步的处理,您可以通过在模型内重写 `fromDateTime($value)` 方法实现。
如果你需要自定义存储时间戳的字段名,可以在模型中设置 `CREATED_AT``UPDATED_AT` 常量的值来实现,其中一个为 `null`,则表明不希望 ORM 处理该字段:
```php
<?php
declare(strict_types=1);
namespace App\Models;
use Hyperf\DbConnection\Model\Model;
class User extends Model
{
const CREATED_AT = 'creation_date';
const UPDATED_AT = 'last_update';
}
```
### 数据库连接
默认情况下Hyperf 模型将使用你的应用程序配置的默认数据库连接 `default`。如果你想为模型指定一个不同的连接,设置 `$connection` 属性:当然,`connection-name` 作为 `key`,必须在 `databases.php` 配置文件中存在。
```php
<?php
declare(strict_types=1);
namespace App\Models;
use Hyperf\DbConnection\Model\Model;
class User extends Model
{
protected $connection = 'connection-name';
}
```
### 默认属性值
如果要为模型的某些属性定义默认值,可以在模型上定义 `$attributes` 属性:
```php
<?php
declare(strict_types=1);
namespace App\Models;
use Hyperf\DbConnection\Model\Model;
class User extends Model
{
protected $attributes = [
'delayed' => false,
];
}
```
## 模型查询
```php
use App\Models\User;
/** @var User $user */
$user = User::query()->where('id', 1)->first();
$user->name = 'Hyperf';
$user->save();
```
### 重新加载模型
你可以使用 `fresh``refresh` 方法重新加载模型。 `fresh` 方法会重新从数据库中检索模型。现有的模型实例不受影响:
```php
use App\Models\User;
/** @var User $user */
$user = User::query()->find(1);
$freshUser = $user->fresh();
```
`refresh` 方法使用数据库中的新数据重新赋值现有模型。此外,已经加载的关系会被重新加载:
```php
use App\Models\User;
/** @var User $user */
$user = User::query()->where('name','Hyperf')->first();
$user->name = 'Hyperf2';
$user->refresh();
echo $user->name; // Hyperf
```
### 集合
对于模型中的 `all``get` 方法可以查询多个结果,返回一个 `Hyperf\Database\Model\Collection` 实例。 `Collection` 类提供了很多辅助函数来处理查询结果:
```php
$users = $users->reject(function ($user) {
// 排除所有已删除的用户
return $user->deleted;
});
```
### 检索单个模型
除了从指定的数据表检索所有记录外,你可以使用 `find``first` 方法来检索单条记录。这些方法返回单个模型实例,而不是返回模型集合:
```php
use App\Models\User;
$user = User::query()->where('id', 1)->first();
$user = User::query()->find(1);
```
### 检索多个模型
当然 `find` 的方法不止支持单个模型。
```php
use App\Models\User;
$users = User::query()->find([1, 2, 3]);
```
### 聚合函数
你还可以使用 查询构造器 提供的 `count``sum`, `max`, 和其他的聚合函数。这些方法只会返回适当的标量值而不是一个模型实例:
```php
use App\Models\User;
$count = User::query()->where('gender', 1)->count();
```
## 插入&更新模型
### 插入
要往数据库新增一条记录,先创建新模型实例,给实例设置属性,然后调用 `save` 方法:
```php
use App\Models\User;
/** @var User $user */
$user = new User();
$user->name = 'Hi Hyperf';
$user->save();
```
在这个示例中,我们赋值给了 `App\Models\User` 模型实例的 `name` 属性。当调用 `save` 方法时,将会插入一条新记录。 `created_at``updated_at` 时间戳将会自动设置,不需要手动赋值。
### 更新
`save` 方法也可以用来更新数据库已经存在的模型。更新模型,你需要先检索出来,设置要更新的属性,然后调用 `save` 方法。同样, `updated_at` 时间戳会自动更新,所以也不需要手动赋值:
```php
use App\Models\User;
/** @var User $user */
$user = User::query()->find(1);
$user->name = 'Hi Hyperf';
$user->save();
```
### 批量更新
也可以更新匹配查询条件的多个模型。在这个示例中,所有的 `gender` 为1的用户修改 `gender_show` 为 男性:
```php
use App\Models\User;
User::query()->where('gender', 1)->update(['gender_show' => '男性']);
```
> 批量更新时, 更新的模型不会触发 saved 和 updated 事件。因为在批量更新时,从不会去检索模型。
### 批量赋值
你也可以使用 `create` 方法来保存新模型,此方法会返回模型实例。不过,在使用之前,你需要在模型上指定 `fillable``guarded` 属性,因为所有的模型都默认不可进行批量赋值。
当用户通过 HTTP 请求传入一个意外的参数,并且该参数更改了数据库中你不需要更改的字段时。比如:恶意用户可能会通过 HTTP 请求传入 `is_admin` 参数,然后将其传给 `create` 方法,此操作能让用户将自己升级成管理员。
所以,在开始之前,你应该定义好模型上的哪些属性是可以被批量赋值的。你可以通过模型上的 `$fillable` 属性来实现。 例如:让 `User` 模型的 `name` 属性可以被批量赋值:
```php
<?php
declare(strict_types=1);
namespace App\Models;
use Hyperf\DbConnection\Model\Model;
class User extends Model
{
protected $fillable = ['name'];
}
```
一旦我们设置好了可以批量赋值的属性,就可以通过 `create` 方法插入新数据到数据库中了。 `create` 方法将返回保存的模型实例:
```php
use App\Models\User;
$user = User::create(['name' => 'Hyperf']);
```
如果你已经有一个模型实例,你可以传递一个数组给 fill 方法来赋值:
```php
$user->fill(['name' => 'Hyperf']);
```
### 保护属性
`$fillable` 可以看作批量赋值的「白名单」, 你也可以使用 `$guarded` 属性来实现。 `$guarded` 属性包含的是不允许批量赋值的数组。也就是说, `$guarded` 从功能上将更像是一个「黑名单」。注意:你只能使用 `$fillable``$guarded` 二者中的一个,不可同时使用。下面这个例子中,除了 `gender_show` 属性,其他的属性都可以批量赋值:
```php
<?php
declare(strict_types=1);
namespace App\Models;
use Hyperf\DbConnection\Model\Model;
class User extends Model
{
protected $guarded = ['gender_show'];
}
```
### 删除模型
可以在模型实例上调用 `delete` 方法来删除实例:
```php
use App\Models\User;
$user = User::query()->find(1);
$user->delete();
```
### 通过查询删除模型
您可通过在查询上调用 `delete` 方法来删除模型数据,在这个例子中,我们将删除所有 `gender``1` 的用户。与批量更新一样,批量删除不会为删除的模型启动任何模型事件:
```php
use App\Models\User;
// 注意使用 delete 方法时必须建立在某些查询条件基础之上才能安全删除数据,不存在 where 条件,会导致删除整个数据表
User::query()->where('gender', 1)->delete();
```
### 通过主键直接删除数据
在上面的例子中,在调用 `delete` 之前需要先去数据库中查找对应的模型。事实上,如果你知道了模型的主键,您可以直接通过 `destroy` 静态方法来删除模型数据,而不用先去数据库中查找。 `destroy` 方法除了接受单个主键作为参数之外,还接受多个主键,或者使用数组,集合来保存多个主键:
```php
use App\Models\User;
User::destroy(1);
User::destroy([1,2,3]);
```
### 软删除
除了真实删除数据库记录,`Hyperf` 也可以「软删除」模型。软删除的模型并不是真的从数据库中删除了。事实上,是在模型上设置了 `deleted_at` 属性并将其值写入数据库。如果 `deleted_at` 值非空,代表这个模型已被软删除。如果要开启模型软删除功能,你需要在模型上使用 `Hyperf\Database\Model\SoftDeletes` trait
> `SoftDeletes` trait 会自动将 `deleted_at` 属性转换成 `DateTime / Carbon` 实例
```php
<?php
namespace App;
use Hyperf\Database\Model\Model;
use Hyperf\Database\Model\SoftDeletes;
class Flight extends Model
{
use SoftDeletes;
}
```

View File

@ -1,6 +1,6 @@
# 查询分页
在使用 [hyperf/database](https://github.com/hyperf/database) 来查询数据时,可以很方便的通过与 [hyperf/paginator](https://github.com/hyperf/paginator) 组件配合便捷地对查询结果进行分页。
在使用 [hyperf/database](https://github.com/hyperf-cloud/database) 来查询数据时,可以很方便的通过与 [hyperf/paginator](https://github.com/hyperf-cloud/paginator) 组件配合便捷地对查询结果进行分页。
# 使用方法

681
doc/en/db/querybuilder.md Normal file
View File

@ -0,0 +1,681 @@
# 查询构造器
## 简介
Hyperf 的数据库查询构造器为创建和运行数据库查询提供了一个方便的接口。它可用于执行应用程序中大部分数据库操作,且可在所有支持的数据库系统上运行。
Hyperf 的查询构造器使用 PDO 参数绑定来保护您的应用程序免受 SQL 注入攻击。因此没有必要清理作为绑定传递的字符串。
这里只提供一部分常用的教程,具体教程可以到 Laravel 官网查看。
[Laravel Query Builder](https://laravel.com/docs/5.8/queries)
## 获取结果
```php
use Hyperf\DbConnection\Db;
$users = Db::select('SELECT * FROM user;');
$users = Db::table('user')->get();
$users = Db::table('user')->select('name', 'gender as user_gender')->get();
```
`Db::select()` 方法会返回一个array`get` 方法会返回 `Hyperf\Utils\Collection`。其中元素是 `stdClass`,所以可以通过以下代码返回各个元素的数据
```php
<?php
foreach ($users as $user) {
echo $user->name;
}
```
### 将结果转为数组格式
在某些场景下,您可能会希望查询出来的结果内采用 `数组(Array)` 而不是 `stdClass` 对象结构时,而 `Eloquent` 又去除了通过配置的形式配置默认的 `FetchMode`,那么此时可以通过监听器来监听 `Hyperf\Database\Events\StatementPrepared` 事件来变更该配置:
```php
<?php
declare(strict_types=1);
namespace App\Listener;
use Hyperf\Database\Events\StatementPrepared;
use Hyperf\Event\Annotation\Listener;
use Hyperf\Event\Contract\ListenerInterface;
use PDO;
/**
* @Listener
*/
class FetchModeListener implements ListenerInterface
{
public function listen(): array
{
return [
StatementPrepared::class,
];
}
public function process(object $event)
{
if ($event instanceof StatementPrepared) {
$event->statement->setFetchMode(PDO::FETCH_ASSOC);
}
}
}
```
### 获取一列的值
如果你想获取包含单列值的集合,则可以使用 `pluck` 方法。在下面的例子中,我们将获取角色表中标题的集合:
```php
<?php
use Hyperf\DbConnection\Db;
$names = Db::table('user')->pluck('name');
foreach ($names as $name) {
echo $names;
}
```
你还可以在返回的集合中指定字段的自定义键值:
```php
<?php
use Hyperf\DbConnection\Db;
$roles = Db::table('roles')->pluck('title', 'name');
foreach ($roles as $name => $title) {
echo $title;
}
```
### 分块结果
如果你需要处理上千条数据库记录,你可以考虑使用 `chunk` 方法。该方法一次获取结果集的一小块,并将其传递给 `闭包` 函数进行处理。该方法在 `Command` 编写数千条处理数据的时候非常有用。例如,我们可以将全部 user 表数据切割成一次处理 100 条记录的一小块:
```php
<?php
use Hyperf\DbConnection\Db;
Db::table('user')->orderBy('id')->chunk(100, function ($users) {
foreach ($users as $user) {
//
}
});
```
你可以通过在 闭包 中返回 `false` 来终止继续获取分块结果:
```php
use Hyperf\DbConnection\Db;
Db::table('user')->orderBy('id')->chunk(100, function ($users) {
return false;
});
```
如果要在分块结果时更新数据库记录,则块结果可能会和预计的返回结果不一致。 因此,在分块更新记录时,最好使用 chunkById 方法。 此方法将根据记录的主键自动对结果进行分页:
```php
use Hyperf\DbConnection\Db;
Db::table('user')->where('gender', 1)->chunkById(100, function ($users) {
foreach ($users as $user) {
Db::table('user')
->where('id', $user->id)
->update(['update_time' => time()]);
}
});
```
> 在块的回调里面更新或删除记录时,对主键或外键的任何更改都可能影响块查询。 这可能会导致记录没有包含在分块结果中。
### 聚合查询
框架还提供了聚合类方法,例如 `count`, `max`, `min`, `avg`, `sum`
```php
use Hyperf\DbConnection\Db;
$count = Db::table('user')->count();
```
#### 判断记录是否存在
除了通过 `count` 方法可以确定查询条件的结果是否存在之外,还可以使用 `exists``doesntExist` 方法:
```php
return Db::table('orders')->where('finalized', 1)->exists();
return Db::table('orders')->where('finalized', 1)->doesntExist();
```
## 查询
### 指定一个 Select 语句
当然你可能并不总是希望从数据库表中获取所有列。使用 select 方法,你可以自定义一个 select 查询语句来查询指定的字段:
```php
$users = Db::table('user')->select('name', 'email as user_email')->get();
```
`distinct` 方法会强制让查询返回的结果不重复:
```php
$users = Db::table('user')->distinct()->get();
```
如果你已经有了一个查询构造器实例,并且希望在现有的查询语句中加入一个字段,那么你可以使用 addSelect 方法:
```php
$query = Db::table('users')->select('name');
$users = $query->addSelect('age')->get();
```
## 原始表达式
有时你需要在查询中使用原始表达式,例如实现 `COUNT(0) AS count`,这就需要用到`raw`方法。
```php
use Hyperf\DbConnection\Db;
$res = Db::table('user')->select('gender', Db::raw('COUNT(0) AS `count`'))->groupBy('gender')->get();
```
### 原生方法
可以使用以下方法代替 `Db::raw`,将原生表达式插入查询的各个部分。
`selectRaw` 方法可以代替 `select(Db::raw(...))`。该方法的第二个参数是可选项,值是一个绑定参数的数组:
```php
$orders = Db::table('order')
->selectRaw('price * ? as price_with_tax', [1.0825])
->get();
```
`whereRaw``orWhereRaw` 方法将原生的 `where` 注入到你的查询中。这两个方法的第二个参数还是可选项,值还是绑定参数的数组:
```php
$orders = Db::table('order')
->whereRaw('price > IF(state = "TX", ?, 100)', [200])
->get();
```
`havingRaw``orHavingRaw` 方法可以用于将原生字符串设置为 `having` 语句的值:
```php
$orders = Db::table('order')
->select('department', Db::raw('SUM(price) as total_sales'))
->groupBy('department')
->havingRaw('SUM(price) > ?', [2500])
->get();
```
`orderByRaw` 方法可用于将原生字符串设置为 `order by` 子句的值:
```php
$orders = Db::table('order')
->orderByRaw('updated_at - created_at DESC')
->get();
```
## 表连接
### Inner Join Clause
查询构造器也可以编写 `join` 方法。若要执行基本的`「内链接」`,你可以在查询构造器实例上使用 `join` 方法。传递给 `join` 方法的第一个参数是你需要连接的表的名称,而其他参数则使用指定连接的字段约束。你还可以在单个查询中连接多个数据表:
```php
$users = Db::table('users')
->join('contacts', 'users.id', '=', 'contacts.user_id')
->join('orders', 'users.id', '=', 'orders.user_id')
->select('users.*', 'contacts.phone', 'orders.price')
->get();
```
### Left Join
如果你想使用`「左连接」`或者`「右连接」`代替`「内连接」`,可以使用 `leftJoin` 或者 `rightJoin` 方法。这两个方法与 `join` 方法用法相同:
```php
$users = Db::table('users')
->leftJoin('posts', 'users.id', '=', 'posts.user_id')
->get();
$users = Db::table('users')
->rightJoin('posts', 'users.id', '=', 'posts.user_id')
->get();
```
### Cross Join 语句
使用 `crossJoin` 方法和你想要连接的表名做`「交叉连接」`。交叉连接在第一个表和被连接的表之间会生成笛卡尔积:
```php
$users = Db::table('sizes')
->crossJoin('colours')
->get();
```
### 高级 Join 语句
你可以指定更高级的 `join` 语句。比如传递一个 `闭包` 作为 `join` 方法的第二个参数。此 `闭包` 接收一个 `JoinClause` 对象,从而指定 `join` 语句中指定的约束:
```php
Db::table('users')
->join('contacts', function ($join) {
$join->on('users.id', '=', 'contacts.user_id')->orOn(...);
})
->get();
```
如果你想要在连接上使用`「where」`风格的语句,你可以在连接上使用 `where``orWhere` 方法。这些方法会将列和值进行比较,而不是列和列进行比较:
```php
Db::table('users')
->join('contacts', function ($join) {
$join->on('users.id', '=', 'contacts.user_id')
->where('contacts.user_id', '>', 5);
})
->get();
```
### 子连接查询
你可以使用 `joinSub``leftJoinSub``rightJoinSub` 方法关联一个查询作为子查询。他们每一种方法都会接收三个参数:子查询,表别名和定义关联字段的闭包:
```php
$latestPosts = Db::table('posts')
->select('user_id', Db::raw('MAX(created_at) as last_post_created_at'))
->where('is_published', true)
->groupBy('user_id');
$users = Db::table('users')
->joinSub($latestPosts, 'latest_posts', function($join) {
$join->on('users.id', '=', 'latest_posts.user_id');
})->get();
```
## 联合查询
查询构造器还提供了将两个查询 「联合」 的快捷方式。比如,你可以先创建一个查询,然后使用 `union` 方法将其和第二个查询进行联合:
```php
$first = Db::table('users')->whereNull('first_name');
$users = Db::table('users')
->whereNull('last_name')
->union($first)
->get();
```
## Where 语句
### 简单的 Where 语句
在构造 `where` 查询实例的中,你可以使用 `where` 方法。调用 `where` 最基本的方式是需要传递三个参数:第一个参数是列名,第二个参数是任意一个数据库系统支持的运算符,第三个是该列要比较的值。
例如,下面是一个要验证 gender 字段的值等于 1 的查询:
```php
$users = Db::table('user')->where('gender', '=', 1)->get();
```
为了方便,如果你只是简单比较列值和给定数值是否相等,可以将数值直接作为 `where` 方法的第二个参数:
```php
$users = Db::table('user')->where('gender', 1)->get();
```
当然,你也可以使用其他的运算符来编写 where 子句:
```php
$users = Db::table('users')->where('gender', '>=', 0)->get();
$users = Db::table('users')->where('gender', '<>', 1)->get();
$users = Db::table('users')->where('name', 'like', 'T%')->get();
```
你还可以传递条件数组到 where 函数中:
```php
$users = Db::table('user')->where([
['status', '=', '1'],
['gender', '=', '1'],
])->get();
```
### Or 语句
你可以一起链式调用 `where` 约束,也可以在查询中添加 `or` 字句。 `orWhere` 方法和 `where` 方法接收的参数一样:
```php
$users = Db::table('user')
->where('gender', 1)
->orWhere('name', 'John')
->get();
```
### 其他 Where 语句
#### whereBetween
`whereBetween` 方法验证字段值是否在给定的两个值之间:
```php
$users = Db::table('users')->whereBetween('votes', [1, 100])->get();
```
#### whereNotBetween
`whereNotBetween` 方法验证字段值是否在给定的两个值之外:
```php
$users = Db::table('users')->whereNotBetween('votes', [1, 100])->get();
```
#### whereIn / whereNotIn
`whereIn` 方法验证字段的值必须存在指定的数组里:
```php
$users = Db::table('users')->whereIn('id', [1, 2, 3])->get();
```
`whereNotIn` 方法验证字段的值必须不存在于指定的数组里:
```php
$users = Db::table('users')->whereNotIn('id', [1, 2, 3])->get();
```
### 参数分组
有时候你需要创建更高级的 `where` 子句,例如`「where exists」`或者嵌套的参数分组。查询构造器也能够处理这些。下面,让我们看一个在括号中进行分组约束的例子:
```php
Db::table('users')->where('name', '=', 'John')
->where(function ($query) {
$query->where('votes', '>', 100)
->orWhere('title', '=', 'Admin');
})
->get();
```
你可以看到,通过一个 `Closure` 写入 `where` 方法构建一个查询构造器 来约束一个分组。这个 `Closure` 接收一个查询实例,你可以使用这个实例来设置应该包含的约束。上面的例子将生成以下 SQL:
```sql
select * from users where name = 'John' and (votes > 100 or title = 'Admin')
```
> 你应该用 orWhere 调用这个分组,以避免应用全局作用出现意外.
#### Where Exists 语句
`whereExists` 方法允许你使用 `where exists SQL` 语句。 `whereExists` 方法接收一个 `Closure` 参数,该 `whereExists` 方法接受一个 `Closure` 参数,该闭包获取一个查询构建器实例从而允许你定义放置在 `exists` 字句中查询:
```php
Db::table('users')->whereExists(function ($query) {
$query->select(Db::raw(1))
->from('orders')
->whereRaw('orders.user_id = users.id');
})
->get();
```
上述查询将产生如下的 SQL 语句:
```sql
select * from users
where exists (
select 1 from orders where orders.user_id = users.id
)
```
#### JSON Where 语句
`Hyperf` 也支持查询 `JSON` 类型的字段(仅在对 `JSON` 类型支持的数据库上)。
```php
$users = Db::table('users')
->where('options->language', 'en')
->get();
$users = Db::table('users')
->where('preferences->dining->meal', 'salad')
->get();
```
你也可以使用 `whereJsonContains` 来查询 `JSON` 数组:
```php
$users = Db::table('users')
->whereJsonContains('options->languages', 'en')
->get();
```
你可以使用 `whereJsonLength` 来查询 `JSON` 数组的长度:
```php
$users = Db::table('users')
->whereJsonLength('options->languages', 0)
->get();
$users = Db::table('users')
->whereJsonLength('options->languages', '>', 1)
->get();
```
## Ordering, Grouping, Limit, & Offset
### orderBy
`orderBy` 方法允许你通过给定字段对结果集进行排序。 `orderBy` 的第一个参数应该是你希望排序的字段,第二个参数控制排序的方向,可以是 `asc``desc`
```php
$users = Db::table('users')
->orderBy('name', 'desc')
->get();
```
### latest / oldest
`latest``oldest` 方法可以使你轻松地通过日期排序。它默认使用 `created_at` 列作为排序依据。当然,你也可以传递自定义的列名:
```php
$user = Db::table('users')->latest()->first();
```
### inRandomOrder
`inRandomOrder` 方法被用来将结果随机排序。例如,你可以使用此方法随机找到一个用户。
```php
$randomUser = Db::table('users')->inRandomOrder()->first();
```
### groupBy / having
`groupBy``having` 方法可以将结果分组。 `having` 方法的使用与 `where` 方法十分相似:
```php
$users = Db::table('users')
->groupBy('account_id')
->having('account_id', '>', 100)
->get();
```
你可以向 `groupBy` 方法传递多个参数:
```php
$users = Db::table('users')
->groupBy('first_name', 'status')
->having('account_id', '>', 100)
->get();
```
> 对于更高级的 having 语法,参见 havingRaw 方法。
### skip / take
要限制结果的返回数量,或跳过指定数量的结果,你可以使用 `skip``take` 方法:
```php
$users = Db::table('users')->skip(10)->take(5)->get();
```
或者你也可以使用 limit 和 offset 方法:
```php
$users = Db::table('users')->offset(10)->limit(5)->get();
```
## 条件语句
有时候你可能想要子句只适用于某个情况为真是才执行查询。例如你可能只想给定值在请求中存在的情况下才应用 `where` 语句。 你可以通过使用 `when` 方法:
```php
$role = $request->input('role');
$users = Db::table('users')
->when($role, function ($query, $role) {
return $query->where('role_id', $role);
})
->get();
```
`when` 方法只有在第一个参数为 `true` 的时候才执行给的的闭包。如果第一个参数为 `false` ,那么这个闭包将不会被执行
你可以传递另一个闭包作为 `when` 方法的第三个参数。 该闭包会在第一个参数为 `false` 的情况下执行。为了说明如何使用这个特性,我们来配置一个查询的默认排序:
```php
$sortBy = null;
$users = Db::table('users')
->when($sortBy, function ($query, $sortBy) {
return $query->orderBy($sortBy);
}, function ($query) {
return $query->orderBy('name');
})
->get();
```
## 插入
查询构造器还提供了 `insert` 方法用于插入记录到数据库中。 `insert` 方法接收数组形式的字段名和字段值进行插入操作:
```php
Db::table('users')->insert(
['email' => 'john@example.com', 'votes' => 0]
);
```
你甚至可以将数组传递给 `insert` 方法,将多个记录插入到表中
```php
Db::table('users')->insert([
['email' => 'taylor@example.com', 'votes' => 0],
['email' => 'dayle@example.com', 'votes' => 0]
]);
```
### 自增 ID
如果数据表有自增 `ID` ,使用 `insertGetId` 方法来插入记录并返回 `ID`
```php
$id = Db::table('users')->insertGetId(
['email' => 'john@example.com', 'votes' => 0]
);
```
## 更新
当然, 除了插入记录到数据库中,查询构造器也可以通过 `update` 方法更新已有的记录。 `update` 方法和 `insert` 方法一样,接受包含要更新的字段及值的数组。你可以通过 `where` 子句对 `update` 查询进行约束:
```php
Db::table('users')->where('id', 1)->update(['votes' => 1]);
```
### 更新或者新增
有时您可能希望更新数据库中的现有记录,或者如果不存在匹配记录则创建它。 在这种情况下,可以使用 `updateOrInsert` 方法。 `updateOrInsert` 方法接受两个参数:一个用于查找记录的条件数组,以及一个包含要更该记录的键值对数组。
`updateOrInsert` 方法将首先尝试使用第一个参数的键和值对来查找匹配的数据库记录。 如果记录存在,则使用第二个参数中的值去更新记录。 如果找不到记录,将插入一个新记录,更新的数据是两个数组的集合:
```php
Db::table('users')->updateOrInsert(
['email' => 'john@example.com', 'name' => 'John'],
['votes' => '2']
);
```
### 更新 JSON 字段
更新 JSON 字段时,你可以使用 -> 语法访问 JSON 对象中相应的值,此操作只能支持 MySQL 5.7+
```php
Db::table('users')->where('id', 1)->update(['options->enabled' => true]);
```
### 自增与自减
查询构造器还为给定字段的递增或递减提供了方便的方法。此方法提供了一个比手动编写 `update` 语句更具表达力且更精练的接口。
这两种方法都至少接收一个参数:需要修改的列。第二个参数是可选的,用于控制列递增或递减的量:
```php
Db::table('users')->increment('votes');
Db::table('users')->increment('votes', 5);
Db::table('users')->decrement('votes');
Db::table('users')->decrement('votes', 5);
```
你也可以在操作过程中指定要更新的字段:
```php
Db::table('users')->increment('votes', 1, ['name' => 'John']);
```
## 删除
查询构造器也可以使用 `delete` 方法从表中删除记录。 在使用 `delete` 前,可以添加 `where` 子句来约束 `delete` 语法:
```php
Db::table('users')->delete();
Db::table('users')->where('votes', '>', 100)->delete();
```
如果你需要清空表,你可以使用 `truncate` 方法,它将删除所有行,并重置自增 `ID` 为零:
```php
Db::table('users')->truncate();
```
## 悲观锁
查询构造器也包含一些可以帮助你在 `select` 语法上实现`「悲观锁定」`的函数。若想在查询中实现一个`「共享锁」` 你可以使用 `sharedLock` 方法。 共享锁可防止选中的数据列被篡改,直到事务被提交为止
```php
Db::table('users')->where('votes', '>', 100)->sharedLock()->get();
```
或者,你可以使用 `lockForUpdate` 方法。使用`「update」`锁可避免行被其它共享锁修改或选取:
```php
Db::table('users')->where('votes', '>', 100)->lockForUpdate()->get();
```

339
doc/en/db/quick-start.md Normal file
View File

@ -0,0 +1,339 @@
# 快速开始
## 前言
> [hyperf/database](https://github.com/hyperf-cloud/database) 衍生于 [illuminate/database](https://github.com/illuminate/database),我们对它进行了一些改造,大部分功能保持了相同。在这里感谢一下 Laravel 开发组,实现了如此强大好用的 ORM 组件。
[hyperf/database](https://github.com/hyperf-cloud/database) 组件是基于 [illuminate/database](https://github.com/illuminate/database) 衍生出来的组件,我们对它进行了一些改造,从设计上是允许用于其它 PHP-FPM 框架或基于 Swoole 的框架中的,而在 Hyperf 里就需要提一下 [hyperf/db-connection](https://github.com/hyperf-cloud/db-connection) 组件,它基于 [hyperf/pool](https://github.com/hyperf-cloud/pool) 实现了数据库连接池并对模型进行了新的抽象以它作为桥梁Hyperf 才能把数据库组件及事件组件接入进来。
## Installation
### Hyperf 框架
```bash
composer require hyperf/db-connection
```
### 其它框架
```bash
composer require hyperf/database
```
## 配置
默认配置如下,数据库支持多库配置,默认为 `default`
| 配置项 | 类型 | 默认值 | 备注 |
|:--------------------:|:------:|:---------------:|:------------------:|
| driver | string | 无 | 数据库引擎 |
| host | string | 无 | 数据库地址 |
| database | string | 无 | 数据库默认DB |
| username | string | 无 | 数据库用户名 |
| password | string | null | 数据库密码 |
| charset | string | utf8 | 数据库编码 |
| collation | string | utf8_unicode_ci | 数据库编码 |
| prefix | string | '' | 数据库模型前缀 |
| pool.min_connections | int | 1 | 连接池内最少连接数 |
| pool.max_connections | int | 10 | 连接池内最大连接数 |
| pool.connect_timeout | float | 10.0 | 连接等待超时时间 |
| pool.wait_timeout | float | 3.0 | 超时时间 |
| pool.heartbeat | int | -1 | 心跳 |
| pool.max_idle_time | float | 60.0 | 最大闲置时间 |
| options | array | | PDO 配置 |
```php
<?php
return [
'default' => [
'driver' => env('DB_DRIVER', 'mysql'),
'host' => env('DB_HOST', 'localhost'),
'database' => env('DB_DATABASE', 'hyperf'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'charset' => env('DB_CHARSET', 'utf8'),
'collation' => env('DB_COLLATION', 'utf8_unicode_ci'),
'prefix' => env('DB_PREFIX', ''),
'pool' => [
'min_connections' => 1,
'max_connections' => 10,
'connect_timeout' => 10.0,
'wait_timeout' => 3.0,
'heartbeat' => -1,
'max_idle_time' => (float)env('DB_MAX_IDLE_TIME', 60),
]
],
];
```
有时候用户需要修改 PDO 默认配置,比如所有字段需要返回为 string。这时候就需要修改 PDO 配置项 `ATTR_STRINGIFY_FETCHES` 为 true。
```php
<?php
return [
'default' => [
'driver' => env('DB_DRIVER', 'mysql'),
'host' => env('DB_HOST', 'localhost'),
'database' => env('DB_DATABASE', 'hyperf'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'charset' => env('DB_CHARSET', 'utf8'),
'collation' => env('DB_COLLATION', 'utf8_unicode_ci'),
'prefix' => env('DB_PREFIX', ''),
'pool' => [
'min_connections' => 1,
'max_connections' => 10,
'connect_timeout' => 10.0,
'wait_timeout' => 3.0,
'heartbeat' => -1,
'max_idle_time' => (float) env('DB_MAX_IDLE_TIME', 60),
],
'options' => [
// 框架默认配置
PDO::ATTR_CASE => PDO::CASE_NATURAL,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_ORACLE_NULLS => PDO::NULL_NATURAL,
PDO::ATTR_STRINGIFY_FETCHES => false,
PDO::ATTR_EMULATE_PREPARES => false,
],
],
];
```
### 读写分离
有时候你希望 `SELECT` 语句使用一个数据库连接,而 `INSERT``UPDATE`,和 `DELETE` 语句使用另一个数据库连接。在 `Hyperf` 中,无论你是使用原生查询,查询构造器,或者是模型,都能轻松的实现
为了弄明白读写分离是如何配置的,我们先来看个例子:
```php
<?php
return [
'default' => [
'driver' => env('DB_DRIVER', 'mysql'),
'read' => [
'host' => ['192.168.1.1'],
],
'write' => [
'host' => ['196.168.1.2'],
],
'sticky' => true,
'database' => env('DB_DATABASE', 'hyperf'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'charset' => env('DB_CHARSET', 'utf8'),
'collation' => env('DB_COLLATION', 'utf8_unicode_ci'),
'prefix' => env('DB_PREFIX', ''),
'pool' => [
'min_connections' => 1,
'max_connections' => 10,
'connect_timeout' => 10.0,
'wait_timeout' => 3.0,
'heartbeat' => -1,
'max_idle_time' => (float) env('DB_MAX_IDLE_TIME', 60),
],
],
];
```
注意在以上的例子中,配置数组中增加了三个键,分别是 `read` `write``sticky``read``write` 的键都包含一个键为 `host` 的数组。而 `read``write` 的其他数据库都在键为 mysql 的数组中。
如果你想重写主数组中的配置,只需要修改 `read``write` 数组即可。所以,这个例子中: 192.168.1.1 将作为 「读」 连接主机,而 192.168.1.2 将作为 「写」 连接主机。这两个连接会共享 mysql 数组的各项配置,如数据库的凭据(用户名 / 密码),前缀,字符编码等。
`sticky` 是一个 可选值,它可用于立即读取在当前请求周期内已写入数据库的记录。若 `sticky` 选项被启用,并且当前请求周期内执行过 「写」 操作,那么任何 「读」 操作都将使用 「写」 连接。这样可确保同一个请求周期内写入的数据可以被立即读取到,从而避免主从延迟导致数据不一致的问题。不过是否启用它,取决于应用程序的需求。
### 多库配置
多库配置如下。
```php
<?php
return [
'default' => [
'driver' => env('DB_DRIVER', 'mysql'),
'host' => env('DB_HOST', 'localhost'),
'database' => env('DB_DATABASE', 'hyperf'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'charset' => env('DB_CHARSET', 'utf8'),
'collation' => env('DB_COLLATION', 'utf8_unicode_ci'),
'prefix' => env('DB_PREFIX', ''),
'pool' => [
'min_connections' => 1,
'max_connections' => 10,
'connect_timeout' => 10.0,
'wait_timeout' => 3.0,
'heartbeat' => -1,
'max_idle_time' => (float) env('DB_MAX_IDLE_TIME', 60),
],
],
'test'=>[
'driver' => env('DB_DRIVER', 'mysql'),
'host' => env('DB_HOST2', 'localhost'),
'database' => env('DB_DATABASE', 'hyperf'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'charset' => env('DB_CHARSET', 'utf8'),
'collation' => env('DB_COLLATION', 'utf8_unicode_ci'),
'prefix' => env('DB_PREFIX', ''),
'pool' => [
'min_connections' => 1,
'max_connections' => 10,
'connect_timeout' => 10.0,
'wait_timeout' => 3.0,
'heartbeat' => -1,
'max_idle_time' => (float) env('DB_MAX_IDLE_TIME', 60),
],
],
];
```
使用时,只需要规定 `connection``test`,就可以使用 `test` 中的配置,如下。
```php
<?php
use Hyperf\DbConnection\Db;
// default
Db::select('SELECT * FROM user;');
Db::connection('default')->select('SELECT * FROM user;');
// test
Db::connection('test')->select('SELECT * FROM user;');
```
模型中修改 `connection` 字段,即可使用对应配置,例如一下 `Model` 使用 `test` 配置。
```php
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf-cloud/hyperf/blob/master/LICENSE
*/
namespace App\Model;
/**
* @property int $id
* @property string $mobile
* @property string $realname
*/
class User extends Model
{
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'user';
/**
* The connection name for the model.
*
* @var string
*/
protected $connection = 'test';
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = ['id', 'mobile', 'realname'];
/**
* The attributes that should be cast to native types.
*
* @var array
*/
protected $casts = ['id' => 'integer'];
}
```
## 执行原生 SQL 语句
配置好数据库后,便可以使用 `Hyperf\DbConnection\Db` 进行查询。
### Query 查询类
这里主要包括 `Select`、属性为 `READS SQL DATA` 的存储过程、函数等查询语句。
`select` 方法将始终返回一个数组,数组中的每个结果都是一个 `StdClass` 对象
```php
<?php
use Hyperf\DbConnection\Db;
$users = Db::select('SELECT * FROM `user` WHERE gender = ?',[1]); // 返回array
foreach($users as $user){
echo $user->name;
}
```
### Execute 执行类
这里主要包括 `Insert`、`Update`、`Delete`,属性为 `MODIFIES SQL DATA` 的存储过程等执行语句。
```php
<?php
use Hyperf\DbConnection\Db;
$inserted = Db::insert('INSERT INTO user (id, name) VALUES (?, ?)', [1, 'Hyperf']); // 返回是否成功 bool
$affected = Db::update('UPDATE user set name = ? WHERE id = ?', ['John', 1]); // 返回受影响的行数 int
$affected = Db::delete('DELETE FROM user WHERE id = ?', [1]); // 返回受影响的行数 int
$result = Db::statement("CALL pro_test(?, '?')", [1, 'your words']); // 返回 bool CALL pro_test(??) 为存储过程,属性为 MODIFIES SQL DATA
```
### 自动管理数据库事务
你可以使用 `Db``transaction` 方法在数据库事务中运行一组操作。如果事务的闭包 `Closure` 中出现一个异常,事务将会回滚。如果事务闭包 `Closure` 执行成功,事务将自动提交。一旦你使用了 `transaction` 就不再需要担心手动回滚或提交的问题:
```php
<?php
use Hyperf\DbConnection\Db;
Db::transaction(function () {
Db::table('user')->update(['votes' => 1]);
Db::table('posts')->delete();
});
```
### 手动管理数据库事务
如果你想要手动开始一个事务,并且对回滚和提交能够完全控制,那么你可以使用 `Db``beginTransaction`, `commit`, `rollBack`:
```php
use Hyperf\DbConnection\Db;
Db::beginTransaction();
try{
// Do something...
Db::commit();
} catch(\Throwable $ex){
Db::rollBack();
}
```

30
doc/en/devtool.md Normal file
View File

@ -0,0 +1,30 @@
# 开发者工具
## Installation
```
composer require hyperf/devtool
```
# 支持的命令
```bash
php bin/hyperf.php
```
通过执行上面的命令可获得 Command 所支持的所有命令,其中返回结果 `gen` 系列命令和 `vendor:publish` 命令主要为 `devtool` 组件提供支持
```
gen
gen:amqp-consumer Create a new amqp consumer class
gen:amqp-producer Create a new amqp producer class
gen:aspect Create a new aspect class
gen:command Create a new command class
gen:controller Create a new controller class
gen:job Create a new job class
gen:listener Create a new listener class
gen:middleware Create a new middleware class
gen:process Create a new process class
vendor
vendor:publish Publish any publishable configs from vendor packages.
```x

284
doc/en/di.md Normal file
View File

@ -0,0 +1,284 @@
# 依赖注入
## 简介
Hyperf 默认采用 [hyperf/di](https://github.com/hyperf-cloud/di) 作为框架的依赖注入管理容器,尽管从设计上我们允许您更换其它的依赖注入管理容器,但我们强烈不建议您更换该组件。
[hyperf/di](https://github.com/hyperf-cloud/di) 是一个强大的用于管理类的依赖关并完成自动注入的组件,与传统依赖注入容器的区别在于更符合长生命周期的应用使用、提供了 [注解及注解注入](zh/annotation.md) 的支持、提供了无比强大的 [AOP 面向切面编程](zh/aop.md) 能力,这些能力及易用性作为 Hyperf 的核心输出,我们自信的认为该组件是最优秀的。
## Installation
该组件默认存在 [hyperf-skeleton](https://github.com/hyperf-cloud/hyperf-skeleton) 项目中并作为主要组件存在,如希望在其它框架内使用该组件可通过下面的命令安装。
```bash
composer require hyperf/di
```
## 绑定对象关系
### 简单对象注入
通常来说,类的关系及注入是无需显性定义的,这一切 Hyperf 都会默默的为您完成,我们通过一些代码示例来说明一下相关的用法。
假设我们需要在 `IndexController` 内调用 `UserService` 类的 `getInfoById(int $id)` 方法。
```php
<?php
namespace App\Service;
class UserService
{
public function getInfoById(int $id)
{
// 我们假设存在一个 Info 实体
return (new Info())->fill($id);
}
}
```
#### 通过构造方法注入
```php
<?php
namespace App\Controller;
use App\Service\UserService;
use Hyperf\HttpServer\Annotation\AutoController;
class IndexController
{
/**
* @var UserService
*/
private $userService;
// 通过在构造函数的参数上声明参数类型完成自动注入
public function __construct(UserService $userService)
{
$this->userService = $userService;
}
public function index()
{
$id = 1;
// 直接使用
return $this->userService->getInfoById($id);
}
}
```
> 注意调用方也就是 `IndexController` 必须是由 DI 创建的对象才能完成自动注入Controller 默认是由 DI 创建的
#### 通过 `@Inject` 注解注入
```php
<?php
namespace App\Controller;
use App\Service\UserService;
use Hyperf\Di\Annotation\Inject;
use Hyperf\HttpServer\Annotation\AutoController;
class IndexController
{
/**
* 通过 `@Inject` 注解注入由 `@var` 注解声明的属性类型对象
*
* @Inject
* @var UserService
*/
private $userService;
public function index()
{
$id = 1;
// 直接使用
return $this->userService->getInfoById($id);
}
}
```
> 注意调用方也就是 `IndexController` 必须是由 DI 创建的对象才能完成自动注入Controller 默认是由 DI 创建的
> 使用 `@Inject` 注解时需 `use Hyperf\Di\Annotation\Inject;` 命名空间;
### 抽象对象注入
基于上面的例子从合理的角度上来说Controller 面向的不应该直接是一个 `UserService` 类,可能更多的是一个 `UserServiceInterface` 的接口类,此时我们可以通过 `config/autoload/dependencies.php` 来绑定对象关系达到目的,我们还是通过代码来解释一下。
定义一个接口类:
```php
<?php
namespace App\Service;
interface UserServiceInterface
{
public function getInfoById(int $id);
}
```
`UserService` 实现接口类:
```php
<?php
namespace App\Service;
class UserService implements UserServiceInterface
{
public function getInfoById(int $id)
{
// 我们假设存在一个 Info 实体
return (new Info())->fill($id);
}
}
```
`config/autoload/dependencies.php` 内完成关系配置:
```php
<?php
return [
\App\Service\UserServiceInterface::class => \App\Service\UserService::class
];
```
这样配置后就可以直接通过 `UserServiceInterface` 来注入 `UserService` 对象了,我们仅通过注解注入的方式来举例,构造函数注入也是一样的:
```php
<?php
namespace App\Controller;
use App\Service\UserServiceInterface;
use Hyperf\Di\Annotation\Inject;
use Hyperf\HttpServer\Annotation\AutoController;
class IndexController
{
/**
* @Inject
* @var UserServiceInterface
*/
private $userService;
public function index()
{
$id = 1;
// 直接使用
return $this->userService->getInfoById($id);
}
}
```
### 工厂对象注入
我们假设 `UserService` 的实现会更加复杂一些,在创建 `UserService` 对象时构造函数还需要传递进来一些非直接注入型的参数,假设我们需要从配置中取得一个值,然后 `UserService` 需要根据这个值来决定是否开启缓存模式(顺带一说 Hyperf 提供了更好用的 [模型缓存](zh/db/model-cache.md) 功能)
我们需要创建一个工厂来生成 `UserService` 对象:
```php
<?php
namespace App\Service;
use Hyperf\Contract\ConfigInterface;
use Psr\Container\ContainerInterface;
class UserServiceFactory
{
// 实现一个 __invoke() 方法来完成对象的生产,方法参数会自动注入一个当前的容器实例
public function __invoke(ContainerInterface $container)
{
$config = $container->get(ConfigInterface::class);
// 我们假设对应的配置的 key 为 cache.enable
$enableCache = $config->get('cache.enable', false);
// make(string $name, array $parameters = []) 方法等同于 new ,使用 make() 方法是为了允许 AOP 的介入,而直接 new 会导致 AOP 无法正常介入流程
return make(UserService::class, compact('enableCache'));
}
}
```
`UserService` 也许在构造函数提供一个参数接收对应的值:
```php
<?php
namespace App\Service;
class UserService implements UserServiceInterface
{
/**
* @var bool
*/
private $enableCache;
public function __construct(bool $enableCache)
{
// 接收值并储存于类属性中
$this->enableCache = $enableCache;
}
public function getInfoById(int $id)
{
return (new Info())->fill($id);
}
}
```
`config/autoload/dependencies.php` 调整绑定关系:
```php
<?php
return [
\App\Service\UserServiceInterface::class => \App\Service\UserServiceFactory::class
];
```
这样在注入 `UserServiceInterface` 的时候容器就会交由 `UserServiceFactory` 来创建对象了。
> 当然在该场景中可以通过 `@Value` 注解来更便捷的注入配置而无需构建工厂类,此仅为举例
## 注意事项
### 容器仅管理长生命周期的对象
换种方式理解就是容器内管理的对象**都是单例**,这样的设计对于长生命周期的应用来说会更加的高效,减少了大量无意义的对象创建和销毁,这样的设计也就意味着所有需要交由 DI 容器管理的对象**均不能包含** `状态` 值。
`状态` 可直接理解为会随着请求而变化的值,事实上在 [协程](zh/coroutine.md) 编程中,这些状态值也是应该存放于 `协程上下文` 中的,即 `Hyperf\Utils\Context`
## 短生命周期对象
通过 `new` 关键词创建的对象毫无疑问的短生命周期的,那么如果希望创建一个短生命周期的对象但又希望通过依赖注入容器注入相关的依赖呢?这是我们可以通过 `make(string $name, array $parameters = [])` 函数来创建 `$name` 对应的的实例,代码示例如下:
```php
$userService = make(UserService::class, ['enableCache' => true]);
```
> 注意仅 `$name` 对应的对象为短生命周期对象,该对象的所有依赖都是通过 `get()` 方法获取的,即为长生命周期的对象
## 获取容器对象
有些时候我们可能希望去实现一些更动态的需求时,会希望可以直接获取到 `容器(Container)` 对象在绝大部分情况下框架的入口类比如命令类、控制器、RPC服务提供者等都是由 `容器(Container)` 创建并维护的,也就意味着您所写的绝大部分业务代码都是在 `容器(Container)` 的管理作用之下的,也就意味着在绝大部分情况下您都可以通过在 `构造函数(Constructor)` 声明或通过 `@Inject` 注解注入 `Psr\Container\ContainerInterface` 接口类都能够获得 `Hyperf\Di\Container` 容器对象,我们通过代码来演示一下:
```php
<?php
namespace App\Controller;
use Hyperf\HttpServer\Annotation\AutoController;
use Psr\Container\ContainerInterface;
class IndexController
{
/**
* @var ContainerInterface
*/
private $container;
// 通过在构造函数的参数上声明参数类型完成自动注入
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
}
```
在某些更极端动态的情况下,或者非 `容器(Container)` 的管理作用之下时,想要获取到 `容器(Container)` 对象还可以通过 `\Hyperf\Utils\ApplicationContext::getContaienr()` 方法来获得 `容器(Container)` 对象。
```php
$container = \Hyperf\Utils\ApplicationContext::getContainer();
```

107
doc/en/exception-handler.md Normal file
View File

@ -0,0 +1,107 @@
# 异常处理器
`Hyperf` 里,业务代码都运行在 `Worker进程` 上,也就意味着一旦任意一个请求的业务存在没有捕获处理的异常的话,都会导致对应的 `Worker进程` 被中断退出,虽然被中断的 `Worker进程` 仍会被重新拉起,但对服务而已也是不能接受的,且捕获异常并输出合理的报错内容给客户端也是更加友好的。
我们可以通过对各个 `server` 定义不同的 `异常处理器(ExceptionHandler)`,一旦业务流程存在没有捕获的异常,到会被传递到已注册的 `异常处理器(ExceptionHandler)` 去处理。
## 自定义一个异常处理
### 注册异常处理器
目前仅支持配置文件的形式注册 `异常处理器(ExceptionHandler)`,配置文件位于 `config/autoload/exceptions.php`,将您的自定义异常处理器配置在对应的 `server` 下即可:
```php
<?php
// config/autoload/exceptions.php
return [
'handler' => [
// 这里的 http 对应 config/autoload/server.php 内的 server 所对应的 name 值
'http' => [
// 这里配置完整的类命名空间地址已完成对该异常处理器的注册
\App\Exception\Handler\FooExceptionHandler::class,
],
],
];
```
> 每个异常处理器配置数组的顺序决定了异常在处理器间传递的顺序。
### 定义异常处理器
我们可以在任意位置定义一个 `类(Class)` 并继承抽象类 ` Hyperf\ExceptionHandler\ExceptionHandler` 并实现其中的抽象方法,如下:
```php
<?php
namespace App\Exception\Handler;
use Hyperf\ExceptionHandler\ExceptionHandler;
use Hyperf\HttpMessage\Stream\SwooleStream;
use Psr\Http\Message\ResponseInterface;
use App\Exception\FooException;
use Throwable;
class FooExceptionHandler extends ExceptionHandler
{
public function handle(Throwable $throwable, ResponseInterface $response)
{
// 判断被捕获到的异常是希望被捕获的异常
if ($throwable instanceof FooException) {
// 格式化输出
$data = json_encode([
'code' => $throwable->getCode(),
'message' => $throwable->getMessage(),
], JSON_UNESCAPED_UNICODE);
// 阻止异常冒泡
$this->stopPropagation();
return $response->withStatus(500)->withBody(new SwooleStream($data));
}
// 交给下一个异常处理器
return $respose;
// 或者不做处理直接屏蔽异常
}
/**
* 判断该异常处理器是否要对该异常进行处理
*/
public function isValid(Throwable $throwable): bool
{
return true;
}
}
```
### 定义异常类
```php
<?php
namespace App\Exception;
use App\Constants\ErrorCode;
use Hyperf\Server\Exception\ServerException;
use Throwable;
class FooException extends ServerException
{
}
```
### 触发异常
```php
namespace App\Controller;
use App\Exception\FooException;
class IndexController extends Controller
{
public function index()
{
throw new FooException('Foo Exception...', 800);
}
}
```
在上面这个例子,我们先假设 `FooException` 是存在的一个异常,以及假设已经完成了该处理器的配置,那么当业务抛出一个没有被捕获处理的异常时,就会根据配置的顺序依次传递,整一个处理流程可以理解为一个管道,若前一个异常处理器调用 `$this->stopPropagation()` 则不再往后传递,若最后一个配置的异常处理器仍不对该异常进行捕获处理,那么就会交由 Hyperf 的默认异常处理器处理了。

159
doc/en/grpc.md Normal file
View File

@ -0,0 +1,159 @@
# gRPC 服务
gRPC 官方文档中的 quickstart - php, 很容易给 PHPer 产生误导, 按照官网的文档, 运行起来 gRPC 服务就很麻烦, 更不用说整套的 RPC 服务了.
推荐阅读 [tech| 再探 grpc](https://www.jianshu.com/p/f3221df39e6f), 讲解了在 PHP 中实现 gRPC 相关基础知识.
hyperf 对 gRPC 支持做了更进一步的封装, hyperf-skeleton 项目为例, 详细讲解整个步骤:
- .proto 文件以及相关配置实例
- gRPC server 示例
- gRPC client 示例
## .proto 文件以及相关配置实例
- 定义好 proto 文件 `grpc.proto`
```proto3
syntax = "proto3";
package grpc;
service hi {
rpc sayHello (HiUser) returns (HiReply) {
}
}
message HiUser {
string name = 1;
int32 sex = 2;
}
message HiReply {
string message = 1;
HiUser user = 2;
}
```
- 使用 protoc 生成示例代码
```
# 使用 linux 包管理工具安装 protoc, 下面以 alpine 为例, 也可以参考 hyper-skeleton 下的 Dockerfile
apk add protobuf
# 使用 protoc 自动生成代码
protoc --php_out=grpc/ grpc.proto
# tree grpc
grpc
├── GPBMetadata
│   └── Grpc.php
└── Grpc
├── HiReply.php
└── HiUser.php
```
- 配置 composer.json, 使用 `grpc/` 下代码的自动加载. 如果 proto 文件中使用不同的 `package` 设置, 或者使用了不同的目录, 进行相应调整即可
```json
"autoload": {
"psr-4": {
"App\\": "app/",
"GPBMetadata\\": "grpc/GPBMetadata",
"Grpc\\": "grpc/Grpc"
},
"files": [
]
},
```
## gRPC server 示例
- gRPC server 服务器配置
`server.php` 文件(参考 [配置](zh/config.md)):
```php
'servers' => [
....
[
'name' => 'grpc',
'type' => Server::SERVER_HTTP,
'host' => '0.0.0.0',
'port' => 9503,
'sock_type' => SWOOLE_SOCK_TCP,
'callbacks' => [
SwooleEvent::ON_REQUEST => [\Hyperf\GrpcServer\Server::class, 'onRequest'],
],
],
],
```
- gRPC server 路由配置
`routes.php` 文件(参考 [路由](zh/router.md)):
```php
Router::addServer('grpc', function () {
Router::addGroup('/grpc.hi', function () {
Router::post('/sayHello', 'App\Controller\HiController@sayHello');
});
});
```
.proto 文件中的定义和 gRPC server 路由的对应关系: `/{package}.{service}/{rpc}`
- 如果想更深入一点
gRPC server 如何对 gRPC 请求进行处理的: `\Hyperf\GrpcServer\CoreMiddleware::process()` (`vendor/hyperf/grpc-server/src/CoreMiddleware.php:46`, 复制后直接使用 phpstorm 打开), 解析出 `request_uri`, 即得到 `/{package}.{service}/{rpc}` 信息, 然后调用好封装好的 gRPC 编解码类 `\Hyperf\Grpc\Parser::deserializeMessage`(`vendor/hyperf/grpc-server/src/CoreMiddleware.php:137`), 就可以获取到请求的明文信息
gRPC server 如何进行 gRPC 响应, 相信你可以根据上面的信息, 自己发现.
## gRPC client 示例
示例代码可以在 `GrpcController` 中找到:
```php
public function hello()
{
$client = new \App\Grpc\HiClient('127.0.0.1:9503', [
'credentials' => null,
]);
$request = new \Grpc\HiUser();
$request->setName('hyperf');
$request->setSex(1);
/**
* @var \Grpc\HiReply $reply
*/
list($reply, $status) = $client->sayHello($request);
$message = $reply->getMessage();
$user = $reply->getUser();
$client->close();
var_dump(memory_get_usage(true));
return $message;
}
```
hyperf 已经封装好了 `\Hyperf\GrpcClient\BaseClient`, 只要根据 .proto 文件中的定义, 按需扩展:
```php
class HiClient extends BaseClient
{
public function sayHello(HiUser $argument)
{
return $this->simpleRequest(
'/grpc.hi/sayHello',
$argument,
[HiReply::class, 'decode']
);
}
}
```
## 写在后面
如果你是 gRPC 的重度使用者, 欢迎关注 hyperf 的后续开发者工具, 可以根据 .proto 文件生成全套 gRCP 代码.

111
doc/en/guzzle.md Normal file
View File

@ -0,0 +1,111 @@
# Guzzle HTTP 客户端
[hyperf/guzzle](https://github.com/hyperf-cloud/guzzle) 组件基于 Guzzle 进行协程处理,通过 Swoole HTTP 客户端作为协程驱动替换到 Guzzle 内,以达到 HTTP 客户端的协程化。
## Installation
```bash
composer require hyperf/guzzle
```
## 使用
只需要该组件内的 `Hyperf\Guzzle\CoroutineHandler` 作为处理器设置到 Guzzle 客户端内即可转为协程化运行,为了方便创建协程的 Guzzle 对象,我们提供了一个工厂类 `Hyperf\Guzzle\ClientFactory` 来便捷的创建客户端,代码示例如下:
```php
<?php
use Hyperf\Guzzle\ClientFactory;
class Foo {
/**
* @var \Hyperf\Guzzle\ClientFactory
*/
private $clientFactory;
public function __construct(ClientFactory $clientFactory)
{
$this->clientFactory = $clientFactory;
}
public function bar()
{
// $options 等同于 GuzzleHttp\Client 构造函数的 $config 参数
$options = [];
// $client 为协程化的 GuzzleHttp\Client 对象
$client = $this->clientFactory->create($options);
}
}
```
## 使用Swoole配置
有时候我们想直接修改 `Swoole` 配置,所以我们也提供了相关配置项,不过这项配置在 `Curl Guzzle 客户端` 中是无法生效的,所以谨慎使用。
> 这项配置会替换原来的配置,比如以下 timeout 会被 10 替换。
```php
<?php
use GuzzleHttp\Client;
use Hyperf\Guzzle\CoroutineHandler;
use GuzzleHttp\HandlerStack;
$client = new Client([
'base_uri' => 'http://127.0.0.1:8080',
'handler' => HandlerStack::create(new CoroutineHandler()),
'timeout' => 5,
'swoole' => [
'timeout' => 10,
'socket_buffer_size' => 1024 * 1024 * 2,
],
]);
$response = $client->get('/');
```
## 连接池
Hyperf 除了实现了 `Hyperf\Guzzle\CoroutineHandler` 外,还基于 `Hyperf\Pool\SimplePool` 实现了 `Hyperf\Guzzle\PoolHandler`
### 原因
简单来说,主机 TCP连接数 是有上限的当我们并发大到超过这个上限值时就导致请求无法正常建立。另外TCP连接结束后还会有一个 TIME-WAIT 阶段,所以也无法实时释放连接。这就导致了实际并发可能远低于 TCP 上限值。所以,我们需要一个连接池来维持这个阶段,尽量减少 TIME-WAIT 造成的影响让TCP连接进行复用。
### 使用
```php
use GuzzleHttp\Client;
use Hyperf\Utils\Coroutine;
use GuzzleHttp\HandlerStack;
use Hyperf\Guzzle\PoolHandler;
use Hyperf\Guzzle\RetryMiddleware;
function default_guzzle_handler(): HandlerStack
{
$handler = null;
if (Coroutine::inCoroutine()) {
$handler = make(PoolHandler::class, [
'option' => [
'max_connections' => 50,
],
]);
}
// 默认的重试Middleware
$retry = make(RetryMiddleware::class, [
'retries' => 1,
'delay' => 10,
]);
$stack = HandlerStack::create($handler);
$stack->push($retry->getMiddleware(), 'retry');
return $stack;
}
$client = make(Client::class, [
'config' => [
'handler' => default_guzzle_handler(),
],
]);
```

186
doc/en/json-rpc.md Normal file
View File

@ -0,0 +1,186 @@
# JSON RPC 服务
JSON RPC 是一种基于 JSON 格式的轻量级的 RPC 协议标准,易于使用和阅读。在 Hyperf 里由 [hyperf/json-rpc](https://github.com/hyperf-cloud/json-rpc) 组件来实现,可自定义基于 HTTP 协议来传输,或直接基于 TCP 协议来传输。
# 安装
```bash
composer require hyperf/json-rpc
```
# 使用
服务有两种角色,一种是 `服务提供者(ServiceProvider)`,即为其它服务提供服务的服务,另一种是 `服务消费者(ServiceConsumer)`,即依赖其它服务的服务,一个服务既可能是 `服务提供者(ServiceProvider)`,同时又是 `服务消费者(ServiceConsumer)`。而两者直接可以通过 `服务契约` 来定义和约束接口的调用,在 Hyperf 里,可直接理解为就是一个 `接口类(Interface)`,通常来说这个接口类会同时出现在提供者和消费者下。
## 定义服务提供者
目前仅支持通过注解的形式来定义 `服务提供者(ServiceProvider)`,后续迭代会增加配置的形式。
我们可以直接通过 `@RpcService` 注解对一个类进行定义即可发布这个服务了:
```php
<?php
namespace App\JsonRpc;
use Hyperf\RpcServer\Annotation\RpcService;
/**
* @RpcService(name="CalculatorService", protocol="jsonrpc-http", server="jsonrpc-http")
*/
class CalculatorService implements CalculatorServiceInterface
{
// 实现一个加法方法,这里简单的认为参数都是 int 类型
public function add(int $a, int $b): int
{
// 这里是服务方法的具体实现
return $a + $b;
}
}
```
`@RpcService` 共有 `4` 个参数:
`name` 属性为定义该服务的名称这里定义一个全局唯一的名字即可Hyperf 会根据该属性生成对应的 ID 注册到服务中心去;
`protocol` 属性为定义该服务暴露的协议,目前仅支持 `jsonrpc``jsonrpc-http`,分别对应于 TCP 协议和 HTTP 协议下的两种协议,默认值为 `jsonrpc-http`,这里的值对应在 `Hyperf\Rpc\ProtocolManager` 里面注册的协议的 `key`,这两个本质上都是 JSON RPC 协议,区别在于数据格式化、数据打包、数据传输器等不同。
`server` 属性为绑定该服务类发布所要承载的 `Server`,默认值为 `jsonrpc-http`,该属性对应 `config/autoload/server.php` 文件内 `servers` 下所对应的 `name`,这里也就意味着我们需要定义一个对应的 `Server`,我们下一章节具体阐述这里应该怎样去处理;
`publishTo` 属性为定义该服务所要发布的服务中心,目前仅支持 `consul` 或为空,为空时代表不发布该服务到服务中心去,但也就意味着您需要手动处理服务发现的问题,当值为 `consul` 时需要对应配置好 [hyperf/consul](./consul.md) 组件的相关配置;
> 使用 `@RpcService` 注解需 `use Hyperf\RpcServer\Annotation\RpcService;` 命名空间。
### 定义 JSON RPC Server
HTTP Server (适配 `jsonrpc-http` 协议)
```php
<?php
use Hyperf\Server\Server;
use Hyperf\Server\SwooleEvent;
return [
// 这里省略了该文件的其它配置
'servers' => [
[
'name' => 'jsonrpc-http',
'type' => Server::SERVER_HTTP,
'host' => '0.0.0.0',
'port' => 9504,
'sock_type' => SWOOLE_SOCK_TCP,
'callbacks' => [
SwooleEvent::ON_REQUEST => [\Hyperf\JsonRpc\HttpServer::class, 'onRequest'],
],
],
],
];
```
TCP Server (适配 `jsonrpc` 协议)
```php
<?php
use Hyperf\Server\Server;
use Hyperf\Server\SwooleEvent;
return [
// 这里省略了该文件的其它配置
'servers' => [
[
'name' => 'jsonrpc',
'type' => Server::SERVER_TCP,
'host' => '0.0.0.0',
'port' => 9503,
'sock_type' => SWOOLE_SOCK_TCP,
'callbacks' => [
SwooleEvent::ON_RECEIVE => [\Hyperf\JsonRpc\TcpServer::class, 'onReceive'],
],
'settings' => [
'open_eof_split' => true,
'package_eof' => "\r\n",
],
],
],
];
```
## 发布到服务中心
目前仅支持发布服务到 `consul`,后续会增加其它服务中心。
发布服务到 `consul` 在 Hyperf 也是非常容易的一件事情,通过 `composer require hyperf/consul` 加载 Consul 组件(如果已安装则可忽略该步骤),然后再在 `config/autoload/consul.php` 配置文件内配置您的 `Consul` 配置即可,示例如下:
```php
<?php
return [
'uri' => 'http://127.0.0.1:8500',
];
```
配置完成后在启动服务时Hyperf 会自动地将 `@RpcService` 定义了 `publishTo` 属性为 `consul` 的服务注册到服务中心去。
> 目前仅支持 `jsonrpc-http` 协议发布到服务中心去,其它协议的健康检查尚未实现
## 定义服务消费者
一个 `服务消费者(ServiceConsumer)` 可以理解为就是一个客户端类,但在 Hyperf 里您无需处理连接和请求相关的事情只需要定义一个类及相关属性即可。v1.1会提供动态代理实现的客户端,使之更加简单便捷)
```php
<?php
namespace App\JsonRpc;
use Hyperf\RpcClient\AbstractServiceClient;
class CalculatorService extends AbstractServiceClient implements CalculatorServiceInterface
{
/**
* 定义对应服务提供者的服务名称
* @var string
*/
protected $serviceName = 'CalculatorService';
/**
* 定义对应服务提供者的服务协议
* @var string
*/
protected $protocol = 'jsonrpc-http';
public function add(int $a, int $b): int
{
return $this->__request(__FUNCTION__, compact('a', 'b'));
}
}
```
然后还需要在配置文件定义一个配置标记要从何服务中心获取节点信息,位于 `config/autoload/services.php` (如不存在可自行创建)
```php
<?php
return [
'consumers' => [
[
// 对应消费者类的 $serviceName
'name' => 'CalculatorService',
// 这个消费者要从哪个服务中心获取节点信息,如不配置则不会从服务中心获取节点信息
'registry' => [
'protocol' => 'consul',
'address' => 'http://127.0.0.1:8500',
],
// 如果没有指定上面的 registry 配置,即为直接对指定的节点进行消费,通过下面的 nodes 参数来配置服务提供者的节点信息
'nodes' => [
['host' => '127.0.0.1', 'port' => 9504],
],
]
],
];
```
这样我们便可以通过 `CalculatorService` 类来实现对服务的消费了,为了让这里的关系逻辑更加的合理,还应该在 `config/autoload/dependencies.php` 内定义 `CalculatorServiceInterface``CalculatorService` 的关系,示例如下:
```php
return [
App\JsonRpc\CalculatorServiceInterface::class => App\JsonRpc\CalculatorService::class,
];
```
这样便可以通过注入 `CalculatorServiceInterface` 接口来使用客户端了。

244
doc/en/logger.md Normal file
View File

@ -0,0 +1,244 @@
# 日志
`hyperf/logger` 组件是基于 [psr/logger](https://github.com/php-fig/logger) 实现的,默认使用 [monolog/monolog](https://github.com/Seldaek/monolog) 作为驱动,在 `hyperf-skeleton` 项目内默认提供了一些日志配置,默认使用 `Monolog\Handler\StreamHandler`, 由于 `Swoole` 已经对 `fopen`, `fwrite` 等函数进行了协程化处理,所以只要不将 `useLocking` 参数设置为 `true`,就是协程安全的。
## Installation
```
composer require hyperf/logger
```
## 配置
`hyperf-skeleton` 项目内默认提供了一些日志配置,默认情况下,日志的配置文件为 `config/autoload/logger.php` ,示例如下:
```php
<?php
return [
'default' => [
'handler' => [
'class' => \Monolog\Handler\StreamHandler::class,
'constructor' => [
'stream' => BASE_PATH . '/runtime/logs/hyperf.log',
'level' => \Monolog\Logger::DEBUG,
],
],
'formatter' => [
'class' => \Monolog\Formatter\LineFormatter::class,
'constructor' => [
'format' => null,
'dateFormat' => null,
'allowInlineLineBreaks' => true,
]
],
],
];
```
## 使用
```php
<?php
declare(strict_types=1);
namespace App\Service;
use Psr\Container\ContainerInterface;
use Hyperf\Logger\LoggerFactory;
class DemoService
{
/**
* @var \Psr\Log\LoggerInterface
*/
protected $logger;
public function __construct(LoggerFactory $loggerFactory)
{
// default 对应 config/autoload/logger.php 内的 key
$this->logger = $loggerFactory->get('default');
}
public function method()
{
// Do somthing.
$this->logger->info("Your log message.");
}
}
```
## 关于 monolog 的基础知识
我们结合代码来看一些 `monolog` 中所涉及到的基础概念:
```php
use Monolog\Formatter\LineFormatter;
use Monolog\Handler\FirePHPHandler;
use Monolog\Handler\StreamHandler;
use Monolog\Logger;
// 创建一个 Channel参数 log 即为 Channel 的名字
$log = new Logger('log');
// 创建两个 Handler对应变量 $stream 和 $fire
$stream = new StreamHandler('test.log', Logger::WARNING);
$fire = new FirePHPHandler();
// 定义时间格式为 "Y-m-d H:i:s"
$dateFormat = "Y n j, g:i a";
// 定义日志格式为 "[%datetime%] %channel%.%level_name%: %message% %context% %extra%\n"
$output = "%datetime%||%channel||%level_name%||%message%||%context%||%extra%\n";
// 根据 时间格式 和 日志格式,创建一个 Formatter
$formatter = new LineFormatter($output, $dateFormat);
// 将 Formatter 设置到 Handler 里面
$stream->setFormatter($formatter);
// 讲 Handler 推入到 Channel 的 Handler 队列内
$log->pushHandler($stream);
$log->pushHandler($fire);
// clone new log channel
$log2 = $log->withName('log2');
// add records to the log
$log->warning('Foo');
// add extra data to record
// 1. log context
$log->error('a new user', ['username' => 'daydaygo']);
// 2. processor
$log->pushProcessor(function ($record) {
$record['extra']['dummy'] = 'hello';
return $record;
});
$log->pushProcessor(new \Monolog\Processor\MemoryPeakUsageProcessor());
$log->alert('czl');
```
- 首先, 实例化一个 `Logger`, 取个名字, 名字对应的就是 `channel`
- 可以为 `Logger` 绑定多个 `Handler`, `Logger` 打日志, 交由 `Handler` 来处理
- `Handler` 可以指定需要处理那些 **日志级别** 的日志, 比如 `Logger::WARNING`, 只处理日志级别 `>=Logger::WARNING` 的日志
- 谁来格式化日志? `Formatter`, 设置好 Formatter 并绑定到相应的 `Handler`
- 日志包含那些部分: `"%datetime%||%channel||%level_name%||%message%||%context%||%extra%\n"`
- 区分一下日志中添加的额外信息 `context``extra`: `context` 由用户打日志时额外指定, 更加灵活; `extra` 由绑定到 `Logger` 上的 `Processor` 固定添加, 比较适合收集一些 **常见信息**
## 更多用法
### 封装 `Log`
可能有些时候您更想保持大多数框架使用日志的习惯,那么您可以在 `App` 下创建一个 `Log` 类,并通过 `__callStatic` 魔术方法静态方法调用实现对 `Logger` 的取用以及各个等级的日志记录,我们通过代码来演示一下:
```php
namespace App;
use Hyperf\Logger\Logger;
use Hyperf\Utils\ApplicationContext;
/**
* @method static Logger get($name)
* @method static void log($level, $message, array $context = array())
* @method static void emergency($message, array $context = array())
* @method static void alert($message, array $context = array())
* @method static void critical($message, array $context = array())
* @method static void error($message, array $context = array())
* @method static void warning($message, array $context = array())
* @method static void notice($message, array $context = array())
* @method static void info($message, array $context = array())
* @method static void debug($message, array $context = array())
*/
class Log
{
public static function __callStatic($name, $arguments)
{
$container = ApplicationContext::getContainer();
$factory = $container->get(\Hyperf\Logger\LoggerFactory::class);
if ($name === 'get') {
return $factory->get(...$arguments);
}
$log = $factory->get('default');
$log->$name(...$arguments);
}
}
```
默认使用 `default``Channel` 来记录日志,您也可以通过使用 `Log::get($name)` 方法获得不同 `Channel``Logger`, 强大的 `容器(Container)` 帮您解决了这一切
### stdout 日志
框架组件所输出的日志在默认情况下是由 `Hyperf\Contract\StdoutLoggerInterface` 接口的实现类 `Hyperf\Framework\Logger\StdoutLogger` 提供支持的,该实现类只是为了将相关的信息通过 `print_r()` 输出在 `标准输出(stdout)`,即为启动 `Hyperf``终端(Terminal)` 上,也就意味着其实并没有使用到 `monolog` 的,那么如果想要使用 `monolog` 来保持一致要怎么处理呢?
是的, 还是通过强大的 `容器(Container)`.
- 首先, 实现一个 `StdoutLoggerFactory` 类,关于 `Factory` 的用法可在 [依赖注入](zh/di.md) 章节获得更多详细的说明。
```php
<?php
declare(strict_types=1);
namespace App;
use Psr\Container\ContainerInterface;
class StdoutLoggerFactory
{
public function __invoke(ContainerInterface $container)
{
return Log::get('sys');
}
}
```
- 申明依赖, 使用 `StdoutLoggerInterface` 的地方, 由实际依赖的 `StdoutLoggerFactory` 实例化的类来完成
```php
// config/autoload/dependencies.php
return [
\Hyperf\Contract\StdoutLoggerInterface::class => \App\StdoutLoggerFactory::class,
];
```
### 不同环境下输出不同格式的日志
上面这么多的使用, 都还只在 monolog 中的 `Logger` 这里打转, 这里来看看 `Handler``Formatter`
```php
// config/autoload/logger.php
$appEnv = env('APP_ENV', 'dev');
if ($appEnv == 'dev') {
$formatter = [
'class' => \Monolog\Formatter\LineFormatter::class,
'constructor' => [
'format' => "||%datetime%||%channel%||%level_name%||%message%||%context%||%extra%\n",
'allowInlineLineBreaks' => true,
'includeStacktraces' => true,
],
];
} else {
$formatter = [
'class' => \Monolog\Formatter\JsonFormatter::class,
'constructor' => [],
];
}
return [
'default' => [
'handler' => [
'class' => \Monolog\Handler\StreamHandler::class,
'constructor' => [
'stream' => 'php://stdout',
'level' => \Monolog\Logger::INFO,
],
],
'formatter' => $formatter,
],
]
```
- 默认配置了名为 `default``Handler`, 并包含了此 `Handler` 及其 `Formatter` 的信息
- 获取 `Logger` 时, 如果没有指定 `Handler`, 底层会自动把 `default` 这一 `Handler` 绑定到 `Logger`
- dev(开发)环境: 日志使用 `php://stdout` 输出到 `标准输出(stdout)`, 并且 `Formatter` 中设置 `allowInlineLineBreaks`, 方便查看多行日志
- 非 dev 环境: 日志使用 `JsonFormatter`, 会被格式为 json, 方便投递到第三方日志服务

55
doc/en/microservice.md Normal file
View File

@ -0,0 +1,55 @@
# 微服务
微服务就是一些协同工作的,小而自治的服务。
## 很小,专注做好一件事
随着需求的迭代,新功能的增加,代码库往往会变得越来越大,尽管我们极力希望在巨大的代码库中做到清晰的模块化,但事实上模块与模块之间的界限很难划分得清楚,逐渐地相似的功能代码在代码库中随处可见,以致于在迭代时想要知道该在什么地方做修改都很困难,修复 `Bug` 和增加新特性新功能越来越难。
在一个单体系统中,通常会创建一些抽象层或者实现模块化来保证代码的 `内聚性`,从而避免上面提到的问题。
> 根据 Robert C. Martin 的一个对 [单一职责原则 (Single Responsibility Principle)](https://baike.baidu.com/item/单一职责原则/9456515) 的论述:“*把因为相同原因而变化的东西聚合到一起,把因为不同原因而变化的东西分离开来。*” 该论述很好地强调了内聚性这一概念。
微服务则将这一理念应用在独立的服务上,根据业务的边界确定服务的边界,每个服务专注于服务边界之内的事情,因为可以避免很多由于代码库过大衍生出来的各种问题。
那么一个微服务到底应该多微小?足够小即可,不要过小。那么怎么衡量一个系统是否拆足够小了呢?当你面对这个系统时,不会再有 "过大" 想要拆小它的欲望时,那么它应该就足够小了。服务越小,`微服务架构(Microservice)` 的优点和缺点也就越明显,使用的服务越小,独立性带来的好处就越多,但管理大量的服务也会更加复杂。
## 自治性
一个微服务就是一个独立的实体,它可以独立被部署,也可以作为一个操作系统进程存在。服务与服务之间存在隔离性,服务之间均通过网络调用进行通信,从而加强服务之间的隔离性,避免紧耦合。服务之间应该可以彼此独立进行修改,并且某一个服务的部署不应该引起该 `服务消费方(Service Consumer)` 的变动。这就要求我们需要考虑这些 `服务提供方(Service Provider)` 什么应该暴露,什么应该隐藏,如果暴露得过多,那么 `服务消费方(Service Consumer)` 会与该服务的内部实现产生耦合,这会使得服务直接产生额外的协调工作,从而降低了服务的自治性。
## 主要好处
### 技术的异构性
在一个由多个服务相互协作的系统中,可以在不同的服务中选用最适合该服务的技术,且由于服务间通过网络调用,服务的实现不会受限于系统的实现语言或框架。也就意味着当系统中一部分需要做性能提升,可以使用性能更好的技术栈重新构建该部分的实现。
### 弹性
实现弹性系统的一个关键概念就是 `舱壁(Bulkhead)`,如果系统中的一个组件或一个服务不可用了,但并没有导致级联故障,那么系统的其它部分还可以正常运行。微服务的 `服务边界` 很显然就是一个 `舱壁(Bulkhead)`,在 `单体架构(Monolithic architecture)` 系统中,也就是传统的 `PHP-FPM` 架构下的系统中,如果某个部分不可用,那么大部分情况是所有功能都不可用,虽然可以通过负载均衡等技术将系统部署在多个节点上面降低系统完全不可用的概率,但对于 `微服务架构(Microservice)` 系统而言,其架构本身就能够很好地处理服务不可用和功能降级等问题。
### 扩展
一个 `单体架构(Monolithic architecture)` 的系统只能作为一个整体进行扩展,即使系统中只有一小部分存在性能问题。如果使用较小的多个服务,则可以只对需要扩展的服务进行扩展,这样可以把那些不需要扩展的服务运行在更廉价的服务器上,节省成本。
### 简化部署
在代码量庞大的 `单体架构(Monolithic architecture)` 系统中,即使只修改了一行代码,也需要重新部署整个系统才能够发布该变更,这种部署影响很大、风险很高,因此涉及到的干系人不敢轻易做部署,于是在实际的操作中部署的频率就不变得很低,版本与版本之间会对系统做了很多的功能或 `Bugfix`,并一次性将大量的变更一次性发布到生产环境中去,但两次发布之间的差异越大,出错的可能性就更大。
当然在传统的 `PHP-FPM` 架构下的开发中,我们可能不会存在这样的问题,因为热更新是一种天然的存在,但利弊是同时存在的。
### 与组织架构相匹配
`单体架构(Monolithic architecture)` 下,且团队的结构也是 "分布式" (异地) 的情况下,大量工程师的代码提交导致的代码冲突与异地的迭代沟通会使维护系统变得更加的复杂,我们都知道一个合适的团队规模的人在一个小型的代码库上工作可获得更高的生产力,那么服务的拆分和归属便能很好的划分相关的职责。
### 可组合性
`分布式系统``面向服务架构(SOA)` 声称的主要好处是易于重用已有的功能,那么在 `微服务架构(Microservice)` 下,更细粒度的服务拆分会将这一优点体现得更淋漓尽致。
### 可重构性高
如果您面对的是一个大型的 `单体架构(Monolithic architecture)` 系统,里面的代码混乱而丑陋,所有人都不敢轻易去重构它。但当你面对的是一个小规模的细粒度的服务时,重构一个服务甚至重写一个对应的服务都是一件相对可操作的事情。在一个大型的 `单体架构(Monolithic architecture)` 系统一天删除上百行代码,您可以确信不会引发任何的问题吗?但在良好的 `微服务架构(Microservice)` 下,我相信直接删除一个服务您也可以游刃有余。
## 没有银弹 No Silver Bullet
虽然 `微服务架构(Microservice)` 好处众多,但 **微服务不是银弹 ** ,您需要面对所有分布式系统都需要面对的复杂性,你可能需要在部署、测试和监控上做很多的工作,在服务间调用、服务的可靠性上做很多工作,甚至您还需要处理类似于 分布式事务 或者与 CAP 相关的问题。尽管 `Hyperf` 已经为您解决了许多的问题,但在实施 `微服务架构(Microservice)` 之前您的团队必须储备足够的分布式系统相关的知识体系,以面对很多您在 `单体架构(Monolithic architecture)` 下可能没有面临过甚至没有考虑过的问题。
*| 本章节部分内容译自 Sam Newman 的 《Building Microservices 》*

View File

@ -100,6 +100,7 @@ use App\Middleware\BarMiddleware;
use App\Middleware\FooMiddleware;
use Hyperf\HttpServer\Annotation\AutoController;
use Hyperf\HttpServer\Annotation\Middleware;
use Hyperf\HttpServer\Annotation\Middlewares;
/**
* @AutoController()

100
doc/en/paginator.md Normal file
View File

@ -0,0 +1,100 @@
# 分页器
在您需要对数据进行分页处理时,可以借助 [hyperf/paginator](https://github.com/hyperf-cloud/paginator) 组件很方便的解决您的问题,您可对您的数据查询进行一定的封装处理,以便更好的使用分页功能,该组件也可用于其它框架上。
通常情况下,您对分页器的需求可能都是存在于数据库查询上,[hyperf/database](https://github.com/hyperf-cloud/database) 数据库组件已经与分页器组件进行了结合,您可以在进行数据查询时很方便的调用分页器来实现分页,具体可查阅 [数据库模型-分页](zh/db/paginator.md) 章节。
# 安装
```bash
composer require hyperf/paginator
```
# 基本使用
只需存在数据集和分页需求,便可通过实例化一个 `Hyperf\Paginator\Paginator` 类来进行分页处理,该类的构造函数接收 `__construct($items, int $perPage, ?int $currentPage = null, array $options = [])` 参数,我们只需以 `数组(Array)``Hyperf\Utils\Colletion` 集合类的形式传递数据集到 `$items` 参数,并设定每页数据量 `$perPage` 和当前页数 `$currentPage` 即可,`$options` 参数则可以通过 `Key-Value` 的形式定义分页器实例内的所有属性,具体可查阅分页器类的内部属性。
```php
<?php
namespace App\Controller;
use Hyperf\HttpServer\Annotation\AutoController;
use Hyperf\HttpServer\Contract\RequestInterface;
use Hyperf\Paginator\Paginator;
/**
* @AutoController()
*/
class UserController
{
public function index(RequestInterface $request)
{
$currentPage = $request->input('page', 1);
$perPage = 2;
$users = [
['id' => 1, 'name' => 'Tom'],
['id' => 2, 'name' => 'Sam'],
['id' => 3, 'name' => 'Tim'],
['id' => 4, 'name' => 'Joe'],
];
return new Paginator($users, $perPage, $currentPage);
}
}
```
# 分页器方法
## 获取当前页数
```php
<?php
$currentPage = $paginator->currentPage();
```
## 获取当前页的条数
```php
<?php
$count = $paginator->count();
```
## 获取当前页中第一条数据的编号
```php
<?php
$firstItem = $paginator->firstItem();
```
## 获取当前页中最后一条数据的编号
```php
<?php
$lastItem = $paginator->lastItem();
```
## 获取是否还有更多的分页
```php
<?php
if ($paginator->hasMorePages()) {
// ...
}
```
## 获取上一页和下一页的 URL
```php
<?php
// 下一页的 URL
$nextPageUrl = $paginator->nextPageUrl();
// 上一页的 URL
$previousPageUrl = $paginator->previousPageUrl();
```
## 获取指定页数的 URL
```php
<?php
// 获取指定 $page 页数的 URL
$url = $paginator->url($page);
```

63
doc/en/pool.md Normal file
View File

@ -0,0 +1,63 @@
# 连接池
## Installation
```bash
composer require hyperf/pool
```
## 为什么需要连接池?
当并发量很低的时候,连接可以临时建立,但当服务吞吐达到几百、几千的时候,频繁 `建立连接 Connect``销毁连接 Close` 就有可能会成为服务的一个瓶颈,那么当服务启动的时候,先建立好若干个连接并存放于一个队列中,当需要使用时从队列中取出一个并使用,使用完后再反还到队列去,而对这个队列数据结构进行维护的,就是连接池。
## 使用连接池
对于 Hyperf 官方提供的组件,都是已经对接好连接池的,在使用上无任何的感知,底层自动完成连接的取用和归还。
## 自定义连接池
定义一个连接池首先需要实现一个继承了 `Hyperf\Pool\Pool` 的子类并实现抽象方法 `createConnection`,并返回一个实现了 `Hyperf\Contract\ConnectionInterface` 接口的对象,这样您创建的连接池对象就已经完成了,如下示例:
```php
<?php
namespace App\Pool;
use Hyperf\Contract\ConnectionInterface;
use Hyperf\Pool\Pool;
class MyConnectionPool extends Pool
{
public function createConnection(): ConnectionInteface
{
return new MyConnection();
}
}
```
这样便可以通过对实例化后的 `MyConnectionPool` 对象调用 `get(): ConnectionInterface``release(ConnectionInterface $connection): void` 方法执行连接的取用和归还了。
## SimplePool
这里框架提供了一个非常简单的连接池实现。
```php
<?php
use Hyperf\Pool\SimplePool\PoolFactory;
use Swoole\Coroutine\Http\Client;
$factory = $container->get(PoolFactory::class);
$pool = $factory->get('your pool name', function () use ($host, $port, $ssl) {
return new Client($host, $port, $ssl);
}, [
'max_connections' => 50
]);
$connection = $pool->get();
$client = $connection->getConnection(); // 即上述 Client.
// Do somethind.
$connection->release();
```

182
doc/en/process.md Normal file
View File

@ -0,0 +1,182 @@
# 自定义进程
[hyperf/process](https://github.com/hyperf-cloud/process) 可以添加一个用户自定义的工作进程,此函数通常用于创建一个特殊的工作进程,用于监控、上报或者其他特殊的任务。在 Server 启动时会自动创建进程并执行指定的子进程函数进程意外退出时Server 会重新拉起进程。
## 创建一个自定义进程
在任意位置实现一个继承 `Hyperf\Process\AbstractProcess` 的子类,并实现接口方法 `handle(): void`,方法内实现您的逻辑代码,我们通过代码来举例:
```php
<?php
declare(strict_types=1);
namespace App\Process;
use Hyperf\Process\AbstractProcess;
class FooProcess extends AbstractProcess
{
public function handle(): void
{
// 您的代码 ...
}
}
```
这样即完成了一个自定义进程类,但该自定义进程类尚未被注册到 `进程管理器(ProcessManager)` 内,我们可以通过 `配置文件``注解` 两种方式的任意一种来完成注册工作。
### 通过配置文件注册
只需在 `config/autoload/processes.php` 内加上您的自定义进程类即可:
```php
// config/autoload/processes.php
return [
\App\Process\FooProcess::class,
];
```
### 通过注解注册
只需在自定义进程类上定义 `@Process` 注解Hyperf 会收集并自动完成注册工作:
```php
<?php
declare(strict_types=1);
namespace App\Process;
use Hyperf\Process\AbstractProcess;
use Hyperf\Process\Annotation\Process;
/**
* @Process(name="foo_process")
*/
class FooProcess extends AbstractProcess
{
public function handle(): void
{
// 您的代码 ...
}
}
```
> 使用 `@Process` 注解时需 `use Hyperf\Process\Annotation\Process;` 命名空间;
## 为进程启动加上条件
有些时候,并不是所有时候都应该启动一个自定义进程,一个自定义进程的启动与否可能会根据某些配置或者某些条件来决定,我们可以通过在自定义进程类内重写 `isEnable(): bool` 方法来实现,默认返回 `true`,即会跟随服务一同启动,如方法返回 `false`,则服务启动时不会启动该自定义进程。
```php
<?php
declare(strict_types=1);
namespace App\Process;
use Hyperf\Process\AbstractProcess;
use Hyperf\Process\Annotation\Process;
/**
* @Process(name="foo_process")
*/
class FooProcess extends AbstractProcess
{
public function handle(): void
{
// 您的代码 ...
}
public function isEnable(): bool
{
// 不跟随服务启动一同启动
return false;
}
}
```
## 设置自定义进程
自定义进程存在一些可设置的参数,均可以通过 在子类上重写参数对应的属性 或 在 `@Process` 注解内定义对应的属性 两种方式来进行定义。
```php
<?php
declare(strict_types=1);
namespace App\Process;
use Hyperf\Process\AbstractProcess;
use Hyperf\Process\Annotation\Process;
/**
* @Process(name="foo_process", name="user-process"redirectStdinStdout=false, pipeType=2, enableCoroutine=true)
*/
class FooProcess extends AbstractProcess
{
/**
* 进程数量
* @var int
*/
public $nums = 1;
/**
* 进程名称
* @var string
*/
public $name = 'user-process';
/**
* 重定向自定义进程的标准输入和输出
* @var bool
*/
public $redirectStdinStdout = false;
/**
* 管道类型
* @var int
*/
public $pipeType = 2;
/**
* 是否启用协程
* @var bool
*/
public $enableCoroutine = true;
}
```
## 使用示例
我们创建一个用于监控失败队列数量的子进程,当失败队列有数据时,报出警告。
```php
<?php
declare(strict_types=1);
namespace App\Process;
use Hyperf\Process\AbstractProcess;
use Hyperf\Process\Annotation\Process;
use Hyperf\Contract\StdoutLoggerInterface;
/**
* @Process(name="demo_process")
*/
class DemoProcess extends AbstractProcess
{
public function handle(): void
{
$logger = $this->container->get(StdoutLoggerInterface::class);
while (true) {
$redis = $this->container->get(\Redis::class);
$count = $redis->llen('queue:failed');
if ($count > 0) {
$logger->warning('The num of failed queue is ' . $count);
}
sleep(1);
}
}
}
```

110
doc/en/rate-limit.md Normal file
View File

@ -0,0 +1,110 @@
# 令牌桶限流器
## Installation
```bash
composer require hyperf/rate-limit
```
## 默认配置
| 配置 | 默认值 | 备注 |
|:--------------:|:------:|:-------------------:|
| create | 1 | 每秒生成令牌数 |
| consume | 1 | 每次请求消耗令牌数 |
| capacity | 2 | 令牌桶最大容量 |
| limitCallback | NULL | 触发限流时回调方法 |
| key | NULL | 生成令牌桶的key |
| waitTimeout | 3 | 排队超时时间 |
```php
<?php
return [
'create' => 1,
'consume' => 1,
'capacity' => 2,
'limitCallback' => null,
'key' => null,
'waitTimeout' => 3,
];
```
## 使用限流器
组件提供 `Hyperf\RateLimit\Annotation\RateLimit` 注解,作用于类、类方法,可以覆盖配置文件。 例如,
```php
<?php
namespace App\Controller;
use Hyperf\HttpServer\Annotation\Controller;
use Hyperf\HttpServer\Annotation\RequestMapping;
use Hyperf\RateLimit\Annotation\RateLimit;
/**
* @Controller(prefix="rate-limit")
*/
class RateLimitController
{
/**
* @RequestMapping(path="test")
* @RateLimit(create=1, capacity=3)
*/
public function test()
{
return ["QPS 1, 峰值3"];
}
/**
* @RequestMapping(path="test2")
* @RateLimit(create=2, consume=2, capacity=4)
*/
public function test2()
{
return ["QPS 2, 峰值2"];
}
}
```
配置优先级 `方法注解 > 类注解 > 配置文件 > 默认配置`
## 触发限流
当限流被触发时, 默认会抛出 `Hyperf\RateLimit\Exception\RateLimitException` 异常
可以通过[异常处理](zh/exception-handler.md)或者配置 `limitCallback` 限流回调处理。
例如:
```php
<?php
namespace App\Controller;
use Hyperf\Di\Aop\ProceedingJoinPoint;
use Hyperf\HttpServer\Annotation\Controller;
use Hyperf\HttpServer\Annotation\RequestMapping;
use Hyperf\RateLimit\Annotation\RateLimit;
/**
* @Controller(prefix="rate-limit")
* @RateLimit(limitCallback={RateLimitController::class, 'limitCallback'})
*/
class RateLimitController
{
/**
* @RequestMapping(path="test")
* @RateLimit(create=1, capacity=3)
*/
public function test()
{
return ["QPS 1, 峰值3"];
}
public static function limitCallback(float $seconds, ProceedingJoinPoint $proceedingJoinPoint)
{
// $seconds 下次生成Token 的间隔, 单位为秒
// $proceedingJoinPoint 此次请求执行的切入点
// 可以通过调用 `$proceedingJoinPoint->process()` 继续执行或者自行处理
return $proceedingJoinPoint->process();
}
}
```

130
doc/en/redis.md Normal file
View File

@ -0,0 +1,130 @@
# Redis
## Installation
```
composer require hyperf/redis
```
## 配置
| 配置项 | 类型 | 默认值 | 备注 |
|:------:|:-------:|:-----------:|:---------:|
| host | string | 'localhost' | Redis地址 |
| auth | string | 无 | 密码 |
| port | integer | 6379 | 端口 |
| db | integer | 0 | DB |
```php
<?php
return [
'default' => [
'host' => env('REDIS_HOST', 'localhost'),
'auth' => env('REDIS_AUTH', ''),
'port' => (int) env('REDIS_PORT', 6379),
'db' => (int) env('REDIS_DB', 0),
'pool' => [
'min_connections' => 1,
'max_connections' => 10,
'connect_timeout' => 10.0,
'wait_timeout' => 3.0,
'heartbeat' => -1,
'max_idle_time' => (float) env('REDIS_MAX_IDLE_TIME', 60),
],
],
];
```
## 使用
`hyperf/redis` 实现了 `ext-redis` 代理和连接池,用户可以直接使用\Redis客户端。
```php
<?php
$redis = $this->container->get(\Redis::class);
$result = $redis->keys('*');
```
## 多库配置
有时候在实际使用中,一个 `Redis` 库并不满足需求,一个项目往往需要配置多个库,这个时候,我们就需要修改一下配置文件 `redis.php`,如下:
```php
<?php
return [
'default' => [
'host' => env('REDIS_HOST', 'localhost'),
'auth' => env('REDIS_AUTH', ''),
'port' => (int) env('REDIS_PORT', 6379),
'db' => (int) env('REDIS_DB', 0),
'pool' => [
'min_connections' => 1,
'max_connections' => 10,
'connect_timeout' => 10.0,
'wait_timeout' => 3.0,
'heartbeat' => -1,
'max_idle_time' => (float) env('REDIS_MAX_IDLE_TIME', 60),
],
],
// 增加一个名为 foo 的 Redis 连接池
'foo' => [
'host' => env('REDIS_HOST', 'localhost'),
'auth' => env('REDIS_AUTH', ''),
'port' => (int) env('REDIS_PORT', 6379),
'db' => 1,
'pool' => [
'min_connections' => 1,
'max_connections' => 10,
'connect_timeout' => 10.0,
'wait_timeout' => 3.0,
'heartbeat' => -1,
'max_idle_time' => (float) env('REDIS_MAX_IDLE_TIME', 60),
],
],
];
```
### 通过代理类使用
我们可以重写一个 `FooRedis` 类并继承 `Hyperf\Redis\Redis` 类,修改 `poolName` 为上述的 `foo`,即可完成对连接池的切换,示例:
```php
<?php
use Hyperf\Redis\Redis;
class FooRedis extends Redis
{
// 对应的 Pool 的 key 值
protected $poolName = 'foo';
}
// 通过 DI 容器获取或直接注入当前类
$redis = $this->container->get(FooRedis::class);
$result = $redis->keys('*');
```
### 使用工厂类
在每个库对应一个静态的场景时,通过代理类是一种很好的区分的方法,但有时候需求可能会更加的动态,这时候我们可以通过 `Hyperf\Redis\RedisFactory` 工厂类来动态的传递 `poolName` 来获得对应的连接池的客户端,而无需为每个库创建代理类,示例如下:
```php
<?php
use Hyperf\Redis\RedisFactory;
// 通过 DI 容器获取或直接注入 RedisFactory 类
$redis = $this->container->get(RedisFactory::class)->get('foo');
$result = $redis->keys('*');
```

275
doc/en/request.md Normal file
View File

@ -0,0 +1,275 @@
# 请求对象
`请求对象(Request)` 是完全基于 [PSR-7](https://www.php-fig.org/psr/psr-7/) 标准实现的,由 [hyperf/http-message](https://github.com/hyperf-cloud/http-message) 组件提供实现支持。
> 注意 [PSR-7](https://www.php-fig.org/psr/psr-7/) 标准为 `请求(Request)` 进行了 `immutable 机制` 的设计,所有以 `with` 开头的方法的返回值都是一个新对象,不会修改原对象的值
## Installation
该组件完全独立,适用于任何一个框架项目。
```bash
composer require hyperf/http-message
```
> 如用于其它框架项目则仅支持 PSR-7 提供的 API具体可直接查阅 PSR-7 的相关规范,该文档所描述的使用方式仅限于使用 Hyperf 时的用法。
## 获得请求对象
可以通过容器注入 `Hyperf\HttpServer\Contract\RequestInterface` 获得 对应的 `Hyperf\HttpServer\Request`,实际注入的对象为一个代理对象,代理的对象为每个请求的 `PSR-7 请求对象(Request)`,也就意味着仅可在 `onRequest` 声明周期内可获得此对象,下面是一个获取示例:
```php
declare(strict_types=1);
namespace App\Controller;
use Hyperf\HttpServer\Contract\RequestInterface;
use Hyperf\HttpServer\Annotation\AutoController;
/**
* @AutoController()
*/
class IndexController
{
public function info(RequestInterface $request)
{
// ...
}
}
```
### 依赖注入与参数
如果希望通过控制器方法参数获取路由参数,可以在依赖项之后列出对应的参数,框架会自动将对应的参数注入到方法参数内,比如您的路由是这样定义的:
```php
// 注解方式
/**
* @GetMapping(path="/user/{id:\d+}")
*/
// 配置方式
use Hyperf\HttpServer\Router\Router;
Router::addRoute(['GET', 'HEAD'], '/user/{id:\d+}', [\App\Controller\IndexController::class, 'user']);
```
则可以通过在方法参数上声明 `$id` 参数获得 `Query` 参数 `id`,如下所示:
```php
declare(strict_types=1);
namespace App\Controller;
use Hyperf\HttpServer\Contract\RequestInterface;
use Hyperf\HttpServer\Annotation\AutoController;
/**
* @AutoController()
*/
class IndexController
{
public function info(RequestInterface $request, int $id)
{
// ...
}
}
```
### 请求路径 & 方法
`Hyperf\HttpServer\Contract\RequestInterface` 除了使用 [PSR-7](https://www.php-fig.org/psr/psr-7/) 标准定义的 `APIs` 之外,还提供了多种方法来检查请求,下面我们提供一些方法的示例:
#### 获取请求路径
`path()` 方法返回请求的路径信息。也就是说,如果传入的请求的目标地址是 `http://domain.com/foo/bar?baz=1`,那么 `path()` 将会返回 `foo/bar`
```php
$uri = $request->path();
```
`is(...$patterns)` 方法可以验证传入的请求路径和指定规则是否匹配。使用这个方法的时,你也可以传递一个 `*` 字符作为通配符:
```php
if ($request->is('user/*')) {
// ...
}
```
#### 获取请求的 URL
你可以使用 `url()``fullUrl()` 方法去获取传入请求的完整 `URL`。`url()` 方法返回不带有 `Query 参数``URL`,而 `fullUrl()` 方法的返回值包含 `Query 参数`
```php
// 没有查询参数
$url = $request->url();
// 带上查询参数
$url = $request->fullUrl();
```
#### 获取请求方法
`getMethod()` 方法将返回 `HTTP` 的请求方法。你也可以使用 `isMethod(string $method)` 方法去验证 `HTTP` 的请求方法与指定规则是否匹配:
```php
$method = $request->getMethod();
if ($request->isMethod('post')) {
// ...
}
```
### PSR-7 请求及方法
[hyperf/http-message](https://github.com/hyperf-cloud/http-message) 组件本身是一个实现了 [PSR-7](https://www.php-fig.org/psr/psr-7/) 标准的组件,相关方法都可以通过注入的 `请求对象(Request)` 来调用。
如果注入时声明为 [PSR-7](https://www.php-fig.org/psr/psr-7/) 标准的 `Psr\Http\Message\ServerRequestInterface` 接口,则框架会自动转换为等同于 `Hyperf\HttpServer\Contract\RequestInterface``Hyperf\HttpServer\Request` 对象。
> 建议使用 `Hyperf\HttpServer\Contract\RequestInterface` 来注入,这样可获得 IDE 对专属方法的自动完成提醒支持。
## 输入预处理 & 规范化
## 获取输入
### 获取所有输入
您可以使用 `all()` 方法以 `数组` 形式获取到所有输入数据:
```php
$all = $request->all();
```
### 获取指定输入值
通过 `input(string $key, $default = null)``inputs(array $keys, $default = null): array` 获取 `一个``多个` 任意形式的输入值:
```php
// 存在则返回,不存在则返回 null
$name = $request->input('name');
// 存在则返回,不存在则返回默认值 Hyperf
$name = $request->input('name', 'Hyperf');
```
如果传输表单数据中包含「数组」形式的数据,那么可以使用「点」语法来获取数组:
```php
$name = $request->input('products.0.name');
$names = $request->input('products.*.name');
```
### 从查询字符串获取输入
使用 `input`, `inputs` 方法可以从整个请求中获取输入数据(包括 `Query 参数`),而 `query(?string $key = null, $default = null)` 方法可以只从查询字符串中获取输入数据:
```php
// 存在则返回,不存在则返回 null
$name = $request->query('name');
// 存在则返回,不存在则返回默认值 Hyperf
$name = $request->query('name', 'Hyperf');
// 不传递参数则以关联数组的形式返回所有 Query 参数
$name = $request->query();
```
### 获取 `JSON` 输入信息
如果请求的 `Body` 数据格式是 `JSON`,则只要 `请求对象(Request)``Content-Type` `Header值` 正确设置为 `application/json`,就可以通过 `input(string $key, $default = null)` 方法访问 `JSON` 数据,你甚至可以使用 「点」语法来读取 `JSON` 数组:
```php
// 存在则返回,不存在则返回 null
$name = $request->input('user.name');
// 存在则返回,不存在则返回默认值 Hyperf
$name = $request->input('user.name', 'Hyperf');
// 不传递参数则以数组形式返回所有 Json 数据
$name = $request->input();
```
### 确定是否存在输入值
要判断请求是否存在某个值,可以使用 `has($keys)` 方法。如果请求中存在该值则返回 `true`,不存在则返回 `false``$keys` 可以传递一个字符串,或传递一个数组包含多个字符串,只有全部存在才会返回 `true`
```php
// 仅判断单个值
if ($request->has('name')) {
// ...
}
// 同时判断多个值
if ($request->has(['name', 'email'])) {
// ...
}
```
## Cookies
### 从请求中获取 Cookies
使用 `getCookieParams()` 方法从请求中获取所有的 `Cookies`,结果会返回一个关联数组。
```php
$cookies = $request->getCookieParams();
```
如果希望获取某一个 `Cookie` 值,可通过 `cookie(string $key, $default = null)` 方法来获取对应的值:
```php
// 存在则返回,不存在则返回 null
$name = $request->cookie('name');
// 存在则返回,不存在则返回默认值 Hyperf
$name = $request->cookie('name', 'Hyperf');
```
## 文件
### 获取上传文件
你可以使用 `file(string $key, $default): ?Hyperf\HttpMessage\Upload\UploadedFile` 方法从请求中获取上传的文件对象。如果上传的文件存在则该方法返回一个 `Hyperf\HttpMessage\Upload\UploadedFile` 类的实例,该类继承了 `PHP``SplFileInfo` 类的同时也提供了各种与文件交互的方法:
```php
// 存在则返回一个 Hyperf\HttpMessage\Upload\UploadedFile 对象,不存在则返回 null
$file = $request->file('photo');
```
### 检查文件是否存在
您可以使用 `hasFile(string $key): bool` 方法确认请求中是否存在文件:
```php
if ($request->hasFile('photo')) {
// ...
}
```
### 验证成功上传
除了检查上传的文件是否存在外,您也可以通过 `isValid(): bool` 方法验证上传的文件是否有效:
```php
if ($request->file('photo')->isValid()) {
// ...
}
```
### 文件路径 & 扩展名
`UploadedFile` 类还包含访问文件的完整路径及其扩展名方法。`getExtension()` 方法会根据文件内容判断文件的扩展名。该扩展名可能会和客户端提供的扩展名不同:
```php
// 该路径为上传文件的临时路径
$path = $request->file('photo')->getPath();
// 由于 Swoole 上传文件的 tmp_name 并没有保持文件原名,所以这个方法已重写为获取原文件名的后缀名
$extension = $request->file('photo')->getExtension();
```
### 存储上传文件
上传的文件在未手动储存之前,都是存在一个临时位置上的,如果您没有对该文件进行储存处理,则在请求结束后会从临时位置上移除,所以我们可能需要对文件进行持久化储存处理,通过 `moveTo(string $targetPath): void` 将临时文件移动到 `$targetPath` 位置持久化储存,代码示例如下:
```php
$file = $request->file('photo');
$file->moveTo('/foo/bar.jpg');
// 通过 isMoved(): bool 方法判断方法是否已移动
if ($file->isMoved()) {
// ...
}
```

128
doc/en/response.md Normal file
View File

@ -0,0 +1,128 @@
# 响应
在 Hyperf 里可通过 `Hyperf\HttpServer\Contract\ResponseInterface` 接口类来注入 `Response` 代理对象对响应进行处理,默认返回 `Hyperf\HttpServer\Response` 对象,该对象可直接调用所有 `Psr\Http\Message\ResponseInterface` 的方法。
## 返回 Json 格式
`Hyperf\HttpServer\Contract\ResponseInterface` 提供了 `json($data)` 方法用于快速返回 `Json` 格式,并设置 `Content-Type``application/json``$data` 接受一个数组或为一个实现了 `Hyperf\Utils\Contracts\Arrayable` 接口的对象。
```php
<?php
namespace App\Controller;
use Hyperf\HttpServer\Contract\ResponseInterface;
use Psr\Http\Message\ResponseInterface as Psr7ResponseInterface;
class IndexController
{
public function json(ResponseInterface $response): Psr7ResponseInterface
{
$data = [
'key' => 'value'
];
return $response->json($data);
}
}
```
## 返回 Xml 格式
`Hyperf\HttpServer\Contract\ResponseInterface` 提供了 `xml($data)` 方法用于快速返回 `XML` 格式,并设置 `Content-Type``application/xml``$data` 接受一个数组或为一个实现了 `Hyperf\Utils\Contracts\Xmlable` 接口的对象。
```php
<?php
namespace App\Controller;
use Hyperf\HttpServer\Contract\ResponseInterface;
use Psr\Http\Message\ResponseInterface as Psr7ResponseInterface;
class IndexController
{
public function xml(ResponseInterface $response): Psr7ResponseInterface
{
$data = [
'key' => 'value'
];
return $response->xml($data);
}
}
```
## 返回 Raw 格式
`Hyperf\HttpServer\Contract\ResponseInterface` 提供了 `raw($data)` 方法用于快速返回 `raw` 格式,并设置 `Content-Type``plain/text``$data` 接受一个字符串或为一个实现了 `__toString()` 方法的对象。
```php
<?php
namespace App\Controller;
use Hyperf\HttpServer\Contract\ResponseInterface;
use Psr\Http\Message\ResponseInterface as Psr7ResponseInterface;
class IndexController
{
public function raw(ResponseInterface $response): Psr7ResponseInterface
{
return $response->raw('Hello Hyperf.');
}
}
```
## 返回视图
Hyperf 暂不支持视图返回,欢迎社区贡献相关的 PR。
## 重定向
`Hyperf\HttpServer\Contract\ResponseInterface` 提供了 `redirect(string $toUrl, int $status = 302, string $schema = 'http')` 返回一个已设置重定向状态的 `Psr7ResponseInterface` 对象。
`redirect` 方法:
| 参数 | 类型 | 默认值 | 备注 |
|:-------------------:|:------:|:---------------:|:------------------:|
| toUrl | string | 无 | 如果参数不存在 `http://``https://` 则根据当前服务的 Host 自动拼接对应的 URL且根据 `$schema` 参数拼接协议 |
| status | int | 302 | 响应状态码 |
| schema | string | http | 当 `$toUrl` 不存在 `http://``https://` 时生效,仅可传递 `http``https` |
```php
<?php
namespace App\Controller;
use Hyperf\HttpServer\Contract\ResponseInterface;
use Psr\Http\Message\ResponseInterface as Psr7ResponseInterface;
class IndexController
{
public function redirect(ResponseInterface $response): Psr7ResponseInterface
{
// redirect() 方法返回的是一个 Psr\Http\Message\ResponseInterface 对象,需再 return 回去
return $response->redirect('/anotherUrl');
}
}
```
## Cookie 设置
```php
<?php
namespace App\Controller;
use Hyperf\HttpServer\Contract\ResponseInterface;
use Psr\Http\Message\ResponseInterface as Psr7ResponseInterface;
use Swoft\Http\Message\Cookie\Cookie;
class IndexController
{
public function cookie(ResponseInterface $response): Psr7ResponseInterface
{
$cookie = new Cookie('key', 'value');
return $response->withCookie($cookie)->withContent('Hello Hyperf.');
}
}
```
## Gzip 压缩
## 分块传输编码 Chunk
## 返回文件下载

189
doc/en/router.md Normal file
View File

@ -0,0 +1,189 @@
# 路由
默认情况下路由由 [nikic/fast-route](https://github.com/nikic/FastRoute) 提供支持,并由 [hyperf/http-server](https://github.com/hyperf-cloud/http-server) 组件负责接入到 `Hyperf` 中,`RPC` 路由由对应的 [hyperf/rpc-server](https://github.com/hyperf-cloud/rpc-server) 组件负责。
## HTTP 路由
### 通过配置文件定义路由
在 [hyperf-skeleton](https://github.com/hyperf-cloud/hyperf-skeleton) 骨架下,默认在 `config/routes.php` 文件内完成所有的路由定义,当然如果您路由众多,您也可以对该文件进行扩展,以适应您的需求,但 `Hyperf` 还支持 `注解路由`,我们更推荐使用 `注解路由`,特别是在路由众多的情况下。
#### 通过闭包定义路由
构建一个最基本的路由只需一个 URI 和一个 `闭包(Closure)`,我们直接通过代码来演示一下:
```php
<?php
use Hyperf\HttpServer\Router\Router;
Router::get('/hello-hyperf', function () {
return 'Hello Hyperf.';
});
```
您可以通过 浏览器 或 `cURL` 命令行来请求 `http://host:port/hello-hyperf` 来访问该路由。
#### 定义标准路由
所谓标准路由指的是由 `控制器(Controller)``操作(Action)` 来处理的路由,如果您使用 `请求处理器(Request Handler)` 模式也是类似的,我们通过代码来演示一下:
```php
<?php
use Hyperf\HttpServer\Router\Router;
// 下面三种方式的任意一种都可以达到同样的效果
Router::get('/hello-hyperf', 'App\Controller\IndexController::hello');
Router::get('/hello-hyperf', 'App\Controller\IndexController@hello');
Router::get('/hello-hyperf', [App\Controller\IndexController::class, 'hello']);
```
该路由定义为将 `/hello-hyperf` 路径绑定到 `App\Controller\IndexController` 下的 `hello` 方法。
#### 可用的路由方法
路由器提供了多种方法帮助您注册任何的 HTTP 请求的路由:
```php
use Hyperf\HttpServer\Router\Router;
// 注册与方法名一致的 HTTP METHOD 的路由
Router::get($uri, $callback);
Router::post($uri, $callback);
Router::put($uri, $callback);
Router::patch($uri, $callback);
Router::delete($uri, $callback);
Router::head($uri, $callback);
// 注册任意 HTTP METHOD 的路由
Router::addRoute($httpMethod, $uri, $callback);
```
有时候您可能需要注册一个可以同时响应多种 HTTP METHOD 请求的路由,可以通过 `addRoute` 方法实现定义:
```php
use Hyperf\HttpServer\Router\Router;
Router::addRoute(['GET', 'POST','PUT','DELETE'], $uri, $callback);
```
#### 路由组的定义方式
实际路由为 `gourp/route`, 即 `/user/index`, `/user/store`, `/user/update`, `/user/delete`
```php
Router::addGroup('/user/',function (){
Router::get('index','App\Controller\UserController@index');
Router::post('store','App\Controller\UserController@store');
Router::get('update','App\Controller\UserController@update');
Router::post('delete','App\Controller\UserController@delete');
});
```
### 通过注解定义路由
`Hyperf` 提供了非常便利的 [注解](zh/annotation.md) 路由功能,您可以直接在任意类上通过定义 `@Controller``@AutoController` 注解来完成一个路由的定义。
#### `@AutoController` 注解
`@AutoController` 为绝大多数简单的访问场景提供路由绑定支持,使用 `@AutoController` 时则 `Hyperf` 会自动解析所在类的所有 `public` 方法并提供 `GET``POST` 两种请求方式。
> 使用 `@AutoController` 注解时需 `use Hyperf\HttpServer\Annotation\AutoController;` 命名空间;
```php
<?php
declare(strict_types=1);
namespace App\Controller;
use Hyperf\HttpServer\Contract\RequestInterface;
use Hyperf\HttpServer\Annotation\AutoController;
/**
* @AutoController()
*/
class UserController
{
// Hyperf 会自动为此方法生成一个 /user/index 的路由,允许通过 GET 或 POST 方式请求
public function index(RequestInterface $request)
{
// 从请求中获得 id 参数
$id = $request->input('id', 1);
return (string)$id;
}
}
```
#### `@Controller` 注解
`@Controller` 为满足更细致的路由定义需求而存在,使用 `@Controller` 注解用于表名当前类为一个 `Controller` 类,同时需配合 `@RequestMapping` 注解来对请求方法和请求路径进行更详细的定义。
我们也提供了多种快速便捷的 `Mapping` 注解,如 `@GetMapping`、`@PostMapping`、`@PutMapping`、`@PatchMapping`、`@DeleteMapping` 5种便捷的注解用于表明允许不同的请求方法。
> 使用 `@Controller` 注解时需 `use Hyperf\HttpServer\Annotation\Controller;` 命名空间;
> 使用 `@RequestMapping` 注解时需 `use Hyperf\HttpServer\Annotation\RequestMapping;` 命名空间;
> 使用 `@GetMapping` 注解时需 `use Hyperf\HttpServer\Annotation\GetMapping;` 命名空间;
> 使用 `@PostMapping` 注解时需 `use Hyperf\HttpServer\Annotation\PostMapping;` 命名空间;
> 使用 `@PutMapping` 注解时需 `use Hyperf\HttpServer\Annotation\PutMapping;` 命名空间;
> 使用 `@PatchMapping` 注解时需 `use Hyperf\HttpServer\Annotation\PatchMapping;` 命名空间;
> 使用 `@DeleteMapping` 注解时需 `use Hyperf\HttpServer\Annotation\DeleteMapping;` 命名空间;
```php
<?php
declare(strict_types=1);
namespace App\Controller;
use Hyperf\HttpServer\Contract\RequestInterface;
use Hyperf\HttpServer\Annotation\Controller;
use Hyperf\HttpServer\Annotation\RequestMapping;
/**
* @Controller()
*/
class UserController
{
// Hyperf 会自动为此方法生成一个 /user/index 的路由,允许通过 GET 或 POST 方式请求
/**
* @RequestMapping(path="index", methods="get,post")
*/
public function index(RequestInterface $request)
{
// 从请求中获得 id 参数
$id = $request->input('id', 1);
return (string)$id;
}
}
```
#### 注解参数
`@Controller``@AutoController` 都提供了 `prefix``server` 两个参数。
`prefix` 表示该 `Controller` 下的所有方法路由的前缀,默认为类名的小写,如 `UserController``prefix` 默认为 `user`,如类内某一方法的 `path``index`,则最终路由为 `/user/index`
需要注意的是 `prefix` 并非一直有效,当类内的方法的 `path``/` 开头时,则表明路径从 `URI` 头部开始定义,也就意味着会忽略 `prefix` 的值。
`server` 表示该路由是定义在哪个 `Server` 之上的,由于 `Hyperf` 支持同时启动多个 `Server`,也就意味着有可能会同时存在多个 `HTTP Server`,则在定义路由是可以通过 `server` 参数来进行区分这个路由是为了哪个 `Server` 定义的,默认为 `http`
### 路由参数
> 本框架定义的路由参数必须和控制器参数键名、类型保持一致,否则控制器无法接受到相关参数
```php
Router::get('/user/{id}', 'App\Controller\UserController::info')
```
```php
public function index(int $id)
{
$user = User::find($id);
return $user->toArray();
}
```
#### 必填参数
我们可以对 `$uri` 进行一些参数定义,通过 `{}` 来声明参数,如 `/user/{id}` 则声明了 `id` 值为一个必填参数。
#### 可选参数
有时候您可能会希望这个参数是可选的,您可以通过 `[]` 来声明中括号内的参数为一个可选参数,如 `/user/[{id}]`

View File

@ -0,0 +1,36 @@
# 服务注册
在进行服务拆分之后服务的数量会变得非常多而每个服务又可能会有非常多的集群节点来提供服务那么为保障系统的正常运行必然需要有一个中心化的组件完成对各个服务的整合即将分散于各处的服务进行汇总汇总的信息可以是提供服务的组件名称、地址、数量等每个组件拥有一个监听设备当本组件内的某个服务的状态变化时报告至中心化的组件进行状态的更新。服务的调用方在请求某项服务时首先到中心化组件获取可提供该项服务的组件信息IP、端口等通过默认或自定义的策略选择该服务的某一提供者进行访问实现服务的调用。那么这个中心化的组件我们一般称之为 `服务中心`,在 Hyperf 里,我们实现了以 `Consul` 为服务中心的组件支持,后续将适配更多的服务中心。
# 注册服务
注册服务可通过 `@RpcService` 注解对一个类进行定义,即为发布这个服务了,目前 Hyperf 仅适配了 JSON RPC 协议,具体内容也可到 [JSON RPC 服务](./zh/json-rpc.md) 章节了解详情。
```php
<?php
namespace App\JsonRpc;
use Hyperf\RpcServer\Annotation\RpcService;
/**
* @RpcService(name="CalculatorService", protocol="jsonrpc-http", server="jsonrpc-http")
*/
class CalculatorService implements CalculatorServiceInterface
{
// 实现一个加法方法,这里简单的认为参数都是 int 类型
public function caculate(int $a, int $b): int
{
// 这里是服务方法的具体实现
return $a + $b;
}
}
```
`@RpcService` 共有 `4` 个参数:
`name` 属性为定义该服务的名称这里定义一个全局唯一的名字即可Hyperf 会根据该属性生成对应的 ID 注册到服务中心去;
`protocol` 属性为定义该服务暴露的协议,目前仅支持 `jsonrpc``jsonrpc-http`,分别对应于 TCP 协议和 HTTP 协议下的两种协议,默认值为 `jsonrpc-http`,这里的值对应在 `Hyperf\Rpc\ProtocolManager` 里面注册的协议的 `key`,这两个本质上都是 JSON RPC 协议,区别在于数据格式化、数据打包、数据传输器等不同。
`server` 属性为绑定该服务类发布所要承载的 `Server`,默认值为 `jsonrpc-http`,该属性对应 `config/autoload/server.php` 文件内 `servers` 下所对应的 `name`,这里也就意味着我们需要定义一个对应的 `Server`,我们下一章节具体阐述这里应该怎样去处理;
`publishTo` 属性为定义该服务所要发布的服务中心,目前仅支持 `consul` 或为空,为空时代表不发布该服务到服务中心去,但也就意味着您需要手动处理服务发现的问题,当值为 `consul` 时需要对应配置好 [hyperf/consul](./consul.md) 组件的相关配置;
> 使用 `@RpcService` 注解需 use Hyperf\RpcServer\Annotation\RpcService; 命名空间。

128
doc/en/swoole-dashboard.md Normal file
View File

@ -0,0 +1,128 @@
# Swoole Dashboard
[dashboard](https://www.swoole-cloud.com/dashboard.html) 作为 `Swoole` 官方出品、更专一、更专业。
- 时刻掌握应用架构模型
- 分布式跨应用链路追踪
- 完善的系统监控
- 零成本接入
- 全面分析报告服务状况
## Installation
注册完账户后,进入[控制台](https://www.swoole-cloud.com/dashboard/catdemo/),并申请试用,下载对应客户端。
相关文档,请移步 [试用文档](https://www.yuque.com/swoole-wiki/try) 或 [详细文档](https://www.yuque.com/swoole-wiki/dam5n7)
> 具体文档地址,以从控制台下载的对应客户端中展示的为准。
将客户端中的所有文件以及以下两个文件复制到项目目录 `.build` 中,
entrypoint.sh
```bash
#!/usr/bin/env bash
/opt/swoole/script/php/swoole_php /opt/swoole/node-agent/src/node.php &
php /opt/www/bin/hyperf.php start
```
swoole-tracker.ini
```bash
[swoole_tracker]
extension=/opt/swoole_tracker.so
apm.enable=1 #打开总开关
apm.sampling_rate=100 #采样率 例如100%
# 手动埋点时再添加
apm.enable_xhprof=1 #开启性能分析功能 默认0 即为关闭模式
apm.enable_memcheck=1 #开启内存泄漏检测 默认0 关闭
```
然后将下面的 `Dockerfile` 复制到项目根目录中。
```dockerfile
FROM hyperf/hyperf:7.2-alpine-cli
LABEL maintainer="Hyperf Developers <group@hyperf.io>" version="1.0" license="MIT"
##
# ---------- env settings ----------
##
# --build-arg timezone=Asia/Shanghai
ARG timezone
ENV TIMEZONE=${timezone:-"Asia/Shanghai"} \
COMPOSER_VERSION=1.8.6 \
APP_ENV=prod
RUN set -ex \
&& apk update \
# install composer
&& cd /tmp \
&& wget https://github.com/composer/composer/releases/download/${COMPOSER_VERSION}/composer.phar \
&& chmod u+x composer.phar \
&& mv composer.phar /usr/local/bin/composer \
# show php version and extensions
&& php -v \
&& php -m \
# ---------- some config ----------
&& cd /etc/php7 \
# - config PHP
&& { \
echo "upload_max_filesize=100M"; \
echo "post_max_size=108M"; \
echo "memory_limit=1024M"; \
echo "date.timezone=${TIMEZONE}"; \
} | tee conf.d/99-overrides.ini \
# - config timezone
&& ln -sf /usr/share/zoneinfo/${TIMEZONE} /etc/localtime \
&& echo "${TIMEZONE}" > /etc/timezone \
# ---------- clear works ----------
&& rm -rf /var/cache/apk/* /tmp/* /usr/share/man \
&& echo -e "\033[42;37m Build Completed :).\033[0m\n"
COPY . /opt/www
WORKDIR /opt/www/.build
# 这里的地址,以客户端中显示的为准
RUN ./deploy_env.sh www.swoole-cloud.com \
&& chmod 755 entrypoint.sh \
&& cp swoole_tracker72.so /opt/swoole_tracker.so \
&& cp swoole-tracker.ini /etc/php7/conf.d/swoole-tracker.ini \
&& php -m
WORKDIR /opt/www
RUN composer install --no-dev \
&& composer dump-autoload -o \
&& php /opt/www/bin/hyperf.php di:init-proxy
EXPOSE 9501
ENTRYPOINT ["sh", ".build/entrypoint.sh"]
```
## 使用
首先安装一下对应组件
```bash
composer require hyperf/swoole-dashboard dev-master
```
然后将以下 `Middleware` 写到 `middleware.php` 中。
```php
<?php
return [
'http' => [
Hyperf\SwooleDashboard\Middleware\HttpServerMiddleware::class
],
];
```

203
doc/en/testing.md Normal file
View File

@ -0,0 +1,203 @@
# 自动化测试
在 Hyperf 里测试默认通过 `phpunit` 来实现,但由于 Hyperf 是一个协程框架,所以默认的 `phpunit` 并不能很好的工作,因此我们提供了一个 `co-phpunit` 脚本来进行适配,您可直接调用脚本或者使用对应的 composer 命令来运行。自动化测试没有特定的组件,但是在 Hyperf 提供的骨架包里都会有对应实现。
```
composer require hyperf/testing
```
```json
"scripts": {
"test": "./test/co-phpunit -c phpunit.xml --colors=always"
},
```
## Bootstrap
Hyperf 提供了默认的 `bootstrap.php` 文件,它让用户在运行单元测试时,扫描并加载对应的库到内存里。
```php
<?php
declare(strict_types=1);
error_reporting(E_ALL);
! defined('BASE_PATH') && define('BASE_PATH', dirname(__DIR__, 1));
\Swoole\Runtime::enableCoroutine(true);
require BASE_PATH . '/vendor/autoload.php';
require BASE_PATH . '/config/container.php';
```
> 当用户修改的代码需要重新生成代理类时,需要主动运行一下脚本。因为你单元测试运行时,并不会重置代理类。
```
# 重新生成代理类
php bin/hyperf.php di:init-proxy
# 运行单元测试
composer test
```
## 模拟 HTTP 请求
在开发接口时我们通常需要一段自动化测试脚本来保证我们提供的接口按预期在运行Hyperf 框架下提供了 `Hyperf\Testing\Client` 类,可以让您在不启动 Server 的情况下,模拟 HTTP 服务的请求:
```php
<?php
use Hyperf\Testing\Client;
$client = make(Client::class);
$result = $client->get('/');
```
因为 Hyperf 支持多端口配置,除了验证默认的端口接口外,如果验证其他端口的接口呢?
```php
<?php
use Hyperf\Testing\Client;
$client = make(Client::class,['server' => 'adminHttp']);
$result = $client->json('/user/0',[
'nickname' => 'Hyperf'
]);
```
## 示例
让我们写个小 DEMO 来测试一下。
```php
<?php
declare(strict_types=1);
namespace HyperfTest\Cases;
use Hyperf\Testing\Client;
use PHPUnit\Framework\TestCase;
/**
* @internal
* @coversNothing
*/
class ExampleTest extends TestCase
{
/**
* @var Client
*/
protected $client;
public function __construct($name = null, array $data = [], $dataName = '')
{
parent::__construct($name, $data, $dataName);
$this->client = make(Client::class);
}
public function testExample()
{
$this->assertTrue(true);
$res = $this->client->get('/');
$this->assertSame(0, $res['code']);
$this->assertSame('Hello Hyperf.', $res['data']['message']);
$this->assertSame('GET', $res['data']['method']);
$this->assertSame('Hyperf', $res['data']['user']);
$res = $this->client->get('/', ['user' => 'developer']);
$this->assertSame(0, $res['code']);
$this->assertSame('developer', $res['data']['user']);
$res = $this->client->post('/', [
'user' => 'developer',
]);
$this->assertSame('Hello Hyperf.', $res['data']['message']);
$this->assertSame('POST', $res['data']['method']);
$this->assertSame('developer', $res['data']['user']);
$res = $this->client->json('/', [
'user' => 'developer',
]);
$this->assertSame('Hello Hyperf.', $res['data']['message']);
$this->assertSame('POST', $res['data']['method']);
$this->assertSame('developer', $res['data']['user']);
$res = $this->client->file('/', ['name' => 'file', 'file' => BASE_PATH . '/README.md']);
$this->assertSame('Hello Hyperf.', $res['data']['message']);
$this->assertSame('POST', $res['data']['method']);
$this->assertSame('README.md', $res['data']['file']);
}
}
```
## 调试代码
在FPM场景下我们通常改完代码然后打开浏览器访问对应接口所以我们通常会需要两个函数 `dd``dump`,但 `Hyperf` 跑在 `CLI` 模式下,就算提供了这两个函数,也需要在 `CLI` 中重启 `Server`,然后再到浏览器中调用对应接口查看结果。这样其实并没有简化流程,反而更麻烦了。
接下来,我来介绍如何通过配合 `testing`,来快速调试代码,顺便完成单元测试。
假设我们在 `UserDao` 中实现了一个查询用户信息的函数
```php
namespace App\Service\Dao;
use App\Constants\ErrorCode;
use App\Exception\BusinessException;
use App\Model\User;
class UserDao extends Dao
{
/**
* @param $id
* @param bool $throw
* @return
*/
public function first($id, $throw = true)
{
$model = User::query()->find($id);
if ($throw && empty($model)) {
throw new BusinessException(ErrorCode::USRE_NOT_EXIST);
}
return $model;
}
}
```
那我们编写对应的单元测试
```php
namespace HyperfTest\Cases;
use HyperfTest\HttpTestCase;
use App\Service\Dao\UserDao;
/**
* @internal
* @coversNothing
*/
class UserTest extends HttpTestCase
{
public function testUserDaoFirst()
{
$model = \Hyperf\Utils\ApplicationContext::getContainer()->get(UserDao::class)->first(1);
var_dump($model);
$this->assertSame(1, $model->id);
}
}
```
然后执行我们的单测
```
composer test -- --filter=testUserDaoFirst
```

85
doc/en/tracer.md Normal file
View File

@ -0,0 +1,85 @@
# 调用链追踪
在微服务场景下,我们会拆分出来很多的服务,也就意味着一个业务请求,少则跨越 3-4 个服务,多则几十个甚至更多,在这种架构下我们需要对某一个问题进行 Debug 的时候是极其困难的一件事情,那么我们就需要一个调用链追踪系统来帮助我们动态地展示服务调用的链路,以便我们可以快速地对问题点进行定位,亦可根据链路信息对服务进行调优。
`Hyperf` 里我们提供了 [hyperf/tracer](https://github.com/hyperf-cloud/tracer) 组件来对各个跨网络请求来进行调用的追踪以及分析,目前仅根据 [Opentracing](https://opentracing.io) 协议对接了 [Zipkin](https://zipkin.io/) 系统。
# 安装
## 通过 Composer 安装组件
```bash
composer require hyperf/tracer
```
## 增加组件配置
如文件不存在,可执行下面的命令增加 `config/autoload/opentracing.php` 配置文件:
```bash
php bin/hyperf.php vendor:publish
```
# 使用
## 配置
默认提供了对 `Guzzle HTTP` 调用、`Redis` 调用、`DB` 调用进行了监听或 `AOP` 切面处理,以实现对调用链的传播与追踪,默认情况下这些追踪不会打开,您需要通过更改 `config/autoload/opentracing.php` 配置文件内的 `enable` 项内的开关来打开对某些远程调用的追踪。
```php
<?php
return [
'enable' => [
// 打开或关闭对 Guzzle HTTP 调用的追踪
'guzzle' => false,
// 打开或关闭对 Redis 调用的追踪
'redis' => false,
// 打开或关闭对 DB 调用的追踪
'db' => false,
],
];
```
在开始追踪之前我们还需要配置一下 `zipkin` 的服务配置,还是在同一个文件下
```php
<?php
use Zipkin\Samplers\BinarySampler;
return [
// 这里的代码演示不对 enable 内的配置进行展开
'enable' => [],
// zipkin 服务配置
'zipkin' => [
// 当前应用的配置
'app' => [
'name' => env('APP_NAME', 'skeleton'),
// 如果 ipv6 和 ipv6 为空组件会自动从 Server 中检测
'ipv4' => '127.0.0.1',
'ipv6' => null,
'port' => 9501,
],
'options' => [
// zipkin 服务的 endpoint 地址
'endpoint_url' => env('ZIPKIN_ENDPOINT_URL', 'http://localhost:9411/api/v2/spans'),
// 请求超时秒数
'timeout' => env('ZIPKIN_TIMEOUT', 1),
],
// 采样器,默认为所有请求的都追踪
'sampler' => BinarySampler::createAsAlwaysSample(),
],
];
```
## 更换采样器
默认的采样器为所有请求都记录调用链,这对性能会存在一定程度上的影响,尤其是内存的占用,所以我们只需要在我们希望的时候才对调用链进行追踪,那么我们就需要对采样器进行更换,更换也很简单,只需对配置项 `opentracing.zipkin.sampler` 对应的值改为您的采样器对象实例即可,只要您的采样器对象实现了 `Zipkin\Sampler` 接口类即可。
## 接入阿里云链路追踪服务
当我们在使用阿里云的链路追踪服务时,由于对端也是支持 `Zipkin` 的协议的,故可以直接通过在 `condif/autoload/opentracing.php` 配置文件内修改 `endpoint_url` 的值为您对应的阿里云 `region` 的地址,具体地址可在阿里云的链路追踪服务内得到,更多细节可参考 [阿里云链路追踪服务帮助文档](https://help.aliyun.com/document_detail/100031.html?spm=a2c4g.11186623.6.547.68f974dcZlg4Mv)。
# Reference
- [Opentracing](https://opentracing.io)
- [Zipkin](https://zipkin.io/)
- [Dapper, 大规模分布式系统的跟踪系统](https://bigbully.github.io/Dapper-translation/)

View File

@ -0,0 +1,47 @@
# DaoCloud Devops搭建教程
作为个人开发者,使用自建 `Gitlab``Docker Swarm 集群` 显然成本是无法接受的。这里介绍一个 `Devops` 服务 `DaoCloud`
推荐理由很简单,因为它免费,而且还能正常使用。
[DaoCloud](https://dashboard.daocloud.io)
## 如何使用
大家只需要关注 `项目``应用` 和 `集群管理` 三个切页即可。
### 创建项目
首先我们需要在 `项目` 里新建一个项目。DaoCloud 支持多种镜像仓库,这个可以按需选择。
这里我以 [hyperf-demo](https://github.com/limingxinleo/hyperf-demo) 仓库为例配置。当创建成功后,在对应 `Github仓库``WebHooks` 下面就会有对应的url。
接下来我们修改一下仓库里的 `Dockerfile`,在 `apk add` 下面增加 `&& apk add wget \`。这里具体原因不是很清楚,如果不更新 `wget`, 使用时就会有问题。但是自建 Gitlab CI 就没有任何问题。
当提交代码后,`DaoCloud` 就会执行对应的打包操作了。
### 创建集群
然后我们到 `集群管理` 中,创建一个 `集群`,然后添加 `主机`
这里就不详述了,按照上面的步骤一步一步来就行。
### 创建应用
点击 应用 -> 创建应用 -> 选择刚刚的项目 -> 部署
按照指示操作,主机端口用户可以自主选择一个未使用的端口,因为 `DaoCloud` 没有 `Swarm``Config` 功能,所以我们主动把 `.env` 映射到 容器里。
添加 `Volume`,容器目录 `/opt/www/.env`,主机目录 使用你存放 `.env` 文件的地址,是否可写 为不可写。
然后点击 立即部署。
### 测试
到宿主机里访问刚刚的端口号,就可以看到 `Hyperf` 的欢迎接口数据了。
```
$ curl http://127.0.0.1:9501
{"code":0,"data":{"user":"Hyperf","method":"GET","message":"Hello Hyperf."}}
```

View File

@ -0,0 +1,236 @@
# Docker Swarm集群搭建教程
现阶段Docker容器技术已经相当成熟就算是中小型公司也可以基于 Gitlab、Aliyun镜像服务、Docker Swarm 轻松搭建自己的 Docker集群服务。
## Installation Docker
```
curl -sSL https://get.daocloud.io/docker | sh
```
## 搭建自己的Gitlab
### InstallationGitlab
首先我们修改一下端口号,把 `sshd` 服务的 `22` 端口改为 `2222`,让 `gitlab` 可以使用 `22` 端口。
```
$ vim /etc/ssh/sshd_config
# 默认 Port 改为 2222
Port 2222
# 重启服务
$ systemctl restart sshd.service
```
重新登录机器
```
ssh -p 2222 root@host
```
安装 Gitlab
```
sudo docker run -d --hostname gitlab.xxx.cn \
--publish 443:443 --publish 80:80 --publish 22:22 \
--name gitlab --restart always --volume /srv/gitlab/config:/etc/gitlab \
--volume /srv/gitlab/logs:/var/log/gitlab \
--volume /srv/gitlab/data:/var/opt/gitlab \
gitlab/gitlab-ce:latest
```
首次登录 `Gitlab` 会重置密码,用户名是 `root`
### Installationgitlab-runner
[官方地址](https://docs.gitlab.com/runner/install/linux-repository.html)
`CentOS` 为例
```
curl -L https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.rpm.sh | sudo bash
yum install gitlab-runner
```
当然,可以用 `curl https://setup.ius.io | sh` 命令,更新为最新的 `git` 源,然后直接使用 yum 安装 git 和 gitlab-runner。
```
$ curl https://setup.ius.io | sh
$ yum -y install git2u
$ git version
$ yum install gitlab-runner
```
### 注册 gitlab-runner
```
$ gitlab-runner register --clone-url http://内网ip/
Please enter the gitlab-ci coordinator URL (e.g. https://gitlab.com/):
http://gitlab.xxx.cc/
Please enter the gitlab-ci token for this runner:
xxxxx
Please enter the gitlab-ci description for this runner:
xxx
Please enter the gitlab-ci tags for this runner (comma separated):
builder
Please enter the executor: docker-ssh, shell, docker+machine, docker-ssh+machine, docker, parallels, ssh, virtualbox, kubernetes:
shell
```
## 初始化 Swarm 集群
登录另外一台机器,初始化集群
```
$ docker swarm init
```
创建自定义 Overlay 网络
```
docker network create \
--driver overlay \
--subnet 10.0.0.0/24 \
--opt encrypted \
default-network
```
加入集群
```
# 显示manager节点的TOKEN
$ docker swarm join-token manager
# 加入manager节点到集群
$ docker swarm join --token <token> ip:2377
# 显示worker节点的TOKEN
$ docker swarm join-token worker
# 加入worker节点到集群
$ docker swarm join --token <token> ip:2377
```
然后配置发布用的 gitlab-runner
> 其他与 builder 一致,但是 tag 却不能一样。线上环境可以设置为 tags测试环境设置为 test
## Installation Portainer
[Portainer](https://github.com/portainer/portainer)
```
docker service create \
--name portainer \
--publish 9000:9000 \
--replicas=1 \
--constraint 'node.role == manager' \
--mount type=volume,src=portainer_data,dst=/data \
--mount type=bind,src=//var/run/docker.sock,dst=/var/run/docker.sock \
portainer/portainer
```
## 创建一个Demo项目
登录 Gitlab 创建一个 Demo 项目。并导入我们的项目 [hyperf-skeleton](https://github.com/hyperf-cloud/hyperf-skeleton)
## 配置镜像仓库
> 我们直接使用阿里云的即可
首先创建一个命名空间 test_namespace然后创建一个镜像仓库 demo并使用本地仓库。
然后到我们直接打包用的服务器中,登录阿里云 Docker Registry
```
usermod -aG docker gitlab-runner
su gitlab-runner
docker login --username=your_name registry.cn-shanghai.aliyuncs.com
```
修改我们项目里的 .gitlab-ci.yml
```
variables:
PROJECT_NAME: demo
REGISTRY_URL: registry.cn-shanghai.aliyuncs.com/test_namespace
```
还有 deploy.test.yml需要仔细对比以下文件哦。
```yml
version: '3.7'
services:
demo:
image: $REGISTRY_URL/$PROJECT_NAME:test
environment:
- "APP_PROJECT=demo"
- "APP_ENV=test"
ports:
- 9501:9501
deploy:
replicas: 1
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 5
update_config:
parallelism: 2
delay: 5s
order: start-first
networks:
- default-network
configs:
- source: demo_v1.0
target: /opt/www/.env
configs:
demo_v1.0:
external: true
networks:
default-network:
external: true
```
然后在我们的 portainer 中,创建对应的 Config demo_v1.0。当然以下参数需要根据实际情况调整因为我们的Demo中没有任何IO操作所以填默认的即可。
```
APP_NAME=demo
DB_DRIVER=mysql
DB_HOST=localhost
DB_PORT=3306
DB_DATABASE=hyperf
DB_USERNAME=root
DB_PASSWORD=
DB_CHARSET=utf8mb4
DB_COLLATION=utf8mb4_unicode_ci
DB_PREFIX=
REDIS_HOST=localhost
REDIS_AUTH=
REDIS_PORT=6379
REDIS_DB=0
```
因为我们配置的 gitlab-ci.yml 会检测 test 分支和 tags所以我们把修改的内容合并到test分支然后推到gitlab上。
接下来我们就可以访问集群任意一台机器的 9501 端口。进行测试了
```
curl http://127.0.0.1:9501/
```
## 意外情况
### fatal: git fetch-pack: expected shallow list
这种情况是 `gitlab-runner` 使用的 `git` 版本过低,更新 `git` 版本即可。
```
$ curl https://setup.ius.io | sh
$ yum remove -y git
$ yum -y install git2u
$ git version
# 重新安装 gitlab-runner 并重新注册 gitlab-runner
$ yum install gitlab-runner
```

View File

@ -0,0 +1,63 @@
# Supervisor 部署应用
[Supervisor](http://www.supervisord.org/) 是 `Linux/Unix` 系统下的一个进程管理工具。可以很方便的监听、启动、停止和重启一个或多个进程。通过 [Supervisor](http://www.supervisord.org/) 管理的进程,当进程意外被 `Kill` 时,[Supervisor](http://www.supervisord.org/) 会自动将它重启,可以很方便地做到进程自动恢复的目的,而无需自己编写 `shell` 脚本来管理进程。
## Installation Supervisor
这里仅举例 `CentOS` 系统下的安装方式:
```bash
# 安装 epel 源,如果此前安装过,此步骤跳过
yum install -y epel-release
yum install -y supervisor
```
## 创建一个配置文件
```bash
cp /etc/supervisord.conf /etc/supervisord.d/supervisord.conf
```
编辑新复制出来的配置文件 `/etc/supervisord.d/supervisord.conf`,并在文件结尾处添加以下内容后保存文件:
```ini
# 新建一个应用并设置一个名称,这里设置为 hyperf
[program:hyperf]
# 这里为您要管理的项目的启动命令,对应您的项目的真实路径
command=php /var/www/hyperf/bin/hyperf.php start
# 以哪个用户来运行该进程
user=root
# supervisor 启动时自动该应用
autostart=true
# 进程退出后自动重启进程
autorestart=true
# 重试间隔秒数
startsecs=5
# 重试次数
startretries=3
# stderr 日志输出位置
stderr_logfile=/var/www/hyperf/runtime/stderr.log
# stdout 日志输出位置
stdout_logfile=/var/www/hyperf/runtime/stdout.log
```
## 启动 Supervisor
运行下面的命令基于配置文件启动 Supervisor 程序:
```bash
supervisord -c /etc/supervisord.d/supervisord.conf
```
## 使用 supervisorctl 管理项目
```bash
# 启动 hyperf 应用
supervisorctl start hyperf
# 重启 hyperf 应用
supervisorctl restart hyperf
# 停止 hyperf 应用
supervisorctl stop hyperf
# 查看所有被管理项目运行状态
supervisorctl status
```

25
doc/en/utils.md Normal file
View File

@ -0,0 +1,25 @@
# 辅助类
Hyperf 提供了大量便捷的辅助类,这里会列出一些常用的好用的,不会列举所有,可自行查看 [hyperf/utils](https://github.com/hyperf-cloud/utils) 组件的代码获得更多信息。
## 协程辅助类
### Hyperf\Utils\Coroutine
该辅助类用于协助进行协程相关的判断或操作。
#### id(): int
通过静态方法 `id()` 获得当前所处的 `协程 ID`,如当前不处于协程环境下,则返回 `-1`
#### create(callable $callable): int
通过静态方法 `create(callable $callable)` 可创建一个协程,还可以通过全局函数 `co(callable $callable)``go(callable $callable)` 达到同样的目的,该方法是对 `Swoole` 创建协程方法的一个封装,区别在于不会抛出未捕获的异常,未捕获的异常会通过 `Hyperf\Contract\StdoutLoggerInterface``warning` 等级输出。
#### inCoroutine(): bool
通过静态方法 `inCoroutine()` 判断当前是否处于协程环境下。
### Hyperf\Utils\Context
用于处理协程上下文,本质上是对 `Swoole\Coroutine::getContext()` 方法的一个封装,但区别在于这里兼容了非协程环境下的运行。

View File

@ -0,0 +1,57 @@
# WebSocket 协程客户端
Hyperf 提供了对 WebSocket Client 的封装,可基于 [hyperf/websocket-client](https://github.com/hyperf-cloud/websocket-client) 组件对 WebSocket Server 进行访问;
## Installation
```bash
composer require hyperf/websocket-client
```
## 使用
组件提供了一个 `Hyperf\WebSocketClient\ClientFactory` 来创建客户端对象 `Hyperf\WebSocketClient\Client`,我们直接通过代码来演示一下:
```php
<?php
declare(strict_types=1);
namespace App\Controller;
use Hyperf\Di\Annotation\Inject;
use Hyperf\WebSocketClient\ClientFactory;
use Hyperf\WebSocketClient\Frame;
class IndexController
{
/**
* @Inject
* @var ClientFactory
*/
protected $clientFactory;
public function index()
{
// 对端服务的地址,如没有提供 ws:// 或 wss:// 前缀,则默认补充 ws://
$host = '127.0.0.1:9502';
// 通过 ClientFactory 创建 Client 对象,创建出来的对象为短生命周期对象
$client = $this->clientFactory->create($host);
// 向 WebSocket 服务端发送消息
$client->push('HttpServer 中使用 WebSocket Client 发送数据。');
// 获取服务端响应的消息,服务端需要通过 push 向本客户端的 fd 投递消息,才能获取;以下设置超时时间 2s接收到的数据类型为 Frame 对象。
/** @var Frame $msg */
$msg = $client->recv(2);
// 获取文本数据:$res_msg->data
return $msg->data;
}
}
```
## 关闭自动关闭
默认情况下,创建出来的 `Client` 对象会通过 `defer` 自动 `close` 连接,如果您希望不自动 `close`,可在创建 `Client` 对象时传递第二个参数 `$autoClose``false`
```php
$autoClose = false;
$client = $clientFactory->create($host, $autoClose);
```

View File

@ -0,0 +1,90 @@
# WebSocket 服务
Hyperf 提供了对 WebSocket Server 的封装,可基于 [hyperf/websocket-server](https://github.com/hyperf-cloud/websocket-server) 组件快速搭建一个 WebSocket 应用。
## Installation
```bash
composer require hyperf/websocket-server
```
## 配置 Server
修改 `config/autoload/server.php`,增加以下配置。
```php
<?php
'servers' => [
[
'name' => 'ws',
'type' => Server::SERVER_WEBSOCKET,
'host' => '0.0.0.0',
'port' => 9502,
'sock_type' => SWOOLE_SOCK_TCP,
'callbacks' => [
SwooleEvent::ON_HAND_SHAKE => [Hyperf\WebSocketServer\Server::class, 'onHandShake'],
SwooleEvent::ON_MESSAGE => [Hyperf\WebSocketServer\Server::class, 'onMessage'],
SwooleEvent::ON_CLOSE => [Hyperf\WebSocketServer\Server::class, 'onClose'],
],
],
],
```
## 配置路由
> 目前暂时只支持配置文件的模式配置路由,后续会提供注解模式。
`config/routes.php` 文件内增加对应 `ws` 的 Server 的路由配置,这里的 `ws` 值取决于您在 `config/autoload/server.php` 内配置的 WebSocket Server 的 `name` 值。
```php
<?php
Router::addServer('ws', function () {
Router::get('/', 'App\Controller\WebSocketController');
});
```
## 创建对应控制器
```php
<?php
declare(strict_types=1);
namespace App\Controller;
use Hyperf\Contract\OnCloseInterface;
use Hyperf\Contract\OnMessageInterface;
use Hyperf\Contract\OnOpenInterface;
use Swoole\Http\Request;
use Swoole\Server;
use Swoole\Websocket\Frame;
class WebSocketController implements OnMessageInterface, OnOpenInterface, OnCloseInterface
{
public function onMessage(Server $server, Frame $frame): void
{
$server->push($frame->fd, 'Recv: ' . $frame->data);
}
public function onClose(Server $server, int $fd, int $reactorId): void
{
var_dump('closed');
}
public function onOpen(Server $server, Request $request): void
{
$server->push($request->fd, 'Opened');
}
}
```
接下来启动 Server便能看到对应启动了一个 WebSocket Server 并监听于 9502 端口,此时您便可以通过各种 WebSocket Client 来进行连接和进行数据传输了。
```
$ php bin/hyperf.php start
[INFO] Worker#0 started.
[INFO] WebSocket Server listening at 0.0.0.0:9502
[INFO] HTTP Server listening at 0.0.0.0:9501
```

View File

@ -26,7 +26,7 @@
repo: 'hyperf/hyperf',
loadSidebar: 'summary.md',
loadNavbar: true,
fallbackLanguages: ['zh', 'en'],
fallbackLanguages: ['zh-cn', 'en'],
mergeNavbar: true,
themeColor: '#3F51B5',
logo: '/logo.png',
@ -34,10 +34,25 @@
autoHeader: false,
subMaxLevel: 4,
search: {
depth: 6
depth: 6,
noData: {
'/en/': 'No results!',
'/zh-cn/': '没有找到结果!',
'/zh-hk/': '沒有找到結果!',
'/zh-tw/': '沒有找到結果!',
'/': '没有找到结果!'
},
paths: 'auto',
placeholder: {
'/en/': 'Type to Search',
'/zh-cn/': '输入关键词搜索',
'/zh-hk/': '輸入關鍵詞搜索',
'/zh-tw/': '輸入關鍵詞搜索',
'/': '输入关键词搜索'
}
},
alias: {
'/summary.md': './zh/summary.md'
'/summary.md': './zh-cn/summary.md'
},
plugins: [
EditOnGithubPlugin.create('https://github.com/hyperf/hyperf/tree/master/doc/')
@ -46,6 +61,7 @@
</script>
<script src="//unpkg.com/docsify-copy-code"></script>
<script src="//unpkg.com/docsify/lib/plugins/search.min.js"></script>
<script type="text/javascript">var cnzz_protocol = (("https:" == document.location.protocol) ? "https://" : "http://");document.write(unescape("%3Cspan style='display:none;' id='cnzz_stat_icon_1278274447'%3E%3C/span%3E%3Cscript src='" + cnzz_protocol + "v1.cnzz.com/z_stat.php%3Fid%3D1278274447' type='text/javascript'%3E%3C/script%3E"));</script>
</body>
</html>

View File

@ -107,7 +107,7 @@ class DemoProducer extends ProducerMessage
```
通过 DI Container 获取 `Hyperf\Amqp\Producer` 实例,即可投递消息。以下实例直接使用 `ApplicationContext` 获取 `Hyperf\Amqp\Producer` 其实并不合理DI Container 具体使用请到 [依赖注入](zh/di.md) 章节中查看。
通过 DI Container 获取 `Hyperf\Amqp\Producer` 实例,即可投递消息。以下实例直接使用 `ApplicationContext` 获取 `Hyperf\Amqp\Producer` 其实并不合理DI Container 具体使用请到 [依赖注入](zh-cn/di.md) 章节中查看。
```php
<?php

View File

@ -9,7 +9,7 @@
## 如何适配 Hyperf ?
我们为您提供了一份 [Hyperf 组件开发指南](zh/component-guide/intro.md),以帮助您开发 Hyperf 组件或适配 Hyperf 框架。
我们为您提供了一份 [Hyperf 组件开发指南](zh-cn/component-guide/intro.md),以帮助您开发 Hyperf 组件或适配 Hyperf 框架。
# 组件列表
@ -117,9 +117,13 @@
- [firstphp/wsdebug](https://github.com/lamplife/wsdebug) 通过 `WebSocket` 实时观测异常错误的开发调试组件
## 权限认证
- [donjan-deng/hyperf-permission](https://github.com/donjan-deng/hyperf-permission) 基于 [spatie/laravel-permission](https://github.com/spatie/laravel-permission) 开发的适配 Hyperf 的权限组件
## 第三方 SDK
- [yurunsoft/pay-sdk](https://github.com/Yurunsoft/PaySDK) 支持 Swoole 协程的支付宝/微信支付 SDK
- [yurunsoft/yurun-oauth-login](https://github.com/Yurunsoft/YurunOAuthLogin) 支持 Swoole 协程的第三方登录授权 SDKQQ、微信、微博、Github、Gitee 等)
- [overtrue/wechat](zh/sdks/wechat) EasyWeChat一个流行的非官方微信 SDK
- [overtrue/wechat](zh-cn/sdks/wechat) EasyWeChat一个流行的非官方微信 SDK
- [Yurunsoft/PHPMailer-Swoole](https://github.com/Yurunsoft/PHPMailer-Swoole) Swoole 协程环境下的可用的 PHPMailer

View File

@ -44,7 +44,7 @@ $cache = $container->get(\Psr\SimpleCache\CacheInterface::class);
组件提供 `Hyperf\Cache\Annotation\Cacheable` 注解,作用于类方法,可以配置对应的缓存前缀、失效时间、监听器和缓存组。
例如UserService 提供一个 user 方法,可以查询对应 id 的用户信息。当加上 `Hyperf\Cache\Annotation\Cacheable` 注解后,会自动生成对应的 Redis 缓存key 值为 `user:id` ,超时时间为 `9000` 秒。首次查询时,会从数据库中查,后面查询时,会从缓存中查。
> 缓存注解基于 [aop](zh/aop.md) 和 [di](zh/di.md),所以只有在 `Container` 中获取到的对象实例才有效,比如通过 `$container->get``make` 方法所获得的对象,直接 `new` 出来的对象无法使用。
> 缓存注解基于 [aop](zh-cn/aop.md) 和 [di](zh-cn/di.md),所以只有在 `Container` 中获取到的对象实例才有效,比如通过 `$container->get``make` 方法所获得的对象,直接 `new` 出来的对象无法使用。
```php
<?php

View File

@ -1,6 +1,6 @@
# 指南前言
为了帮助开发者更好的为 Hyperf 开发组件,共建生态,我们提供了本指南用于指导开发者进行组件开发,在阅读本指南前,需要您对 Hyperf 的文档进行了 **全面** 的阅读,特别是 [协程](zh/coroutine.md) 和 [依赖注入](zh/di.md) 章节,如果对 Hyperf 的基础组件缺少充分的理解,可能会导致开发时出现错误。
为了帮助开发者更好的为 Hyperf 开发组件,共建生态,我们提供了本指南用于指导开发者进行组件开发,在阅读本指南前,需要您对 Hyperf 的文档进行了 **全面** 的阅读,特别是 [协程](zh-cn/coroutine.md) 和 [依赖注入](zh-cn/di.md) 章节,如果对 Hyperf 的基础组件缺少充分的理解,可能会导致开发时出现错误。
# 组件开发的目的

View File

@ -38,6 +38,65 @@ config
└── routes.php // 用于管理路由
```
## server.php 配置说明
以下为 Hyperf-Skeleton 中的 `config/autoload/server.php` 所提供的默认 `settings`
```php
<?php
declare(strict_types=1);
use Hyperf\Server\Server;
use Hyperf\Server\SwooleEvent;
return [
// 这里省略了该文件的其它配置
'settings' => [
'enable_coroutine' => true, // 开启内置协程
'worker_num' => swoole_cpu_num(), // 设置启动的 Worker 进程数
'pid_file' => BASE_PATH . '/runtime/hyperf.pid', // master 进程的 PID
'open_tcp_nodelay' => true, // TCP 连接发送数据时会关闭 Nagle 合并算法,立即发往客户端连接
'max_coroutine' => 100000, // 设置当前工作进程最大协程数量
'open_http2_protocol' => true, // 启用 HTTP2 协议解析
'max_request' => 100000, // 设置 worker 进程的最大任务数
'socket_buffer_size' => 2 * 1024 * 1024, // 配置客户端连接的缓存区长度
],
];
```
此配置文件用于管理 Server 服务,其中的 `settings` 选项可以直接使用由 `Swoole Server` 提供的选项,其他选项可参考 [Swoole 官方文档](https://wiki.swoole.com/wiki/page/274.html) 。
如需要设置守护进程化,可在 `settings` 中增加 `'daemonize' => 1`,执行 `php bin/hyperf.php start`后,程序将转入后台作为守护进程运行
单独的 Server 配置需要添加在对应 `servers``settings` 当中,如 `jsonrpc` 协议的 TCP Server 配置启用 EOF 自动分包,和设置 EOF 字符串
```php
<?php
use Hyperf\Server\Server;
use Hyperf\Server\SwooleEvent;
return [
// 这里省略了该文件的其它配置
'servers' => [
[
'name' => 'jsonrpc',
'type' => Server::SERVER_BASE,
'host' => '0.0.0.0',
'port' => 9503,
'sock_type' => SWOOLE_SOCK_TCP,
'callbacks' => [
SwooleEvent::ON_RECEIVE => [\Hyperf\JsonRpc\TcpServer::class, 'onReceive'],
],
'settings' => [
'open_eof_split' => true, // 启用 EOF 自动分包
'package_eof' => "\r\n", // 设置 EOF 字符串
],
],
],
];
```
## `config.php``autoload` 文件夹内的配置文件的关系
`config.php``autoload` 文件夹内的配置文件在服务启动时都会被扫描并注入到 `Hyperf\Contract\ConfigInterface` 对应的对象中,配置的结构为一个键值对的大数组,两种配置形式不同的在于 `autoload` 内配置文件的文件名会作为第一层 键(Key) 存在,而 `config.php` 内的则以您定义的为第一层,我们通过下面的例子来演示一下。
@ -76,7 +135,7 @@ Config 组件提供了三种方式获取配置,通过 `Hyperf\Config\Config`
#### 通过 Config 对象获取配置
这种方式要求你已经拿到了 `Config` 对象的实例,默认对象为 `Hyperf\Config\Config`,注入实例的细节可查阅 [依赖注入](zh/di.md) 章节;
这种方式要求你已经拿到了 `Config` 对象的实例,默认对象为 `Hyperf\Config\Config`,注入实例的细节可查阅 [依赖注入](zh-cn/di.md) 章节;
```php
/**
@ -88,7 +147,7 @@ $config->get($key$default);
#### 通过 `@Value` 注解获取配置
这种方式要求注解的应用对象必须是通过 [hyperf/di](https://github.com/hyperf/di) 组件创建的,注入实例的细节可查阅 [依赖注入](zh/di.md) 章节,示例中我们假设 `IndexController` 就是一个已经定义好的 `Controller` 类,`Controller` 类一定是由 `DI` 容器创建出来的;
这种方式要求注解的应用对象必须是通过 [hyperf/di](https://github.com/hyperf/di) 组件创建的,注入实例的细节可查阅 [依赖注入](zh-cn/di.md) 章节,示例中我们假设 `IndexController` 就是一个已经定义好的 `Controller` 类,`Controller` 类一定是由 `DI` 容器创建出来的;
`@Value()` 内的字符串则对应到 `$config->get($key)` 内的 `$key` 参数,在创建该对象实例时,对应的配置会自动注入到定义的类属性中。
```php
@ -175,6 +234,6 @@ Hyperf 采用组件化设计,在添加一些组件进来骨架项目后,我
## 配置中心
Hyperf 为您提供了分布式系统的外部化配置支持,目前支持由携程开源的 `Apollo`、阿里云 ACM 应用配置管理、ETCD 以及 Zookeeper 作为配置中心的支持。
关于配置中心的使用细节我们由 [配置中心](zh/config-center.md) 章节来阐述。
关于配置中心的使用细节我们由 [配置中心](zh-cn/config-center.md) 章节来阐述。

View File

@ -1,7 +1,7 @@
# 控制器
通过控制器来处理 HTTP 请求,需要通过 `配置文件``注解` 的形式将路由与控制器方法进行绑定,具体请查阅 [路由](zh/router.md) 章节。
对于 `请求(Request)``响应(Response)`Hyperf 提供了 `Hyperf\HttpServer\Contract\RequestInterface``Hyperf\HttpServer\Contract\ResponseInterface` 方便您获取入参和返回数据,关于 [请求](zh/request.md) 与 [响应](zh/response.md) 的详细内容请查阅对应的章节。
通过控制器来处理 HTTP 请求,需要通过 `配置文件``注解` 的形式将路由与控制器方法进行绑定,具体请查阅 [路由](zh-cn/router.md) 章节。
对于 `请求(Request)``响应(Response)`Hyperf 提供了 `Hyperf\HttpServer\Contract\RequestInterface``Hyperf\HttpServer\Contract\ResponseInterface` 方便您获取入参和返回数据,关于 [请求](zh-cn/request.md) 与 [响应](zh-cn/response.md) 的详细内容请查阅对应的章节。
## 编写控制器

503
doc/zh-cn/db/migration.md Normal file
View File

@ -0,0 +1,503 @@
# 数据库迁移
数据库迁移可以理解为对数据库结构的版本管理,可以有效的解决团队中跨成员对数据库结构的管理。
# 生成迁移
通过 `gen:migration` 生成一个迁移文件,命令后面跟的是一个文件名参数,通常为这个迁移要打算做的事情。
```bash
php bin/hyperf.php gen:migration create_users_table
```
生成的迁移文件位于根目录下的 `migrations` 文件夹内,每个迁移文件都包含一个时间戳,以便迁移程序确定迁移的顺序。
`--table` 选项可以用来指定数据表的名称,指定的表名将会默认生成在迁移文件中。
`--create` 选项也是用来指定数据表的名称,但跟 `--table` 的差异在于该选项是生成创建表的迁移文件,而 `--table` 是用于修改表的迁移文件。
```bash
php bin/hyperf.php gen:migration create_users_table --table=users
php bin/hyperf.php gen:migration create_users_table --create=users
```
# 迁移结构
迁移类默认会包含 `2` 个方法:`up` 和 `down`
`up` 方法用于添加新的数据表,字段或者索引到数据库,而 `down` 方法就是 `up` 方法的反操作,和 `up` 里的操作相反,以便在回退的时候执行。
```php
<?php
use Hyperf\Database\Schema\Schema;
use Hyperf\Database\Schema\Blueprint;
use Hyperf\Database\Migrations\Migration;
class CreateUsersTable extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('true', function (Blueprint $table) {
$table->bigIncrements('id');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('true');
}
}
```
# 运行迁移
通过执行 `migrate` 命令运行所有尚未完成的迁移文件:
```bash
php bin/hyperf.php migrate
```
## 强制执行迁移
一些迁移操作是具有破坏性的,这意味着可能会导致数据丢失,为了防止有人在生产环境中运行这些命令,系统会在这些命令运行之前与你进行确认,但如果您希望忽略这些确认信息强制运行命令,可以使用 `--force` 标记:
```bash
php bin/hyperf.php migrate --force
```
## 回滚迁移
若您希望回滚最后一次的迁移,可以通过 `migrate:rollback` 命令回滚最后一侧的迁移,注意一次迁移可能会包含多个迁移文件:
```bash
php bin/hyperf.php migrate:rollback
```
您还可以在 `migrate:rollback` 命令后面加上 `step` 参数来设置回滚迁移的次数,比如以下命令将回滚最近 5 次迁移:
```bash
php bin/hyperf.php migrate:rollback --step=5
```
如果您希望回滚所有的迁移,可以通过 `migrate:reset` 来回滚:
```bash
php bin/hyperf.php migrate:reset
```
## 回滚并迁移
`migrate:refresh` 命令不仅会回滚迁移还会接着运行 `migrate` 命令,这样可以高效地重建某些迁移:
```bash
php bin/hyperf.php migrate:refresh
// 重建数据库结构并执行数据填充
php bin/hyperf.php migrate:refresh --seed
```
通过 `--step` 参数指定回滚和重建次数,比如以下命令将回滚并重新执行最后 5 次迁移:
```bash
php bin/hyperf.php migrate:refresh --step=5
```
## 重建数据库
通过 `migrate:fresh` 命令可以高效地重建整个数据库,这个命令会先删除所有的数据库,然后再执行 `migrate` 命令:
```bash
php bin/hyperf.php migrate:fresh
// 重建数据库结构并执行数据填充
php bin/hyperf.php migrate:fresh --seed
```
# 数据表
在迁移文件中主要通过 `Hyperf\Database\Schema\Schema` 类来定义数据表和管理迁移流程。
## 创建数据表
通过 `create` 方法来创建新的数据库表。 `create` 方法接受两个参数:第一个参数为数据表的名称,第二个参数是一个 `闭包(Closure)`,此闭包会接收一个用于定义新数据表的 `Hyperf\Database\Schema\Blueprint` 对象:
```php
<?php
use Hyperf\Database\Schema\Schema;
use Hyperf\Database\Schema\Blueprint;
use Hyperf\Database\Migrations\Migration;
class CreateUsersTable extends Migration
{
public function up(): void
{
Schema::create('users', function (Blueprint $table) {
$table->bigIncrements('id');
$table->timestamps();
});
}
}
```
您可以在数据库结构生成器上使用以下命令来定义表的选项:
```php
// 指定表存储引擎
$table->engine = 'InnoDB';
// 指定数据表的默认字符集
$table->charset = 'utf8';
// 指定数据表默认的排序规则
$table->collation = 'utf8_unicode_ci';
// 创建临时表
$table->temporary();
```
## 重命名数据表
若您希望重命名一个数据表,可以通过 `rename` 方法:
```php
Schema::rename($from, $to);
```
### 重命名带外键的数据表
在重命名表之前,您应该验证表上的所有外键约束在迁移文件中都有明确的名称,而不是让迁移程序按照约定来设置一个名称,否则,外键的约束名称将引用旧表名。
## 删除数据表
删除一个已存在的数据表,可以通过 `drop``dropIfExists` 方法:
```php
Schema::drop('users');
Schema::dropIfExists('users');
```
## 检查数据表或字段是否存在
可以通过 `hasTable``hasColumn` 方法来检查数据表或字段是否存在:
```php
if (Schema::hasTable('users')) {
//
}
if (Schema::hasColumn('name', 'email')) {
//
}
```
## 数据库连接选项
如果在同时管理多个数据库的情况下,不同的迁移会对应不同的数据库连接,那么此时我们可以在迁移文件中通过重写父类的 `$connection` 类属性来定义不同的数据库连接:
```php
<?php
use Hyperf\Database\Schema\Schema;
use Hyperf\Database\Schema\Blueprint;
use Hyperf\Database\Migrations\Migration;
class CreateUsersTable extends Migration
{
// 这里对应 config/autoload/databases.php 内的连接 key
protected $connection = 'foo';
public function up(): void
{
Schema::create('users', function (Blueprint $table) {
$table->bigIncrements('id');
$table->timestamps();
});
}
}
```
# 字段
## 创建字段
`table``create` 方法的第二个参数的 `闭包(Closure)` 内定义该迁移文件要执行的定义或变更,比如下面的代码为定义一个 `name` 的字符串字段:
```php
<?php
use Hyperf\Database\Schema\Schema;
use Hyperf\Database\Schema\Blueprint;
use Hyperf\Database\Migrations\Migration;
class CreateUsersTable extends Migration
{
public function up(): void
{
Schema::create('users', function (Blueprint $table) {
$table->string('name');
});
}
}
```
## 可用的字段定义方法
| 命令 | 描述
| --- | --- |
| $table->bigIncrements('id'); | 递增 ID主键相当于「UNSIGNED BIG INTEGER」 |
| $table->bigInteger('votes'); | 相当于 BIGINT |
| $table->binary('data'); | 相当于 BLOB |
| $table->boolean('confirmed'); | 相当于 BOOLEAN |
| $table->char('name', 100); | 相当于带有长度的 CHAR |
| $table->date('created_at'); | 相当于 DATE |
| $table->dateTime('created_at'); | 相当于 DATETIME |
| $table->dateTimeTz('created_at'); | 相当于带时区 DATETIME |
| $table->decimal('amount', 8, 2); | 相当于带有精度与基数 DECIMAL |
| $table->double('amount', 8, 2); | 相当于带有精度与基数 DOUBLE |
| $table->enum('level', ['easy', 'hard']); | 相当于 ENUM |
| $table->float('amount', 8, 2); | 相当于带有精度与基数 FLOAT |
| $table->geometry('positions'); | 相当于 GEOMETRY |
| $table->geometryCollection('positions'); | 相当于 GEOMETRYCOLLECTION |
| $table->increments('id'); | 递增的 ID (主键)相当于「UNSIGNED INTEGER」 |
| $table->integer('votes'); | 相当于 INTEGER |
| $table->ipAddress('visitor'); | 相当于 IP 地址 |
| $table->json('options'); | 相当于 JSON |
| $table->jsonb('options'); | 相当于 JSONB |
| $table->lineString('positions'); | 相当于 LINESTRING |
| $table->longText('description'); | 相当于 LONGTEXT |
| $table->macAddress('device'); | 相当于 MAC 地址 |
| $table->mediumIncrements('id'); | 递增 ID (主键) 相当于「UNSIGNED MEDIUM INTEGER」 |
| $table->mediumInteger('votes'); | 相当于 MEDIUMINT |
| $table->mediumText('description'); | 相当于 MEDIUMTEXT |
| $table->morphs('taggable'); | 相当于加入递增的 taggable_id 与字符串 taggable_type |
| $table->multiLineString('positions'); | 相当于 MULTILINESTRING |
| $table->multiPoint('positions'); | 相当于 MULTIPOINT |
| $table->multiPolygon('positions'); | 相当于 MULTIPOLYGON |
| $table->nullableMorphs('taggable'); | 相当于可空版本的 morphs() 字段 |
| $table->nullableTimestamps(); | 相当于可空版本的 timestamps() 字段 |
| $table->point('position'); | 相当于 POINT |
| $table->polygon('positions'); | 相当于 POLYGON |
| $table->rememberToken(); | 相当于可空版本的 VARCHAR (100) 的 remember_token 字段 |
| $table->smallIncrements('id'); | 递增 ID (主键) 相当于「UNSIGNED SMALL INTEGER」 |
| $table->smallInteger('votes'); | 相当于 SMALLINT |
| $table->softDeletes(); | 相当于为软删除添加一个可空的 deleted_at 字段 |
| $table->softDeletesTz(); | 相当于为软删除添加一个可空的 带时区的 deleted_at 字段 |
| $table->string('name', 100); | 相当于带长度的 VARCHAR |
| $table->text('description'); | 相当于 TEXT |
| $table->time('sunrise'); | 相当于 TIME |
| $table->timeTz('sunrise'); | 相当于带时区的 TIME |
| $table->timestamp('added_on'); | 相当于 TIMESTAMP |
| $table->timestampTz('added_on'); | 相当于带时区的 TIMESTAMP |
| $table->timestamps(); | 相当于可空的 created_at 和 updated_at TIMESTAMP |
| $table->timestampsTz(); | 相当于可空且带时区的 created_at 和 updated_at TIMESTAMP |
| $table->tinyIncrements('id'); | 相当于自动递增 UNSIGNED TINYINT |
| $table->tinyInteger('votes'); | 相当于 TINYINT |
| $table->unsignedBigInteger('votes'); | 相当于 Unsigned BIGINT |
| $table->unsignedDecimal('amount', 8, 2); | 相当于带有精度和基数的 UNSIGNED DECIMAL |
| $table->unsignedInteger('votes'); | 相当于 Unsigned INT |
| $table->unsignedMediumInteger('votes'); | 相当于 Unsigned MEDIUMINT |
| $table->unsignedSmallInteger('votes'); | 相当于 Unsigned SMALLINT |
| $table->unsignedTinyInteger('votes'); | 相当于 Unsigned TINYINT |
| $table->uuid('id'); | 相当于 UUID |
| $table->year('birth_year'); | 相当于 YEAR |
## 修改字段
### 先决条件
在修改字段之前,请确保将 `doctrine/dbal` 依赖添加到 `composer.json` 文件中。Doctrine DBAL 库用于确定字段的当前状态, 并创建对该字段进行指定调整所需的 SQL 查询:
```bash
composer require doctrine/dbal
```
### 更新字段属性
`change` 方法可以将现有的字段类型修改为新的类型或修改其它属性。
```php
<?php
Schema::create('users', function (Blueprint $table) {
// 将字段的长度修改为 50
$table->string('name', 50)->change();
});
```
或修改字段为 `可为空`
```php
<?php
Schema::table('users', function (Blueprint $table) {
// 将字段的长度修改为 50 并允许为空
$table->string('name', 50)->nullable()->change();
});
```
> 只有下面的字段类型能被 "修改" bigInteger、 binary、 boolean、date、dateTime、dateTimeTz、decimal、integer、json、 longText、mediumText、smallInteger、string、text、time、 unsignedBigInteger、unsignedInteger and unsignedSmallInteger。
### 重命名字段
可以通过 `renameColumn` 方法来重命名字段:
```php
<?php
Schema::table('users', function (Blueprint $table) {
// 将字段从 from 重命名为 to
$table->renameColumn('from', 'to')->change();
});
```
> 当前不支持 enum 类型的字段重命名。
### 删除字段
可以通过 `dropColumn` 方法来删除字段:
```php
<?php
Schema::table('users', function (Blueprint $table) {
// 删除 name 字段
$table->dropColumn('name');
// 删除多个字段
$table->dropColumn(['name', 'age']);
});
```
#### 可用的命令别名
| 命令 | 描述
| --- | --- |
| $table->dropRememberToken(); | 删除 remember_token 字段。
| $table->dropSoftDeletes(); | 删除 deleted_at 字段。
| $table->dropSoftDeletesTz(); | dropSoftDeletes() 方法的别名。
| $table->dropTimestamps(); | 删除 created_at and updated_at 字段。
| $table->dropTimestampsTz(); | dropTimestamps() 方法的别名。
## 索引
### 创建索引
### 唯一索引
通过 `unique` 方法来创建一个唯一索引:
```php
<?php
// 在定义时创建索引
$table->string('name')->unique();
// 在定义完字段之后创建索引
$table->unique('name');
```
#### 复合索引
```php
<?php
// 创建一个复合索引
$table->index(['account_id', 'created_at'], 'index_account_id_and_created_at');
```
#### 定义索引名称
迁移程序会自动生成一个合理的索引名称,每个索引方法都接受一个可选的第二个参数来指定索引的名称:
```php
<?php
// 定义唯一索引名称为 unique_name
$table->unique('name', 'unique_name');
// 定义一个复合索引名称为 index_account_id_and_created_at
$table->index(['account_id', 'created_at'], '');
```
##### 可用的索引类型
| 命令 | 描述
| --- | --- |
| $table->primary('id'); | 添加主键
| $table->primary(['id', 'parent_id']); | 添加复合键
| $table->unique('email'); | 添加唯一索引
| $table->index('state'); | 添加普通索引
| $table->spatialIndex('location'); | 添加空间索引
### 重命名索引
您可通过 `renameIndex` 方法重命名一个索引的名称:
```php
<?php
$table->renameIndex('from', 'to');
```
### 删除索引
您可通过下面的方法来删除索引,默认情况下迁移程序会自动将数据库名称、索引的字段名及索引类型简单地连接在一起作为名称。举例如下:
| 命令 | 描述
| --- | --- |
| $table->dropPrimary('users_id_primary'); | 从 users 表中删除主键
| $table->dropUnique('users_email_unique'); | 从 users 表中删除唯一索引
| $table->dropIndex('geo_state_index'); | 从 geo 表中删除基本索引
| $table->dropSpatialIndex('geo_location_spatialindex'); | 从 geo 表中删除空间索引
您也可以通过传递字段数组到 `dropIndex` 方法,迁移程序会根据表名、字段和键类型生成的索引名称:
```php
<?php
Schema:table('users', function (Blueprint $table) {
$table->dropIndex(['account_id', 'created_at']);
});
```
### 外键约束
我们还可以通过 `foreign`、`references`、`on` 方法创建数据库层的外键约束。比如我们让 `posts` 表定义一个引用 `users` 表的 `id` 字段的 `user_id` 字段:
```php
Schema::table('posts', function (Blueprint $table) {
$table->unsignedInteger('user_id');
$table->foreign('user_id')->references('id')->on('users');
});
```
还可以为 `on delete``on update` 属性指定所需的操作:
```php
$table->foreign('user_id')
->references('id')->on('users')
->onDelete('cascade');
```
您可以通过 `dropForeign` 方法来删除外键。外键约束采用的命名方式与索引相同,然后加上 `_foreign` 后缀:
```php
$table->dropForeign('posts_user_id_foreign');
```
或者传递一个字段数组,让迁移程序按照约定的规则生成名称:
````php
$table->dropForeign(['user_id'']);
```
您可以在迁移文件中使用以下方法来开启或关闭外键约束:
```php
// 开启外键约束
Schema::enableForeignKeyConstraints();
// 禁用外键约束
Schema::disableForeignKeyConstraints();
```

View File

@ -21,7 +21,7 @@ $ php bin/hyperf.php db:model table_name
| 参数 | 类型 | 默认值 | 备注 |
|:------------------:|:------:|:-----------------:|:-----------------------------------------------:|
| --pool | string | `default` | 连接池,脚本会根据当前连接池配置创建 |
| --path | string | `app\Model` | 模型路径 |
| --path | string | `app/Model` | 模型路径 |
| --force-casts | bool | `false` | 是否强制重置 `casts` 参数 |
| --prefix | string | 空字符串 | 表前缀 |
| --inheritance | string | `Model` | 父类 |

39
doc/zh-cn/db/paginator.md Normal file
View File

@ -0,0 +1,39 @@
# 查询分页
在使用 [hyperf/database](https://github.com/hyperf/database) 来查询数据时,可以很方便的通过与 [hyperf/paginator](https://github.com/hyperf/paginator) 组件配合便捷地对查询结果进行分页。
# 使用方法
在您通过 [查询构造器](zh-cn/db/querybuilder.md) 或 [模型](zh-cn/db/model.md) 查询数据时,可以通过 `paginate` 方法来处理分页,该方法会自动根据用户正在查看的页面来设置限制和偏移量,默认情况下,通过当前 HTTP 请求所带的 `page` 参数的值来检测当前的页数:
> 由于 Hyperf 当前并不支持视图,所以分页组件尚未支持对视图的渲染,直接返回分页结果默认会以 application/json 格式输出。
## 查询构造器分页
```php
<?php
// 展示应用中的所有用户,每页显示 10 条数据
return Db::table('users')->paginate(10);
```
## 模型分页
您可以直接通过静态方法调用 `paginate` 方法来进行分页:
```php
<?php
// 展示应用中的所有用户,每页显示 10 条数据
return User::paginate(10);
```
当然您也可以设置查询的条件或其它查询的设置方法:
```php
<?php
// 展示应用中的所有用户,每页显示 10 条数据
return User::where('gender', 1)->paginate(10);
```
## 分页器实例方法
这里仅说明分页器在数据库查询上的使用方法,更多关于分页器的细节可阅读 [分页](zh-cn/paginator.md) 章节。

View File

@ -0,0 +1,266 @@
# 模型关联
## 定义关联
关联在 `Hyperf` 模型类中以方法的形式呈现。如同 `Hyperf` 模型本身,关联也可以作为强大的 `查询语句构造器` 使用,提供了强大的链式调用和查询功能。例如,我们可以在 role 关联的链式调用中附加一个约束条件:
```php
$user->role()->where('level', 1)->get();
```
### 一对一
一对一是最基本的关联关系。例如,一个 `User` 模型可能关联一个 `Role` 模型。为了定义这个关联,我们要在 `User` 模型中写一个 `role` 方法。在 `role` 方法内部调用 `hasOne` 方法并返回其结果:
```php
<?php
declare(strict_types=1);
namespace App\Models;
use Hyperf\DbConnection\Model\Model;
class User extends Model
{
public function role()
{
return $this->hasOne(Role::class, 'user_id', 'id');
}
}
```
`hasOne` 方法的第一个参数是关联模型的类名。一旦定义了模型关联,我们就可以使用 `Hyperf` 动态属性获得相关的记录。动态属性允许你访问关系方法就像访问模型中定义的属性一样:
```php
$role = User::query()->find(1)->role;
```
### 一对多
『一对多』关联用于定义单个模型拥有任意数量的其它关联模型。例如,一个作者可能写有多本书。正如其它所有的 `Hyperf` 关联一样,一对多关联的定义也是在 `Hyperf` 模型中写一个方法:
```php
<?php
declare(strict_types=1);
namespace App\Models;
use Hyperf\DbConnection\Model\Model;
class User extends Model
{
public function books()
{
return $this->hasMany(Book::class, 'user_id', 'id');
}
}
```
记住一点,`Hyperf` 将会自动确定 `Book` 模型的外键属性。按照约定,`Hyperf` 将会使用所属模型名称的 『snake case』形式再加上 `_id` 后缀作为外键字段。因此,在上面这个例子中,`Hyperf` 将假定 `User` 对应到 `Book` 模型上的外键就是 `book_id`
一旦关系被定义好以后,就可以通过访问 `User` 模型的 `books` 属性来获取评论的集合。记住,由于 Hyperf 提供了『动态属性』 ,所以我们可以像访问模型的属性一样访问关联方法:
```php
$books = User::query()->find(1)->books;
foreach ($books as $book) {
//
}
```
当然,由于所有的关联还可以作为查询语句构造器使用,因此你可以使用链式调用的方式,在 books 方法上添加额外的约束条件:
```php
$book = User::query()->find(1)->books()->where('title', '一个月精通Hyperf框架')->first();
```
### 一对多(反向)
现在,我们已经能获得一个作者的所有作品,接着再定义一个通过书获得其作者的关联关系。这个关联是 `hasMany` 关联的反向关联,需要在子级模型中使用 `belongsTo` 方法定义它:
```php
<?php
declare(strict_types=1);
namespace App\Models;
use Hyperf\DbConnection\Model\Model;
class Book extends Model
{
public function author()
{
return $this->belongsTo(User::class, 'user_id', 'id');
}
}
```
这个关系定义好以后,我们就可以通过访问 `Book` 模型的 author 这个『动态属性』来获取关联的 `User` 模型了:
```php
$book = Book::find(1);
echo $book->author->name;
```
### 多对多
多对多关联比 `hasOne``hasMany` 关联稍微复杂些。举个例子,一个用户可以拥有很多种角色,同时这些角色也被其他用户共享。例如,许多用户可能都有 「管理员」 这个角色。要定义这种关联,需要三个数据库表: `users``roles` 和 `role_user`。`role_user` 表的命名是由关联的两个模型按照字母顺序来的,并且包含了 `user_id``role_id` 字段。
多对多关联通过调用 `belongsToMany` 这个内部方法返回的结果来定义,例如,我们在 `User` 模型中定义 `roles` 方法:
```php
<?php
namespace App;
use Hyperf\DbConnection\Model\Model;
class User extends Model
{
public function roles()
{
return $this->belongsToMany(Role::class);
}
}
```
一旦关联关系被定义后,你可以通过 `roles` 动态属性获取用户角色:
```php
$user = User::query()->find(1);
foreach ($user->roles as $role) {
//
}
```
当然,像其它所有关联模型一样,你可以使用 `roles` 方法,利用链式调用对查询语句添加约束条件:
```php
$roles = User::find(1)->roles()->orderBy('name')->get();
```
正如前面所提到的,为了确定关联连接表的表名,`Hyperf` 会按照字母顺序连接两个关联模型的名字。当然,你也可以不使用这种约定,传递第二个参数到 belongsToMany 方法即可:
```php
return $this->belongsToMany(Role::class, 'role_user');
```
除了自定义连接表的表名,你还可以通过传递额外的参数到 `belongsToMany` 方法来定义该表中字段的键名。第三个参数是定义此关联的模型在连接表里的外键名,第四个参数是另一个模型在连接表里的外键名:
```php
return $this->belongsToMany(Role::class, 'role_user', 'user_id', 'role_id');
```
#### 获取中间表字段
就如你刚才所了解的一样,多对多的关联关系需要一个中间表来提供支持, `Hyperf` 提供了一些有用的方法来和这张表进行交互。例如,假设我们的 `User` 对象关联了多个 `Role` 对象。在获得这些关联对象后,可以使用模型的 `pivot` 属性访问中间表的数据:
```php
$user = User::find(1);
foreach ($user->roles as $role) {
echo $role->pivot->created_at;
}
```
需要注意的是,我们获取的每个 `Role` 模型对象,都会被自动赋予 `pivot` 属性,它代表中间表的一个模型对象,并且可以像其他的 `Hyperf` 模型一样使用。
默认情况下,`pivot` 对象只包含两个关联模型的主键,如果你的中间表里还有其他额外字段,你必须在定义关联时明确指出:
```php
return $this->belongsToMany(Role::class)->withPivot('column1', 'column2');
```
如果你想让中间表自动维护 `created_at``updated_at` 时间戳,那么在定义关联时附加上 `withTimestamps` 方法即可:
```php
return $this->belongsToMany(Role::class)->withTimestamps();
```
#### 自定义 `pivot` 属性名称
如前所述,来自中间表的属性可以使用 `pivot` 属性访问。但是,你可以自由定制此属性的名称,以便更好的反应其在应用中的用途。
例如,如果你的应用中包含可能订阅的用户,则用户与博客之间可能存在多对多的关系。如果是这种情况,你可能希望将中间表访问器命名为 `subscription` 取代 `pivot` 。这可以在定义关系时使用 `as` 方法完成:
```php
return $this->belongsToMany(Podcast::class)->as('subscription')->withTimestamps();
```
一旦定义完成,你可以使用自定义名称访问中间表数据:
```php
$users = User::with('podcasts')->get();
foreach ($users->flatMap->podcasts as $podcast) {
echo $podcast->subscription->created_at;
}
```
#### 通过中间表过滤关系
在定义关系时,你还可以使用 `wherePivot``wherePivotIn` 方法来过滤 `belongsToMany` 返回的结果:
```php
return $this->belongsToMany('App\Role')->wherePivot('approved', 1);
return $this->belongsToMany('App\Role')->wherePivotIn('priority', [1, 2]);
```
## 预加载
当以属性方式访问 `Hyperf` 关联时,关联数据「懒加载」。这着直到第一次访问属性时关联数据才会被真实加载。不过 `Hyperf` 能在查询父模型时「预先载入」子关联。预加载可以缓解 N + 1 查询问题。为了说明 N + 1 查询问题,考虑 `User` 模型关联到 `Role` 的情形:
```php
<?php
declare(strict_types=1);
namespace App\Models;
use Hyperf\DbConnection\Model\Model;
class User extends Model
{
public function role()
{
return $this->hasOne(Role::class, 'user_id', 'id');
}
}
```
现在,我们来获取所有的用户及其对应角色
```php
$users = User::query()->get();
foreach ($users as $user){
echo $user->role->name;
}
```
此循环将执行一个查询,用于获取全部用户,然后为每个用户执行获取角色的查询。如果我们有 10 个人,此循环将运行 11 个查询1 个用于查询用户10 个附加查询对应的角色。
谢天谢地,我们能够使用预加载将操作压缩到只有 2 个查询。在查询时,可以使用 with 方法指定想要预加载的关联:
```php
$users = User::query()->with('role')->get();
foreach ($users as $user){
echo $user->role->name;
}
```
在这个例子中,仅执行了两个查询
```
SELECT * FROM `user`;
SELECT * FROM `role` WHERE id in (1, 2, 3, ...);
```

View File

@ -3,7 +3,7 @@
## 简介
Hyperf 默认采用 [hyperf/di](https://github.com/hyperf/di) 作为框架的依赖注入管理容器,尽管从设计上我们允许您更换其它的依赖注入管理容器,但我们强烈不建议您更换该组件。
[hyperf/di](https://github.com/hyperf/di) 是一个强大的用于管理类的依赖关系并完成自动注入的组件,与传统依赖注入容器的区别在于更符合长生命周期的应用使用、提供了 [注解及注解注入](zh/annotation.md) 的支持、提供了无比强大的 [AOP 面向切面编程](zh/aop.md) 能力,这些能力及易用性作为 Hyperf 的核心输出,我们自信的认为该组件是最优秀的。
[hyperf/di](https://github.com/hyperf/di) 是一个强大的用于管理类的依赖关系并完成自动注入的组件,与传统依赖注入容器的区别在于更符合长生命周期的应用使用、提供了 [注解及注解注入](zh-cn/annotation.md) 的支持、提供了无比强大的 [AOP 面向切面编程](zh-cn/aop.md) 能力,这些能力及易用性作为 Hyperf 的核心输出,我们自信的认为该组件是最优秀的。
## 安装
@ -235,7 +235,7 @@ class IndexController
### 工厂对象注入
我们假设 `UserService` 的实现会更加复杂一些,在创建 `UserService` 对象时构造函数还需要传递进来一些非直接注入型的参数,假设我们需要从配置中取得一个值,然后 `UserService` 需要根据这个值来决定是否开启缓存模式(顺带一说 Hyperf 提供了更好用的 [模型缓存](zh/db/model-cache.md) 功能)
我们假设 `UserService` 的实现会更加复杂一些,在创建 `UserService` 对象时构造函数还需要传递进来一些非直接注入型的参数,假设我们需要从配置中取得一个值,然后 `UserService` 需要根据这个值来决定是否开启缓存模式(顺带一说 Hyperf 提供了更好用的 [模型缓存](zh-cn/db/model-cache.md) 功能)
我们需要创建一个工厂来生成 `UserService` 对象:
@ -306,7 +306,7 @@ Hyperf 的长生命周期依赖注入在项目启动时完成。这意味着长
* 构造函数时还不是协程环境,如果注入了可能会触发协程切换的类,就会导致框架启动失败。
* 构造函数中要避免循依赖(比较典型的例子为 `Listener``EventDispatcherInterface`),不然也会启动失败。
* 构造函数中要避免循依赖(比较典型的例子为 `Listener``EventDispatcherInterface`),不然也会启动失败。
目前解决方案是:只在实例中注入 `Psr\Container\ContainerInterface` ,而其他的组件在非构造函数执行时通过 `container` 获取。但 PSR-11 中指出:
@ -397,7 +397,7 @@ unset($proxy->someProperty);
### 容器仅管理长生命周期的对象
换种方式理解就是容器内管理的对象**都是单例**,这样的设计对于长生命周期的应用来说会更加的高效,减少了大量无意义的对象创建和销毁,这样的设计也就意味着所有需要交由 DI 容器管理的对象**均不能包含** `状态` 值。
`状态` 可直接理解为会随着请求而变化的值,事实上在 [协程](zh/coroutine.md) 编程中,这些状态值也是应该存放于 `协程上下文` 中的,即 `Hyperf\Utils\Context`
`状态` 可直接理解为会随着请求而变化的值,事实上在 [协程](zh-cn/coroutine.md) 编程中,这些状态值也是应该存放于 `协程上下文` 中的,即 `Hyperf\Utils\Context`
## 短生命周期对象

View File

@ -71,7 +71,7 @@ grpc
- gRPC server 服务器配置
`server.php` 文件(参考 [配置](zh/config.md)):
`server.php` 文件(参考 [配置](zh-cn/config.md)):
```php
'servers' => [
@ -91,7 +91,7 @@ grpc
- gRPC server 路由配置
`routes.php` 文件(参考 [路由](zh/router.md)):
`routes.php` 文件(参考 [路由](zh-cn/router.md)):
```php
Router::addServer('grpc', function () {

View File

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 90 KiB

View File

Before

Width:  |  Height:  |  Size: 151 KiB

After

Width:  |  Height:  |  Size: 151 KiB

View File

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

View File

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 99 KiB

View File

Before

Width:  |  Height:  |  Size: 185 KiB

After

Width:  |  Height:  |  Size: 185 KiB

View File

@ -57,7 +57,7 @@ class CalculatorService implements CalculatorServiceInterface
`name` 属性为定义该服务的名称这里定义一个全局唯一的名字即可Hyperf 会根据该属性生成对应的 ID 注册到服务中心去;
`protocol` 属性为定义该服务暴露的协议,目前仅支持 `jsonrpc``jsonrpc-http`,分别对应于 TCP 协议和 HTTP 协议下的两种协议,默认值为 `jsonrpc-http`,这里的值对应在 `Hyperf\Rpc\ProtocolManager` 里面注册的协议的 `key`,这两个本质上都是 JSON RPC 协议,区别在于数据格式化、数据打包、数据传输器等不同。
`server` 属性为绑定该服务类发布所要承载的 `Server`,默认值为 `jsonrpc-http`,该属性对应 `config/autoload/server.php` 文件内 `servers` 下所对应的 `name`,这里也就意味着我们需要定义一个对应的 `Server`,我们下一章节具体阐述这里应该怎样去处理;
`publishTo` 属性为定义该服务所要发布的服务中心,目前仅支持 `consul` 或为空,为空时代表不发布该服务到服务中心去,但也就意味着您需要手动处理服务发现的问题,当值为 `consul` 时需要对应配置好 [hyperf/consul](zh/consul.md) 组件的相关配置,要使用此功能需安装 [hyperf/service-governance](https://github.com/hyperf/service-governance) 组件,具体可参考 [服务注册](zh/service-register.md) 章节;
`publishTo` 属性为定义该服务所要发布的服务中心,目前仅支持 `consul` 或为空,为空时代表不发布该服务到服务中心去,但也就意味着您需要手动处理服务发现的问题,当值为 `consul` 时需要对应配置好 [hyperf/consul](zh-cn/consul.md) 组件的相关配置,要使用此功能需安装 [hyperf/service-governance](https://github.com/hyperf/service-governance) 组件,具体可参考 [服务注册](zh-cn/service-register.md) 章节;
> 使用 `@RpcService` 注解需 `use Hyperf\RpcServer\Annotation\RpcService;` 命名空间。
@ -111,6 +111,41 @@ return [
'settings' => [
'open_eof_split' => true,
'package_eof' => "\r\n",
'package_max_length' => 1024 * 1024 * 2,
],
],
],
];
```
TCP Server (适配 `jsonrpc-tcp-length-check` 协议)
当前协议为 `jsonrpc` 的扩展协议,用户可以很方便的修改对应的 `settings` 使用此协议,示例如下。
```php
<?php
use Hyperf\Server\Server;
use Hyperf\Server\SwooleEvent;
return [
// 这里省略了该文件的其它配置
'servers' => [
[
'name' => 'jsonrpc',
'type' => Server::SERVER_BASE,
'host' => '0.0.0.0',
'port' => 9503,
'sock_type' => SWOOLE_SOCK_TCP,
'callbacks' => [
SwooleEvent::ON_RECEIVE => [\Hyperf\JsonRpc\TcpServer::class, 'onReceive'],
],
'settings' => [
'open_length_check' => true,
'package_length_type' => 'N',
'package_length_offset' => 0,
'package_body_offset' => 4,
'package_max_length' => 1024 * 1024 * 2,
],
],
],
@ -154,6 +189,7 @@ return [
// 对应容器对象 ID可选默认值等于 service 配置的值,用来定义依赖注入的 key
'id' => \App\JsonRpc\CalculatorServiceInterface::class,
// 服务提供者的服务协议,可选,默认值为 jsonrpc-http
// 可选 jsonrpc-http jsonrpc jsonrpc-tcp-length-check
'protocol' => 'jsonrpc-http',
// 负载均衡算法,可选,默认值为 random
'load_balancer' => 'random',
@ -166,6 +202,29 @@ return [
'nodes' => [
['host' => '127.0.0.1', 'port' => 9504],
],
// 配置项,会影响到 Packer 和 Transporter
'options' => [
'connect_timeout' => 5.0,
'recv_timeout' => 5.0,
'settings' => [
// 根据协议不同,区分配置
'open_eof_split' => true,
'package_eof' => "\r\n",
// 'open_length_check' => true,
// 'package_length_type' => 'N',
// 'package_length_offset' => 0,
// 'package_body_offset' => 4,
],
// 当使用 JsonRpcPoolTransporter 时会用到以下配置
'pool' => [
'min_connections' => 1,
'max_connections' => 32,
'connect_timeout' => 10.0,
'wait_timeout' => 3.0,
'heartbeat' => -1,
'max_idle_time' => 60.0,
],
],
]
],
];
@ -370,9 +429,7 @@ use Hyperf\JsonRpc\JsonRpcPoolTransporter;
use Hyperf\JsonRpc\JsonRpcTransporter;
return [
JsonRpcTransporter::class => function () {
return make(JsonRpcPoolTransporter::class);
},
JsonRpcTransporter::class => JsonRpcPoolTransporter::class,
];
```

View File

@ -8,5 +8,5 @@ Hyperf 的命令管理默认由 [symfony/console](https://github.com/symfony/con
## 请求与协程生命周期
Swoole 在处理每个连接时,会默认创建一个协程去处理,主要体现在 `onRequest`、`onReceive`、`onConnect` 事件,所以可以理解为每个请求都是一个协程,由于创建协程也是个常规操作,所以一个请求协程里面可能会包含很多个协程,同一个进程内协程之间是内存共享的,但调度顺序是非顺序的,且协程间本质上是相互独立的没有父子关系,所以对每个协程的状态处理都需要通过 [协程上下文](zh/coroutine.md#协程上下文) 来管理。
Swoole 在处理每个连接时,会默认创建一个协程去处理,主要体现在 `onRequest`、`onReceive`、`onConnect` 事件,所以可以理解为每个请求都是一个协程,由于创建协程也是个常规操作,所以一个请求协程里面可能会包含很多个协程,同一个进程内协程之间是内存共享的,但调度顺序是非顺序的,且协程间本质上是相互独立的没有父子关系,所以对每个协程的状态处理都需要通过 [协程上下文](zh-cn/coroutine.md#协程上下文) 来管理。

View File

@ -155,7 +155,7 @@ class Log
是的, 还是通过强大的 `容器(Container)`.
- 首先, 实现一个 `StdoutLoggerFactory` 类,关于 `Factory` 的用法可在 [依赖注入](zh/di.md) 章节获得更多详细的说明。
- 首先, 实现一个 `StdoutLoggerFactory` 类,关于 `Factory` 的用法可在 [依赖注入](zh-cn/di.md) 章节获得更多详细的说明。
```php
<?php

0
doc/zh-cn/memory.md Normal file
View File

View File

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

View File

@ -102,6 +102,7 @@ use App\Middleware\BarMiddleware;
use App\Middleware\FooMiddleware;
use Hyperf\HttpServer\Annotation\AutoController;
use Hyperf\HttpServer\Annotation\Middleware;
use Hyperf\HttpServer\Annotation\Middlewares;
/**
* @AutoController()

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