hyperf| 編碼實踐一: 基礎(chǔ)篇

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ù)中使用
    }
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末灶伊,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子鲸湃,更是在濱河造成了極大的恐慌讽坏,老刑警劉巖溃列,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異懊渡,居然都是意外死亡刽射,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進店門距贷,熙熙樓的掌柜王于貴愁眉苦臉地迎上來柄冲,“玉大人,你說我怎么就攤上這事忠蝗∠趾幔” “怎么了?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵阁最,是天一觀的道長戒祠。 經(jīng)常有香客問我,道長速种,這世上最難降的妖魔是什么姜盈? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮配阵,結(jié)果婚禮上馏颂,老公的妹妹穿的比我還像新娘。我一直安慰自己棋傍,他們只是感情好救拉,可當(dāng)我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著瘫拣,像睡著了一般亿絮。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上麸拄,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天派昧,我揣著相機與錄音,去河邊找鬼拢切。 笑死蒂萎,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的淮椰。 我是一名探鬼主播岖是,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼帮毁,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了豺撑?” 一聲冷哼從身側(cè)響起烈疚,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎聪轿,沒想到半個月后爷肝,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡陆错,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年灯抛,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片音瓷。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡对嚼,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出绳慎,到底是詐尸還是另有隱情纵竖,我是刑警寧澤,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布杏愤,位于F島的核電站靡砌,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏珊楼。R本人自食惡果不足惜通殃,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望厕宗。 院中可真熱鬧画舌,春花似錦、人聲如沸已慢。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蛇受。三九已至句葵,卻和暖如春厕鹃,著一層夾襖步出監(jiān)牢的瞬間兢仰,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工剂碴, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留把将,地道東北人。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓忆矛,卻偏偏與公主長得像察蹲,于是被迫代替她去往敵國和親请垛。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,979評論 2 355

推薦閱讀更多精彩內(nèi)容