hyperf
基于强大的依赖注入设计
设计理念
Hyperspeed + Flexibility = Hyperf
,从名字上我们就将 超高速
和 灵活性
作为 Hyperf 的基因。
- 对于超高速,我们基于 Swoole 协程并在框架设计上进行大量的优化以确保超高性能的输出。
- 对于灵活性,我们基于 Hyperf 强大的依赖注入组件,组件均基于 PSR 标准 的契约和由 Hyperf 定义的契约实现,达到框架内的绝大部分的组件或类都是可替换的。
基于以上的特点,Hyperf 将存在丰富的可能性,如实现 Web 服务,网关服务,分布式中间件,微服务架构,游戏服务器,物联网(IOT)等。
安装hyperf
- 下载并运行 hyperf/hyperf 镜像,并将镜像内的项目目录绑定到宿主机的/www/skeleton目录
$ docker run -v /www/skeleton:/hyperf-skeleton -p 9501:9501 -it --entrypoint /bin/sh hyperf/hyperf:latest
- 镜像容器运行后,在容器内安装 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
- 通过 Composer 安装 hyperf/hyperf-skeleton 项目
$ composer create-project hyperf/hyperf-skeleton
- 进入安装好的 Hyperf 项目目录
$ cd hyperf-skeleton
- 启动 Hyperf
$ php bin/hyperf.php start
接下来,就可以在/www/skeleton
中看到您安装好的代码了。由于 Hyperf 是持久化的 CLI 框架,当您修改完您的代码后,通过 CTRL + C
终止当前启动的进程实例,并重新执行 php bin/hyperf.php start
启动命令即可。
安装基于swoole实现的热加载组件
hyperf-watch组件
git地址:https://github.com/ha-ni-cc/hyperf-watch.git
在项目根目录下执行
$ wget -O watch https://gitee.com/hanicc/hyperf-watch/raw/master/watch
启动监听:
php watch
启动监听并删除代理类缓存(./runtime/container):
php watch -c
退出监听:
Control + C
安装Wsdebug插件
路由
普通路由
可用的路由方法
- Router::get(callback);
- Router::post(callback);
- Router::put(callback);
- Router::patch(callback);
- Router::delete(callback);
- Router::head(callback);
//注册任意HTTP METHOD 的路由
Router::addRoute(['GET', 'POST', 'HEAD'], '/', 'App\Controller\IndexController@index');
//标准路由
Router::get('/hello-hyperf', 'App\Controller\IndexController@hello');
//路由组
Router::addGroup('/user/',function (){
Router::get('index','App\Controller\UserController@index');
});
通过注解定义路由
@Controller 注解,需配合@RequestMapping
use Hyperf\HttpServer\Annotation\GetMapping;
//会默认给UserController类自动生成 /user/test 的路由
//以控制器的前缀作为前缀 UserController的前缀就是/user
//方法的path是test,所以生成/user/test
//方法的Mapping是GET,所以生成的访问方式是 GET
/**
* @Controller()
*/
class UserController{
/**
* @GetMapping(path="test")
*/
public function test(){
}
}
@Controller有个prefix参数,是可以修改前缀的
例如:
use Hyperf\HttpServer\Annotation\GetMapping;
/**
* @Controller(prefix="api/base")
*/
class UserController{
/**
* @GetMapping(path="test")
*/
public function test(){
}
}
会生成 /api/base/test
路由
注意:Mapping()的path参数,加了“/”之后是会自动忽略点prefix前缀的
使用
@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;
命名空间;
请求
/**
* @Author xue
* @DateTime 2020-08-11
* @RequestMapping(path="info",methods="get,post")
*/
public function info(RequestInterface $request)
{
var_dump($request->all());//所有参数
var_dump($request->path());//返回请求路径
var_dump($request->is('index/*'));//判断请求路径是否符合规则
var_dump($request->url());//方法返回不带有 Query 参数 的 URL
var_dump($request->fullUrl());//返回值包含 Query 参数
var_dump($request->getMethod());//返回 HTTP 的请求方法
var_dump($request->input('name.0'));//如果传输表单数据中包含「数组」形式的数据,那么可以使用「点」语法来获取数组
var_dump($request->query('age'));//query方法可以只从查询字符串中获取输入数据
var_dump($request->query());//不传递参数则以关联数组的形式返回所有 Query 参数
var_dump($request->input('message'));//如果请求的 Body 数据格式是 JSON,则只要 请求对象(Request) 的 Content-Type Header 值 正确设置为 application/json,就可以通过 input(string $key, $default = null) 方法访问 JSON 数据
var_dump($request->has('toto'));//要判断请求是否存在某个值 bool
var_dump($request->has(['name']));//可以传数组
var_dump($request->getCookieParams());//从请求中获取所有的 Cookies,结果会返回一个关联数组
var_dump($request->file('panda'));//存在则返回一个 Hyperf\HttpMessage\Upload\UploadedFile 对象,不存在则返回 null
$file = $request->file('panda');
var_dump($file->getClientFilename());//获取源文件名
var_dump($file->getExtension());//获取文件扩展
var_dump($file->getPath());//获取文件临时路径
var_dump($request->hasFile('panda'));//检查文件是否存在
var_dump($request->file('panda')->isValid());//检查文件是否有效
var_dump(BASE_PATH);//当前路径
$file->moveTo('static/image.jpg');//路径默认是容器里的项目根目录路径,不可在static前加"/"
var_dump($file->isMoved());
}
响应
public function info(ResponseInterface $response)
{
var_dump($response->json(['code'=>0,'data'=>['name','age']]));
$cookie = new Cookie('键','值');//设置cookie
return $response->withCookie($cookie)->json(['code'=>0,'data'=>['name','age']]);
}
文件下载
download(string $file, string $name = '')
- $file:要返回下载文件的绝对路径,同通过 BASE_PATH 常量来定位到项目的根目录
- $name:客户端下载文件的文件名,为空则会使用下载文件的原名
/**
* @Author xue
* @DateTime 2020-08-12
* @GetMapping(path="fileDownload")
*/
public function fileDownload(ResponseInterface $response)
{
return $response->download(BASE_PATH.'/static/image.jpg','小熊猫.jpg');
}
异常处理
定义一个异常处理器
我们可以在任意位置定义一个 类(Class)
并继承抽象类 Hyperf\ExceptionHandler\ExceptionHandler
并实现其中的抽象方法,如下:
<?php
namespace App\Exception\Handler;
use App\Exception\FooException;
use Hyperf\ExceptionHandler\ExceptionHandler;
use Hyperf\HttpMessage\Stream\SwooleStream;
use Psr\Http\Message\ResponseInterface;
use Throwable;
class TestExceptionHandler 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))->withHeader('Content-Type','application/json');//带上请求头放置乱码输出
}
// 交给下一个异常处理器
return $response;
// 或者不做处理直接屏蔽异常
}
/**
* 判断该异常处理器是否要对该异常进行处理
*/
public function isValid(Throwable $throwable): bool
{
return true;
}
}
定义异常类
<?php
namespace App\Exception;
use Hyperf\Server\Exception\ServerException;
class FooException extends ServerException
{
}
注册异常处理器
目前仅支持配置文件的形式注册 异常处理器(ExceptionHandler)
,配置文件位于 config/autoload/exceptions.php
,将您的自定义异常处理器配置在对应的 server
下即可:
<?php
declare(strict_types=1);
return [
'handler' => [
'http' => [
App\Exception\Handler\TestExceptionHandler::class
],
],
];
触发异常
在indexController中
/**
* @Author xue
* @DateTime 2020-08-12
* @GetMapping(path="exception")
*/
public function testException()
{
throw new FooException('这是一个异常',600);
}
在上面这个例子,我们先假设
FooException
是存在的一个异常,以及假设已经完成了该处理器的配置,那么当业务抛出一个没有被捕获处理的异常时,就会根据配置的顺序依次传递,整一个处理流程可以理解为一个管道,若前一个异常处理器调用$this->stopPropagation()
则不再往后传递,若最后一个配置的异常处理器仍不对该异常进行捕获处理,那么就会交由 Hyperf 的默认异常处理器处理了
集成whoops
首先安装 Whoops
composer require --dev filp/whoops
然后配置 Whoops 专用异常处理器。
// config/autoload/exceptions.php
return [
'handler' => [
'http' => [
\Hyperf\ExceptionHandler\Handler\WhoopsExceptionHandler::class,//集成whoops异常处理
],
],
];
Error监听器
框架提供了 error_reporting()
错误级别的监听器 Hyperf\ExceptionHandler\Listener\ErrorExceptionHandler
。
在 config/autoload/listeners.php
中添加监听器
<?php
return [
\Hyperf\ExceptionHandler\Listener\ErrorExceptionHandler::class
];
则当出现类似以下的代码时会抛出 \ErrorException
异常
<?php
try {
$a = [];
var_dump($a[1]);
} catch (\Throwable $throwable) {
var_dump(get_class($throwable), $throwable->getMessage());
}
// string(14) "ErrorException"
// string(19) "Undefined offset: 1"
如果不配置监听器则如下,且不会抛出异常。
PHP Notice: Undefined offset: 1 in IndexController.php on line 24
Notice: Undefined offset: 1 in IndexController.php on line 24
NULL
缓存
使用Simple Cache的方式
/**
* @Author xue
* @DateTime 2020-08-12
* @GetMapping(path="cache")
*/
public function cache()
{
$cache = $this->container->get(\Psr\SimpleCache\CacheInterface::class);
// var_dump($cache->set('name','panda'));
var_dump($cache->get('name','nothing'));
}
ps:需要注意的是需要修改redis的ip,不用localhost
宿主机需要安装redis,yum install -y redis
之后修改vim /etc/redis.conf
配置文件
bind 0.0.0.0 #将127.0.0.1改成0.0.0.0
protected-mode no #将yes 改为no
注解方式
组件提供 Hyperf\Cache\Annotation\Cacheable
注解,作用于类方法,可以配置对应的缓存前缀、失效时间、监听器和缓存组。 例如,UserService 提供一个 user 方法,可以查询对应 id 的用户信息。当加上 Hyperf\Cache\Annotation\Cacheable
注解后,会自动生成对应的 Redis 缓存,key 值为 user:id
,超时时间为 9000
秒。首次查询时,会从数据库中查,后面查询时,会从缓存中查。
/**
* @Author xue
* @DateTime 2020-08-12
* @GetMapping(path="user/{id}")
* @Cacheable(prefix="user",ttl=2000,listener="user-update")
*/
public function user(int $id)
{
$user = User::query()->where('ID',$id)->first();
return $this->response->json(['code'=>0,'data'=>$user->toArray()]);
}
日志
配置
在 hyperf-skeleton
项目内默认提供了一些日志配置,默认情况下,日志的配置文件为 config/autoload/logger.php
,示例如下:
declare(strict_types=1);
return [
'sso' => [
'handler' => [
'class' => Monolog\Handler\StreamHandler::class,
'constructor' => [
'stream' => BASE_PATH . '/runtime/logs/sso.log',
'level' => Monolog\Logger::DEBUG,
],
],
'formatter' => [
'class' => Monolog\Formatter\LineFormatter::class,
'constructor' => [
'format' => null,
'dateFormat' => 'Y-m-d H:i:s',
'allowInlineLineBreaks' => true,
],
],
],
];
//stream:是日志的路径
//handler:是处理日志的
//formatter:是设置好日志格式的
使用
/**
* @Author xue
* @DateTime 2020-08-13
* @var \Psr\Log\LoggerInterface
*/
protected $logger;//注解LoggerInterface
/**
* @Author xue
* @DateTime 2020-08-13
* @GetMapping(path="log")
*/
public function log()
{
$loggerFactory =ApplicationContext::getContainer()->get(LoggerFactory::class);
//获取日志的配置,第一个参数是日志开头的名字,第二个是日志配置里的名字
$this->logger = $loggerFactory->get('sso','sso');
try {
$a = [];
var_dump($a[1]);
} catch (\Throwable $throwable) {
//写入日志
$this->logger->info('sso日志:'.$throwable->getMessage()."\r\n行数:".$throwable->getLine());
}
}
执行以上代码之后,在runtime/logs
目录下会生成一个文件sso.log
,这个路径就是配置文件config/autoload/logger.php
里stream
的路径。文件内容:
[2020-08-13 02:28:57] sso.INFO: sso日志:Undefined offset: 1
行数:183 [] []
以上内容在日期后是以sso.INFO
开头,这里的sso
就是$loggerFactory->get('sso','sso')
的第一个参数。如果改为get('xxx','sso')
,则日志开头是xxx.INFO
分页
安装
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
的形式定义分页器实例内的所有属性,具体可查阅分页器类的内部属性。
/**
* @Author xue
* @DateTime 2020-08-13
* @GetMapping(path="paginator")
*/
public function paginator(RequestInterface $request)
{
$currentPage = (int)$request->input('page',1);
$perPage = (int)$request->input('per_page',3);
$data = collect([
['id' => 1, 'name' => 'Tom'],
['id' => 2, 'name' => 'Sam'],
['id' => 3, 'name' => 'Tim'],
['id' => 4, 'name' => 'Joe'],
['id' => 5, 'name' => 'Ada'],
['id' => 6, 'name' => 'LiNa']
]);
$users = array_values($data->forPage($currentPage, $perPage)->toArray());
return new Paginator($users, $perPage, $currentPage);
}
==踩坑==
如果在运行热加载的时候,关闭了当前的控制台。难么你将无法重新在执行php watch -c
命令,会提示
WARNING swSocket_bind(:483): bind(0.0.0.0:9501) failed, Error: Address in use
这时候只需要在容器中执行:
$ netstat -ntlp
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 0.0.0.0:9501 0.0.0.0:* LISTEN 19047/skeleton.Mast
查出当前进程,找到hyperf的主进程。然后杀掉即可
$ kill -9 19047