date: 2019-12-26 17:25:34
title: hyperf| 編碼實踐一: 基礎(chǔ)篇
整個團隊使用 hyperf 開發(fā)已經(jīng)超過半年, 積累了一些最佳實踐和規(guī)約, 方便團隊后續(xù)開發(fā), 提供給大家參考~
<?php
declare(strict_types=1);
namespace App\Command;
use App\Amqp\Producer\TestProducer;
use App\Model\Logistic;
use Carbon\Carbon;
use Hyperf\Amqp\Producer;
use Hyperf\Command\Command as HyperfCommand;
use Hyperf\Command\Annotation\Command;
use Hyperf\DbConnection\Db;
use Hyperf\Logger\Logger;
use Hyperf\Logger\LoggerFactory;
use Mt\Util\Log;
use PDO;
/**
* @Command
*/
class HyperfDemoCommand extends HyperfCommand
{
protected $name = 'hd';
public function configure()
{
parent::configure();
$this->setDescription('Hyperf Demo Command');
}
public function handle()
{
$this->line('Hello Hyperf!', 'info');
}
/**
* 生命周期
* @link https://doc.hyperf.io/#/zh-cn/lifecycle
*/
public function lifecycle()
{
// 生命周期分析法是非常常用的一種分析手法, 可以幫忙快速理解一個新系統(tǒng)
// 分析方式一: 流程圖, 入口->出口, 一次 http 請求經(jīng)過了哪些步驟?
// 分析方式二: 分層, 分解/拆分問題
// hyperf 生命周期: container application swoole http coroutine
}
/**
* 協(xié)程
* @link http://www.reibang.com/p/12d645ac02b2 swoole-wiki 筆記, 全面梳理 swoole 基礎(chǔ)知識
* @link https://doc.hyperf.io/#/zh-cn/coroutine
*/
public function coroutine()
{
// demo: basic
go(function () {
sleep(1);
echo 'go' . PHP_EOL;
});
echo 'main' . PHP_EOL;
// demo: 性能測試
// 使用命令行 time 命令查看耗時, 可以看到 sys/user 分別耗時
// 推薦使用 for 循環(huán), 明確使用的協(xié)程的數(shù)量, 避免把下游服務(wù)(db/第三方接口)打爆
for ($i = 0; $i < 10; $i++) {
go(function () use ($i) {
// do some **io task**
sleep(1);
echo "go $i \n";
});
}
// swoole runtime
// 暫時不要使用 SWOOLE_HOOK_CURL, curl api 目前只兼容了大部分常用的
!defined('SWOOLE_HOOK_FLAGS') && define('SWOOLE_HOOK_FLAGS', SWOOLE_HOOK_ALL | SWOOLE_HOOK_CURL);
// todo: 協(xié)程更多特性 demo
// 特性一定要使用其 **最佳實踐**, 不能為了使用而使用
}
/**
* 配置
* 配置哲學(xué): 約定大于配置, 不必要的配置導(dǎo)致不必要的靈活性
* @link http://deploy-dev.mengtuiapp.com/doc#/ms?id=config-%e6%a8%a1%e5%9d%97%e8%af%a6%e8%a7%a3
* @link https://doc.hyperf.io/#/zh-cn/config
*/
public function config()
{
// 推薦統(tǒng)一使用使用 config()
var_dump(config('app_name'));
// 不推薦使用 Config 對象重新設(shè)置配置
// 只允許在配置文件中使用 env()
}
/**
* 容器 container
* 依賴注入 DI
* @link https://doc.hyperf.io/#/zh-cn/di
*/
public function container()
{
// Config 加載后, container 會根據(jù) config 實例化, 并處理類之間的依賴關(guān)
// container 在手, 天下我有
// 封裝了非常好用 container() 的方法, 推薦統(tǒng)一使用此方法
// 想要 logger
/** @var Logger $logger */
$logger = container(LoggerFactory::class)->get('test');
$logger->info('test');
// 當(dāng)然, 這樣常用的類已經(jīng)封裝好了
// 效果和上面等同
Log::get('test')->info('test2');
// 一個常犯的錯誤: new + @Inject
// new 出來的類里面使用了注解, 那么這個類必須交給 container 管理, 注解才能生效
}
/**
* 事件機制
* @link https://doc.hyperf.io/#/zh-cn/event
* @link https://doc.hyperf.io/#/zh-cn/event?id=%e6%b3%a8%e6%84%8f%e4%ba%8b%e9%a1%b9 事件中的循環(huán)依賴問題
*/
public function listener()
{
// 典型場景: swoole event
// \Hyperf\Server\SwooleEvent
// config/autoload/server.php server中配置 swoole event 的回調(diào)
// 典型場景: application event
// \Mt\Listener\BootAppConfListener::process kms 處理等
// 典型場景: db event
// \Mt\Listener\DbQueryExecutedListener::process db 日志/監(jiān)控都是在這里實現(xiàn)
// 事件機制使用場景非常多, 并且可以有效擴展系統(tǒng)能力, 務(wù)必熟悉并掌握
}
/**
* 路由
* @link https://doc.hyperf.io/#/zh-cn/router
*/
public function routes()
{
// 統(tǒng)一在 routes.php 中定義路由
// apidog 組件已經(jīng)設(shè)置 @AutoController 注解失效
// 可以使用 route 文件, 定義更多路由
// 實現(xiàn): \Mt\Util\RoutesDispatcher
// 還有一種簡單的方式: 在 routes.php 中 require
}
/**
* 中間件
* @link https://doc.hyperf.io/#/zh-cn/middleware/middleware
*/
public function middleware()
{
// 這里是狹義的中間件, 只處理 request->response
// 執(zhí)行順序: 洋蔥模型 全局->類級別->方法級別
// demo1: http log, 記錄 http 請求日志
// \Mt\Middleware\HttpLogMiddleware
// demo2: 鑒權(quán)
// \Mt\Middleware\AuthMiddleware
// demo3: sso, 單點登錄
// \Mt\Middleware\SsoMiddleware
// demo4: 跨域中間件, 跨域配置也可以直接掛在 nginx 上
// CorsMiddleware
}
/**
* 控制器
* @link https://doc.hyperf.io/#/zh-cn/controller
*/
public function controller()
{
// 最佳實踐: 封裝好 BaseController, 約定好 response 數(shù)據(jù)格式等常用功能
// \Mt\Util\AbstractController
// 知識點一: swoole 會自動為每個請求分配好協(xié)程
// 知識點二: 貢獻數(shù)據(jù)要使用 **協(xié)程上下文(Context)**, 不可使用 類屬性/類常量
}
/**
* 請求
* @link https://doc.hyperf.io/#/zh-cn/request
*/
public function request()
{
// 統(tǒng)一使用 \Hyperf\HttpServer\Request 來處理請求
// 或者基于 \Hyperf\HttpServer\Request 對象進行封裝
// psr-7 標(biāo)準(zhǔn) api
// \Hyperf\HttpServer\Request 提供: 請求路徑 輸入預(yù)處理 json cookie file
// 注意: header 相關(guān)方法要注意返回值, 可能嵌套一層 array
}
/**
* 響應(yīng)
* @link https://doc.hyperf.io/#/zh-cn/response
*/
public function response()
{
// 統(tǒng)一使用 Hyperf\HttpServer\Response 對象
// 響應(yīng)格式以及自動設(shè)置 `content-type`: json xml raw view
// 其他: redirect file 等
}
/**
* 異常處理器
* @link https://doc.hyperf.io/#/zh-cn/exception-handler
*/
public function exceptionHandler()
{
// 如果 swoole worker 進程遇到未捕獲的異常, 會導(dǎo)致進程退出, 所以必須要有異常處理器
// demo1: 統(tǒng)一處理 user exception
// \Mt\Handler\HttpExceptionHandler 輸出日志并根據(jù)環(huán)境返回 response
// demo2: error_reporting() 錯誤
// Hyperf\ExceptionHandler\Listener\ErrorExceptionHandler
// 還有一種方式: php bin/hyperf.php > runtime/run.log 中
}
/**
* 日志
* @link https://doc.hyperf.io/#/zh-cn/logger
*/
public function log()
{
// 已經(jīng)封裝好了, 直接使用
// 注意日志的結(jié)構(gòu): channel / level / message / context
// 不同環(huán)境: dev 會直接打到 stdout, 其他環(huán)境打到日志文件->日志服務(wù)
Log::get('test')->info('test');
}
/**
* 命令行
* @link https://doc.hyperf.io/#/zh-cn/command
*/
public function command()
{
// 腳本有制作好的腳手架, 使用腳手架可以減少大量重復(fù)性的開發(fā)工作
// mslib/core/Util/ScriptJob/AbstractScriptScaffold.php
// doc/script_scaffold.md
// 常見錯誤一: 腳本中有語法錯誤, 導(dǎo)致 `Command "hd" is not defined.`
// echo 'error 1'
// 常見錯誤二: 使用 amqp producer 報錯
// 這是因為 amqp 連接池在 command 執(zhí)行完后沒有比較好的方式進行關(guān)閉, 實際腳本邏輯是正常執(zhí)行的
/** @var Producer $producer */
$producer = container(Producer::class);
$producer->produce(new TestProducer(Carbon::now()));
}
/**
* 單元測試
* @link https://doc.hyperf.io/#/zh-cn/testing
*/
public function test()
{
// 調(diào)試代碼
// 傳統(tǒng)方式: 修改 -> 重啟 server -> 瀏覽器/其他工具 測試接口
// 單元測試: 通過配合 testing普办,來快速調(diào)試代碼稻励,順便完成單元測試
// 測試替身 mock
// 有時候?qū)?被測系統(tǒng)(SUT) 進行測試是很困難的糊昙,因為它依賴于其他無法在測試環(huán)境中使用的組件
}
/**
* 視圖
* @link https://doc.hyperf.io/#/zh-cn/view
*/
public function view()
{
// 不建議使用, 需要使用單獨的進程完成視圖的渲染
// 前后端分離 + 組件化平臺構(gòu)建
}
/**
* 驗證器
* @link https://doc.hyperf.io/#/zh-cn/validation
*/
public function validation()
{
// 推薦使用, 可以將請求校驗從 Controller 邏輯中解耦出來
}
/**
* session
* @link https://doc.hyperf.io/#/zh-cn/session
*/
public function session()
{
// 兼容必須使用 session 的場景
// 微服務(wù)中不建議使用
}
/**
* db
* @link https://doc.hyperf.io/#/zh-cn/db/quick-start
* @link https://doc.hyperf.io/#/zh-cn/db/querybuilder
* @link https://doc.hyperf.io/#/zh-cn/db/model
*/
public function db()
{
// 配置最佳實踐: 已 db 為粒度, 哪怕在同一個 db 實例上, 也分開進行配置
// 讀寫分離: 優(yōu)先使用 db 層的讀寫分離, 業(yè)務(wù)中顯式使用 可寫連接/只讀連接
// 不允許使用 default, 顯式命名并使用
// 連接池配置
$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 配置
$option = [
// 框架默認配置
PDO::ATTR_CASE => PDO::CASE_NATURAL,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_ORACLE_NULLS => PDO::NULL_NATURAL,
PDO::ATTR_STRINGIFY_FETCHES => false,
// 不支持 MySQL prepare 協(xié)議的 db, 需要配置為 true
PDO::ATTR_EMULATE_PREPARES => false,
];
// 事務(wù): 必須顯式指定連接
$db = Db::connection('pt_logistic');
// 自動管理事務(wù)
$db->transaction(function () {
// do something
});
// 手動管理事務(wù)
$db->beginTransaction();
try {
// do something
$db->commit();
} catch (\Throwable $ex) {
$db->rollBack();
}
// query builder
// 所有連接從 Model 中獲取, 不允許直接使用 Db::connection()
// 查詢結(jié)果, 統(tǒng)一轉(zhuǎn)化為數(shù)組
// 原則上不允許使用 join
// 返回一行
Logistic::query()->first();
// 返回單個值
Logistic::query()->value('id');
// 返回一列值
Logistic::query()->pluck('name', 'id');
// 返回多行
Logistic::query()->limit(10)->get();
// 悲觀鎖
Logistic::query()->sharedLock()->limit(10)->get();
Logistic::query()->lockForUpdate()->limit(10)->get();
// 批量插入/更新: 傳入數(shù)組即可
Logistic::query()->insert([['id' => time()]]);
Logistic::query()->update([['id' => 1]]);
// 軟刪除: use SoftDeletes;
// 極簡 db 組件: https://doc.hyperf.io/#/zh-cn/db/db
// 性能更好, 不推薦使用在較重業(yè)務(wù)中使用
}
}