date: 2019-12-26 17:25:34
title: hyperf| 編碼實踐一: 基礎篇
整個團隊使用 hyperf 開發已經超過半年, 積累了一些最佳實踐和規約, 方便團隊后續開發, 提供給大家參考~
<?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() { // 生命周期分析法是非常常用的一種分析手法, 可以幫忙快速理解一個新系統 // 分析方式一: 流程圖, 入口->出口, 一次 http 請求經過了哪些步驟? // 分析方式二: 分層, 分解/拆分問題 // hyperf 生命周期: container application swoole http coroutine } /** * 協程 * @link https://www.jianshu.com/p/12d645ac02b2 swoole-wiki 筆記, 全面梳理 swoole 基礎知識 * @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 循環, 明確使用的協程的數量, 避免把下游服務(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: 協程更多特性 demo // 特性一定要使用其 **最佳實踐**, 不能為了使用而使用 } /** * 配置 * 配置哲學: 約定大於配置, 不必要的配置導致不必要的靈活性 * @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() { // 推薦統一使用使用 config() var_dump(config('app_name')); // 不推薦使用 Config 對象重新設置配置 // 只允許在配置文件中使用 env() } /** * 容器 container * 依賴注入 DI * @link https://doc.hyperf.io/#/zh-cn/di */ public function container() { // Config 加載后, container 會根據 config 實例化, 並處理類之間的依賴關 // container 在手, 天下我有 // 封裝了非常好用 container() 的方法, 推薦統一使用此方法 // 想要 logger /** @var Logger $logger */ $logger = container(LoggerFactory::class)->get('test'); $logger->info('test'); // 當然, 這樣常用的類已經封裝好了 // 效果和上面等同 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 事件中的循環依賴問題 */ public function listener() { // 典型場景: swoole event // \Hyperf\Server\SwooleEvent // config/autoload/server.php server中配置 swoole event 的回調 // 典型場景: application event // \Mt\Listener\BootAppConfListener::process kms 處理等 // 典型場景: db event // \Mt\Listener\DbQueryExecutedListener::process db 日志/監控都是在這里實現 // 事件機制使用場景非常多, 並且可以有效擴展系統能力, 務必熟悉並掌握 } /** * 路由 * @link https://doc.hyperf.io/#/zh-cn/router */ public function routes() { // 統一在 routes.php 中定義路由 // apidog 組件已經設置 @AutoController 注解失效 // 可以使用 route 文件, 定義更多路由 // 實現: \Mt\Util\RoutesDispatcher // 還有一種簡單的方式: 在 routes.php 中 require } /** * 中間件 * @link https://doc.hyperf.io/#/zh-cn/middleware/middleware */ public function middleware() { // 這里是狹義的中間件, 只處理 request->response // 執行順序: 洋蔥模型 全局->類級別->方法級別 // demo1: http log, 記錄 http 請求日志 // \Mt\Middleware\HttpLogMiddleware // demo2: 鑒權 // \Mt\Middleware\AuthMiddleware // demo3: sso, 單點登錄 // \Mt\Middleware\SsoMiddleware // demo4: 跨域中間件, 跨域配置也可以直接掛在 nginx 上 // CorsMiddleware } /** * 控制器 * @link https://doc.hyperf.io/#/zh-cn/controller */ public function controller() { // 最佳實踐: 封裝好 BaseController, 約定好 response 數據格式等常用功能 // \Mt\Util\AbstractController // 知識點一: swoole 會自動為每個請求分配好協程 // 知識點二: 貢獻數據要使用 **協程上下文(Context)**, 不可使用 類屬性/類常量 } /** * 請求 * @link https://doc.hyperf.io/#/zh-cn/request */ public function request() { // 統一使用 \Hyperf\HttpServer\Request 來處理請求 // 或者基於 \Hyperf\HttpServer\Request 對象進行封裝 // psr-7 標准 api // \Hyperf\HttpServer\Request 提供: 請求路徑 輸入預處理 json cookie file // 注意: header 相關方法要注意返回值, 可能嵌套一層 array } /** * 響應 * @link https://doc.hyperf.io/#/zh-cn/response */ public function response() { // 統一使用 Hyperf\HttpServer\Response 對象 // 響應格式以及自動設置 `content-type`: json xml raw view // 其他: redirect file 等 } /** * 異常處理器 * @link https://doc.hyperf.io/#/zh-cn/exception-handler */ public function exceptionHandler() { // 如果 swoole worker 進程遇到未捕獲的異常, 會導致進程退出, 所以必須要有異常處理器 // demo1: 統一處理 user exception // \Mt\Handler\HttpExceptionHandler 輸出日志並根據環境返回 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() { // 已經封裝好了, 直接使用 // 注意日志的結構: channel / level / message / context // 不同環境: dev 會直接打到 stdout, 其他環境打到日志文件->日志服務 Log::get('test')->info('test'); } /** * 命令行 * @link https://doc.hyperf.io/#/zh-cn/command */ public function command() { // 腳本有制作好的腳手架, 使用腳手架可以減少大量重復性的開發工作 // mslib/core/Util/ScriptJob/AbstractScriptScaffold.php // doc/script_scaffold.md // 常見錯誤一: 腳本中有語法錯誤, 導致 `Command "hd" is not defined.` // echo 'error 1' // 常見錯誤二: 使用 amqp producer 報錯 // 這是因為 amqp 連接池在 command 執行完后沒有比較好的方式進行關閉, 實際腳本邏輯是正常執行的 /** @var Producer $producer */ $producer = container(Producer::class); $producer->produce(new TestProducer(Carbon::now())); } /** * 單元測試 * @link https://doc.hyperf.io/#/zh-cn/testing */ public function test() { // 調試代碼 // 傳統方式: 修改 -> 重啟 server -> 瀏覽器/其他工具 測試接口 // 單元測試: 通過配合 testing,來快速調試代碼,順便完成單元測試 // 測試替身 mock // 有時候對 被測系統(SUT) 進行測試是很困難的,因為它依賴於其他無法在測試環境中使用的組件 } /** * 視圖 * @link https://doc.hyperf.io/#/zh-cn/view */ public function view() { // 不建議使用, 需要使用單獨的進程完成視圖的渲染 // 前后端分離 + 組件化平台構建 } /** * 驗證器 * @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 的場景 // 微服務中不建議使用 } /** * 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 實例上, 也分開進行配置 // 讀寫分離: 優先使用 db 層的讀寫分離, 業務中顯式使用 可寫連接/只讀連接 // 不允許使用 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 協議的 db, 需要配置為 true PDO::ATTR_EMULATE_PREPARES => false, ]; // 事務: 必須顯式指定連接 $db = Db::connection('pt_logistic'); // 自動管理事務 $db->transaction(function () { // do something }); // 手動管理事務 $db->beginTransaction(); try { // do something $db->commit(); } catch (\Throwable $ex) { $db->rollBack(); } // query builder // 所有連接從 Model 中獲取, 不允許直接使用 Db::connection() // 查詢結果, 統一轉化為數組 // 原則上不允許使用 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(); // 批量插入/更新: 傳入數組即可 Logistic::query()->insert([['id' => time()]]); Logistic::query()->update([['id' => 1]]); // 軟刪除: use SoftDeletes; // 極簡 db 組件: https://doc.hyperf.io/#/zh-cn/db/db // 性能更好, 不推薦使用在較重業務中使用 } }