Hyperf学习

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(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(['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.phpstream的路径。文件内容:

[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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,108评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,699评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,812评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,236评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,583评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,739评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,957评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,704评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,447评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,643评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,133评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,486评论 3 256
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,151评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,889评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,782评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,681评论 2 272