mirror of
https://gitee.com/hyperf/hyperf.git
synced 2024-12-05 13:18:22 +08:00
Merge remote-tracking branch 'hyperf/master' into bugfix#90
This commit is contained in:
commit
c2edb838a5
2
.gitignore
vendored
2
.gitignore
vendored
@ -9,4 +9,4 @@ vendor/
|
||||
.phpintel/
|
||||
.env
|
||||
.DS_Store
|
||||
*.lock
|
||||
*.lock
|
||||
|
@ -11,15 +11,15 @@ matrix:
|
||||
- php: 7.2
|
||||
env: SW_VERSION="4.3.5"
|
||||
- php: 7.2
|
||||
env: SW_VERSION="4.4.0-beta"
|
||||
env: SW_VERSION="4.4.0"
|
||||
- php: 7.3
|
||||
env: SW_VERSION="4.3.5"
|
||||
- php: 7.3
|
||||
env: SW_VERSION="4.4.0-beta"
|
||||
env: SW_VERSION="4.4.0"
|
||||
- php: master
|
||||
env: SW_VERSION="4.3.5"
|
||||
- php: master
|
||||
env: SW_VERSION="4.4.0-beta"
|
||||
env: SW_VERSION="4.4.0"
|
||||
|
||||
allow_failures:
|
||||
- php: master
|
||||
|
17
CHANGELOG.md
17
CHANGELOG.md
@ -1,13 +1,20 @@
|
||||
# v1.0.4 - TBD
|
||||
# v1.0.5 - TBD
|
||||
|
||||
## Fixed
|
||||
|
||||
- [#176](https://github.com/hyperf-cloud/hyperf/pull/176) Fixed TypeError: Return value of LengthAwarePaginator::nextPageUrl() must be of the type string or null, none returned.
|
||||
|
||||
# v1.0.4 - 2019-07-08
|
||||
|
||||
## Added
|
||||
|
||||
- Nothing
|
||||
- [#140](https://github.com/hyperf-cloud/hyperf/pull/140) Support Swoole v4.4.0.
|
||||
- [#163](https://github.com/hyperf-cloud/hyperf/pull/163) Added custom arguments support to AbstractConstants::__callStatic in `hyperf/constants`.
|
||||
|
||||
# Changed
|
||||
|
||||
- [#124](https://github.com/hyperf-cloud/hyperf/pull/124) Added `$delay` parameter for `DriverInterface::push`, and marked `DriverInterface::delay` method to deprecated.
|
||||
- [#125](https://github.com/hyperf-cloud/hyperf/pull/125) Changed the default value of parameter $default of config() function to null
|
||||
- [#125](https://github.com/hyperf-cloud/hyperf/pull/125) Changed the default value of parameter $default of config() function to null.
|
||||
|
||||
# Fixed
|
||||
|
||||
@ -16,6 +23,10 @@
|
||||
- [#132](https://github.com/hyperf-cloud/hyperf/pull/132) Fixed request->hasFile does not work expected.
|
||||
- [#135](https://github.com/hyperf-cloud/hyperf/pull/135) Fixed response->redirect does not work expected.
|
||||
- [#139](https://github.com/hyperf-cloud/hyperf/pull/139) Fixed the BaseUri of ConsulAgent will be replaced by default BaseUri.
|
||||
- [#148](https://github.com/hyperf-cloud/hyperf/pull/148) Fixed cannot generate the migration when migrates directory does not exist.
|
||||
- [#152](https://github.com/hyperf-cloud/hyperf/pull/152) Fixed db connection will not be closed when a low use frequency.
|
||||
- [#169](https://github.com/hyperf-cloud/hyperf/pull/169) Fixed array parse failed when handle http request.
|
||||
- [#170](https://github.com/hyperf-cloud/hyperf/pull/170) Fixed websocket server interrupt when request a not exist route.
|
||||
|
||||
# Removed
|
||||
|
||||
|
@ -82,6 +82,9 @@
|
||||
"hyperf/rate-limit": "self.version",
|
||||
"hyperf/redis": "self.version",
|
||||
"hyperf/server": "self.version",
|
||||
"hyperf/service-governance": "self.version",
|
||||
"hyperf/swagger": "self.version",
|
||||
"hyperf/swoole-dashboard": "self.version",
|
||||
"hyperf/tracer": "self.version",
|
||||
"hyperf/utils": "self.version",
|
||||
"hyperf/websocket-client": "self.version",
|
||||
@ -129,6 +132,9 @@
|
||||
"Hyperf\\RateLimit\\": "src/rate-limit/src/",
|
||||
"Hyperf\\Redis\\": "src/redis/src/",
|
||||
"Hyperf\\Server\\": "src/server/src/",
|
||||
"Hyperf\\ServiceGovernance\\": "src/service-governance/src/",
|
||||
"Hyperf\\Swagger\\": "src/swagger/src/",
|
||||
"Hyperf\\SwooleDashboard\\": "src/swoole-dashboard/src/",
|
||||
"Hyperf\\Tracer\\": "src/tracer/src/",
|
||||
"Hyperf\\Utils\\": "src/utils/src/",
|
||||
"Hyperf\\WebSocketClient\\": "src/websocket-client/src/",
|
||||
@ -149,7 +155,9 @@
|
||||
"HyperfTest\\HttpServer\\": "./src/http-server/tests/",
|
||||
"HyperfTest\\Logger\\": "./src/logger/tests/",
|
||||
"HyperfTest\\ModelCache\\": "./src/model-cache/tests/",
|
||||
"HyperfTest\\Paginator\\": "./src/paginator/tests/",
|
||||
"HyperfTest\\Redis\\": "./src/redis/tests/",
|
||||
"HyperfTest\\SwooleDashboard\\": "./src/swoole-dashboard/tests/",
|
||||
"HyperfTest\\Utils\\": "./src/utils/tests/",
|
||||
"HyperfTest\\WebSocketClient\\": "src/websocket-client/tests/",
|
||||
"HyperfTest\\WebSocketServer\\": "src/websocket-server/tests/"
|
||||
|
22
doc/README.md
Normal file
22
doc/README.md
Normal file
@ -0,0 +1,22 @@
|
||||
# 介绍
|
||||
|
||||
Hyperf 是基于 `Swoole 4.3+` 实现的高性能、高灵活性的 PHP 协程框架,内置协程服务器及大量常用的组件,性能较传统基于 `PHP-FPM` 的框架有质的提升,提供超高性能的同时,也保持着极其灵活的可扩展性,标准组件均均基于 [PSR 标准](https://www.php-fig.org/psr) 实现,基于强大的依赖注入设计,保证了绝大部分组件或类都是 `可替换` 与 `可复用` 的。
|
||||
|
||||
框架组件库除了常见的协程版的 `MySQL 客户端`、`Redis 客户端`,还为您准备了协程版的 `Eloquent ORM`、`JSON RPC 服务的及客户端`、`GRPC 服务端及客户端`、`Zipkin (OpenTracing) 客户端`、`Guzzle HTTP 客户端`、`Elasticsearch 客户端`、`Consul 客户端`、`ETCD 客户端`、`AMQP 组件`、`Apollo 配置中心`、`阿里云 ACM 应用配置管理`、`基于令牌桶算法的限流器`、`通用连接池`、`熔断器`、`Swagger 文档生成` 等组件,省去了自己实现对应协程版本的麻烦,Hyperf 还提供了 `依赖注入`、`注解`、`AOP 面向切面编程`、`中间件`、`自定义进程`、`事件管理器`、`Redis/RabbitMQ 消息队列`、`自动模型缓存` 等非常便捷的功能,满足丰富的技术场景和业务场景,开箱即用。
|
||||
|
||||
# 框架初衷
|
||||
|
||||
尽管现在基于 PHP 语言开发的框架处于一个百花争鸣的时代,但仍旧未能看到一个优雅的设计与超高性能的共存的完美框架,亦没有看到一个真正为 PHP 微服务铺路的框架,此为 Hyperf 及其团队成员的初衷,我们将持续投入并为此付出努力,也欢迎你加入我们参与开源建设。
|
||||
|
||||
# 设计理念
|
||||
|
||||
`Hyperspeed + Flexibility = Hyperf`,从名字上我们就将 `超高速` 和 `灵活性` 作为 Hyperf 的基因。
|
||||
|
||||
- 对于超高速,我们基于 Swoole 协程并在框架设计上进行大量的优化以确保超高性能的输出。
|
||||
- 对于灵活性,我们基于 Hyperf 强大的依赖注入组件,组件均基于 [PSR 标准](https://www.php-fig.org/psr) 的契约和由 Hyperf 定义的契约实现,达到框架内的绝大部分的组件或类都是可替换的。
|
||||
|
||||
基于以上的特点,Hyperf 将存在丰富的可能性,如实现 Web 服务,网关服务,分布式中间件,微服务架构,游戏服务器,物联网(IOT)等。
|
||||
|
||||
# 生产可用
|
||||
|
||||
我们为组件进行了大量的单元测试以保证逻辑的正确,同时维护了高质量的文档,在 Hyperf 正式对外开放之前,便已在一家 C轮 和一家 B轮 互联网公司上线了多个服务并以稳定的姿态完美的运行了超过半年时间,经过了严酷的生产环境的考验,我们才正式的对外开放该项目。
|
3
doc/_navbar.md
Normal file
3
doc/_navbar.md
Normal file
@ -0,0 +1,3 @@
|
||||
* 多语言/Translations
|
||||
* [中文](zh/)
|
||||
* [English](en/)
|
4
doc/en/README.md
Normal file
4
doc/en/README.md
Normal file
@ -0,0 +1,4 @@
|
||||
# Introduction
|
||||
|
||||
[components](https://github.com/hyperf-cloud/hyperf)
|
||||
|
125
doc/en/amqp.md
Normal file
125
doc/en/amqp.md
Normal file
@ -0,0 +1,125 @@
|
||||
# AMQP
|
||||
|
||||
[https://github.com/hyperf-cloud/amqp](https://github.com/hyperf-cloud/amqp)
|
||||
|
||||
## Default Config
|
||||
```php
|
||||
<?php
|
||||
|
||||
return [
|
||||
'default' => [
|
||||
'host' => 'localhost',
|
||||
'port' => 5672,
|
||||
'user' => 'guest',
|
||||
'password' => 'guest',
|
||||
'vhost' => '/',
|
||||
'pool' => [
|
||||
'min_connections' => 1,
|
||||
'max_connections' => 10,
|
||||
'connect_timeout' => 10.0,
|
||||
'wait_timeout' => 3.0,
|
||||
'heartbeat' => -1,
|
||||
],
|
||||
'params' => [
|
||||
'insist' => false,
|
||||
'login_method' => 'AMQPLAIN',
|
||||
'login_response' => null,
|
||||
'locale' => 'en_US',
|
||||
'connection_timeout' => 3.0,
|
||||
'read_write_timeout' => 3.0,
|
||||
'context' => null,
|
||||
'keepalive' => false,
|
||||
'heartbeat' => 0,
|
||||
],
|
||||
],
|
||||
];
|
||||
```
|
||||
|
||||
## Deliver Message
|
||||
|
||||
Use generator command to create a producer.
|
||||
```
|
||||
php bin/hyperf.php gen:amqp-producer DemoProducer
|
||||
```
|
||||
|
||||
We can modify the Producer annotation to replace exchange and routingKey.
|
||||
Payloload is the data that is finally delivered to the message queue, so we can rewrite the _construct method easyly,just make sure payload is assigned.
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Amqp\Producers;
|
||||
|
||||
use Hyperf\Amqp\Annotation\Producer;
|
||||
use Hyperf\Amqp\Message\ProducerMessage;
|
||||
use App\Models\User;
|
||||
|
||||
/**
|
||||
* DemoProducer
|
||||
* @Producer(exchange="hyperf", routingKey="hyperf")
|
||||
*/
|
||||
class DemoProducer extends ProducerMessage
|
||||
{
|
||||
public function __construct($id)
|
||||
{
|
||||
$user = User::where('id', $id)->first();
|
||||
$this->payload = [
|
||||
'id' => $id,
|
||||
'data' => $user->toArray()
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Get the Producer instance through container, and you can deliver the message. It is not reasonable for the following examples to use Application Context directly to get the Producer. For the specific use of container, see the di module.
|
||||
|
||||
```php
|
||||
<?php
|
||||
use Hyperf\Amqp\Producer;
|
||||
use App\Amqp\Producers\DemoProducer;
|
||||
use Hyperf\Utils\ApplicationContext;
|
||||
|
||||
$message = new DemoProducer(1);
|
||||
$producer = ApplicationContext::getContainer()->get(Producer::class);
|
||||
$result = $producer->produce($message);
|
||||
|
||||
```
|
||||
|
||||
## Consume Message
|
||||
|
||||
Use generator command to create a consumer.
|
||||
```
|
||||
php bin/hyperf.php gen:amqp-consumer DemoConsumer
|
||||
```
|
||||
|
||||
we can modify the Consumer annotation to replace exchange, routingKey and queue.
|
||||
And $data is parsed metadata.
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Amqp\Consumers;
|
||||
|
||||
use Hyperf\Amqp\Annotation\Consumer;
|
||||
use Hyperf\Amqp\Message\ConsumerMessage;
|
||||
use Hyperf\Amqp\Result;
|
||||
|
||||
/**
|
||||
* @Consumer(exchange="hyperf", routingKey="hyperf", queue="hyperf", nums=1)
|
||||
*/
|
||||
class DemoConsumer extends ConsumerMessage
|
||||
{
|
||||
public function consume($data): string
|
||||
{
|
||||
print_r($data);
|
||||
return Result::ACK;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The framework automatically creates the process according to Consumer annotations, and the process will be pulled up again after unexpected exit.
|
79
doc/en/cache.md
Normal file
79
doc/en/cache.md
Normal file
@ -0,0 +1,79 @@
|
||||
# Cache
|
||||
|
||||
[https://github.com/hyperf-cloud/cache](https://github.com/hyperf-cloud/cache)
|
||||
|
||||
## Config
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
return [
|
||||
'default' => [
|
||||
'driver' => Hyperf\Cache\Driver\RedisDriver::class,
|
||||
'packer' => Hyperf\Cache\Packer\PhpSerializer::class,
|
||||
],
|
||||
];
|
||||
```
|
||||
|
||||
## How to use
|
||||
|
||||
Components provide Cacheable annotation to configure cache prefix, expiration times, listeners, and cache groups.
|
||||
For example, UserService provides a user method to query user information. When the Cacheable annotation is added, the Redis cache is automatically generated with the key value of `user:id` and the timeout time of 9000 seconds. When querying for the first time, it will be fetched from DB, and when querying later, it will be fetched from Cache.
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\User;
|
||||
use Hyperf\Cache\Annotation\Cacheable;
|
||||
|
||||
class UserService
|
||||
{
|
||||
/**
|
||||
* @Cacheable(key="user", ttl=9000, listener="user-update")
|
||||
*/
|
||||
public function user($id)
|
||||
{
|
||||
$user = User::query()->where('id',$id)->first();
|
||||
|
||||
if($user){
|
||||
return $user->toArray();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Clear Cache
|
||||
|
||||
Of course, if the data changes, how to delete the cache? Here we need to use the listener. Next, a new Service provides a way to help us deal with it.
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use Hyperf\Di\Annotation\Inject;
|
||||
use Hyperf\Cache\Listener\DeleteListenerEvent;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
class SystemService
|
||||
{
|
||||
/**
|
||||
* @Inject
|
||||
* @var EventDispatcherInterface
|
||||
*/
|
||||
protected $dispatcher;
|
||||
|
||||
public function flushCache($userId)
|
||||
{
|
||||
$this->dispatcher->dispatch(new DeleteListenerEvent('user-update', [$userId]));
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
```
|
0
doc/en/core/README.md
Normal file
0
doc/en/core/README.md
Normal file
0
doc/en/microservice/README.md
Normal file
0
doc/en/microservice/README.md
Normal file
0
doc/en/model/README.md
Normal file
0
doc/en/model/README.md
Normal file
6
doc/en/quick_start/install.md
Normal file
6
doc/en/quick_start/install.md
Normal file
@ -0,0 +1,6 @@
|
||||
# Install
|
||||
|
||||
## composer
|
||||
```
|
||||
composer create-project hyperf/hyperf-skeleton
|
||||
```
|
146
doc/en/quick_start/overview.md
Normal file
146
doc/en/quick_start/overview.md
Normal file
@ -0,0 +1,146 @@
|
||||
# Overview
|
||||
|
||||
To give you a full understanding of how Hyperf works, this document gives you technical details, but it's not an introductory tutorial or a reference document (we've also prepared these for you, of course).
|
||||
|
||||
## Write Routes
|
||||
|
||||
Hyperf uses `nikic/fast-route` to provide routing capabilities, and you can easily write your route in `config/routes.php`. In addition, the framework also provides routing annotation [routing]().
|
||||
|
||||
```php
|
||||
// config/routes.php
|
||||
|
||||
use Hyperf\HttpServer\Router\Router;
|
||||
|
||||
Router::get('/', 'App\Controllers\IndexController@index');
|
||||
```
|
||||
|
||||
## Handle Request
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use Hyperf\HttpServer\Contract\RequestInterface;
|
||||
|
||||
class IndexController
|
||||
{
|
||||
public function index(RequestInterface $request)
|
||||
{
|
||||
$id = $request->input('id', 1);
|
||||
|
||||
return (string)$id;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Auto Inject
|
||||
|
||||
When you use `make` and `get`, the framework automatically injects container's singleton objects. For example, we implement a UserService.
|
||||
The framework provides constructor injection and annotation injection.
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use App\Service\Dao\BookDao;
|
||||
use App\Service\Dao\UserDao;
|
||||
use Hyperf\Di\Annotation\Inject;
|
||||
use Psr\Container\ContainerInterface;
|
||||
|
||||
class UserService
|
||||
{
|
||||
/**
|
||||
* @var ContainerInterface
|
||||
*/
|
||||
protected $container;
|
||||
|
||||
/**
|
||||
* @var UserDao
|
||||
*/
|
||||
protected $dao;
|
||||
|
||||
/**
|
||||
* @Inject
|
||||
* @var BookDao
|
||||
*/
|
||||
protected $book;
|
||||
|
||||
public function __construct(ContainerInterface $container)
|
||||
{
|
||||
$this->container = $container;
|
||||
$this->dao = $container->get(UserDao::class);
|
||||
}
|
||||
|
||||
public function get(int $id)
|
||||
{
|
||||
return $this->dao->first($id);
|
||||
}
|
||||
|
||||
public function book(int $id)
|
||||
{
|
||||
return $this->book->first($id);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Let's rewrite the IndexController above to execute the UserService method.
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Service\UserService;
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Hyperf\HttpServer\Contract\RequestInterface;
|
||||
|
||||
class IndexController
|
||||
{
|
||||
/**
|
||||
* @var ContainerInterface
|
||||
*/
|
||||
protected $container;
|
||||
|
||||
public function __construct(ContainerInterface $container)
|
||||
{
|
||||
$this->container = $container;
|
||||
}
|
||||
|
||||
public function index(RequestInterface $request)
|
||||
{
|
||||
$id = $request->input('id', 1);
|
||||
|
||||
$service = $this->container->get(UserService::class);
|
||||
|
||||
return $service->get($id)->toArray();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Create objects
|
||||
|
||||
Hyperf implements AOP aspect programming based on PHPParser. To support annotations, origin classes are rewritten, so create objects in the following way instead of simply new.
|
||||
Of course, this is only a suggestion, not a requirement.
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
$class = $this->container->get(UserService::class);
|
||||
|
||||
$user = $class->get(1);
|
||||
|
||||
var_dump($user->toArray);
|
||||
|
||||
$form = make(UserForm::class,['data' => $data]);
|
||||
$form->save();
|
||||
```
|
||||
|
2
doc/en/quick_start/summary.md
Normal file
2
doc/en/quick_start/summary.md
Normal file
@ -0,0 +1,2 @@
|
||||
- [overview](en/quick_start/overview.md)
|
||||
- [install](en/quick_start/install.md)
|
41
doc/index.html
Normal file
41
doc/index.html
Normal file
@ -0,0 +1,41 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Hyperf</title>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||
<meta name="description" content="Description">
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
||||
<link rel="stylesheet" href="//unpkg.com/docsify/lib/themes/vue.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script src="//unpkg.com/docsify/lib/docsify.min.js"></script>
|
||||
<script src="//unpkg.com/prismjs/components/prism-php.min.js"></script>
|
||||
<script src="//unpkg.com/docsify-edit-on-github/index.js"></script>
|
||||
<script>
|
||||
window.$docsify = {
|
||||
name: 'Hyperf',
|
||||
repo: 'hyperf-cloud/hyperf',
|
||||
loadSidebar: 'summary.md',
|
||||
loadNavbar: true,
|
||||
fallbackLanguages: ['zh', 'en'],
|
||||
mergeNavbar: true,
|
||||
themeColor: '#3F51B5',
|
||||
auto2top: true,
|
||||
subMaxLevel: 4,
|
||||
search: {
|
||||
depth: 6
|
||||
},
|
||||
plugins: [
|
||||
EditOnGithubPlugin.create('https://github.com/hyperf-cloud/hyperf/tree/gh-pages/')
|
||||
]
|
||||
}
|
||||
</script>
|
||||
<script src="//unpkg.com/docsify-copy-code"></script>
|
||||
<script src="//unpkg.com/docsify/lib/plugins/search.min.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
92
doc/summary.md
Normal file
92
doc/summary.md
Normal file
@ -0,0 +1,92 @@
|
||||
* 前言
|
||||
|
||||
* [项目介绍](zh/README.md)
|
||||
* [线上交流](zh/communication.md)
|
||||
* [捐献及赞助](zh/donate.md)
|
||||
|
||||
* 快速入门
|
||||
|
||||
* [安装](zh/quick-start/install.md)
|
||||
* [快速开始](zh/quick-start/overview.md)
|
||||
* [常见问题](zh/quick-start/questions.md)
|
||||
* [编程须知](zh/quick-start/important.md)
|
||||
|
||||
* 核心架构
|
||||
|
||||
* [生命周期](zh/lifecycle.md)
|
||||
* [协程](zh/coroutine.md)
|
||||
* [配置](zh/config.md)
|
||||
* [注解](zh/annotation.md)
|
||||
* [依赖注入](zh/di.md)
|
||||
* [事件机制](zh/event.md)
|
||||
* [AOP 面向切面编程](zh/aop.md)
|
||||
|
||||
* 基础功能
|
||||
|
||||
* [路由](zh/router.md)
|
||||
* [中间件](zh/middleware/middleware.md)
|
||||
* [控制器](zh/controller.md)
|
||||
* [请求](zh/request.md)
|
||||
* [响应](zh/response.md)
|
||||
* [异常处理](zh/exception-handler.md)
|
||||
* [缓存](zh/cache.md)
|
||||
* [日志](zh/logger.md)
|
||||
* [分页器](zh/paginator.md)
|
||||
* [命令行](zh/command.md)
|
||||
* [自动化测试](zh/testing.md)
|
||||
|
||||
* 数据库模型
|
||||
|
||||
* [快速开始](zh/db/quick-start.md)
|
||||
* [查询构造器](zh/db/querybuilder.md)
|
||||
* [模型](zh/db/model.md)
|
||||
* [模型关系](zh/db/relationship.md)
|
||||
* [查询分页](zh/db/paginator.md)
|
||||
* [模型事件](zh/db/event.md)
|
||||
* [模型缓存](zh/db/model-cache.md)
|
||||
* [数据库迁移](zh/db/migration.md)
|
||||
|
||||
* 微服务
|
||||
|
||||
* [架构理念](zh/microservice.md)
|
||||
* [JSON RPC 服务](zh/json-rpc.md)
|
||||
* [gRPC 服务](zh/grpc.md)
|
||||
* [服务注册](zh/service-register.md)
|
||||
* [服务熔断及降级](zh/circuit-breaker.md)
|
||||
* [服务限流](zh/rate-limit.md)
|
||||
* [配置中心](zh/config-center.md)
|
||||
* [调用链追踪](zh/tracer.md)
|
||||
|
||||
* 其它组件
|
||||
|
||||
* [连接池](zh/pool.md)
|
||||
* [Redis 协程客户端](zh/redis.md)
|
||||
* [Guzzle HTTP 协程客户端](zh/guzzle.md)
|
||||
* [Elasticsearch 协程客户端](zh/elasticsearch.md)
|
||||
* [Consul 协程客户端](zh/consul.md)
|
||||
* [ETCD 协程客户端](zh/etcd.md)
|
||||
* [AMQP](zh/amqp.md)
|
||||
* [异步队列](zh/async-queue.md)
|
||||
* [WebSocket 服务](zh/websocket-server.md)
|
||||
* [WebSocket 协程客户端](zh/websocket-client.md)
|
||||
* [自定义进程](zh/process.md)
|
||||
* [开发者工具](zh/devtool.md)
|
||||
* [辅助类](zh/utils.md)
|
||||
* [限流器](zh/rate-limit.md)
|
||||
* [Swoole Dashboard](zh/swoole-dashboard.md)
|
||||
|
||||
* 应用部署
|
||||
|
||||
* [Docker Swarm 集群搭建](zh/tutorial/docker-swarm.md)
|
||||
* [DaoCloud Devops 搭建](zh/tutorial/daocloud.md)
|
||||
* [Supervisor 部署](zh/tutorial/supervisor.md)
|
||||
|
||||
* Awesome Hyperf
|
||||
|
||||
* [协程组件库](zh/awesome-components.md)
|
||||
|
||||
* 组件开发指南
|
||||
|
||||
* [指南前言](zh/component-guide/intro.md)
|
||||
* [创建新的组件](zh/component-guide/create.md)
|
||||
* Hyperf 框架流程介入
|
22
doc/zh/README.md
Normal file
22
doc/zh/README.md
Normal file
@ -0,0 +1,22 @@
|
||||
# 介绍
|
||||
|
||||
Hyperf 是基于 `Swoole 4.3+` 实现的高性能、高灵活性的 PHP 协程框架,内置协程服务器及大量常用的组件,性能较传统基于 `PHP-FPM` 的框架有质的提升,提供超高性能的同时,也保持着极其灵活的可扩展性,标准组件均均基于 [PSR 标准](https://www.php-fig.org/psr) 实现,基于强大的依赖注入设计,保证了绝大部分组件或类都是 `可替换` 与 `可复用` 的。
|
||||
|
||||
框架组件库除了常见的协程版的 `MySQL 客户端`、`Redis 客户端`,还为您准备了协程版的 `Eloquent ORM`、`JSON RPC 服务的及客户端`、`GRPC 服务端及客户端`、`Zipkin (OpenTracing) 客户端`、`Guzzle HTTP 客户端`、`Elasticsearch 客户端`、`Consul 客户端`、`ETCD 客户端`、`AMQP 组件`、`Apollo 配置中心`、`阿里云 ACM 应用配置管理`、`基于令牌桶算法的限流器`、`通用连接池`、`熔断器`、`Swagger 文档生成` 等组件,省去了自己实现对应协程版本的麻烦,Hyperf 还提供了 `依赖注入`、`注解`、`AOP 面向切面编程`、`中间件`、`自定义进程`、`事件管理器`、`Redis/RabbitMQ 消息队列`、`自动模型缓存` 等非常便捷的功能,满足丰富的技术场景和业务场景,开箱即用。
|
||||
|
||||
# 框架初衷
|
||||
|
||||
尽管现在基于 PHP 语言开发的框架处于一个百花争鸣的时代,但仍旧未能看到一个优雅的设计与超高性能的共存的完美框架,亦没有看到一个真正为 PHP 微服务铺路的框架,此为 Hyperf 及其团队成员的初衷,我们将持续投入并为此付出努力,也欢迎你加入我们参与开源建设。
|
||||
|
||||
# 设计理念
|
||||
|
||||
`Hyperspeed + Flexibility = Hyperf`,从名字上我们就将 `超高速` 和 `灵活性` 作为 Hyperf 的基因。
|
||||
|
||||
- 对于超高速,我们基于 Swoole 协程并在框架设计上进行大量的优化以确保超高性能的输出。
|
||||
- 对于灵活性,我们基于 Hyperf 强大的依赖注入组件,组件均基于 [PSR 标准](https://www.php-fig.org/psr) 的契约和由 Hyperf 定义的契约实现,达到框架内的绝大部分的组件或类都是可替换的。
|
||||
|
||||
基于以上的特点,Hyperf 将存在丰富的可能性,如实现 Web 服务,网关服务,分布式中间件,微服务架构,游戏服务器,物联网(IOT)等。
|
||||
|
||||
# 生产可用
|
||||
|
||||
我们为组件进行了大量的单元测试以保证逻辑的正确,同时维护了高质量的文档,在 Hyperf 正式对外开放之前,便已在一家 C轮 和一家 B轮 互联网公司上线了多个服务并以稳定的姿态完美的运行了超过半年时间,经过了严酷的生产环境的考验,我们才正式的对外开放该项目。
|
141
doc/zh/amqp.md
Normal file
141
doc/zh/amqp.md
Normal file
@ -0,0 +1,141 @@
|
||||
# AMQP组件
|
||||
|
||||
[hyperf/amqp](https://github.com/hyperf-cloud/amqp) 是实现 AMQP 标准的组件,主要适用于对 RabbitMQ 的使用。
|
||||
|
||||
## 安装
|
||||
|
||||
```bash
|
||||
composer require hyperf/amqp
|
||||
```
|
||||
|
||||
## 默认配置
|
||||
```php
|
||||
<?php
|
||||
|
||||
return [
|
||||
'default' => [
|
||||
'host' => 'localhost',
|
||||
'port' => 5672,
|
||||
'user' => 'guest',
|
||||
'password' => 'guest',
|
||||
'vhost' => '/',
|
||||
'pool' => [
|
||||
'min_connections' => 1,
|
||||
'max_connections' => 10,
|
||||
'connect_timeout' => 10.0,
|
||||
'wait_timeout' => 3.0,
|
||||
'heartbeat' => -1,
|
||||
],
|
||||
'params' => [
|
||||
'insist' => false,
|
||||
'login_method' => 'AMQPLAIN',
|
||||
'login_response' => null,
|
||||
'locale' => 'en_US',
|
||||
'connection_timeout' => 3.0,
|
||||
'read_write_timeout' => 3.0,
|
||||
'context' => null,
|
||||
'keepalive' => false,
|
||||
'heartbeat' => 0,
|
||||
],
|
||||
],
|
||||
'pool2' => [
|
||||
...
|
||||
]
|
||||
];
|
||||
```
|
||||
|
||||
可在 `producer` 或者 `consumer` 的 `__construct` 函数中, 设置不同 `pool`.
|
||||
|
||||
## 投递消息
|
||||
|
||||
使用 `generator` 工具新建一个 `producer`
|
||||
```
|
||||
php bin/hyperf.php gen:amqp-producer DemoProducer
|
||||
```
|
||||
|
||||
在DemoProducer文件中,我们可以修改Producer注解对应的字段来替换对应的 `exchange` 和 `routingKey`。
|
||||
其中 `payload` 就是最终投递到消息队列中的数据,所以我们可以随意改写 `__construct` 方法,只要最后赋值 `payload` 即可。
|
||||
示例如下。
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Amqp\Producers;
|
||||
|
||||
use Hyperf\Amqp\Annotation\Producer;
|
||||
use Hyperf\Amqp\Message\ProducerMessage;
|
||||
use App\Models\User;
|
||||
|
||||
/**
|
||||
* DemoProducer
|
||||
* @Producer(exchange="hyperf", routingKey="hyperf")
|
||||
*/
|
||||
class DemoProducer extends ProducerMessage
|
||||
{
|
||||
public function __construct($id)
|
||||
{
|
||||
// 设置不同 pool
|
||||
$this->poolName = 'pool2';
|
||||
|
||||
$user = User::where('id', $id)->first();
|
||||
$this->payload = [
|
||||
'id' => $id,
|
||||
'data' => $user->toArray()
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
通过container获取Producer实例,即可投递消息。以下实例直接使用ApplicationContext获取Producer其实并不合理,container具体使用请到di模块中查看。
|
||||
|
||||
```php
|
||||
<?php
|
||||
use Hyperf\Amqp\Producer;
|
||||
use App\Amqp\Producers\DemoProducer;
|
||||
use Hyperf\Utils\ApplicationContext;
|
||||
|
||||
$message = new DemoProducer(1);
|
||||
$producer = ApplicationContext::getContainer()->get(Producer::class);
|
||||
$result = $producer->produce($message);
|
||||
|
||||
```
|
||||
|
||||
## 消费消息
|
||||
|
||||
使用 `generator` 工具新建一个 `consumer`。
|
||||
```
|
||||
php bin/hyperf.php gen:amqp-consumer DemoConsumer
|
||||
```
|
||||
|
||||
在DemoConsumer文件中,我们可以修改Consumer注解对应的字段来替换对应的 `exchange`、`routingKey` 和 `queue`。
|
||||
其中 `$data` 就是解析后的元数据。
|
||||
示例如下。
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Amqp\Consumers;
|
||||
|
||||
use Hyperf\Amqp\Annotation\Consumer;
|
||||
use Hyperf\Amqp\Message\ConsumerMessage;
|
||||
use Hyperf\Amqp\Result;
|
||||
|
||||
/**
|
||||
* @Consumer(exchange="hyperf", routingKey="hyperf", queue="hyperf", nums=1)
|
||||
*/
|
||||
class DemoConsumer extends ConsumerMessage
|
||||
{
|
||||
public function consume($data): string
|
||||
{
|
||||
print_r($data);
|
||||
return Result::ACK;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
框架会根据Consumer注解自动创建Process进程,进程意外退出后会被重新拉起。
|
150
doc/zh/annotation.md
Normal file
150
doc/zh/annotation.md
Normal file
@ -0,0 +1,150 @@
|
||||
# 注解
|
||||
|
||||
注解是 Hyperf 非常强大的一项功能,可以通过注解的形式减少很多的配置,以及实现很多非常方便的功能。
|
||||
|
||||
## 概念
|
||||
|
||||
### 什么是注解什么是注释?
|
||||
|
||||
在解释注解之前我们需要先定义一下 `注解` 与 `注释` 的区别:
|
||||
- 注释:给程序员看,帮助理解代码,对代码起到解释、说明的作用。
|
||||
- 注解:给应用程序看,用于元数据的定义,单独使用时没有任何作用,需配合应用程序对其元数据进行利用才有作用。
|
||||
|
||||
### 注解解析如何实现?
|
||||
|
||||
Hyperf 使用了 [doctrine/annotations](https://github.com/doctrine/annotations) 包来对代码内的注解进行解析,注解必须写在下面示例的标准注释块才能被正确解析,其它格式均不能被正确解析。
|
||||
注释块示例:
|
||||
```php
|
||||
/**
|
||||
* @AnnotationClass()
|
||||
*/
|
||||
```
|
||||
在标准注释块内通过书写 `@AnnotationClass()` 这样的语法即表明对当前注释块所在位置的对象(类、类方法、类属性)进行了注解的定义, `AnnotationClass` 对应的是一个 `注解类` 的类名,可写全类的命名空间,亦可只写类名,但需要在当前类 `use` 该注解类以确保能够根据命名空间找到正确的注解类。
|
||||
|
||||
### 注解是如何发挥作用的?
|
||||
|
||||
我们有说到注解只是元数据的定义,需配合应用程序才能发挥作用,在 Hyperf 里,注解内的数据会被收集到 `Hyperf\Di\Annotation\AnnotationCollector` 类供应用程序使用,当然根据您的实际情况,也可以收集到您自定义的类去,随后在这些注解本身希望发挥作用的地方对已收集的注解元数据进行读取和利用,以达到期望的功能实现。
|
||||
|
||||
### 忽略某些注解
|
||||
|
||||
在一些情况下我们可能希望忽略某些 注解,比如我们在接入一些自动生成文档的工具时,有不少工具都是通过注解的形式去定义文档的相关结构内容的,而这些注解可能并不符合 Hyperf 的使用方式,我们可以通过在 `config/autoload/annotations.php` 内将相关注解设置为忽略。
|
||||
|
||||
```php
|
||||
return [
|
||||
'scan' => [
|
||||
// ignore_annotations 数组内的注解都会被注解扫描器忽略
|
||||
'ignore_annotations' => [
|
||||
'mixin',
|
||||
],
|
||||
],
|
||||
];
|
||||
```
|
||||
|
||||
## 使用注解
|
||||
|
||||
注解一共有3种应用对象,分别是 `类`、`类方法` 和 `类属性`。
|
||||
|
||||
### 使用类注解
|
||||
|
||||
类注解定义是在 `class` 关键词上方的注释块内,比如常用的 `@Controller` 和 `@AutoController` 就是类注解的使用典范,下面的代码示例则为一个正确使用类注解的示例,表明 `@ClassAnnotation` 注解应用于 `Foo` 类。
|
||||
```php
|
||||
/**
|
||||
* @ClassAnnotation()
|
||||
*/
|
||||
class Foo {}
|
||||
```
|
||||
|
||||
### 使用类方法注解
|
||||
|
||||
类方法注解定义是在方法上方的注释块内,比如常用的 `@RequestMapping` 就是类方法注解的使用典范,下面的代码示例则为一个正确使用类方法注解的示例,表明 `@MethodAnnotation` 注解应用于 `Foo::bar()` 方法。
|
||||
```php
|
||||
class Foo
|
||||
{
|
||||
/**
|
||||
* @MethodAnnotation()
|
||||
*/
|
||||
public function bar()
|
||||
{
|
||||
// some code
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 使用类属性注解
|
||||
|
||||
类属性注解定义是在属性上方的注释块内,比如常用的 `@Value` 和 `@Inject` 就是类属性注解的使用典范,下面的代码示例则为一个正确使用类属性注解的示例,表明 `@MethodAnnotation` 注解应用于 `Foo::bar()` 方法。
|
||||
```php
|
||||
class Foo
|
||||
{
|
||||
/**
|
||||
* @PropertyAnnotation()
|
||||
*/
|
||||
private $bar;
|
||||
}
|
||||
```
|
||||
|
||||
### 注解参数传递
|
||||
|
||||
- 传递主要的单个参数 `@DemoAnnotation("value")`
|
||||
- 传递字符串参数 `@DemoAnnotation(key1="value1", key2="value2")`
|
||||
- 传递数组参数 `@DemoAnnotation(key={"value1", "value2"})`
|
||||
|
||||
## 自定义注解
|
||||
|
||||
### 创建一个注解类
|
||||
|
||||
在任意地方创建注解类,如下代码示例:
|
||||
|
||||
```php
|
||||
namespace App\Annotation;
|
||||
|
||||
use Hyperf\Di\Annotation\AbstractAnnotation;
|
||||
|
||||
/**
|
||||
* @Annotation
|
||||
* @Target({"METHOD","PROPERTY"})
|
||||
*/
|
||||
class Bar extends AbstractAnnotation
|
||||
{
|
||||
// some code
|
||||
}
|
||||
|
||||
/**
|
||||
* @Annotation
|
||||
* @Target("CLASS")
|
||||
*/
|
||||
class Foo extends AbstractAnnotation
|
||||
{
|
||||
// some code
|
||||
}
|
||||
```
|
||||
|
||||
> 注意注解类的 `@Annotation` 和 `@Target` 注解为全局注解,无需 `use`
|
||||
|
||||
其中 `@Target` 有如下参数:
|
||||
- `METHOD` 注解允许定义在类方法上
|
||||
- `PROPERTY` 注解允许定义在类属性上
|
||||
- `CLASS` 注解允许定义在类上
|
||||
- `ALL` 注解允许定义在任何地方
|
||||
|
||||
我们注意一下在上面的示例代码中,注解类都继承了 `Hyperf\Di\Annotation\AbstractAnnotation` 抽象类,对于注解类来说,这个不是必须的,但对于 Hyperf 的注解类来说,继承 `Hyperf\Di\Annotation\AnnotationInterface` 接口类是必须的,那么抽象类在这里的作用是提供极简的定义方式,该抽象类已经为您实现了`注解参数自动分配到类属性`、`根据注解使用位置自动按照规则收集到 AnnotationCollector` 这样非常便捷的功能。
|
||||
|
||||
### 自定义注解收集器
|
||||
|
||||
注解的收集时具体的执行流程也是在注解类内实现的,相关的方法由 `Hyperf\Di\Annotation\AnnotationInterface` 约束着,该接口类要求了下面 3 个方法的实现,您可以根据自己的需求实现对应的逻辑:
|
||||
|
||||
- `public function collectClass(string $className): void;` 当注解定义在类时被扫描时会触发该方法
|
||||
- `public function collectMethod(string $className, ?string $target): void;` 当注解定义在类方法时被扫描时会触发该方法
|
||||
- `public function collectProperty(string $className, ?string $target): void` 当注解定义在类属性时被扫描时会触发该方法
|
||||
|
||||
### 利用注解数据
|
||||
|
||||
在没有自定义注解收集方法时,默认会将注解的元数据统一收集在 `Hyperf\Di\Annotation\AnnotationCollector` 类内,通过该类的静态方法可以方便的获取对应的元数据用于逻辑判断或实现。
|
||||
|
||||
## IDE 注解插件
|
||||
|
||||
因为 `PHP` 并不是原生支持 `注解`,所以 `IDE` 不会默认增加注解支持。但我们可以添加第三方插件,来让 `IDE` 支持 `注解`。
|
||||
|
||||
### PhpStorm
|
||||
|
||||
我们到 `Plugins` 中搜索 `PHP Annotations`,就可以找到对应的组件 [PHP Annotations](https://github.com/Haehnchen/idea-php-annotation-plugin)。然后安装组件,重启 `PhpStorm`,就可以愉快的使用注解功能了,主要提供了为注解类增加自动跳转和代码提醒支持,使用注解时自动引用注解对应的命名空间等非常便捷有用的功能。
|
71
doc/zh/aop.md
Normal file
71
doc/zh/aop.md
Normal file
@ -0,0 +1,71 @@
|
||||
# AOP 面向切面编程
|
||||
|
||||
## 概念
|
||||
|
||||
AOP 为 `Aspect Oriented Programming` 的缩写,意为:`面向切面编程`,通过动态代理等技术实现程序功能的统一维护的一种技术。AOP 是 OOP 的延续,也是 Hyperf 中的一个重要内容,是函数式编程的一种衍生范型。利用 AOP 可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
|
||||
|
||||
用通俗的话来讲,就是在 Hyperf 里可以通过 `切面(Aspect)` 介入到由 [hyperf/di](https://github.com/hyperf-cloud/di) 管理的任意类的任意方法的执行流程中去,从而改变或加强原方法的功能,这就是 AOP。
|
||||
|
||||
> 使用 AOP 功能必须使用 [hyperf/di](https://github.com/hyperf-cloud/di) 来作为依赖注入容器
|
||||
|
||||
## 介绍
|
||||
|
||||
相对于其它框架实现的 AOP 功能的使用方式,我们进一步简化了该功能的使用不做过细的划分,仅存在 `环绕(Arround)` 一种通用的形式:
|
||||
|
||||
- `切面(Aspect)` 为对流程织入的定义类,包括要介入的目标,以及实现对原方法的修改加强处理
|
||||
- `代理类(ProxyClass)` ,每个被介入的目标类最终都会生成一个代理类,来达到执行 `切面(Aspect)` 方法的目的,而非通过原类
|
||||
|
||||
## 定义切面(Aspect)
|
||||
|
||||
每个 `切面(Aspect)` 必须实现 `Hyperf\Di\Aop\ArroundInterface` 接口,并提供 `public` 的 `$classes` 和 `$annotations` 属性,为了方便使用,我们可以通过继承 `Hyperf\Di\Aop\AbstractAspect` 来简化定义过程,我们通过代码来描述一下。
|
||||
|
||||
```php
|
||||
<?php
|
||||
namespace App\Aspect;
|
||||
|
||||
use App\Service\SomeClass;
|
||||
use App\Annotation\SomeAnnotation;
|
||||
use Hyperf\Di\Annotation\Aspect;
|
||||
use Hyperf\Di\Aop\AbstractAspect;
|
||||
use Hyperf\Di\Aop\ProceedingJoinPoint;
|
||||
|
||||
/**
|
||||
* @Aspect
|
||||
*/
|
||||
class FooAspect extends AbstractAspect
|
||||
{
|
||||
// 要切入的类,可以多个,亦可通过 :: 标识到具体的某个方法,通过 * 可以模糊匹配
|
||||
public $classes = [
|
||||
SomeClass::class,
|
||||
'App\Service\SomeClass::someMethod',
|
||||
'App\Service\SomeClass::*Method',
|
||||
];
|
||||
|
||||
// 要切入的注解,具体切入的还是使用了这些注解的类,仅可切入类注解和类方法注解
|
||||
public $annotations = [
|
||||
SomeAnnotation::class,
|
||||
];
|
||||
|
||||
public function process(ProceedingJoinPoint $proceedingJoinPoint)
|
||||
{
|
||||
// 切面切入后,执行对应的方法会由此来负责
|
||||
// $proceedingJoinPoint 为连接点,通过该类的 process() 方法调用原方法并获得结果
|
||||
// 在调用前进行某些处理
|
||||
$result = $proceedingJoinPoint->process();
|
||||
// 在调用后进行某些处理
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
每个 `切面(Aspect)` 必须定义 `@Aspect` 注解或在 `config/autoload/aspects.php` 内配置均可发挥作用。
|
||||
|
||||
> 使用 `@Aspect` 注解时需 `use Hyperf\Di\Annotation\Aspect;` 命名空间;
|
||||
|
||||
## 代理类缓存
|
||||
|
||||
所有被 AOP 影响的类,都会在 `./runtime/container/proxy/` 文件夹内生成对应的 `代理类缓存`,服务启动时,如果类所对应的代理类缓存存在,则不会重新生成直接使用缓存,即时 `Aspect` 的切入范围发生了改变。不存在时,则会自动重新生成新的代理类缓存。
|
||||
|
||||
在部署生成环境时,我们可能会希望 Hyperf 提前将所有代理类提前生成,而不是使用时动态的生成,可以通过 `php bin/hyperf.php di:init-proxy` 命令来生成所有代理类,该命令会忽视现有的代理类缓存,全部重新生成。
|
||||
|
||||
基于以上,我们可以将生成代理类的命令和启动服务的命令结合起来,`php bin/hyperf.php di:init-proxy && php bin/hyperf.php start` 来达到自动重新生成所有代理类缓存然后启动服务的目的。
|
114
doc/zh/async-queue.md
Normal file
114
doc/zh/async-queue.md
Normal file
@ -0,0 +1,114 @@
|
||||
# 异步队列
|
||||
|
||||
异步队列区别于 `RabbitMQ` `Kafka` 等消息队列,它只提供一种 `异步处理` 和 `异步延时处理` 的能力,并不能严格地保证消息的持久化和支持 `ACK 应答机制`。
|
||||
|
||||
## 安装
|
||||
|
||||
```bash
|
||||
composer require hyperf/async-queue
|
||||
```
|
||||
|
||||
## 配置
|
||||
|
||||
配置文件位于 `config/autoload/async_queue.php`,如文件不存在可自行创建。
|
||||
|
||||
> 暂时只支持 `Redis Driver` 驱动。
|
||||
|
||||
| 配置 | 类型 | 默认值 | 备注 |
|
||||
|:-------------:|:------:|:-------------------------------------------:|:------------------:|
|
||||
| driver | string | Hyperf\AsyncQueue\Driver\RedisDriver::class | 无 |
|
||||
| channel | string | queue | 队列前缀 |
|
||||
| retry_seconds | int | 5 | 失败后重新尝试间隔 |
|
||||
| processes | int | 1 | 消费进程数 |
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
return [
|
||||
'default' => [
|
||||
'driver' => Hyperf\AsyncQueue\Driver\RedisDriver::class,
|
||||
'channel' => 'queue',
|
||||
'retry_seconds' => 5,
|
||||
'processes' => 1,
|
||||
],
|
||||
];
|
||||
|
||||
```
|
||||
|
||||
## 使用
|
||||
|
||||
### 消费消息
|
||||
|
||||
组件已经提供了默认子进程,只需要将子进程配置到 `processes.php` 中即可。
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
return [
|
||||
Hyperf\AsyncQueue\Process\ConsumerProcess::class,
|
||||
];
|
||||
|
||||
```
|
||||
|
||||
### 发布消息
|
||||
|
||||
首先我们定义一个消息,如下
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use Hyperf\AsyncQueue\Job;
|
||||
|
||||
class ExampleJob extends Job
|
||||
{
|
||||
public function handle()
|
||||
{
|
||||
var_dump('hello world');
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
发布消息
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Hyperf\AsyncQueue\Driver\DriverFactory;
|
||||
use Hyperf\AsyncQueue\Driver\DriverInterface;
|
||||
|
||||
class DemoService
|
||||
{
|
||||
/**
|
||||
* @var DriverInterface
|
||||
*/
|
||||
protected $driver;
|
||||
|
||||
public function __construct(DriverFactory $driverFactory)
|
||||
{
|
||||
$this->driver = $driverFactory->get('default');
|
||||
}
|
||||
|
||||
public function publish()
|
||||
{
|
||||
// 发布消息
|
||||
// 这里的 ExampleJob 是直接实例化出来的,所以不能在 Job 内使用 @Inject @Value 等注解及注解所对应功能的其它使用方式
|
||||
return $this->driver->push(new ExampleJob());
|
||||
}
|
||||
|
||||
public function delay()
|
||||
{
|
||||
// 发布延迟消息
|
||||
// 第二个参数 $delay 即为延迟的秒数
|
||||
return $this->driver->push(new ExampleJob(), 60);
|
||||
}
|
||||
}
|
||||
|
||||
```
|
77
doc/zh/awesome-components.md
Normal file
77
doc/zh/awesome-components.md
Normal file
@ -0,0 +1,77 @@
|
||||
# 协程组件库
|
||||
|
||||
所有官方提供的组件库均已进行协程化处理,可安全地在 Hyperf 内或其它协程框架内使用,基于 Hyperf 的开放性和可扩展性,社区可对此开发或适配各种各样的组件,得益于此,Hyperf 将存在着无限的可能性。
|
||||
本页将收录各个适配了 Hyperf 的协程组件 和 已经经过验证可安全地用于协程下的常用库,以便您快速的从中选择合适的组件完成您的需求。
|
||||
|
||||
## 如何提交我的组件?
|
||||
|
||||
如果您开发的协程组件适配了 Hyperf,那么您可以直接对 [hyperf-cloud/hyperf](https://github.com/hyperf-cloud/hyperf) 项目的 `gh-pages` 分支发起您的 `Pull Request`,也就是更改当前页`(./awesome-components.md)`。
|
||||
|
||||
## 如何适配 Hyperf ?
|
||||
|
||||
我们为您提供了一份 [Hyperf 组件开发指南](zh/component-guide/intro.md),以帮助您开发 Hyperf 组件或适配 Hyperf 框架。
|
||||
|
||||
# 组件列表
|
||||
|
||||
## 路由
|
||||
|
||||
- [nikic/fastroute](https://github.com/nikic/FastRoute) 一个常用的高速路由
|
||||
|
||||
## 事件
|
||||
|
||||
- [hyperf/event](https://github.com/hyperf-cloud/event) Hyperf 官方提供的基于 PSR-14 的事件管理器
|
||||
|
||||
## 日志
|
||||
|
||||
- [hyperf/logger](https://github.com/hyperf-cloud/logger) Hyperf 官方提供的基于 PSR-3 的日志管理器
|
||||
|
||||
## 命令
|
||||
|
||||
- [hyperf/command](https://github.com/hyperf-cloud/command) Hyperf 官方提供的基于 [symfony/console](https://github.com/symfony/console) 扩展并支持注解的命令管理组件
|
||||
- [symfony/console](https://github.com/symfony/console) Symfony 提供的独立命令管理组件
|
||||
|
||||
## 数据库
|
||||
|
||||
- [hyperf/database](https://github.com/hyperf-cloud/database) Hyperf 官方提供的基于 Eloquent 衍生的数据库 ORM,可复用于其它框架
|
||||
- [hyperf/model](https://github.com/hyperf-cloud/model) Hyperf 官方提供的基于 [hyperf/database](https://github.com/hyperf-cloud/database) 组件的自动模型缓存组件
|
||||
|
||||
## 依赖注入容器
|
||||
|
||||
- [hyperf/di](https://github.com/hyperf-cloud/di) Hyperf 官方提供的支持注解及 AOP 的依赖注入容器
|
||||
|
||||
## 服务
|
||||
|
||||
- [hyperf/http-server](https://github.com/hyperf-cloud/http-server) Hyperf 官方提供的 HTTP 服务端
|
||||
- [hyperf/grpc-server](https://github.com/hyperf-cloud/grpc-server) Hyperf 官方提供的 GRPC 服务端
|
||||
- [hyperf/websocket-server](https://github.com/hyperf-cloud/websocket-server) Hyperf 官方提供的 WebSocket 服务端
|
||||
- [hyperf/rpc-server](https://github.com/hyperf-cloud/rpc-server) Hyperf 官方提供的通用 RPC 抽象服务端
|
||||
|
||||
## 客户端
|
||||
|
||||
- [hyperf/consul](https://github.com/hyperf-cloud/consul) Hyperf 官方提供的 Consul 协程客户端
|
||||
- [hyperf/elasticsearch](https://github.com/hyperf-cloud/elasticsearch) Hyperf 官方提供的 Elasticsearch 协程客户端
|
||||
- [hyperf/grpc-client](https://github.com/hyperf-cloud/grpc-client) Hyperf 官方提供的 GRPC 协程客户端
|
||||
- [hyperf/rpc-client](https://github.com/hyperf-cloud/rpc-client) Hyperf 官方提供的通用 RPC 抽象协程客户端
|
||||
- [hyperf/guzzle](https://github.com/hyperf-cloud/guzzle) Hyperf 官方提供的 Guzzle 协程客户端
|
||||
- [hyperf/redis](https://github.com/hyperf-cloud/redis) Hyperf 官方提供的 Redis 协程客户端
|
||||
- [hyperf/websocket-client](https://github.com/hyperf-cloud/websocket-client) Hyperf 官方提供的 WebSocket 协程客户端
|
||||
- [hyperf/cache](https://github.com/hyperf-cloud/cache) Hyperf 官方提供的基于 PSR-16 的缓存协程客户端
|
||||
|
||||
## 消息队列
|
||||
|
||||
- [hyperf/amqp](https://github.com/hyperf-cloud/amqp) Hyperf 官方提供的 AMQP 协程组件
|
||||
- [hyperf/async-queue](https://github.com/hyperf-cloud/async-queue) Hyperf 官方提供的简单的异步队列组件
|
||||
|
||||
## 配置中心
|
||||
|
||||
- [hyperf/config-apollo](https://github.com/hyperf-cloud/config-apollo) Hyperf 官方提供的 Apollo 配置中心接入组件
|
||||
- [hyperf/config-aliyun-acm](https://github.com/hyperf-cloud/config-aliyun-acm) Hyperf 官方提供的阿里云 ACM 应用配置服务接入组件
|
||||
|
||||
## 服务治理
|
||||
|
||||
- [hyperf/json-rpc](https://github.com/hyperf-cloud/json-rpc) Hyperf 官方提供的 JSON-RPC 协议组件
|
||||
- [hyperf/rate-limit](https://github.com/hyperf-cloud/rate-limit) Hyperf 官方提供的基于令牌桶算法的限流组件
|
||||
- [hyperf/load-balancer](https://github.com/hyperf-cloud/load-balancer) Hyperf 官方提供的负载均衡组件
|
||||
- [hyperf/service-gevernance](https://github.com/hyperf-cloud/service-gevernance) Hyperf 官方提供的服务治理组件
|
||||
- [hyperf/tracer](https://github.com/hyperf-cloud/tracer) Hyperf 官方提供的 OpenTracing 分布式调用链追踪组件
|
||||
- [hyperf/circuit-breaker](https://github.com/hyperf-cloud/circuit-breaker) Hyperf 官方提供的服务熔断组件
|
186
doc/zh/cache.md
Normal file
186
doc/zh/cache.md
Normal file
@ -0,0 +1,186 @@
|
||||
# Cache
|
||||
|
||||
[hyperf/cache](https://github.com/hyperf-cloud/cache) 提供了基于 `Aspect` 实现的切面缓存,也提供了实现 `Psr\SimpleCache\CacheInterface` 的缓存类。
|
||||
|
||||
## 安装
|
||||
```
|
||||
composer require hyperf/cache
|
||||
```
|
||||
|
||||
## 默认配置
|
||||
|
||||
| 配置 | 默认值 | 备注 |
|
||||
|:------:|:----------------------------------------:|:---------------------:|
|
||||
| driver | Hyperf\Cache\Driver\RedisDriver | 缓存驱动,默认为Redis |
|
||||
| packer | Hyperf\Utils\Packer\PhpSerializer | 打包器 |
|
||||
| prefix | c: | 缓存前缀 |
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
return [
|
||||
'default' => [
|
||||
'driver' => Hyperf\Cache\Driver\RedisDriver::class,
|
||||
'packer' => Hyperf\Utils\Packer\PhpSerializer::class,
|
||||
'prefix' => 'c:',
|
||||
],
|
||||
];
|
||||
```
|
||||
|
||||
## 使用
|
||||
|
||||
### SimpleCache 方式
|
||||
|
||||
如果您只想使用实现 `Psr\SimpleCache\CacheInterface` 缓存类,比如重写 `EasyWeChat` 缓存模块,可以很方便的从 `Container` 中获取相应对象。
|
||||
|
||||
```php
|
||||
|
||||
$cache = $container->get(Psr\SimpleCache\CacheInterface::class);
|
||||
|
||||
```
|
||||
|
||||
### 注解方式
|
||||
|
||||
组件提供 `Hyperf\Cache\Annotation\Cacheable` 注解,作用于类方法,可以配置对应的缓存前缀、失效时间、监听器和缓存组。
|
||||
例如,UserService 提供一个 user 方法,可以查询对应id的用户信息。当加上 `Hyperf\Cache\Annotation\Cacheable` 注解后,会自动生成对应的Redis缓存,key值为`user:id`,超时时间为 9000 秒。首次查询时,会从数据库中查,后面查询时,会从缓存中查。
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\User;
|
||||
use Hyperf\Cache\Annotation\Cacheable;
|
||||
|
||||
class UserService
|
||||
{
|
||||
/**
|
||||
* @Cacheable(prefix="user", ttl=9000, listener="user-update")
|
||||
*/
|
||||
public function user($id)
|
||||
{
|
||||
$user = User::query()->where('id',$id)->first();
|
||||
|
||||
if($user){
|
||||
return $user->toArray();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 清理注解缓存
|
||||
|
||||
当然,如果我们数据库中的数据改变了,如果删除缓存呢?这里就需要用到后面的监听器。下面新建一个 Service 提供一方法,来帮我们处理缓存。
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use Hyperf\Di\Annotation\Inject;
|
||||
use Hyperf\Cache\Listener\DeleteListenerEvent;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
class SystemService
|
||||
{
|
||||
/**
|
||||
* @Inject
|
||||
* @var EventDispatcherInterface
|
||||
*/
|
||||
protected $dispatcher;
|
||||
|
||||
public function flushCache($userId)
|
||||
{
|
||||
$this->dispatcher->dispatch(new DeleteListenerEvent('user-update', [$userId]));
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 注解介绍
|
||||
|
||||
### Cacheable
|
||||
|
||||
例如以下配置,缓存前缀为 user, 超时时间为 7200, 删除事件名为 USER_CACHE。生成对应缓存 KEY 为 `c:user:1`。
|
||||
|
||||
```php
|
||||
use App\Models\User;
|
||||
use Hyperf\Cache\Annotation\Cacheable;
|
||||
|
||||
/**
|
||||
* @Cacheable(prefix="user", ttl=7200, listener="USER_CACHE")
|
||||
*/
|
||||
public function user(int $id): array
|
||||
{
|
||||
$user = User::query()->find($id);
|
||||
|
||||
return [
|
||||
'user' => $user->toArray(),
|
||||
'uuid' => $this->unique(),
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
当设置 value 后,框架会根据设置的规则,进行缓存 KEY 键命名。如下实例,当 $user->id = 1 时,缓存 KEY 为 `c:userBook:_1`
|
||||
|
||||
```php
|
||||
use App\Models\User;
|
||||
use Hyperf\Cache\Annotation\Cacheable;
|
||||
|
||||
/**
|
||||
* @Cacheable(prefix="userBook", ttl=6666, value="_#{user.id}")
|
||||
*/
|
||||
public function userBook(User $user): array
|
||||
{
|
||||
return [
|
||||
'book' => $user->book->toArray(),
|
||||
'uuid' => $this->unique(),
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
### CachePut
|
||||
|
||||
CachePut 不同于 Cacheable,它每次调用都会执行函数体,然后再对缓存进行重写。所以当我们想更新缓存时,可以调用相关方法。
|
||||
|
||||
```php
|
||||
use App\Models\User;
|
||||
use Hyperf\Cache\Annotation\CachePut;
|
||||
|
||||
/**
|
||||
* @CachePut(prefix="user", ttl=3601)
|
||||
*/
|
||||
public function updateUser(int $id)
|
||||
{
|
||||
$user = User::query()->find($id);
|
||||
$user->name = 'HyperfDoc';
|
||||
$user->save();
|
||||
|
||||
return [
|
||||
'user' => $user->toArray(),
|
||||
'uuid' => $this->unique(),
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
### CacheEvict
|
||||
|
||||
CacheEvict 更容易理解了,当执行方法体后,会主动清理缓存。
|
||||
|
||||
```php
|
||||
use Hyperf\Cache\Annotation\CacheEvict;
|
||||
|
||||
/**
|
||||
* @CacheEvict(prefix="userBook", value="_#{id}")
|
||||
*/
|
||||
public function updateUserBook(int $id)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
81
doc/zh/circuit-breaker.md
Normal file
81
doc/zh/circuit-breaker.md
Normal file
@ -0,0 +1,81 @@
|
||||
# 熔断器
|
||||
|
||||
## 安装
|
||||
|
||||
```
|
||||
composer require hyperf/circuit-breaker
|
||||
```
|
||||
|
||||
## 为什么要熔断?
|
||||
|
||||
分布式系统中经常会出现由于某个基础服务不可用造成整个系统不可用的情况,这种现象被称为服务雪崩效应。为了应对服务雪崩,一种常见的做法是服务降级。而 [hyperf/circuit-breaker](https://github.com/hyperf-cloud/circuit-breaker) 组件,就是为了来解决这个问题的。
|
||||
|
||||
## 使用熔断器
|
||||
|
||||
熔断器的使用十分简单,只需要加入 `Hyperf\CircuitBreaker\Annotation\CircuitBreaker` 注解,就可以根据规定策略,进行熔断。
|
||||
比如我们需要到另外服务中查询用户列表,用户列表需要关联N多表,查询效率较低,但平常并发量不高的时候,相应速度还说得过去。一旦并发量激增,就会导致响应速度变慢,并会使对方服务出现慢查。这个时候,我们只需要配置一下熔断超时时间 `timeout` 为 0.05 秒,失败计数 `failCounter` 超过 1 次后熔断,相应 `fallback` 为 `App\UserService` 类的 `searchFallback` 方法。这样当响应超时并触发熔断后,就不会再请求对端的服务了,而是直接将服务降级从当前项目中返回数据,即根据 `fallback` 指定的方法来进行返回。
|
||||
|
||||
```php
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\UserServiceClient;
|
||||
use Hyperf\CircuitBreaker\Annotation\CircuitBreaker;
|
||||
use Hyperf\Di\Annotation\Inject;
|
||||
|
||||
class UserService
|
||||
{
|
||||
/**
|
||||
* @Inject
|
||||
* @var UserServiceClient
|
||||
*/
|
||||
private $client;
|
||||
|
||||
/**
|
||||
* @CircuitBreaker(timeout=0.05, failCounter=1, successCounter=1, fallback="App\UserService::searchFallback")
|
||||
*/
|
||||
public function search($offset, $limit)
|
||||
{
|
||||
return $this->client->users($offset, $limit);
|
||||
}
|
||||
|
||||
public function searchFallback($offset, $limit)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
默认熔断策略为`超时策略`,如果您想要自己实现熔断策略,只需要自己实现 `Handler` 继承于 `Hyperf\CircuitBreaker\Handler\AbstractHandler` 即可。
|
||||
|
||||
```php
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Hyperf\CircuitBreaker\Handler;
|
||||
|
||||
use Hyperf\CircuitBreaker\Annotation\CircuitBreaker as Annotation;
|
||||
use Hyperf\CircuitBreaker\CircuitBreaker;
|
||||
use Hyperf\CircuitBreaker\Exception\TimeoutException;
|
||||
use Hyperf\Di\Aop\ProceedingJoinPoint;
|
||||
|
||||
class DemoHandler extends AbstractHandler
|
||||
{
|
||||
const DEFAULT_TIMEOUT = 5;
|
||||
|
||||
protected function process(ProceedingJoinPoint $proceedingJoinPoint, CircuitBreaker $breaker, Annotation $annotation)
|
||||
{
|
||||
$result = $proceedingJoinPoint->process();
|
||||
|
||||
if (is_break()) {
|
||||
throw new TimeoutException('timeout, use ' . $use . 's', $result);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
```
|
163
doc/zh/command.md
Normal file
163
doc/zh/command.md
Normal file
@ -0,0 +1,163 @@
|
||||
# 命令行
|
||||
|
||||
Hyperf 的命令行默认由 [hyperf/command](https://github.com/hyperf-cloud/command) 组件提供,而该组件本身也是基于 [symfony/console](https://github.com/symfony/console) 的抽象。
|
||||
|
||||
# 安装
|
||||
|
||||
通常来说该组件会默认存在,但如果您希望用于非 Hyperf 项目,也可通过下面的命令依赖 [hyperf/command](https://github.com/hyperf-cloud/command) 组件:
|
||||
|
||||
```bash
|
||||
composer require hyperf/command
|
||||
```
|
||||
|
||||
# 查看命令列表
|
||||
|
||||
直接运行 `php bin/hyperf.php` 不带任何的参数即为输出命令列表。
|
||||
|
||||
# 自定义命令
|
||||
|
||||
## 生成命令
|
||||
|
||||
如果你有安装 [hyperf/devtool](https://github.com/hyperf-cloud/devtool) 组件的话,可以通过 `gen:command` 命令来生成一个自定义命令:
|
||||
|
||||
```bash
|
||||
php bin/hyperf.php gen:command FooCommand
|
||||
```
|
||||
执行上述命令后,便会在 `app/Command` 文件夹内生成一个配置好的 `FooCommand` 类了。
|
||||
|
||||
### 定义命令
|
||||
|
||||
定义该命令类所对应的命令有两种形式,一种是通过 `$name` 属性定义,另一种是通过构造函数传参来定义,我们通过代码示例来演示一下,假设我们希望定义该命令类的命令为 `foo:hello`:
|
||||
|
||||
#### `$name` 属性定义:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use Hyperf\Command\Command as HyperfCommand;
|
||||
use Hyperf\Command\Annotation\Command;
|
||||
|
||||
/**
|
||||
* @Command
|
||||
*/
|
||||
class FooCommand extends HyperfCommand
|
||||
{
|
||||
/**
|
||||
* 执行的命令行
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $name = 'foo:hello';
|
||||
}
|
||||
```
|
||||
|
||||
#### 构造函数传参定义:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use Hyperf\Command\Command as HyperfCommand;
|
||||
use Hyperf\Command\Annotation\Command;
|
||||
|
||||
/**
|
||||
* @Command
|
||||
*/
|
||||
class FooCommand extends HyperfCommand
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('foo:hello');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 定义命令类逻辑
|
||||
|
||||
命令类实际运行的逻辑是取决于 `handle` 方法内的代码,也就意味着 `handle` 方法就是命令的入口。
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use Hyperf\Command\Command as HyperfCommand;
|
||||
use Hyperf\Command\Annotation\Command;
|
||||
|
||||
/**
|
||||
* @Command
|
||||
*/
|
||||
class FooCommand extends HyperfCommand
|
||||
{
|
||||
/**
|
||||
* 执行的命令行
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $name = 'foo:hello';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
// 通过内置方法 line 在 Console 输出 Hello Hyperf.
|
||||
$this->line('Hello Hyperf.', 'info');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 定义命令类的参数
|
||||
|
||||
在编写命令时,通常是通过 `参数` 和 `选项` 来收集用户的输入的,在收集一个用户输入前,必须对该 `参数` 或 `选项` 进行定义。
|
||||
|
||||
#### 参数
|
||||
|
||||
假设我们希望定义一个 `name` 参数,然后通过传递任意字符串如 `Hyperf` 于命令一起并执行 `php bin/hyperf.php foo:hello Hyperf` 输出 `Hello Hyperf`,我们通过代码来演示一下:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use Hyperf\Command\Annotation\Command;
|
||||
use Hyperf\Command\Command as HyperfCommand;
|
||||
|
||||
/**
|
||||
* @Command
|
||||
*/
|
||||
class FooCommand extends HyperfCommand
|
||||
{
|
||||
/**
|
||||
* 执行的命令行
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $name = 'foo:hello';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
// 从 $input 获取 name 参数
|
||||
$argument = $this->input->getArgument('name') ?? 'World';
|
||||
$this->line('Hello ' . $argument, 'info');
|
||||
}
|
||||
|
||||
protected function getArguments()
|
||||
{
|
||||
return [
|
||||
['name', InputArgument::OPTIONAL, '这里是对这个参数的解释']
|
||||
];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
执行 `php bin/hyperf.php foo:hello Hyperf` 我们就能看到输出了 `Hello Hyperf` 了。
|
||||
|
13
doc/zh/communication.md
Normal file
13
doc/zh/communication.md
Normal file
@ -0,0 +1,13 @@
|
||||
# 交流
|
||||
|
||||
我们提供了 `QQ 群` 和 `微信群` 两种沟通途径,非本页面提供的其它交流途径均非官方行为。
|
||||
|
||||
## QQ 群
|
||||
|
||||
群号: `862099724`
|
||||
|
||||
## 微信群
|
||||
|
||||
由于微信群无法直接加入,故可先加下方二维码好友,并声明目的,再拉您入群。
|
||||
|
||||
![wechat](./imgs/wechat.jpg ':size=375')
|
97
doc/zh/component-guide/intro.md
Normal file
97
doc/zh/component-guide/intro.md
Normal 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)` 了。
|
89
doc/zh/config-center.md
Normal file
89
doc/zh/config-center.md
Normal file
@ -0,0 +1,89 @@
|
||||
# 简介
|
||||
|
||||
Hyperf 为您提供了分布式系统的外部化配置支持,默认适配了:
|
||||
|
||||
- 由携程开源的 [ctripcorp/apollo](https://github.com/ctripcorp/apollo),由 [hyper/config-apollo](https://github.com/hyperf-cloud/config-apollo) 组件提供功能支持。
|
||||
- 阿里云提供的免费配置中心服务 [应用配置管理(ACM, Application Config Manager)](https://help.aliyun.com/product/59604.html),由 [hyper/config-aliyun-acm](https://github.com/hyperf-cloud/config-aliyun-acm) 组件提供功能支持。
|
||||
|
||||
## 为什么要使用配置中心?
|
||||
|
||||
随着业务的发展,微服务架构的升级,服务的数量、应用的配置日益增多(各种微服务、各种服务器地址、各种参数),传统的配置文件方式和数据库的方式已经可能无法满足开发人员对配置管理的要求,同时对于配置的管理可能还会牵涉到 ACL 权限管理、配置版本管理和回滚、格式验证、配置灰度发布、集群配置隔离等问题,以及:
|
||||
|
||||
- 安全性:配置跟随源代码保存在版本管理系统中,容易造成配置泄漏
|
||||
- 时效性:修改配置,需要每台服务器每个应用修改并重启服务
|
||||
- 局限性:无法支持动态调整,例如日志开关、功能开关等
|
||||
|
||||
因此,我们可以通过一个配置中心以一种科学的管理方式来统一管理相关的配置。
|
||||
|
||||
## 安装
|
||||
|
||||
### Apollo
|
||||
|
||||
```bash
|
||||
composer require hyperf/config-apollo
|
||||
```
|
||||
|
||||
### Aliyun ACM
|
||||
|
||||
```bash
|
||||
composer require hyperf/config-aliyun-acm
|
||||
```
|
||||
|
||||
## 接入 Apollo 配置中心
|
||||
|
||||
如果您没有对配置组件进行替换使用默认的 [hyperf/config](https://github.com/hyperf-cloud/config) 组件的话,接入 Apollo 配置中心则是轻而易举,只需两步。
|
||||
- 通过 Composer 将 [hyperf/config-apollo](https://github.com/hyperf-cloud/config-apollo) ,即执行命令 `composer require hyperf/config-apollo`
|
||||
- 在 `config/autoload` 文件夹内增加一个 `apollo.php` 的配置文件,配置内容如下
|
||||
|
||||
```php
|
||||
<?php
|
||||
return [
|
||||
// 是否开启配置中心的接入流程,为 true 时会自动启动一个 ConfigFetcherProcess 进程用于更新配置
|
||||
'enable' => true,
|
||||
// Apollo Server
|
||||
'server' => 'http://127.0.0.1:8080',
|
||||
// 您的 AppId
|
||||
'appid' => 'test',
|
||||
// 当前应用所在的集群
|
||||
'cluster' => 'default',
|
||||
// 当前应用需要接入的 Namespace,可配置多个
|
||||
'namespaces' => [
|
||||
'application',
|
||||
],
|
||||
// 配置更新间隔(秒)
|
||||
'interval' => 5,
|
||||
];
|
||||
```
|
||||
|
||||
## 接入 Aliyun ACM 配置中心
|
||||
|
||||
接入 Aliyun ACM 配置中心与 Apollo 一样都是轻而易举的,同样只需两步。
|
||||
- 通过 Composer 将 [hyperf/config-aliyun-acm](https://github.com/hyperf-cloud/config-aliyun-acm) ,即执行命令 `composer require hyperf/config-aliyun-acm`
|
||||
- 在 `config/autoload` 文件夹内增加一个 `aliyun_acm.php` 的配置文件,配置内容如下
|
||||
|
||||
```php
|
||||
<?php
|
||||
return [
|
||||
// 是否开启配置中心的接入流程,为 true 时会自动启动一个 ConfigFetcherProcess 进程用于更新配置
|
||||
'enable' => true,
|
||||
// 配置更新间隔(秒)
|
||||
'interval' => 5,
|
||||
// 阿里云 ACM 断点地址,取决于您的可用区
|
||||
'endpoint' => env('ALIYUN_ACM_ENDPOINT', 'acm.aliyun.com'),
|
||||
// 当前应用需要接入的 Namespace
|
||||
'namespace' => env('ALIYUN_ACM_NAMESPACE', ''),
|
||||
// 您的配置对应的 Data ID
|
||||
'data_id' => env('ALIYUN_ACM_DATA_ID', ''),
|
||||
// 您的配置对应的 Group
|
||||
'group' => env('ALIYUN_ACM_GROUP', 'DEFAULT_GROUP'),
|
||||
// 您的阿里云账号的 Access Key
|
||||
'access_key' => env('ALIYUN_ACM_AK', ''),
|
||||
// 您的阿里云账号的 Secret Key
|
||||
'secret_key' => env('ALIYUN_ACM_SK', ''),
|
||||
];
|
||||
```
|
||||
|
||||
## 配置更新的作用范围
|
||||
|
||||
在默认的功能实现下,是由一个 `ConfigFetcherProcess` 进程根据配置的 `interval` 来向 Apollo 拉取对应 `namespace` 的配置,并通过 IPC 通讯将拉取到的新配置传递到各个 Worker 中,并更新到 `Hyperf\Contract\ConfigInterface` 对应的对象内。
|
||||
需要注意的是,更新的配置只会更新 `Config` 对象,故仅限应用层或业务层的配置,不涉及框架层的配置改动,因为框架层的配置改动需要重启服务,如果您有这样的需求,也可以通过自行实现 `ConfigFetcherProcess` 来达到目的。
|
175
doc/zh/config.md
Normal file
175
doc/zh/config.md
Normal file
@ -0,0 +1,175 @@
|
||||
# 配置
|
||||
|
||||
当您使用的是 [hyperf-cloud/hyperf-skeleton](https://github.com/hyperf-cloud/hyperf-skeleton) 项目创建的项目时,或基于 [hyperf-cloud/installer](https://github.com/hyperf-cloud/installer) 创建的项目,Hyperf 的所有配置文件均处于根目录下的 `config` 文件夹内,每个选项都有说明,您可以随时查看并熟悉有哪些选项可以使用。
|
||||
|
||||
# 安装
|
||||
|
||||
```bash
|
||||
composer require hyperf/config
|
||||
```
|
||||
|
||||
# 配置文件结构
|
||||
|
||||
以下结构仅为 Hyperf-Skeleton 所提供的默认配置的情况下的结构,实际情况由于依赖或使用的组件的差异,文件会有差异。
|
||||
```
|
||||
config
|
||||
├── autoload // 此文件夹内的配置文件会被配置组件自己加载,并以文件夹内的文件名作为第一个键值
|
||||
│ ├── amqp.php // 用于管理 AMQP 组件
|
||||
│ ├── annotations.php // 用于管理注解
|
||||
│ ├── apollo.php // 用于管理基于 Apollo 实现的配置中心
|
||||
│ ├── aspects.php // 用于管理 AOP 切面
|
||||
│ ├── async_queue.php // 用于管理基于 Redis 实现的简易队列服务
|
||||
│ ├── cache.php // 用于管理缓存组件
|
||||
│ ├── commands.php // 用于管理自定义命令
|
||||
│ ├── consul.php // 用于管理 Consul 客户端
|
||||
│ ├── databases.php // 用于管理数据库客户端
|
||||
│ ├── devtool.php // 用于管理开发者工具
|
||||
│ ├── exceptions.php // 用于管理异常处理器
|
||||
│ ├── listeners.php // 用于管理事件监听者
|
||||
│ ├── logger.php // 用于管理日志
|
||||
│ ├── middlewares.php // 用于管理中间件
|
||||
│ ├── opentracing.php // 用于管理调用链追踪
|
||||
│ ├── processes.php // 用于管理自定义进程
|
||||
│ ├── redis.php // 用于管理 Redis 客户端
|
||||
│ └── server.php // 用于管理 Server 服务
|
||||
├── config.php // 用于管理用户或框架的配置,如配置相对独立亦可放于 autoload 文件夹内
|
||||
├── container.php // 负责容器的初始化,作为一个配置文件运行并最终返回一个 Psr\Container\ContainerInterface 对象
|
||||
├── dependencies.php // 用于管理 DI 的依赖关系和类对应关系
|
||||
└── routes.php // 用于管理路由
|
||||
```
|
||||
|
||||
## `config.php` 与 `autoload` 文件夹内的配置文件的关系
|
||||
|
||||
`config.php` 与 `autoload` 文件夹内的配置文件在服务启动时都会被扫描并注入到 `Hyperf\Contract\ConfigInterface` 对应的对象中,配置的结构为一个键值对的大数组,两种配置形式不同的在于 `autoload` 内配置文件的文件名会作为第一层 键(Key) 存在,而 `config.php` 内的则以您定义的为第一层,我们通过下面的例子来演示一下。
|
||||
我们假设存在一个 `config/autoload/client.php` 文件,文件内容如下:
|
||||
```php
|
||||
return [
|
||||
'request' => [
|
||||
'timeout' => 10,
|
||||
],
|
||||
];
|
||||
```
|
||||
那么我们想要得到 `timeout` 的值对应的 键(Key) 为 `client.request.timeout`;
|
||||
|
||||
我们假设想要以相同的 键(Key) 获得同样的结果,但配置是写在 `config/config.php` 文件内的,那么文件内容应如下:
|
||||
```php
|
||||
return [
|
||||
'client' => [
|
||||
'request' => [
|
||||
'timeout' => 10,
|
||||
],
|
||||
],
|
||||
];
|
||||
```
|
||||
|
||||
## 使用 Hyperf Config 组件
|
||||
|
||||
该组件是官方提供的默认的配置组件,是面向 `Hyperf\Contract\ConfigInterface` 接口实现的,由 [hyperf/config](https://github.com/hyperf-cloud/config) 组件内的 `ConfigProvider` 将 `Hyperf\Config\Config` 对象绑定到接口上。
|
||||
|
||||
### 设置配置
|
||||
|
||||
只需在 `config/config.php` 与 `config/server.php` 与 `autoload` 文件夹内的配置,都能在服务启动时被扫描并注入到 `Hyperf\Contract\ConfigInterface` 对应的对象中,这个流程是由 `Hyperf\Config\ConfigFactory` 在 Config 对象实例化时完成的。
|
||||
|
||||
### 获取配置
|
||||
|
||||
Config 组件提供了三种方式获取配置,通过 `Hyperf\Config\Config` 对象获取、通过 `@Value` 注解获取和通过 `config(string $key, $default)` 函数获取。
|
||||
|
||||
#### 通过 Config 对象获取配置
|
||||
|
||||
这种方式要求你已经拿到了 `Config` 对象的实例,默认对象为 `Hyperf\Config\Config`,注入实例的细节可查阅 [依赖注入](zh/di.md) 章节;
|
||||
|
||||
```php
|
||||
/**
|
||||
* @var \Hyperf\Contract\ConfigInterface
|
||||
*/
|
||||
// 通过 get(string $key, $default): mixed 方法获取 $key 所对应的配置,$key 值可以通过 . 连接符定位到下级数组,$default 则是当对应的值不存在时返回的默认值
|
||||
$config->get($key,$default);
|
||||
```
|
||||
|
||||
#### 通过 `@Value` 注解获取配置
|
||||
|
||||
这种方式要求注解的应用对象必须是通过 [hyperf/di](https://github.com/hyperf-cloud/di) 组件创建的,注入实例的细节可查阅 [依赖注入](zh/di.md) 章节,示例中我们假设 `IndexController` 就是一个已经定义好的 `Controller` 类,`Controller` 类一定是由 `DI` 容器创建出来的;
|
||||
`@Value()` 内的字符串则对应到 `$config->get($key)` 内的 `$key` 参数,在创建该对象实例时,对应的配置会自动注入到定义的类属性中。
|
||||
|
||||
```php
|
||||
class IndexController
|
||||
{
|
||||
|
||||
/**
|
||||
* @Value("config.key")
|
||||
*/
|
||||
private $configValue;
|
||||
|
||||
public function index()
|
||||
{
|
||||
return $this->configValue;
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
#### 通过 config 函数获取
|
||||
|
||||
在任意地方可以通过 `config(string $key, $default)` 函数获取对应的配置,但这样的使用方式也就意味着您对 [hyperf/config](https://github.com/hyperf-cloud/config) 和 [hyperf/utils](https://github.com/hyperf-cloud/utils) 组件是强依赖的。
|
||||
|
||||
### 判断配置是否存在
|
||||
|
||||
```php
|
||||
/**
|
||||
* @var \Hyperf\Contract\ConfigInterface
|
||||
*/
|
||||
// 通过 has(): bool 方法判断对应的 $key 值是否存在于配置中,$key 值可以通过 . 连接符定位到下级数组
|
||||
$config->has($key);
|
||||
```
|
||||
|
||||
## 环境变量
|
||||
|
||||
对于不同的运行环境使用不同的配置是一种常见的需求,比如在测试环境和生产环境的 Redis 配置不一样,而生产环境的配置又不能提交到源代码版本管理系统中以免信息泄露。
|
||||
|
||||
在 Hyperf 里我们提供了环境变量这一解决方案,通过利用 [vlucas/phpdotenv](https://github.com/vlucas/phpdotenv) 提供的环境变量解析功能,以及 `env()` 函数来获取环境变量的值,这一需求解决起来是相当的容易。
|
||||
|
||||
在新安装好的 Hyperf 应用中,其根目录会包含一个 `.env.example` 文件。如果是通过 Composer 安装的 Hyperf,该文件会自动基于 `.env.example` 复制一个新文件并命名为 `.env`。否则,需要你手动更改一下文件名。
|
||||
|
||||
您的 `.env` 文件不应该提交到应用的源代码版本管理系统中,因为每个使用你的应用的开发人员 / 服务器可能需要有一个不同的环境配置。此外,在入侵者获得你的源代码仓库的访问权的情况下,这会导致严重的安全问题,因为所以敏感的数据都被一览无余了。
|
||||
|
||||
> `.env` 文件中的所有变量均可被外部环境变量所覆盖(比如服务器级或系统级或 Docker 环境变量)。
|
||||
|
||||
### 环境变量类型
|
||||
|
||||
`.env` 文件中的所有变量都会被解析为字符串类型,因此提供了一些保留值以允许您从 `env()` 函数中获取更多类型的变量:
|
||||
|
||||
| .env 值 | env() 值 |
|
||||
| :------ | :----------- |
|
||||
| true | (bool) true |
|
||||
| (true) | (bool) true |
|
||||
| false | (bool) false |
|
||||
| (false) | (bool) false |
|
||||
| empty | (string) '' |
|
||||
| (empty) | (string) '' |
|
||||
| null | (null) null |
|
||||
| (null) | (null) null |
|
||||
|
||||
如果你需要使用包含空格的环境变量,可以通过将值括在双引号中来实现,比如:
|
||||
|
||||
```dotenv
|
||||
APP_NAME="Hyperf Skeleton"
|
||||
```
|
||||
|
||||
### 读取环境变量
|
||||
|
||||
我们在上面也有提到环境变量可以通过 `env()` 函数获取,在应用开发中,环境变量只应作为配置的一个值,通过环境变量的值来覆盖配置的值,对于应用层来说应 **只使用配置**,而不是直接使用环境变量。
|
||||
我们举个合理使用的例子:
|
||||
|
||||
```php
|
||||
// config/config.php
|
||||
return [
|
||||
'app_name' => env('APP_NAME', 'Hyperf Skeleton'),
|
||||
];
|
||||
```
|
||||
|
||||
## 配置中心
|
||||
|
||||
Hyperf 为您提供了分布式系统的外部化配置支持,默认且仅适配了由携程开源的 [ctripcorp/apollo](https://github.com/ctripcorp/apollo),由 [hyper/config-apollo](https://github.com/hyperf-cloud/config-apollo) 组件提供功能支持。
|
||||
关于配置中心的使用细节我们由 [配置中心](zh/config-center.md) 章节来阐述。
|
||||
|
||||
|
69
doc/zh/consul.md
Normal file
69
doc/zh/consul.md
Normal file
@ -0,0 +1,69 @@
|
||||
# Consul 协程客户端
|
||||
|
||||
Hyperf 提供了一个 [Consul](https://www.consul.io/api/index.html) 的协程客户端,由于 Consul 本身的 API 比较简单,也支持 HTTP 的请求方法,故该组件仅对 API 进行了一些封装上的简化,基于 [hyperf/guzzle](https://github.com/hyperf-cloud/guzzle) 提供的协程 HTTP 客户端支持。
|
||||
|
||||
> `ConsulResponse` 类指的是 `Hyperf\Consul\ConsulResponse` 类
|
||||
|
||||
## 安装
|
||||
|
||||
```bash
|
||||
composer require hyperf/consul
|
||||
```
|
||||
|
||||
## KV
|
||||
|
||||
由 `Hyperf\Consul\KV` 实现 `Hyperf\Consul\KVInterface` 提供支持。
|
||||
|
||||
- get($key, array $options = []): ConsulResponse
|
||||
- put($key, $value, array $options = []): ConsulResponse
|
||||
- delete($key, array $options = []): ConsulResponse
|
||||
|
||||
## Agent
|
||||
|
||||
由 `Hyperf\Consul\Agent` 实现 `Hyperf\Consul\AgentInterface` 提供支持。
|
||||
|
||||
- checks(): ConsulResponse
|
||||
- services(): ConsulResponse
|
||||
- members(): ConsulResponse
|
||||
- self(): ConsulResponse
|
||||
- join($address, array $options = []): ConsulResponse
|
||||
- forceLeave($node): ConsulResponse
|
||||
- registerCheck($check): ConsulResponse
|
||||
- deregisterCheck($checkId): ConsulResponse
|
||||
- passCheck($checkId, array $options = []): ConsulResponse
|
||||
- warnCheck($checkId, array $options = []): ConsulResponse
|
||||
- failCheck($checkId, array $options = []): ConsulResponse
|
||||
- registerService($service): ConsulResponse
|
||||
- deregisterService($serviceId): ConsulResponse
|
||||
|
||||
## Catalog
|
||||
|
||||
由 `Hyperf\Consul\Catalog` 实现 `Hyperf\Consul\CatalogInterface` 提供支持。
|
||||
|
||||
- register($node): ConsulResponse
|
||||
- deregister($node): ConsulResponse
|
||||
- datacenters(): ConsulResponse
|
||||
- nodes(array $options = []): ConsulResponse
|
||||
- node($node, array $options = []): ConsulResponse
|
||||
- services(array $options = []): ConsulResponse
|
||||
- service($service, array $options = []): ConsulResponse
|
||||
|
||||
## Health
|
||||
|
||||
由 `Hyperf\Consul\Health` 实现 `Hyperf\Consul\HealthInterface` 提供支持。
|
||||
|
||||
- node($node, array $options = []): ConsulResponse
|
||||
- checks($service, array $options = []): ConsulResponse
|
||||
- service($service, array $options = []): ConsulResponse
|
||||
- state($state, array $options = []): ConsulResponse
|
||||
|
||||
## Session
|
||||
|
||||
由 `Hyperf\Consul\Session` 实现 `Hyperf\Consul\SessionInterface` 提供支持。
|
||||
|
||||
- create($body = null, array $options = []): ConsulResponse
|
||||
- destroy($sessionId, array $options = []): ConsulResponse
|
||||
- info($sessionId, array $options = []): ConsulResponse
|
||||
- node($node, array $options = []): ConsulResponse
|
||||
- all(array $options = []): ConsulResponse
|
||||
- renew($sessionId, array $options = []): ConsulResponse
|
42
doc/zh/controller.md
Normal file
42
doc/zh/controller.md
Normal file
@ -0,0 +1,42 @@
|
||||
# 控制器
|
||||
|
||||
通过控制器来处理 HTTP 请求,需要通过 `配置文件` 或 `注解` 的形式将路由与控制器方法进行绑定,具体请查阅 [路由](zh/route.md) 章节。
|
||||
对于 `请求(Request)` 与 `响应(Response)`,Hyperf 提供了 `Hyperf\HttpServer\Contract\RequestInterface` 和 `Hyperf\HttpServer\Contract\ResponseInterface` 方便您获取入参和返回数据,关于 [请求](zh/request.md) 与 [响应](zh/response.md) 的详细内容请查阅对应的章节。
|
||||
|
||||
## 编写控制器
|
||||
|
||||
```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)` 中获取的。
|
193
doc/zh/coroutine.md
Normal file
193
doc/zh/coroutine.md
Normal file
@ -0,0 +1,193 @@
|
||||
# 协程
|
||||
|
||||
## 概念
|
||||
|
||||
Hyperf 是运行于 `Swoole 4` 的协程之上的,这也是 Hyperf 能提供高性能的其中一个很大的因素。
|
||||
|
||||
### PHP-FPM 的运作模式
|
||||
|
||||
在聊协程是什么之前,我们先聊聊传统 `PHP-FPM` 架构的运作模式,`PHP-FPM` 是一个多进程的 `FastCGI` 管理程序,是绝大多数 `PHP` 应用所使用的运行模式。假设我们使用 `Nginx` 提供 `HTTP` 服务(`Apache` 同理),所有客户端发起的请求最先抵达的都是 `Nginx`,然后 `Nginx` 通过 `FastCGI` 协议将请求转发给 `PHP-FPM` 处理,`PHP-FPM` 的 `Master进程` 会为每个请求分配一个 `Worker进程` 来处理,这个处理指的就是,等待 `PHP` 脚本的解析,等待业务处理的结果返回,完成后回收子进程,这整个的过程是阻塞等待的,也就意味着 `PHP-FPM` 的进程数有多少能处理的请求也就是多少,假设 `PHP-FPM` 有 `200` 个 `Worker进程`,一个请求将耗费 `1` 秒的时间,那么简单的来说整个服务器理论上最多可以处理的请求也就是 `200` 个,`QPS` 即为 `200/s`,在高并发的场景下,这样的性能往往是不够的,尽管可以利用 `Nginx` 作为负载均衡配合多台 `PHP-FPM` 服务器来提供服务,但由于 `PHP-FPM` 的阻塞等待的工作模型,一个请求会占用至少一个 `MySQL` 连接,多节点高并发下会产生大量的 `MySQL` 连接,而 `MySQL` 的最大连接数默认值为 `100`,尽管可以修改,但显而易见该模式没法很好的应对高并发的场景。
|
||||
|
||||
### 异步非阻塞系统
|
||||
|
||||
在高并发的场景下,异步非阻塞就显得优势明显了,直观的优点表现就是 `Worker进程` 不再同步阻塞的去处理一个请求,而是可以同时处理多个请求,无需 `I/O` 等待,并发能力极强,可以同时发起或维护大量的请求。那么最直观的缺点大家可能也都知道,就是永无止境的回调,业务逻辑必须在对应的回调函数内实现,如果业务逻辑存在多次的 `I/O` 请求,则会存在很多层的回调函数,下面示例一段 `Swoole 1.x` 下的异步回调型的伪代码片段。
|
||||
|
||||
```php
|
||||
$db = new swoole_mysql();
|
||||
$config = array(
|
||||
'host' => '127.0.0.1',
|
||||
'port' => 3306,
|
||||
'user' => 'test',
|
||||
'password' => 'test',
|
||||
'database' => 'test',
|
||||
);
|
||||
|
||||
$db->connect($config, function ($db, $r) {
|
||||
// 从 users 表中查询一条数据
|
||||
$sql = 'select * from users where id = 1';
|
||||
$db->query($sql, function(swoole_mysql $db, $r) {
|
||||
if ($r === true) {
|
||||
$rows = $db->affected_rows;
|
||||
// 查询成功后修改一条数据
|
||||
$updateSql = 'update users set name='new name' where id = 1';
|
||||
$db->query($updateSql, function (swoole_mysql $db, $r) {
|
||||
if ($r === true) {
|
||||
return $this->response->end('更新成功');
|
||||
}
|
||||
});
|
||||
}
|
||||
$db->close();
|
||||
});
|
||||
});
|
||||
```
|
||||
从上面的代码片段可以看出,每一个操作几乎就需要一个回调函数,在复杂的业务场景中回调的层次感和代码结构绝对会让你崩溃,其实不难看出这样的写法有点类似 `JavaScript` 上的异步方法的写法,而 `JavaScript` 也为此提供了不少的解决方案(当然方案是源于其它编程语言),如 `Promise`,`yield + generator`, `async/await`,`Promise` 则是对回调的一种封装方式,而 `yield + generator` 和 `async/await` 则需要在代码上显性的增加一些代码语法标记,这些相对比回调函数来说,不妨都是一些非常不错的解决方案,但是你需要另花时间来理解它的实现机制和语法。
|
||||
Swoole 协程也是对异步回调的一种解决方案,在 `PHP` 语言下,`Swoole` 协程与 `yield + generator` 都属于协程的解决方案,协程的解决方案可以使代码以近乎于同步代码的书写方式来书写异步代码,显性的区别则是 `yield + generator` 的协程机制下,每一处 `I/O` 操作的调用代码都需要在前面加上 `yield` 语法实现协程切换,每一层调用都需要加上,否则会出现意料之外的错误,而 `Swoole` 协程的解决方案对比于此就高明多了,在遇到 `I/O` 时底层自动的进行隐式协程切换,无需添加任何的额外语法,无需在代码前加上 `yield`,协程切换的过程无声无息,极大的减轻了维护异步系统的心智负担。
|
||||
|
||||
### 协程是什么?
|
||||
|
||||
我们已经知道了协程可以很好的解决异步非阻塞系统的开发问题,那么协程本身到底是什么呢?从定义上来说,*协程是一种轻量级的线程,由用户代码来调度和管理,而不是由操作系统内核来进行调度,也就是在用户态进行*。可以直接的理解为就是一个非标准的线程实现,但什么时候切换由用户自己来实现,而不是由操作系统分配 `CPU` 时间决定。具体来说,`Swoole` 的每个 `Worker进程` 会存在一个协程调度器来调度协程,协程切换的时机就是遇到 `I/O` 操作或代码显性切换时,进程内以单线程的形式运行协程,也就意味着一个进程内同一时间只会有一个协程在运行且切换时机明确,也就无需处理像多线程编程下的各种同步锁的问题。
|
||||
单个协程内的代码运行仍是串行的,放在一个 HTTP 协程服务上来理解就是每一个请求是一个协程,举个例子,假设为请求A创建了 `协程A`,为 `请求B` 创建了 `协程B`,那么在处理 `协程A` 的时候代码跑到了查询 `MySQL` 的语句上,这个时候 `协程A` 则会触发协程切换,`协程A` 就继续等待 `I/O` 设备返回结果,那么此时就会切换到 `协程B`,开始处理 `协程B` 的逻辑,当又遇到了一个 `I/O` 操作便又触发协程切换,再回过来从 `协程A` 刚才切走的地方继续执行,如此反复,遇到 `I/O` 操作就切换到另一个协程去继续执行而非一直阻塞等待。
|
||||
这里可以发现一个问题就是,*`协程A` 的 `MySQL` 查询操作必须得是一个异步非阻塞的操作,否则会由于阻塞导致协程调度器没法切换到另一个协程继续执行*,这个也是要在协程编程下需要规避的问题之一。
|
||||
|
||||
### 协程与普通线程有哪些区别?
|
||||
|
||||
都说协程是一个轻量级的线程,协程和线程都适用于多任务的场景下,从这个角度上来说,协程与线程很相似,都有自己的上下文,可以共享全局变量,但不同之处在于,在同一时间可以有多个线程处于运行状态,但对于 `Swoole` 协程来说只能有一个,其它的协程都会处于暂停的状态。此外,普通线程是抢占式的,那个线程能得到资源由操作系统决定,而协程是协作式的,执行权由用户态自行分配。
|
||||
|
||||
## 协程编程注意事项
|
||||
|
||||
### 不能存在阻塞代码
|
||||
|
||||
协程内代码的阻塞会导致协程调度器无法切换到另一个协程继续执行代码,所以我们绝不能在协程内存在阻塞代码,假设我们启动了 `4` 个 `Worker` 来处理 `HTTP` 请求(通常启动的 `Worker` 数量与 `CPU` 核心数一致或 `2` 倍),如果代码中存在阻塞,暂且理论的认为每个请求都会阻塞`1` 秒,那么系统的 `QPS` 也将退化为 `4/s`,这无疑就是退化成了与 `PHP-FPM` 类似的情况,所以我们绝对不能在协程中存在阻塞代码。
|
||||
|
||||
那么到底哪些是阻塞代码呢?我们可以简单的认为绝大多数你所熟知的非 `Swoole` 提供的异步函数的 `MySQL`、`Redis`、`Memcache`、`MongoDB`、`HTTP`、`Socket`等客户端,文件操作、`sleep/usleep` 等均为阻塞函数,这几乎涵盖了所有日常操作,那么要如何解决呢?`Swoole` 提供了 `MySQL`、`PostgreSQL`、`Redis`、`HTTP`、`Socket` 的协程客户端可以使用,同时 `Swoole 4.1` 之后提供了一键协程化的方法 `\Swoole\Coroutine::enableCoroutine()`,只需在使用协程前运行这一行代码,`Swoole` 会将 所有使用 `php_stream` 进行 `socket` 操作均变成协程调度的异步 `I/O`,可以理解为除了 `curl` 绝大部分原生的操作都可以适用,关于此部分可查阅 [Swoole 文档](https://wiki.swoole.com/wiki/page/965.html) 获得更具体的信息。
|
||||
|
||||
在 `Hyperf` 中我们已经为您处理好了这一切,您只需关注 `\Swoole\Coroutine::enableCoroutine()` 仍无法协程化的阻塞代码即可。
|
||||
|
||||
### 不能通过全局变量储存状态
|
||||
|
||||
在 `Swoole` 的持久化应用下,一个 `Worker` 内的全局变量是 `Worker` 内共享的,而从协程的介绍我们可以知道同一个 `Worker` 内还会存在多个协程并存在协程切换,也就意味着一个 `Worker` 会在一个时间周期内同时处理多个协程(或直接理解为请求)的代码,也就意味着如果使用了全局变量来储存状态可能会被多个协程所使用,也就是说不同的请求之间可能会混淆数据,这里的全局变量指的是 `$_GET/$_POST/$_REQUEST/$_SESSION/$_COOKIE/$_SERVER`等`$_`开头的变量、`global` 变量,以及 `static` 静态属性。
|
||||
那么当我们需要使用到这些特性时应该怎么办?
|
||||
|
||||
对于全局变量,均是跟随着一个 `请求(Request)` 而产生的,而 `Hyperf` 的 `请求(Request)/响应(Response)` 是由 [hyperf/http-message](https://github.com/hyperf-cloud/http-message) 通过实现 [PSR-7](https://www.php-fig.org/psr/psr-7/) 处理的,故所有的全局变量均可以在 `请求(Request)` 对象中得到相关的值;
|
||||
|
||||
对于 `global` 变量和 `static` 变量,在 `PHP-FPM` 模式下,本质都是存活于一个请求生命周期内的,而在 `Hyperf` 内因为是 `CLI` 应用,会存在 `全局周期` 和 `请求周期(协程周期)` 两种长生命周期。
|
||||
- 全局周期,我们只需要创建一个静态变量供全局调用即可,静态变量意味着在服务启动后,任意协程和代码逻辑均共享此静态变量内的数据,也就意味着存放的数据不能是特别服务于某一个请求或某一个协程;
|
||||
- 协程周期,由于 `Hyperf` 会为每个请求自动创建一个协程来处理,那么一个协程周期在此也可以理解为一个请求周期,在协程内,所有的状态数据均应存放于 `Hyperf\Utils\Context` 类中,通过该类的 `get`、`set` 来读取和存储任意结构的数据,这个 `Context(协程上下文)` 类在执行任意协程时读取或存储的数据都是仅限对应的协程的,同时在协程结束时也会自动销毁相关的上下文数据。
|
||||
|
||||
### 最大协程数限制
|
||||
|
||||
对 `Swoole Server` 通过 `set` 方法设置 `max_coroutine` 参数,用于配置一个 `Worker` 进程最多可存在的协程数量。因为随着 `Worker` 进程处理的协程数目的增加,其对应占用的内存也会随之增加,为了避免超出 `PHP` 的 `memory_limit` 限制,请根据实际业务的压测结果设置该值,`Swoole` 的默认值为 `3000`, 在 `hyperf-skeleton` 项目中默认设置为 `100000`。
|
||||
|
||||
## 使用协程
|
||||
|
||||
### 创建一个协程
|
||||
|
||||
只需通过 `co(callable $callable)` 或 `go(callable $callable)` 函数或 `Hyperf\Utils\Coroutine::create(callable $callable)` 即可创建一个协程,协程内可以使用协程相关的方法和客户端。
|
||||
|
||||
### 判断当前是否处于协程环境内
|
||||
|
||||
在一些情况下我们希望判断一些当前是否运行于协程环境内,对于一些兼容协程环境与非协程环境的代码来说会作为一个判断的依据,我们可以通过 `Hyperf\Utils\Coroutine::inCoroutine(): bool` 方法来得到结果。
|
||||
|
||||
### 获得当前协程的 ID
|
||||
|
||||
在一些情况下,我们需要根据 `协程 ID` 去做一些逻辑,比如 `协程上下文` 之类的逻辑,可以通过 `Hyperf\Utils\Coroutine::id(): int` 获得当前的 `协程 ID`,如不处于协程环境下,会返回 `-1`。
|
||||
|
||||
### Channel 通道
|
||||
|
||||
类似于 go 语言的 `chan`,`Channel` 可为多生产者协程和多消费者协程模式提供支持。底层自动实现了协程的切换和调度。 `Channel` 与 PHP 的数组类似,仅占用内存,没有其他额外的资源申请,所有操作均为内存操作,无 `I/O` 消耗,使用方法与 `SplQueue` 队列类似。
|
||||
`Channel` 主要用于协程间通讯,当我们希望从一个协程里返回一些数据到另一个协程时,就可通过 `Channel` 来进行传递。
|
||||
|
||||
主要方法:
|
||||
- `Channel->push` :当队列中有其他协程正在等待 `pop` 数据时,自动按顺序唤醒一个消费者协程。当队列已满时自动 `yield` 让出控制器,等待其他协程消费数据
|
||||
- `Channel->pop` :当队列为空时自动 `yield`,等待其他协程生产数据。消费数据后,队列可写入新的数据,自动按顺序唤醒一个生产者协程。
|
||||
|
||||
下面是一个协程间通讯的简单例子:
|
||||
|
||||
```php
|
||||
<?php
|
||||
co(function () {
|
||||
$channel = new \Swoole\Coroutine\Channel();
|
||||
co(function () use ($channel) {
|
||||
$channel->push('data');
|
||||
});
|
||||
$data = $channel->pop();
|
||||
});
|
||||
```
|
||||
|
||||
### Defer 特性
|
||||
|
||||
当我们希望在协程结束时运行一些代码时,可以通过 `defer(callable $callable)` 函数或 `Hyperf\Coroutine::defer(callable $callable)` 将一段函数以 `栈(stack)` 的形式储存起来,`栈(stack)` 内的函数会在当前协程结束时以 `先进后出` 的流程逐个执行。
|
||||
|
||||
### WaitGroup 特性
|
||||
|
||||
`WaitGroup` 是基于 `Channel` 衍生出来的一个特性,如果接触过 `Go` 语言,我们都会知道 `WaitGroup` 这一特性,在 `Hyperf` 里,`WaitGroup` 的用途是使得主协程一直阻塞等待直到所有相关的子协程都已经完成了任务后再继续运行,这里说到的阻塞等待是仅对于主协程(即当前协程)来说的,并不会阻塞当前进程。
|
||||
我们通过一段代码来演示该特性:
|
||||
|
||||
```php
|
||||
<?php
|
||||
$wg = new \Hyperf\Utils\WaitGroup();
|
||||
// 计数器加二
|
||||
$wg->add(2);
|
||||
// 创建协程 A
|
||||
co(function () use ($wg) {
|
||||
// some code
|
||||
// 计数器减一
|
||||
$wg->done();
|
||||
});
|
||||
// 创建协程 B
|
||||
co(function () use ($wg) {
|
||||
// some code
|
||||
// 计数器减一
|
||||
$wg->done();
|
||||
});
|
||||
// 等待协程 A 和协程 B 运行完成
|
||||
$wg->wait();
|
||||
```
|
||||
|
||||
> 注意 `WaitGroup` 本身也需要在协程内才能使用
|
||||
|
||||
### Parallel 特性
|
||||
|
||||
`Parallel` 特性是 Hyperf 基于 `WaitGroup` 特性抽象出来的一个更便捷的使用方法,我们通过一段代码来演示一下。
|
||||
|
||||
```php
|
||||
<?php
|
||||
$parallel = new \Hyperf\Utils\Parallel();
|
||||
$parallel->add(function () {
|
||||
\Hyperf\Utils\Coroutine::sleep(1);
|
||||
return \Hyperf\Utils\Coroutine::id();
|
||||
});
|
||||
$parallel->add(function () {
|
||||
\Hyperf\Utils\Coroutine::sleep(1);
|
||||
return \Hyperf\Utils\Coroutine::id();
|
||||
});
|
||||
// $result 结果为 [1, 2]
|
||||
$result = $parallel->wait();
|
||||
```
|
||||
|
||||
通过上面的代码我们可以看到仅花了 1 秒就得到了两个不同的协程的 ID,在调用 `add(callable $callable)` 的时候 `Parallel` 类会为之自动创建一个协程,并加入到 `WaitGroup` 的调度去。
|
||||
不仅如此,我们还可以通过 `parallel(array $callables)` 函数进行更进一步的简化上面的代码,达到同样的目的,下面为简化后的代码。
|
||||
|
||||
```php
|
||||
<?php
|
||||
use Hyperf\Utils\Coroutine;
|
||||
|
||||
// 传递的数组参数您也可以带上 key 便于区分子协程,返回的结果也会根据 key 返回对应的结果
|
||||
$result = parallel([
|
||||
function () {
|
||||
Coroutine::sleep(1);
|
||||
return Coroutine::id();
|
||||
},
|
||||
function () {
|
||||
Coroutine::sleep(1);
|
||||
return Coroutine::id();
|
||||
}
|
||||
]);
|
||||
```
|
||||
|
||||
> 注意 `Parallel` 本身也需要在协程内才能使用
|
||||
|
||||
### 协程上下文
|
||||
|
||||
由于同一个进程内协程间是内存共享的,但协程的执行/切换是非顺序的,也就意味着我们很难掌控当前的协程是哪一个*(事实上可以,但通常没人这么干)*,所以我们需要在发生协程切换时能够同时切换对应的上下文。
|
||||
在 Hyperf 里实现协程的上下文管理将非常简单,基于 `Hyperf\Utils\Context` 类的 `set(string $id, $value)`、`get(string $id, $default = null)`、`has(string $id)` 静态方法即可完成上下文数据的管理,通过这些方法设置和获取的值,都仅限于当前的协程,在协程结束时,对应的上下文也会自动跟随释放掉,无需手动管理,无需担忧内存泄漏的风险。
|
178
doc/zh/db/event.md
Normal file
178
doc/zh/db/event.md
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
503
doc/zh/db/migration.md
Normal file
503
doc/zh/db/migration.md
Normal 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();
|
||||
```
|
128
doc/zh/db/model-cache.md
Normal file
128
doc/zh/db/model-cache.md
Normal file
@ -0,0 +1,128 @@
|
||||
# 模型缓存
|
||||
|
||||
在高频场景下,我们会频繁的查询数据库,虽然有主键加持,但也会影响到数据库性能。这种kv查询方式,我们可以很方便的使用 `模型缓存` 来减缓数据库压力。本模块实现了自动缓存,删除和修改模型时,自动删除缓存。累加、减操作时,直接操作缓存进行对应累加、减。
|
||||
|
||||
> 模型缓存暂支持 `Redis`存储,其他存储引擎会慢慢补充。
|
||||
|
||||
## 安装
|
||||
|
||||
```
|
||||
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` 方法,然后由自己实现对应监听即可。
|
407
doc/zh/db/model.md
Normal file
407
doc/zh/db/model.md
Normal file
@ -0,0 +1,407 @@
|
||||
# 模型
|
||||
|
||||
模型组件衍生于 [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]);
|
||||
```
|
39
doc/zh/db/paginator.md
Normal file
39
doc/zh/db/paginator.md
Normal file
@ -0,0 +1,39 @@
|
||||
# 查询分页
|
||||
|
||||
在使用 [hyperf/database](https://github.com/hyperf-cloud/database) 来查询数据时,可以很方便的通过与 [hyperf/paginator](https://github.com/hyperf-cloud/paginator) 组件配合便捷地对查询结果进行分页。
|
||||
|
||||
# 使用方法
|
||||
|
||||
在您通过 [查询构造器](zh/db/querybuilder.md) 或 [模型](zh/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/paginator.md) 章节。
|
681
doc/zh/db/querybuilder.md
Normal file
681
doc/zh/db/querybuilder.md
Normal 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();
|
||||
```
|
||||
|
223
doc/zh/db/quick-start.md
Normal file
223
doc/zh/db/quick-start.md
Normal file
@ -0,0 +1,223 @@
|
||||
# 快速开始
|
||||
|
||||
## 前言
|
||||
|
||||
> [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 才能把数据库组件及事件组件接入进来。
|
||||
|
||||
## 安装
|
||||
|
||||
### 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` 选项被启用,并且当前请求周期内执行过 「写」 操作,那么任何 「读」 操作都将使用 「写」 连接。这样可确保同一个请求周期内写入的数据可以被立即读取到,从而避免主从延迟导致数据不一致的问题。不过是否启用它,取决于应用程序的需求。
|
||||
|
||||
## 执行原生 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();
|
||||
}
|
||||
```
|
266
doc/zh/db/relationship.md
Normal file
266
doc/zh/db/relationship.md
Normal 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, ...);
|
||||
```
|
30
doc/zh/devtool.md
Normal file
30
doc/zh/devtool.md
Normal file
@ -0,0 +1,30 @@
|
||||
# 开发者工具
|
||||
|
||||
## 安装
|
||||
|
||||
```
|
||||
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
|
288
doc/zh/di.md
Normal file
288
doc/zh/di.md
Normal file
@ -0,0 +1,288 @@
|
||||
# 依赖注入
|
||||
|
||||
## 简介
|
||||
|
||||
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 的核心输出,我们自信的认为该组件是最优秀的。
|
||||
|
||||
## 安装
|
||||
|
||||
该组件默认存在 [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/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/dependencies.php` 内完成关系配置:
|
||||
|
||||
```php
|
||||
<?php
|
||||
return [
|
||||
'dependencies' => [
|
||||
\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/dependencies.php` 调整绑定关系:
|
||||
|
||||
```php
|
||||
<?php
|
||||
return [
|
||||
'dependencies' => [
|
||||
\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();
|
||||
```
|
21
doc/zh/donate.md
Normal file
21
doc/zh/donate.md
Normal file
@ -0,0 +1,21 @@
|
||||
# 捐献及赞助
|
||||
|
||||
Hyperf 是采用 MIT 许可的开源项目,使用完全免费。但是随着项目规模的增长,也需要有相应的资金支持才能持续项目的维护的开发。您可以通过下列的方法来赞助 Hyperf 的开发。
|
||||
|
||||
## 赞助 Hyperf 的研发
|
||||
|
||||
如果您是企业经营者并且将 Hyperf 用在商业产品中,那么赞助 Hyperf 有商业上的益处:可以让您的产品所依赖的框架保持健康并得到积极的维护,也能帮助您在 Hyperf 社区里获得更高的曝光度,从而更容易地吸引到 Hyperf 开发者。
|
||||
如果您是个人开发者并且享受 Hyperf 带来的高开发效率,可以用一次性赞助来表示您的谢意 —— 就好像偶尔给我们买杯咖啡 :)
|
||||
|
||||
### 一次性赞助
|
||||
|
||||
我们通过以下方式接受赞助:
|
||||
|
||||
![alipay](./imgs/alipay.jpg ':size=375')
|
||||
![wechat](./imgs/wechatpay.jpg ':size=375')
|
||||
|
||||
### 周期性赞助
|
||||
|
||||
周期性赞助可以获得额外的回报,比如您的名字会出现在 Hyperf 的 GitHub 仓库中,再比如您的公司 LOGO 会出现在我们的官网和 GitHub 仓库上。
|
||||
|
||||
> 如您希望为 Hyperf 团队提供周期性的赞助,可加入官方的微信群或 QQ 群并与群主联系,或邮件至 h@hyperf.io
|
25
doc/zh/elasticsearch.md
Normal file
25
doc/zh/elasticsearch.md
Normal file
@ -0,0 +1,25 @@
|
||||
# Elasticsearch
|
||||
|
||||
[hyperf/elasticsearch](https://github.com/hyperf-cloud/elasticsearch) 主要为 [elasticsearch-php](https://github.com/elastic/elasticsearch-php) 进行了客户端对象创建的工厂类封装,[elasticsearch-php](https://github.com/elastic/elasticsearch-php) 默认使用 `Guzzle Ring` 客户端,在 [hyperf/guzzle](https://github.com/hyperf-cloud/guzzle) 中我们实现了协程版本的 `Handler`,所以可以直接使用 `Hyperf\Elasticsearch\ClientBuilderFactory` 创建一个新的 `Builder`。
|
||||
|
||||
## 安装
|
||||
|
||||
```bash
|
||||
composer require hyperf/elasticsearch
|
||||
```
|
||||
## 使用
|
||||
|
||||
### 创建客户端
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
use Hyperf\Elasticsearch\ClientBuilderFactory;
|
||||
|
||||
// 如果在协程环境下创建,则会自动使用协程版的 Handler,非协程环境下无改变
|
||||
$builder = $this->container->get(ClientBuilderFactory::class)->create();
|
||||
|
||||
$client = $builder->setHosts(['http://127.0.0.1:9200'])->build();
|
||||
|
||||
$info = $client->info();
|
||||
```
|
3
doc/zh/etcd.md
Normal file
3
doc/zh/etcd.md
Normal file
@ -0,0 +1,3 @@
|
||||
# ETCD
|
||||
|
||||
`hyperf/etcd` 组件将于 `hyperf 1.1` 版本释出,可留意 [Project Version 1.1](https://github.com/hyperf-cloud/hyperf/projects/2) 的了解具体进度。
|
161
doc/zh/event.md
Normal file
161
doc/zh/event.md
Normal file
@ -0,0 +1,161 @@
|
||||
# 事件机制
|
||||
|
||||
## 前言
|
||||
|
||||
事件模式必须基于 [PSR-14](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-14-event-dispatcher.md) 去实现。
|
||||
Hyperf 的事件管理器默认由 [hyperf/event](https://github.com/hyperf-cloud/event) 实现,该组件亦可用于其它框架或应用,只需通过 Composer 将该组件引入即可。
|
||||
|
||||
```bash
|
||||
composer require hyperf/event
|
||||
```
|
||||
|
||||
## 概念
|
||||
|
||||
事件模式是一种经过了充分测试的可靠机制,是一种非常适用于解耦的机制,分别存在以下 3 种角色:
|
||||
|
||||
- `事件(Event)` 是传递于应用代码与 `监听器(Listener)` 之间的通讯对象
|
||||
- `监听器(Listener)` 是用于监听 `事件(Event)` 的发生的监听对象
|
||||
- `事件调度器(EventDispatcher)` 是用于触发 `事件(Event)` 和管理 `监听器(Listener)` 与 `事件(Event)` 之间的关系的管理者对象
|
||||
|
||||
用通俗易懂的例子来说明就是,假设我们存在一个 `UserService::register()` 方法用于注册一个账号,在账号注册成功后我们可以通过事件调度器触发 `UserRegistered` 事件,由监听器监听该事件的发生,在触发时进行某些操作,比如发送用户注册成功短信,在业务发展的同时我们可能会希望在用户注册成功之后做更多的事情,比如发送用户注册成功的邮件等待,此时我们就可以通过再增加一个监听器监听 `UserRegistered` 事件即可,无需在 `UserService::register()` 方法内部增加与之无关的代码。
|
||||
|
||||
## 使用事件管理器
|
||||
|
||||
### 定义一个事件
|
||||
|
||||
一个事件其实就是一个用于管理状态数据的普通类,触发时将应用数据传递到事件里,然后监听器对事件类进行操作,一个事件可被多个监听器监听。
|
||||
|
||||
```php
|
||||
<?php
|
||||
namespace App\Event;
|
||||
|
||||
class UserRegistered
|
||||
{
|
||||
// 建议这里定义成 public 属性,以便监听器对该属性的直接使用,或者你提供该属性的 Getter
|
||||
public $user;
|
||||
|
||||
public function __construct($user)
|
||||
{
|
||||
$this->user = $user;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 定义一个监听器
|
||||
|
||||
监听器都需要实现一下 `Hyperf\Event\Contract\ListenerInterface` 接口的约束方法,示例如下。
|
||||
|
||||
```php
|
||||
<?php
|
||||
namespace App\Listener;
|
||||
|
||||
use App\Event\UserRegistered;
|
||||
use Hyperf\Event\Contract\ListenerInterface;
|
||||
|
||||
class UserRegisteredListener implements ListenerInterface
|
||||
{
|
||||
public function listen(): array
|
||||
{
|
||||
// 返回一个该监听器要监听的事件数组,可以同时监听多个事件
|
||||
return [
|
||||
UserRegistered::class,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param UserRegistered $event
|
||||
*/
|
||||
public function process(object $event)
|
||||
{
|
||||
// 事件触发后该监听器要执行的代码写在这里,比如该示例下的发送用户注册成功短信等
|
||||
// 直接访问 $event 的 user 属性获得事件触发时传递的参数值
|
||||
// $event->user;
|
||||
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 通过配置文件注册监听器
|
||||
|
||||
在定义完监听器之后,我们需要让其能被 `事件调度器(Dispatcher)` 发现,可以在 `config/autoload/listeners.php` 配置文件 *(如不存在可自行创建)* 内添加该监听器即可,监听器的触发顺序根据该配置文件的配置顺序:
|
||||
|
||||
```php
|
||||
<?php
|
||||
return [
|
||||
\App\Listener\UserRegisteredListener::class,
|
||||
];
|
||||
```
|
||||
|
||||
### 通过注解注册监听器
|
||||
|
||||
Hyperf 还提供了一种更加简便的监听器注册方式,就是通过 `@Listener` 注解注册,只要将该注解定义在监听器类上,且监听器类处于 `Hyperf 注解扫描域` 内即可自动完成注册,代码示例如下:
|
||||
|
||||
```php
|
||||
<?php
|
||||
namespace App\Listener;
|
||||
|
||||
use App\Event\UserRegistered;
|
||||
use Hyperf\Event\Annotation\Listener;
|
||||
use Hyperf\Event\Contract\ListenerInterface;
|
||||
|
||||
/**
|
||||
* @Listener
|
||||
*/
|
||||
class UserRegisteredListener implements ListenerInterface
|
||||
{
|
||||
public function listen(): array
|
||||
{
|
||||
// 返回一个该监听器要监听的事件数组,可以同时监听多个事件
|
||||
return [
|
||||
UserRegistered::class,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param UserRegistered $event
|
||||
*/
|
||||
public function process(object $event)
|
||||
{
|
||||
// 事件触发后该监听器要执行的代码写在这里,比如该示例下的发送用户注册成功短信等
|
||||
// 直接访问 $event 的 user 属性获得事件触发时传递的参数值
|
||||
// $event->user;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
在通过注解注册监听器时,我们可以通过设置 `priority` 属性定义当前监听器的顺序,如 `@Listener(priority=1)` ,底层使用 `SplPriorityQueue` 结构储存,`priority` 数字越大优先级越高。
|
||||
|
||||
> 使用 `@Listener` 注解时需 `use Hyperf\Event\Annotation\Listener;` 命名空间;
|
||||
|
||||
### 触发事件
|
||||
|
||||
事件需要通过 `事件调度器(EventDispatcher)` 调度才能让 `监听器(Listener)` 监听到,我们通过一段代码来演示如何触发事件:
|
||||
|
||||
```php
|
||||
<?php
|
||||
namespace App\Service;
|
||||
|
||||
use Hyperf\Di\Annotation\Inject;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use App\Event\UserRegistered;
|
||||
|
||||
class UserService
|
||||
{
|
||||
/**
|
||||
* @Inject
|
||||
* @var EventDispatcherInterface
|
||||
*/
|
||||
private $eventDispatcher;
|
||||
|
||||
public function register()
|
||||
{
|
||||
// 我们假设存在 User 这个实体
|
||||
$user = new User();
|
||||
$result = $user->save();
|
||||
// 完成账号注册的逻辑
|
||||
// 这里 dispatch(object $event) 会逐个运行监听该事件的监听器
|
||||
$this->eventDispatcher->dispatch(new UserRegistered($user));
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
```
|
107
doc/zh/exception-handler.md
Normal file
107
doc/zh/exception-handler.md
Normal 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/zh/grpc.md
Normal file
159
doc/zh/grpc.md
Normal 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 代码.
|
85
doc/zh/guzzle.md
Normal file
85
doc/zh/guzzle.md
Normal file
@ -0,0 +1,85 @@
|
||||
# Guzzle HTTP 客户端
|
||||
|
||||
[hyperf/guzzle](https://github.com/hyperf-cloud/guzzle) 组件基于 Guzzle 进行协程处理,通过 Swoole HTTP 客户端作为协程驱动替换到 Guzzle 内,以达到 HTTP 客户端的协程化。
|
||||
|
||||
## 安装
|
||||
|
||||
```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);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 连接池
|
||||
|
||||
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(),
|
||||
],
|
||||
]);
|
||||
```
|
BIN
doc/zh/imgs/alipay.jpg
Normal file
BIN
doc/zh/imgs/alipay.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 151 KiB |
BIN
doc/zh/imgs/wechat.jpg
Normal file
BIN
doc/zh/imgs/wechat.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 99 KiB |
BIN
doc/zh/imgs/wechatpay.jpg
Normal file
BIN
doc/zh/imgs/wechatpay.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 185 KiB |
188
doc/zh/json-rpc.md
Normal file
188
doc/zh/json-rpc.md
Normal file
@ -0,0 +1,188 @@
|
||||
# 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/dependencies.php` 内定义 `CalculatorServiceInterface` 和 `CalculatorService` 的关系,示例如下:
|
||||
|
||||
```php
|
||||
return [
|
||||
'dependencies' => [
|
||||
App\JsonRpc\CalculatorServiceInterface::class => App\JsonRpc\CalculatorService::class,
|
||||
],
|
||||
];
|
||||
```
|
||||
|
||||
这样便可以通过注入 `CalculatorServiceInterface` 接口来使用客户端了。
|
12
doc/zh/lifecycle.md
Normal file
12
doc/zh/lifecycle.md
Normal file
@ -0,0 +1,12 @@
|
||||
# 生命周期
|
||||
|
||||
## 框架生命周期
|
||||
|
||||
Hyperf 是运行于 [Swoole](http://github.com/swoole/swoole-src) 之上的,想要理解透彻 Hyperf 的生命周期,那么理解 [Swoole](http://github.com/swoole/swoole-src) 的生命周期也至关重要。
|
||||
Hyperf 的命令管理默认由 [symfony/console](https://github.com/symfony/console) 提供支持*(如果您希望更换该组件您也可以通过改变 skeleton 的入口文件更换成您希望使用的组件)*,在执行 `php bin/hyperf.php start` 后,将由 `Hyperf\Server\Command\StartServer` 命令类接管,并根据配置文件 `config/server.php` 内定义的 `Server` 逐个启动。
|
||||
关于依赖注入容器的初始化工作,我们并没有由组件来实现,因为一旦交由组件来实现,这个耦合就会非常的明显,所以在默认的情况下,是由入口文件来加载 `config/container.php` 来实现的。
|
||||
|
||||
## 请求与协程生命周期
|
||||
|
||||
Swoole 在处理每个连接时,会默认创建一个协程去处理,主要体现在 `onRequest`、`onReceive`、`onConnect` 事件,所以可以理解为每个请求都是一个协程,由于创建协程也是个常规操作,所以一个请求协程里面可能会包含很多个协程,同一个进程内协程之间是内存共享的,但调度顺序是非顺序的,且协程间本质上是相互独立的没有父子关系,所以对每个协程的状态处理都需要通过 [协程上下文](zh/coroutine.md#协程上下文) 来管理。
|
||||
|
246
doc/zh/logger.md
Normal file
246
doc/zh/logger.md
Normal file
@ -0,0 +1,246 @@
|
||||
# 日志
|
||||
|
||||
`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`,就是协程安全的。
|
||||
|
||||
## 安装
|
||||
|
||||
```
|
||||
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/dependencies.php
|
||||
return [
|
||||
'dependencies' => [
|
||||
\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, 方便投递到第三方日志服务
|
0
doc/zh/memory.md
Normal file
0
doc/zh/memory.md
Normal file
55
doc/zh/microservice.md
Normal file
55
doc/zh/microservice.md
Normal 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 》*
|
BIN
doc/zh/middleware/middleware.jpg
Normal file
BIN
doc/zh/middleware/middleware.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 44 KiB |
226
doc/zh/middleware/middleware.md
Normal file
226
doc/zh/middleware/middleware.md
Normal file
@ -0,0 +1,226 @@
|
||||
# 中间件
|
||||
|
||||
这里的中间件指的是"中间件模式",该功能属于 [hyperf/http-server](https://github.com/hyperf-cloud/http-server) 组件内的一项主要功能,主要用于编织从 `请求(Request)` 到 `响应(Response)` 的整个流程,该功能完成基于 [PSR-15]() 实现。
|
||||
|
||||
## 原理
|
||||
|
||||
*中间件主要用于编织从 `请求(Request)` 到 `响应(Response)` 的整个流程*,通过对多个中间件的组织,使数据的流动按我们预定的方式进行,中间件的本质是一个 `洋葱模型`,我们通过一个图来解释它:
|
||||
|
||||
![middleware](./middleware.jpg)
|
||||
|
||||
图中的顺序为按照 `Middleware 1 -> Middleware 2 -> Middleware 3` 的顺序组织着,我们可以注意到当中间的横线穿过 `内核` 即 `Middleware 3` 后,又回到了 `Middleware 2`,为一个嵌套模型,那么实际的顺序其实就是:
|
||||
`Request -> Middleware 1 -> Middleware 2 -> Middleware 3 -> Middleware 2 -> Middleware 1 -> Response`
|
||||
重点放在 `核心` 即 `Middleware 3`,它是洋葱的分界点,分界点前面的部分其实都是基于 `请求(Request)` 进行处理,而经过了分界点时,`内核` 就产出了 `响应(Response)` 对象,也是 `内核` 的主要代码目标,在之后便是对 `响应(Response)` 进行处理了,`内核` 通常是由框架负责实现的,而其它的就由您来编排了。
|
||||
|
||||
## 定义全局中间件
|
||||
|
||||
全局中间件只可通过配置文件的方式来配置,配置文件位于 `config/autoload/middlewares.php` ,配置如下:
|
||||
```php
|
||||
<?php
|
||||
return [
|
||||
// http 对应 config/server.php 内每个 server 的 name 属性对应的值,该配置仅应用在该 Server 中
|
||||
'http' => [
|
||||
// 数组内配置您的全局中间件,顺序根据该数组的顺序
|
||||
YourMiddleware::class
|
||||
],
|
||||
];
|
||||
```
|
||||
只需将您的全局中间件配置在该文件及对应的 `Server Name` 内,即该 `Server` 下的所有请求都会应用配置的全局中间件。
|
||||
|
||||
## 定义局部中间件
|
||||
|
||||
当我们有些中间件仅仅面向某些请求或控制器时,即可将其定义为局部中间件,可通过配置文件的方式定义或注解的方式。
|
||||
|
||||
### 通过配置文件定义
|
||||
|
||||
在使用配置文件定义路由时,推荐通过配置文件来定义对应的中间件,局部中间件的配置将在路由配置上完成。
|
||||
`Hyperf\HttpServer\Router\Router` 类的每个定义路由的方法的最后一个参数 `$options` 都将接收一个数组,可通过传递键值 `middleware` 及一个数组值来定义该路由的中间件,我们通过几个路由定义来演示一下:
|
||||
|
||||
```php
|
||||
<?php
|
||||
use App\Middleware\FooMiddleware;
|
||||
use Hyperf\HttpServer\Router\Router;
|
||||
|
||||
// 每个路由定义方法都可接收一个 $options 参数
|
||||
Router::get('/', [\App\Controller\IndexController::class, 'index'], ['middleware' => [ForMiddleware::class]]);
|
||||
Router::post('/', [\App\Controller\IndexController::class, 'index'], ['middleware' => [ForMiddleware::class]]);
|
||||
Router::put('/', [\App\Controller\IndexController::class, 'index'], ['middleware' => [ForMiddleware::class]]);
|
||||
Router::patch('/', [\App\Controller\IndexController::class, 'index'], ['middleware' => [ForMiddleware::class]]);
|
||||
Router::delete('/', [\App\Controller\IndexController::class, 'index'], ['middleware' => [ForMiddleware::class]]);
|
||||
Router::head('/', [\App\Controller\IndexController::class, 'index'], ['middleware' => [ForMiddleware::class]]);
|
||||
Router::addRoute(['GET', 'POST', 'HEAD'], '/index', [\App\Controller\IndexController::class, 'index'], ['middleware' => [ForMiddleware::class]]);
|
||||
|
||||
// 该 Group 下的所有路由都将应用配置的中间件
|
||||
Router::addGroup(
|
||||
'/v2', function () {
|
||||
Router::get('/index', [\App\Controller\IndexController::class, 'index']);
|
||||
},
|
||||
['middleware' => [ForMiddleware::class]]
|
||||
);
|
||||
|
||||
```
|
||||
|
||||
### 通过注解定义
|
||||
|
||||
在通过注解定义路由时,我们推荐通过注解的方式来定义中间件,对中间件的定义有两个注解,分别为:
|
||||
- `@Middleware` 注解为定义单个中间件时使用,在一个地方仅可定义一个该注解,不可重复定义
|
||||
- `@Middlewares` 注解为定义多个中间件时使用,在一个地方仅可定义一个该注解,然后通过在该注解内定义多个 `@Middleware` 注解实现多个中间件的定义
|
||||
|
||||
> 使用 `@Middleware` 注解时需 `use Hyperf\HttpServer\Annotation\Middleware;` 命名空间;
|
||||
> 使用 `@Middlewares` 注解时需 `use Hyperf\HttpServer\Annotation\Middlewares;` 命名空间;
|
||||
|
||||
定义单个中间件:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
use App\Middleware\FooMiddleware;
|
||||
use Hyperf\HttpServer\Annotation\AutoController;
|
||||
use Hyperf\HttpServer\Annotation\Middleware;
|
||||
|
||||
/**
|
||||
* @AutoController()
|
||||
* @Middleware(FooMiddleware::class)
|
||||
*/
|
||||
class IndexController
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
return 'Hello Hyperf.';
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
定义多个中间件:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
use App\Middleware\BarMiddleware;
|
||||
use App\Middleware\FooMiddleware;
|
||||
use Hyperf\HttpServer\Annotation\AutoController;
|
||||
use Hyperf\HttpServer\Annotation\Middleware;
|
||||
|
||||
/**
|
||||
* @AutoController()
|
||||
* @Middlewares({
|
||||
* @Middleware(FooMiddleware::class),
|
||||
* @Middleware(BarMiddleware::class)
|
||||
* })
|
||||
*/
|
||||
class IndexController
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
return 'Hello Hyperf.';
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 定义方法级别的中间件
|
||||
|
||||
在通过配置文件的方式配置中间件时定义到方法级别上很简单,那么要通过注解的形式定义到方法级别呢?您只需将注解直接定义到方法上即可。
|
||||
方法级别上的中间件会优先于类级别的中间件,我们通过代码来举例一下:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
use App\Middleware\BarMiddleware;
|
||||
use App\Middleware\FooMiddleware;
|
||||
use Hyperf\HttpServer\Annotation\AutoController;
|
||||
use Hyperf\HttpServer\Annotation\Middleware;
|
||||
use Hyperf\HttpServer\Annotation\Middlewares;
|
||||
|
||||
/**
|
||||
* @AutoController()
|
||||
* @Middlewares({
|
||||
* @Middleware(FooMiddleware::class)
|
||||
* })
|
||||
*/
|
||||
class IndexController
|
||||
{
|
||||
|
||||
/**
|
||||
* @AutoController()
|
||||
* @Middlewares({
|
||||
* @Middleware(BarMiddleware::class)
|
||||
* })
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
return 'Hello Hyperf.';
|
||||
}
|
||||
}
|
||||
```
|
||||
#### 中间件相关的代码
|
||||
|
||||
生成中间件
|
||||
|
||||
```
|
||||
php ./bin/hyperf.php gen:middleware Auth/FooMiddleware
|
||||
```
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Middleware\Auth;
|
||||
|
||||
use Hyperf\HttpServer\Contract\RequestInterface;
|
||||
use Hyperf\HttpServer\Contract\ResponseInterface as HttpResponse;
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\MiddlewareInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
|
||||
class FooMiddleware implements MiddlewareInterface
|
||||
{
|
||||
/**
|
||||
* @var ContainerInterface
|
||||
*/
|
||||
protected $container;
|
||||
|
||||
/**
|
||||
* @var RequestInterface
|
||||
*/
|
||||
protected $request;
|
||||
|
||||
/**
|
||||
* @var HttpResponse
|
||||
*/
|
||||
protected $response;
|
||||
|
||||
public function __construct(ContainerInterface $container, HttpResponse $response, RequestInterface $request)
|
||||
{
|
||||
$this->container = $container;
|
||||
$this->response = $response;
|
||||
$this->request = $request;
|
||||
}
|
||||
|
||||
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||||
{
|
||||
// 根据具体业务判断逻辑走向,这里假设用户携带的token有效
|
||||
$isValidToken = true;
|
||||
if ($isValidToken) {
|
||||
return $handler->handle($request);
|
||||
}
|
||||
|
||||
return $this->response->json(
|
||||
[
|
||||
'code' => -1,
|
||||
'data' => [
|
||||
'error' => '中间里验证token无效,阻止继续向下执行',
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
中间件的执行顺序为 `BarMiddleware -> FooMiddleware`。
|
||||
|
||||
## 中间件的执行顺序
|
||||
|
||||
我们从上面可以了解到总共有 `3` 种级别的中间件,分别为 `全局中间件`、`类级别中间件`、`方法级别中间件`,如果都定义了这些中间件,执行顺序为:`全局中间件 -> 方法级别中间件 -> 类级别中间件`。
|
||||
|
100
doc/zh/paginator.md
Normal file
100
doc/zh/paginator.md
Normal 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/zh/pool.md
Normal file
63
doc/zh/pool.md
Normal file
@ -0,0 +1,63 @@
|
||||
# 连接池
|
||||
|
||||
## 安装
|
||||
|
||||
```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/zh/process.md
Normal file
182
doc/zh/process.md
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
11
doc/zh/quick-start/important.md
Normal file
11
doc/zh/quick-start/important.md
Normal file
@ -0,0 +1,11 @@
|
||||
# 编程须知
|
||||
|
||||
这里收集各种通过 Hyperf 编程前应该知晓的知识点或内容点。
|
||||
|
||||
## 不能通过全局变量获取属性参数
|
||||
|
||||
在 `PHP-FPM` 下可以通过全局变量获取到请求的参数,服务器的参数等,在 `Hyperf` 和 `Swoole` 内,都 **无法** 通过 `$_GET/$_POST/$_REQUEST/$_SESSION/$_COOKIE/$_SERVER`等`$_`开头的变量获取到任何属性参数。
|
||||
|
||||
## 通过容器获取的类都是单例
|
||||
|
||||
通过依赖注入容器获取的都是进程内持久化的,是多个协程共享的,所以不能包含任何的请求唯一的数据或协程唯一的数据,这类型的数据都通过协程上下文去处理,具体请仔细阅读 [依赖注入](./zh/di.md) 和 [协程](./zh/coroutine.md) 章节。
|
67
doc/zh/quick-start/install.md
Normal file
67
doc/zh/quick-start/install.md
Normal file
@ -0,0 +1,67 @@
|
||||
# 安装
|
||||
|
||||
## 服务器要求
|
||||
|
||||
Hyperf 对系统环境有一些要求,仅可运行于 Linux 和 Mac 环境下,但由于 Docker 虚拟化技术的发展,在 Windows 下也可以通过 Docker for Windows 来作为运行环境,通常来说 Mac 环境下,我们更推荐本地环境部署,以避免 Docker 共享磁盘缓慢导致 Hyperf 启动速度慢的问题。
|
||||
|
||||
[hyperf-cloud\hyperf-docker](https://github.com/hyperf-cloud/hyperf-docker) 项目内已经为您准备好了各种版本的 Dockerfile ,或直接基于已经构建好的 [hyperf\hyperf](https://hub.docker.com/r/hyperf/hyperf) 镜像来运行。
|
||||
|
||||
当您不想采用 Docker 来作为运行的环境基础时,您需要确保您的运行环境达到了以下的要求:
|
||||
|
||||
- PHP >= 7.2
|
||||
- Swoole PHP 扩展 >= 4.3.1,并关闭了 `Short Name`
|
||||
- OpenSSL PHP 扩展
|
||||
- JSON PHP 扩展
|
||||
- PDO PHP 扩展 (如需要使用到 MySQL 客户端)
|
||||
- Redis PHP 扩展 (如需要使用到 Redis 客户端)
|
||||
- Protobuf PHP 扩展 (如需要使用到 gRPC 服务端或客户端)
|
||||
|
||||
|
||||
## 安装 Hyperf
|
||||
|
||||
Hyperf 使用 [Composer](https://getcomposer.org) 来管理项目的依赖,在使用 Hyperf 之前,请确保你的运行环境已经安装好了 Composer。
|
||||
|
||||
### 通过 `Composer` 创建项目
|
||||
|
||||
[hyperf-cloud/hyperf-skeleton](https://github.com/hyperf-cloud/hyperf-skeleton) 项目是我们已经为您准备好的一个骨架项目,内置了一些常用的组件及相关配置的文件及结构,是一个可以快速用于业务开发的 Web 项目基础,在安装时,您可根据您自身的需求,对组件依赖进行选择。
|
||||
执行下面的命令可以于当前所在位置创建一个 hyperf-skeleton 项目
|
||||
```
|
||||
composer create-project hyperf/hyperf-skeleton
|
||||
```
|
||||
|
||||
### Docker 下开发
|
||||
|
||||
假设您的本机环境并不能达到 Hyperf 的环境要求,或对于环境配置不是那么熟悉,那么您可以通过以下方法来运行及开发 Hyperf 项目:
|
||||
|
||||
```
|
||||
# 下载并运行 hyperf/hyperf 镜像,并将镜像内的项目目录绑定到宿主机的 /tmp/skeleton 目录
|
||||
docker run -v /tmp/skeleton:/hyperf-skeleton -p 9501:9501 -it --entrypoint /bin/sh hyperf/hyperf:7.2-alpine-cli
|
||||
|
||||
# 镜像容器运行后,在容器内安装 Composer
|
||||
wget https://github.com/composer/composer/releases/download/1.8.6/composer.phar
|
||||
chmod u+x composer.phar
|
||||
mv composer.phar /usr/local/bin/composer
|
||||
# 将 Composer 镜像设置为阿里云镜像,加速国内下载速度
|
||||
composer config -g repo.packagist composer https://mirrors.aliyun.com/composer
|
||||
|
||||
# 通过 Compose 安装 hyperf/hyperf-skeleton 项目
|
||||
composer create-project hyperf/hyperf-skeleton
|
||||
|
||||
# 进入安装好的 Hyperf 项目目录
|
||||
cd hyperf-skeleton
|
||||
# 启动 Hyperf
|
||||
php bin/hyperf.php start
|
||||
```
|
||||
|
||||
接下来,就可以在 `/tmp/skeleton` 中看到您安装好的代码了。由于 Hyperf 是持久化的 CLI 框架,当您修改完您的代码后,通过 `CTRL + C` 终止当前启动的进程实例,并重新执行 `php bin/hyperf.php start` 启动命令即可。
|
||||
|
||||
## 存在兼容性的扩展
|
||||
|
||||
由于 Hyperf 基于 Swoole 协程实现,而 Swoole 4 带来的协程功能是 PHP 前所未有的,所以在与不少扩展都仍存在兼容性的问题。
|
||||
以下扩展(包括但不限于)都会造成一定的兼容性问题,不能与之共用或共存:
|
||||
|
||||
- xhprof
|
||||
- xdebug
|
||||
- blackfire
|
||||
- trace
|
||||
- uopz
|
231
doc/zh/quick-start/overview.md
Normal file
231
doc/zh/quick-start/overview.md
Normal file
@ -0,0 +1,231 @@
|
||||
# 快速入门
|
||||
|
||||
为了让您更快的了解 `Hyperf` 的使用,本章节将以 `创建一个 HTTP Server` 为例,通过对路由、控制器的定义实现一个简单的 `Web` 服务,但 `Hyperf` 不止于此,完善的服务治理、`gRPC` 服务、注解、`AOP` 等功能将由具体的章节阐述。
|
||||
|
||||
## 定义访问路由
|
||||
|
||||
Hyperf 使用 [nikic/fast-route](https://github.com/nikic/FastRoute) 作为默认的路由组件并提供服务,您可以很方便的在 `config/routes.php` 中定义您的路由。
|
||||
不仅如此,框架还提供了极其强大和方便灵活的`注解路由`功能,关于路由的详情文档请查阅 [路由](zh/router.md) 章节
|
||||
|
||||
### 通过配置文件定义路由
|
||||
路由的文件位于 [hyperf-skeleton](https://github.com/hyperf-cloud/hyperf-skeleton) 项目的 `config/routes.php` ,下面是一些常用的用法示例。
|
||||
```php
|
||||
<?php
|
||||
use Hyperf\HttpServer\Router\Router;
|
||||
|
||||
// 此处代码示例为每个示例都提供了三种不同的绑定定义方式,实际配置时仅可采用一种且仅定义一次相同的路由
|
||||
|
||||
// 设置一个 GET 请求的路由,绑定访问地址 '/get' 到 App\Controller\IndexController 的 get 方法
|
||||
Router::get('/get', 'App\Controller\IndexController::get');
|
||||
Router::get('/get', 'App\Controller\IndexController@get');
|
||||
Router::get('/get', [\App\Controller\IndexController::class, 'get']);
|
||||
|
||||
// 设置一个 POST 请求的路由,绑定访问地址 '/post' 到 App\Controller\IndexController 的 post 方法
|
||||
Router::post('/post', 'App\Controller\IndexController::post');
|
||||
Router::post('/post', 'App\Controller\IndexController@post');
|
||||
Router::post('/post', [\App\Controller\IndexController::class, 'post']);
|
||||
|
||||
// 设置一个允许 GET、POST 和 HEAD 请求的路由,绑定访问地址 '/multi' 到 App\Controller\IndexController 的 multi 方法
|
||||
Router::addRoute(['GET', 'POST', 'HEAD'], '/multi', 'App\Controller\IndexController::multi');
|
||||
Router::addRoute(['GET', 'POST', 'HEAD'], '/multi', 'App\Controller\IndexController@multi');
|
||||
Router::addRoute(['GET', 'POST', 'HEAD'], '/multi', [\App\Controller\IndexController::class, 'multi']);
|
||||
```
|
||||
|
||||
### 通过注解来定义路由
|
||||
|
||||
`Hyperf` 提供了极其强大和方便灵活的 [注解](zh/annotation.md) 功能,在路由的定义上也毫无疑问地提供了注解定义的方式,Hyperf 提供了 `@Controller` 和 `@AutoController` 两种注解来定义一个 `Controller`,此处仅做简单的说明,更多细节请查阅 [路由](zh/router.md) 章节。
|
||||
|
||||
### 通过 `@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 IndexController
|
||||
{
|
||||
// Hyperf 会自动为此方法生成一个 /index/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 IndexController
|
||||
{
|
||||
// Hyperf 会自动为此方法生成一个 /index/index 的路由,允许通过 GET 或 POST 方式请求
|
||||
/**
|
||||
* @RequestMapping(path="index", methods="get,post")
|
||||
*/
|
||||
public function index(RequestInterface $request)
|
||||
{
|
||||
// 从请求中获得 id 参数
|
||||
$id = $request->input('id', 1);
|
||||
return (string)$id;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## 处理 HTTP 请求
|
||||
|
||||
`Hyperf` 是完全开放的,本质上没有规定您必须基于某种模式下去实现请求的处理,您可以采用传统的 `MVC模式`,亦可以采用 `RequestHandler模式` 来进行开发。
|
||||
我们以 `MVC模式` 来举个例子:
|
||||
在 `app` 文件夹内创建一个 `Controller` 文件夹并创建 `IndexController.php` 如下,`index` 方法内从请求中获取了 `id` 参数,并转换为 `字符串` 类型返回到客户端。
|
||||
|
||||
```php
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use Hyperf\HttpServer\Contract\RequestInterface;
|
||||
use Hyperf\HttpServer\Annotation\AutoController;
|
||||
|
||||
/**
|
||||
* @AutoController()
|
||||
*/
|
||||
class IndexController
|
||||
{
|
||||
// Hyperf 会自动为此方法生成一个 /index/index 的路由,允许通过 GET 或 POST 方式请求
|
||||
public function index(RequestInterface $request)
|
||||
{
|
||||
// 从请求中获得 id 参数
|
||||
$id = $request->input('id', 1);
|
||||
// 转换 $id 为字符串格式并以 plain/text 的 Content-Type 返回 $id 的值给客户端
|
||||
return (string)$id;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 依赖自动注入
|
||||
|
||||
依赖自动注入是 `Hyperf` 提供的一个非常强大的功能,也是保持框架灵活性的根基。
|
||||
`Hyperf` 提供了两种注入方式,一种是大家常见的通过构造函数注入,另一种是通过 `@Inject` 注解注入,下面我们举个例子并分别以两种方式展示注入的实现;
|
||||
假设我们存在一个 `\App\Service\UserService` 类,类中存在一个 `getInfoById(int $id)` 方法通过传递一个 `id` 并最终返回一个用户实体,由于返回值并不是我们这里所需要关注的,所以不做过多阐述,我们要关注的是在任意的类中获取 `UserService` 并调用里面的方法,一般的方法是通过 `new UserService()` 来实例化该服务类,但在 `Hyperf` 下,我们有更优的解决方法。
|
||||
|
||||
### 通过构造函数注入
|
||||
只需在构造函数的参数内声明参数的类型,`Hyperf` 会自动注入对应的对象或值。
|
||||
```php
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use Hyperf\HttpServer\Contract\RequestInterface;
|
||||
use Hyperf\HttpServer\Annotation\AutoController;
|
||||
use App\Service\UserService;
|
||||
|
||||
/**
|
||||
* @AutoController()
|
||||
*/
|
||||
class IndexController
|
||||
{
|
||||
/**
|
||||
* @var UserService
|
||||
*/
|
||||
private $userService;
|
||||
|
||||
// 在构造函数声明参数的类型,Hyperf 会自动注入对应的对象或值
|
||||
public function __construct(UserService $userService)
|
||||
{
|
||||
$this->userService = $userService;
|
||||
}
|
||||
|
||||
// /index/info
|
||||
public function info(RequestInterface $request)
|
||||
{
|
||||
$id = $request->input('id', 1);
|
||||
return $this->userService->getInfoById((int)$id);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 通过 `@Inject` 注解注入
|
||||
只需对对应的类属性通过 `@var` 声明参数的类型,并使用 `@Inject` 注解标记属性 ,`Hyperf` 会自动注入对应的对象或值。
|
||||
|
||||
> 使用 `@Inject` 注解时需 `use Hyperf\Di\Annotation\Inject;` 命名空间;
|
||||
|
||||
```php
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use Hyperf\HttpServer\Contract\RequestInterface;
|
||||
use Hyperf\HttpServer\Annotation\AutoController;
|
||||
use Hyperf\Di\Annotation\Inject;
|
||||
use App\Service\UserService;
|
||||
|
||||
/**
|
||||
* @AutoController()
|
||||
*/
|
||||
class IndexController
|
||||
{
|
||||
/**
|
||||
* @Inject()
|
||||
* @var UserService
|
||||
*/
|
||||
private $userService;
|
||||
|
||||
// /index/info
|
||||
public function info(RequestInterface $request)
|
||||
{
|
||||
$id = $request->input('id', 1);
|
||||
return $this->userService->getInfoById((int)$id);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
通过上面的示例我们不难发现 `$userService` 在没有实例化的情况下, 属性对应的类对象被自动注入了。
|
||||
不过这里的案例并未真正体现出依赖自动注入的好处及其强大之处,我们假设一下 `UserService` 也存在很多的依赖,而这些依赖同时又存在很多其它的依赖时,`new` 实例化的方式就需要手动实例化很多的对象并调整好对应的参数位,而在 `Hyperf` 里我们就无须手动管理这些依赖,只需要声明一下最终使用的类即可。
|
||||
而当 `UserService` 需要发生替换等剧烈的内部变化时,比如从一个本地服务替换成了一个 RPC 远程服务,也只需要通过配置调整依赖中 `UserService` 这个键值对应的类为新的RPC服务类即可。
|
||||
|
||||
## 启动 Hyperf 服务
|
||||
|
||||
由于 `Hyperf` 内置了协程服务器,也就意味着 `Hyperf` 将以 `CLI` 的形式去运行,所以在定义好路由及实际的逻辑代码之后,我们需要在项目根目录并通过命令行运行 `php bin/hyperf.php start` 来启动服务。
|
||||
当 `Console` 界面显示服务启动后便可通过 `cURL` 或 浏览器对服务正常发起访问了,默认情况下上面的例子是访问 `http://127.0.0.1:9501/index/info?id=1`。
|
||||
|
||||
## 重新加载代码
|
||||
|
||||
由于 `Hyperf` 是持久化的 `CLI` 应用,也就意味着一旦进程启动,已解析的 `PHP` 代码会持久化在进程中,也就意味着启动服务后您再修改的 `PHP` 代码不会改变已启动的服务,如您希望服务重新加载您修改后的代码,您需要通过在启动的 `Console` 中键入 `CTRL + C` 终止服务,再重新执行启动命令完成重启和重新加载。
|
||||
|
||||
> Tips: 您也可以将启动 Server 的命令配置在 IDE 上,便可直接通过 IDE 的 `启动/停止` 操作快捷的完成 `启动服务` 或 `重启服务` 的操作。
|
54
doc/zh/quick-start/questions.md
Normal file
54
doc/zh/quick-start/questions.md
Normal file
@ -0,0 +1,54 @@
|
||||
# 常见问题
|
||||
|
||||
## Swoole 短名未关闭
|
||||
|
||||
```
|
||||
[ERROR] Swoole short name have to disable before start server, please set swoole.use_shortname = 'Off' into your php.ini.
|
||||
```
|
||||
|
||||
这可能是因为你按以下的方式设置了
|
||||
|
||||
```
|
||||
// 这些都是错误的,注意 `大小写` 和 `引号`
|
||||
swoole.use_shortname = 'off'
|
||||
swoole.use_shortname = off
|
||||
swoole.use_shortname = Off
|
||||
// 下面的才是正确的
|
||||
swoole.use_shortname = 'Off'
|
||||
```
|
||||
|
||||
> 注意该配置必须于 php.ini 内配置,无法通过 ini_set() 函数来重写
|
||||
|
||||
## 代理类缓存
|
||||
|
||||
代理类缓存一旦生成,将不会再重新覆盖。所以当你修改了已经生成代理类的文件时,需要手动清理。
|
||||
|
||||
代理类位置如下
|
||||
```
|
||||
runtime/container/proxy/
|
||||
```
|
||||
|
||||
清理命令
|
||||
```
|
||||
php bin/hyperf.php di:init-proxy
|
||||
```
|
||||
|
||||
所以单测命令可以使用以下代替
|
||||
```
|
||||
php bin/hyperf.php di:init-proxy && composer test
|
||||
```
|
||||
|
||||
同理,启动命令可以使用以下代替
|
||||
```
|
||||
php bin/hyperf.php di:init-proxy && php bin/hyperf.php start
|
||||
```
|
||||
|
||||
## Docker打包失败
|
||||
|
||||
显示 `wget: error getting response: Connection reset by peer`
|
||||
|
||||
修改我们默认的 `Dockerfile`,重新安装一下 `wget`,增加以下代码即可。
|
||||
|
||||
```
|
||||
&& apk add wget \
|
||||
```
|
110
doc/zh/rate-limit.md
Normal file
110
doc/zh/rate-limit.md
Normal file
@ -0,0 +1,110 @@
|
||||
# 令牌桶限流器
|
||||
|
||||
## 安装
|
||||
|
||||
```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/zh/redis.md
Normal file
130
doc/zh/redis.md
Normal file
@ -0,0 +1,130 @@
|
||||
# Redis
|
||||
|
||||
## 安装
|
||||
|
||||
```
|
||||
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/zh/request.md
Normal file
275
doc/zh/request.md
Normal 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` 开头的方法的返回值都是一个新对象,不会修改原对象的值
|
||||
|
||||
## 安装
|
||||
|
||||
该组件完全独立,适用于任何一个框架项目。
|
||||
|
||||
```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/zh/response.md
Normal file
128
doc/zh/response.md
Normal 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/zh/router.md
Normal file
189
doc/zh/router.md
Normal 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}]`。
|
36
doc/zh/service-register.md
Normal file
36
doc/zh/service-register.md
Normal 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/zh/swoole-dashboard.md
Normal file
128
doc/zh/swoole-dashboard.md
Normal file
@ -0,0 +1,128 @@
|
||||
# Swoole Dashboard
|
||||
|
||||
[dashboard](https://www.swoole-cloud.com/dashboard.html) 作为 `Swoole` 官方出品、更专一、更专业。
|
||||
|
||||
- 时刻掌握应用架构模型
|
||||
- 分布式跨应用链路追踪
|
||||
- 完善的系统监控
|
||||
- 零成本接入
|
||||
- 全面分析报告服务状况
|
||||
|
||||
## 安装
|
||||
|
||||
注册完账户后,进入[控制台](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/zh/testing.md
Normal file
203
doc/zh/testing.md
Normal 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/zh/tracer.md
Normal file
85
doc/zh/tracer.md
Normal 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/)
|
47
doc/zh/tutorial/daocloud.md
Normal file
47
doc/zh/tutorial/daocloud.md
Normal 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."}}
|
||||
```
|
||||
|
236
doc/zh/tutorial/docker-swarm.md
Normal file
236
doc/zh/tutorial/docker-swarm.md
Normal file
@ -0,0 +1,236 @@
|
||||
# Docker Swarm集群搭建教程
|
||||
|
||||
现阶段,Docker容器技术已经相当成熟,就算是中小型公司也可以基于 Gitlab、Aliyun镜像服务、Docker Swarm 轻松搭建自己的 Docker集群服务。
|
||||
|
||||
## 安装 Docker
|
||||
|
||||
```
|
||||
curl -sSL https://get.daocloud.io/docker | sh
|
||||
```
|
||||
|
||||
## 搭建自己的Gitlab
|
||||
|
||||
### 安装Gitlab
|
||||
|
||||
首先我们修改一下端口号,把 `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`。
|
||||
|
||||
### 安装gitlab-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
|
||||
|
||||
## 安装 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
|
||||
```
|
63
doc/zh/tutorial/supervisor.md
Normal file
63
doc/zh/tutorial/supervisor.md
Normal 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` 脚本来管理进程。
|
||||
|
||||
## 安装 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/zh/utils.md
Normal file
25
doc/zh/utils.md
Normal 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()` 方法的一个封装,但区别在于这里兼容了非协程环境下的运行。
|
57
doc/zh/websocket-client.md
Normal file
57
doc/zh/websocket-client.md
Normal file
@ -0,0 +1,57 @@
|
||||
# WebSocket 协程客户端
|
||||
|
||||
Hyperf 提供了对 WebSocket Client 的封装,可基于 [hyperf/websocket-client](https://github.com/hyperf-cloud/websocket-client) 组件对 WebSocket Server 进行访问;
|
||||
|
||||
## 安装
|
||||
|
||||
```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);
|
||||
```
|
90
doc/zh/websocket-server.md
Normal file
90
doc/zh/websocket-server.md
Normal file
@ -0,0 +1,90 @@
|
||||
# WebSocket 服务
|
||||
|
||||
Hyperf 提供了对 WebSocket Server 的封装,可基于 [hyperf/websocket-server](https://github.com/hyperf-cloud/websocket-server) 组件快速搭建一个 WebSocket 应用。
|
||||
|
||||
## 安装
|
||||
|
||||
```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
|
||||
```
|
@ -14,11 +14,14 @@
|
||||
<directory suffix="Test.php">./src/constants/tests</directory>
|
||||
<directory suffix="Test.php">./src/consul/tests</directory>
|
||||
<directory suffix="Test.php">./src/database/tests</directory>
|
||||
<directory suffix="Test.php">./src/db-connection/tests</directory>
|
||||
<directory suffix="Test.php">./src/di/tests</directory>
|
||||
<directory suffix="Test.php">./src/elasticsearch/tests</directory>
|
||||
<directory suffix="Test.php">./src/event/tests</directory>
|
||||
<directory suffix="Test.php">./src/http-server/tests</directory>
|
||||
<directory suffix="Test.php">./src/logger/tests</directory>
|
||||
<directory suffix="Test.php">./src/model-cache/tests</directory>
|
||||
<directory suffix="Test.php">./src/paginator/tests</directory>
|
||||
<directory suffix="Test.php">./src/redis/tests</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
|
@ -14,17 +14,15 @@ namespace Hyperf\AsyncQueue\Command;
|
||||
|
||||
use Hyperf\AsyncQueue\Driver\DriverFactory;
|
||||
use Hyperf\Command\Annotation\Command;
|
||||
use Hyperf\Command\Command as HyperfCommand;
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Symfony\Component\Console\Command\Command as SymfonyCommand;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* @Command
|
||||
*/
|
||||
class FlushFailedMessageCommand extends SymfonyCommand
|
||||
class FlushFailedMessageCommand extends HyperfCommand
|
||||
{
|
||||
/**
|
||||
* @var ContainerInterface
|
||||
@ -37,17 +35,17 @@ class FlushFailedMessageCommand extends SymfonyCommand
|
||||
parent::__construct('queue:flush');
|
||||
}
|
||||
|
||||
public function execute(InputInterface $input, OutputInterface $output)
|
||||
public function handle()
|
||||
{
|
||||
$name = $input->getArgument('name');
|
||||
$queue = $input->getOption('queue');
|
||||
$name = $this->input->getArgument('name');
|
||||
$queue = $this->input->getOption('queue');
|
||||
|
||||
$factory = $this->container->get(DriverFactory::class);
|
||||
$driver = $factory->get($name);
|
||||
|
||||
$driver->flush($queue);
|
||||
|
||||
$output->writeln('<fg=red>Flush all message from failed queue.</>');
|
||||
$this->output->writeln('<fg=red>Flush all message from failed queue.</>');
|
||||
}
|
||||
|
||||
protected function configure()
|
||||
|
@ -14,16 +14,14 @@ namespace Hyperf\AsyncQueue\Command;
|
||||
|
||||
use Hyperf\AsyncQueue\Driver\DriverFactory;
|
||||
use Hyperf\Command\Annotation\Command;
|
||||
use Hyperf\Command\Command as HyperfCommand;
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Symfony\Component\Console\Command\Command as SymfonyCommand;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* @Command
|
||||
*/
|
||||
class InfoCommand extends SymfonyCommand
|
||||
class InfoCommand extends HyperfCommand
|
||||
{
|
||||
/**
|
||||
* @var ContainerInterface
|
||||
@ -36,15 +34,15 @@ class InfoCommand extends SymfonyCommand
|
||||
parent::__construct('queue:info');
|
||||
}
|
||||
|
||||
public function execute(InputInterface $input, OutputInterface $output)
|
||||
public function handle()
|
||||
{
|
||||
$name = $input->getArgument('name');
|
||||
$name = $this->input->getArgument('name');
|
||||
$factory = $this->container->get(DriverFactory::class);
|
||||
$driver = $factory->get($name);
|
||||
|
||||
$info = $driver->info();
|
||||
foreach ($info as $key => $count) {
|
||||
$output->writeln(sprintf('<fg=green>%s count is %d.</>', $key, $count));
|
||||
$this->output->writeln(sprintf('<fg=green>%s count is %d.</>', $key, $count));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -14,17 +14,15 @@ namespace Hyperf\AsyncQueue\Command;
|
||||
|
||||
use Hyperf\AsyncQueue\Driver\DriverFactory;
|
||||
use Hyperf\Command\Annotation\Command;
|
||||
use Hyperf\Command\Command as HyperfCommand;
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Symfony\Component\Console\Command\Command as SymfonyCommand;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* @Command
|
||||
*/
|
||||
class ReloadFailedMessageCommand extends SymfonyCommand
|
||||
class ReloadFailedMessageCommand extends HyperfCommand
|
||||
{
|
||||
/**
|
||||
* @var ContainerInterface
|
||||
@ -37,17 +35,17 @@ class ReloadFailedMessageCommand extends SymfonyCommand
|
||||
parent::__construct('queue:reload');
|
||||
}
|
||||
|
||||
public function execute(InputInterface $input, OutputInterface $output)
|
||||
public function handle()
|
||||
{
|
||||
$name = $input->getArgument('name');
|
||||
$queue = $input->getOption('queue');
|
||||
$name = $this->input->getArgument('name');
|
||||
$queue = $this->input->getOption('queue');
|
||||
|
||||
$factory = $this->container->get(DriverFactory::class);
|
||||
$driver = $factory->get($name);
|
||||
|
||||
$num = $driver->reload($queue);
|
||||
|
||||
$output->writeln(sprintf('<fg=green>Reload %d failed message into waiting queue.</>', $num));
|
||||
$this->output->writeln(sprintf('<fg=green>Reload %d failed message into waiting queue.</>', $num));
|
||||
}
|
||||
|
||||
protected function configure()
|
||||
|
@ -371,11 +371,10 @@ abstract class Command extends SymfonyCommand
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
if ($this->coroutine) {
|
||||
// Enable Coroutine in co environment.
|
||||
\Swoole\Runtime::enableCoroutine(true);
|
||||
go(function () {
|
||||
run(function () {
|
||||
call([$this, 'handle']);
|
||||
});
|
||||
|
||||
return 0;
|
||||
}
|
||||
return call([$this, 'handle']);
|
||||
|
@ -31,6 +31,14 @@ abstract class AbstractConstants
|
||||
$name = strtolower(substr($name, 3));
|
||||
$class = get_called_class();
|
||||
|
||||
return ConstantsCollector::getValue($class, $code, $name);
|
||||
$message = ConstantsCollector::getValue($class, $code, $name);
|
||||
|
||||
array_shift($arguments);
|
||||
|
||||
if (count($arguments) > 0) {
|
||||
return sprintf($message, ...$arguments);
|
||||
}
|
||||
|
||||
return $message;
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ declare(strict_types=1);
|
||||
namespace HyperfTest\Constants;
|
||||
|
||||
use Hyperf\Constants\AnnotationReader;
|
||||
use Hyperf\Constants\ConstantsCollector;
|
||||
use HyperfTest\Constants\Stub\ErrorCodeStub;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
@ -22,19 +23,36 @@ use PHPUnit\Framework\TestCase;
|
||||
*/
|
||||
class AnnotationReaderTest extends TestCase
|
||||
{
|
||||
public function testGetAnnotations()
|
||||
protected function setUp()
|
||||
{
|
||||
$reader = new AnnotationReader();
|
||||
|
||||
$ref = new \ReflectionClass(ErrorCodeStub::class);
|
||||
$classConstants = $ref->getReflectionConstants();
|
||||
|
||||
$res = $reader->getAnnotations($classConstants);
|
||||
$data = $reader->getAnnotations($classConstants);
|
||||
ConstantsCollector::set(ErrorCodeStub::class, $data);
|
||||
}
|
||||
|
||||
$this->assertSame('Server Error!', $res[ErrorCodeStub::SERVER_ERROR]['message']);
|
||||
$this->assertSame('SHOW ECHO', $res[ErrorCodeStub::SHOW_ECHO]['message']);
|
||||
$this->assertSame('ECHO', $res[ErrorCodeStub::SHOW_ECHO]['echo']);
|
||||
public function testGetAnnotations()
|
||||
{
|
||||
$data = ConstantsCollector::get(ErrorCodeStub::class);
|
||||
|
||||
$this->assertArrayNotHasKey(ErrorCodeStub::NO_MESSAGE, $res);
|
||||
$this->assertSame('Server Error!', $data[ErrorCodeStub::SERVER_ERROR]['message']);
|
||||
$this->assertSame('SHOW ECHO', $data[ErrorCodeStub::SHOW_ECHO]['message']);
|
||||
$this->assertSame('ECHO', $data[ErrorCodeStub::SHOW_ECHO]['echo']);
|
||||
|
||||
$this->assertArrayNotHasKey(ErrorCodeStub::NO_MESSAGE, $data);
|
||||
}
|
||||
|
||||
public function testGetMessageWithArguments()
|
||||
{
|
||||
$res = ErrorCodeStub::getMessage(ErrorCodeStub::PARAMS_INVALID);
|
||||
|
||||
$this->assertSame('Params[%s] is invalid.', $res);
|
||||
|
||||
$res = ErrorCodeStub::getMessage(ErrorCodeStub::PARAMS_INVALID, 'user_id');
|
||||
|
||||
$this->assertSame('Params[user_id] is invalid.', $res);
|
||||
}
|
||||
}
|
||||
|
@ -12,7 +12,9 @@ declare(strict_types=1);
|
||||
|
||||
namespace HyperfTest\Constants\Stub;
|
||||
|
||||
class ErrorCodeStub
|
||||
use Hyperf\Constants\AbstractConstants;
|
||||
|
||||
class ErrorCodeStub extends AbstractConstants
|
||||
{
|
||||
/**
|
||||
* @Message("Server Error!")
|
||||
@ -26,4 +28,9 @@ class ErrorCodeStub
|
||||
const SHOW_ECHO = 501;
|
||||
|
||||
const NO_MESSAGE = 502;
|
||||
|
||||
/**
|
||||
* @Message("Params[%s] is invalid.")
|
||||
*/
|
||||
const PARAMS_INVALID = 503;
|
||||
}
|
||||
|
26
src/contract/src/FrequencyInterface.php
Normal file
26
src/contract/src/FrequencyInterface.php
Normal file
@ -0,0 +1,26 @@
|
||||
<?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 Hyperf\Contract;
|
||||
|
||||
interface FrequencyInterface
|
||||
{
|
||||
/**
|
||||
* Number of hit per time.
|
||||
*/
|
||||
public function hit(int $number = 1): bool;
|
||||
|
||||
/**
|
||||
* Hits per second.
|
||||
*/
|
||||
public function frequency(): float;
|
||||
}
|
@ -52,12 +52,12 @@ interface PaginatorInterface
|
||||
/**
|
||||
* Get the "index" of the first item being paginated.
|
||||
*/
|
||||
public function firstItem(): int;
|
||||
public function firstItem(): ?int;
|
||||
|
||||
/**
|
||||
* Get the "index" of the last item being paginated.
|
||||
*/
|
||||
public function lastItem(): int;
|
||||
public function lastItem(): ?int;
|
||||
|
||||
/**
|
||||
* Determine how many items are being shown per page.
|
||||
|
@ -108,6 +108,8 @@ class Connection extends BaseConnection implements ConnectionInterface, DbConnec
|
||||
|
||||
public function close(): bool
|
||||
{
|
||||
unset($this->connection);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
93
src/db-connection/src/Frequency.php
Normal file
93
src/db-connection/src/Frequency.php
Normal file
@ -0,0 +1,93 @@
|
||||
<?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 Hyperf\DbConnection;
|
||||
|
||||
use Hyperf\Contract\FrequencyInterface;
|
||||
use Hyperf\Pool\LowFrequencyInterface;
|
||||
|
||||
class Frequency implements FrequencyInterface, LowFrequencyInterface
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $hits = [];
|
||||
|
||||
/**
|
||||
* How much time do you want to calculate the frequency ?
|
||||
* @var int
|
||||
*/
|
||||
protected $time = 10;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $lowFrequency = 5;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $beginTime;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->beginTime = time();
|
||||
}
|
||||
|
||||
public function hit(int $number = 1): bool
|
||||
{
|
||||
$this->flush();
|
||||
|
||||
$now = time();
|
||||
$hit = $this->hits[$now] ?? 0;
|
||||
$this->hits[$now] = $number + $hit;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function frequency(): float
|
||||
{
|
||||
$this->flush();
|
||||
|
||||
$hits = 0;
|
||||
$count = 0;
|
||||
foreach ($this->hits as $hit) {
|
||||
++$count;
|
||||
$hits += $hit;
|
||||
}
|
||||
|
||||
return floatval($hits / $count);
|
||||
}
|
||||
|
||||
public function isLowFrequency(): bool
|
||||
{
|
||||
return $this->frequency() < $this->lowFrequency;
|
||||
}
|
||||
|
||||
protected function flush(): void
|
||||
{
|
||||
$now = time();
|
||||
$latest = $now - $this->time;
|
||||
foreach ($this->hits as $time => $hit) {
|
||||
if ($time < $latest) {
|
||||
unset($this->hits[$time]);
|
||||
}
|
||||
}
|
||||
|
||||
if (count($this->hits) < $this->time) {
|
||||
$beginTime = $this->beginTime < $latest ? $latest : $this->beginTime;
|
||||
for ($i = $beginTime; $i < $now; ++$i) {
|
||||
$this->hits[$i] = $this->hits[$i] ?? 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -15,6 +15,7 @@ namespace Hyperf\DbConnection\Pool;
|
||||
use Hyperf\Contract\ConfigInterface;
|
||||
use Hyperf\Contract\ConnectionInterface;
|
||||
use Hyperf\DbConnection\Connection;
|
||||
use Hyperf\DbConnection\Frequency;
|
||||
use Hyperf\Pool\Pool;
|
||||
use Hyperf\Utils\Arr;
|
||||
use Psr\Container\ContainerInterface;
|
||||
@ -39,6 +40,7 @@ class DbPool extends Pool
|
||||
$this->config = $config->get($key);
|
||||
$options = Arr::get($this->config, 'pool', []);
|
||||
|
||||
$this->frequency = make(Frequency::class);
|
||||
parent::__construct($container, $options);
|
||||
}
|
||||
|
||||
|
47
src/db-connection/tests/FrequencyTest.php
Normal file
47
src/db-connection/tests/FrequencyTest.php
Normal file
@ -0,0 +1,47 @@
|
||||
<?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 HyperfTest\DbConnection;
|
||||
|
||||
use Hyperf\DbConnection\Frequency;
|
||||
use HyperfTest\DbConnection\Stubs\FrequencyStub;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @coversNothing
|
||||
*/
|
||||
class FrequencyTest extends TestCase
|
||||
{
|
||||
public function testFreq()
|
||||
{
|
||||
$freq = new Frequency();
|
||||
$freq->hit();
|
||||
$freq->hit();
|
||||
$freq->hit();
|
||||
$freq->hit(2);
|
||||
|
||||
$this->assertSame(5.0, $freq->frequency());
|
||||
|
||||
$freq = new FrequencyStub();
|
||||
$freq->hit(3);
|
||||
sleep(1);
|
||||
$freq->hit(4);
|
||||
sleep(1);
|
||||
$freq->hit(5);
|
||||
sleep(1);
|
||||
$freq->hit(9);
|
||||
|
||||
$this->assertSame(3, count($freq->getHits()));
|
||||
$this->assertSame(6.0, $freq->frequency());
|
||||
}
|
||||
}
|
@ -18,10 +18,12 @@ use Hyperf\Contract\StdoutLoggerInterface;
|
||||
use Hyperf\Database\Connectors\ConnectionFactory;
|
||||
use Hyperf\Database\Connectors\MySqlConnector;
|
||||
use Hyperf\DbConnection\ConnectionResolver;
|
||||
use Hyperf\DbConnection\Frequency;
|
||||
use Hyperf\DbConnection\Pool\PoolFactory;
|
||||
use Hyperf\Event\EventDispatcher;
|
||||
use Hyperf\Event\ListenerProvider;
|
||||
use Hyperf\Framework\Logger\StdoutLogger;
|
||||
use Hyperf\Utils\ApplicationContext;
|
||||
use Mockery;
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
@ -34,10 +36,10 @@ class ContainerStub
|
||||
$container = Mockery::mock(ContainerInterface::class);
|
||||
|
||||
$factory = new PoolFactory($container);
|
||||
$container->shouldReceive('get')->once()->with(PoolFactory::class)->andReturn($factory);
|
||||
$container->shouldReceive('get')->with(PoolFactory::class)->andReturn($factory);
|
||||
|
||||
$resolver = new ConnectionResolver($container);
|
||||
$container->shouldReceive('get')->once()->with(ConnectionResolver::class)->andReturn($resolver);
|
||||
$container->shouldReceive('get')->with(ConnectionResolver::class)->andReturn($resolver);
|
||||
|
||||
$config = new Config([
|
||||
StdoutLoggerInterface::class => [
|
||||
@ -73,19 +75,21 @@ class ContainerStub
|
||||
],
|
||||
],
|
||||
]);
|
||||
$container->shouldReceive('get')->once()->with(ConfigInterface::class)->andReturn($config);
|
||||
$container->shouldReceive('get')->with(ConfigInterface::class)->andReturn($config);
|
||||
|
||||
$logger = new StdoutLogger($config);
|
||||
$container->shouldReceive('get')->once()->with(StdoutLoggerInterface::class)->andReturn($logger);
|
||||
$container->shouldReceive('get')->with(StdoutLoggerInterface::class)->andReturn($logger);
|
||||
|
||||
$connectionFactory = new ConnectionFactory($container);
|
||||
$container->shouldReceive('get')->once()->with(ConnectionFactory::class)->andReturn($connectionFactory);
|
||||
$container->shouldReceive('get')->with(ConnectionFactory::class)->andReturn($connectionFactory);
|
||||
|
||||
$eventDispatcher = new EventDispatcher(new ListenerProvider(), $logger);
|
||||
$container->shouldReceive('get')->once()->with(EventDispatcherInterface::class)->andReturn($eventDispatcher);
|
||||
$container->shouldReceive('get')->with(EventDispatcherInterface::class)->andReturn($eventDispatcher);
|
||||
|
||||
$container->shouldReceive('get')->once()->with('db.connector.mysql')->andReturn(new MySqlConnector());
|
||||
$container->shouldReceive('get')->with('db.connector.mysql')->andReturn(new MySqlConnector());
|
||||
$container->shouldReceive('has')->andReturn(true);
|
||||
$container->shouldReceive('make')->with(Frequency::class)->andReturn(new Frequency());
|
||||
ApplicationContext::setContainer($container);
|
||||
|
||||
return $container;
|
||||
}
|
||||
|
28
src/db-connection/tests/Stubs/FrequencyStub.php
Normal file
28
src/db-connection/tests/Stubs/FrequencyStub.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?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 HyperfTest\DbConnection\Stubs;
|
||||
|
||||
use Hyperf\DbConnection\Frequency;
|
||||
|
||||
class FrequencyStub extends Frequency
|
||||
{
|
||||
protected $time = 2;
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getHits(): array
|
||||
{
|
||||
return $this->hits;
|
||||
}
|
||||
}
|
@ -12,14 +12,12 @@ declare(strict_types=1);
|
||||
|
||||
namespace Hyperf\Di\Command;
|
||||
|
||||
use Hyperf\Command\Command;
|
||||
use Hyperf\Config\ProviderConfig;
|
||||
use Hyperf\Di\Annotation\Scanner;
|
||||
use Hyperf\Di\Container;
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Exception\LogicException;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Finder\Finder;
|
||||
use Symfony\Component\Finder\SplFileInfo;
|
||||
|
||||
@ -42,7 +40,7 @@ class InitProxyCommand extends Command
|
||||
$this->scanner = $scanner;
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
public function handle()
|
||||
{
|
||||
$scanDirs = $this->getScanDir();
|
||||
|
||||
@ -71,7 +69,7 @@ class InitProxyCommand extends Command
|
||||
}
|
||||
}
|
||||
|
||||
$output->writeln('<info>Proxy class create success.</info>');
|
||||
$this->output->writeln('<info>Proxy class create success.</info>');
|
||||
}
|
||||
|
||||
protected function clearRuntime($paths)
|
||||
|
@ -38,20 +38,18 @@ class MethodDefinitionCollector extends MetadataCollector
|
||||
case 'int':
|
||||
case 'float':
|
||||
case 'string':
|
||||
$definitions[] = [
|
||||
'type' => $type,
|
||||
'name' => $parameter->getName(),
|
||||
'ref' => '',
|
||||
'allowsNull' => $parameter->allowsNull(),
|
||||
];
|
||||
break;
|
||||
case 'array':
|
||||
case 'bool':
|
||||
$definitions[] = [
|
||||
$definition = [
|
||||
'type' => $type,
|
||||
'name' => $parameter->getName(),
|
||||
'ref' => '',
|
||||
'allowsNull' => $parameter->allowsNull(),
|
||||
];
|
||||
if ($parameter->isDefaultValueAvailable()) {
|
||||
$definition['defaultValue'] = $parameter->getDefaultValue();
|
||||
}
|
||||
$definitions[] = $definition;
|
||||
break;
|
||||
default:
|
||||
// Object
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user