php yii框架源码,yii 源码解读
date: 2017-11-21 18:15:18
title: yii 源碼解讀
本篇博客閱讀指南:
php & 代碼提示: 工欲善其事必先利其器
yii 源碼閱讀指南: 整體上全貌上進(jìn)行了解
之后的章節(jié): 細(xì)節(jié)入手, 沒(méi)錯(cuò), 都是知識(shí)點(diǎn)
寫(xiě)完上篇 yii 框架簡(jiǎn)析 后, 發(fā)現(xiàn)干貨有點(diǎn)少, 寫(xiě)來(lái)寫(xiě)去還是 底層是服務(wù)容器 這樣的老生常談. 雖然這個(gè)真的很重要, 我認(rèn)為理解服務(wù)容器的 php 程序員, 算是 境界提升(至少不用自嘲「碼畜」了吧). 這篇就實(shí)實(shí)在在的閱讀 yii 框架的源碼, 希望可以給大家?guī)?lái)更多干貨.
備注: 因?yàn)?yii 有一個(gè)慣用套路, 框架層實(shí)現(xiàn)使用 BaseXXX, 具體使用需要用 Xxx 來(lái)繼承而不是直接使用 BaseXxx 類, 而最底層的基類 BaseObject 使用這種方式后的類 Object, 在 php7.2 中被添加為關(guān)鍵字.
請(qǐng)使用 phpstorm
詳細(xì)介紹一個(gè) IDE 怎么用不太現(xiàn)實(shí), 各種黑科技還是自己體會(huì), 我比較喜歡憑自己落筆時(shí)的印象來(lái)判斷 -- 第一時(shí)間想到的, 往往是最熟悉的.
錯(cuò)誤提示: 單詞拼寫(xiě)錯(cuò)誤, 低級(jí)語(yǔ)法錯(cuò)誤, 這些開(kāi)發(fā)過(guò)程中最常見(jiàn)的問(wèn)題
代碼提示: 函數(shù)以及函數(shù)的參數(shù)和返回; 類以及類的屬性和方法; 等等等等
跳轉(zhuǎn): 方便的跳轉(zhuǎn)對(duì)閱讀代碼有多重要就不多說(shuō)了, 而且可以跳轉(zhuǎn) php 內(nèi)部函數(shù)和類, 明顯減少 php manual 的使用
還有其他很多高級(jí)功能, 比如 重構(gòu)/db連接/版本控制, 這些都不是重點(diǎn), 或者說(shuō)錦上添花, 用 phpstorm 的理由非常簡(jiǎn)單:
明顯提高開(kāi)發(fā)效率. 崇尚極簡(jiǎn)也同樣適用, 只關(guān)心編輯功能也會(huì)發(fā)現(xiàn)效率提升.
友情提示: 開(kāi)發(fā)機(jī)請(qǐng)使用 16G 內(nèi)存. 更多使用小技巧可以參考我的 wiki - tools - ide
代碼提示
phpstorm 之所以會(huì)讓人感覺(jué)很 智能, 很多地方都來(lái)自于完善的 代碼提示. 當(dāng)然現(xiàn)實(shí)是很多人寫(xiě)代碼, 不寫(xiě)注釋.
我 TM 代碼都寫(xiě)不完, 你還要我寫(xiě)注釋?!
不就這個(gè)話題展開(kāi), 但是可以給一個(gè)關(guān)于開(kāi)源代碼選用的標(biāo)準(zhǔn), 如果你打算使用的開(kāi)源代碼注釋和文檔不完善, 建議你最好不要選用. 否則, 一定要確認(rèn)可以對(duì)接的人(同樣適用于接手維護(hù)舊代碼).
這里八卦一下, 之前一直有人 罵 swoole 文檔爛, 到處是 坑. 我這里說(shuō)句公道話, swoole 的 wiki 里寫(xiě)到有 2 個(gè)開(kāi)源項(xiàng)目提供代碼提示(關(guān)于代碼提示, 可以參考之前的 blog - 聊一聊 php 代碼提示), 一種使用 php Reflection(反射) 實(shí)現(xiàn), 一種是提取代碼注釋然后手動(dòng)完善. 并且 swoole 的 wiki 1400+ 頁(yè), 下面的評(píng)論往往也是干貨滿滿. 在你罵別人文檔爛, 坑多的時(shí)候, 你憑什么這樣說(shuō)?
能力足夠, 可以參加核心開(kāi)發(fā)組; 文檔不夠完善, 但是它可直接編輯; 使用發(fā)現(xiàn)代碼提示不夠好, 代碼提示的開(kāi)源項(xiàng)目可以參與. 最后是坑多的說(shuō)法, 有沒(méi)有想過(guò)更多是經(jīng)驗(yàn)不夠, 而不是工具不好用.
發(fā)這個(gè)牢騷不是想 探究人性 之類的, 只是面對(duì)有些現(xiàn)實(shí), 其實(shí)明明可以往 好一點(diǎn) 的方向前進(jìn)一小步. 當(dāng)然, 我可不敢公然和 噴子 叫板.
得益于 php 語(yǔ)言的簡(jiǎn)單, 代碼提示在這里也非常簡(jiǎn)單, 而且 yii 框架的代碼提示做得非常好, 幾乎任何輸入的地方, 都會(huì)有 IDE 的自動(dòng)提示.
注釋的語(yǔ)法很簡(jiǎn)單: 指令(@開(kāi)頭) + 指令內(nèi)容. 基本都是只要看到就能理解什么意思:
// 描述函數(shù)參數(shù), 格式: @param type var define
@param string $name the property name
// 描述函數(shù)返回值, 格式和上面類似: @return type define
@return mixed the property value or the value of a behavior's property
// 變量提示, 格式也類似: @var type define
@var array the attached event handlers (event name => handlers)
當(dāng)然還有一些其他提示, 各有作用, 使用較少, 就不一一列舉了.
備注: 使用 swoole 的過(guò)程中也被回調(diào)函數(shù)難以代碼提示煩惱過(guò), 所以參與了 swoole-ide-helper 項(xiàng)目, 提交 pr, 來(lái)一起改善 swoole 的編程體驗(yàn).
道理很多依舊過(guò)不好人生什么的, 原因可能并沒(méi)有那么復(fù)雜, 真的只是因?yàn)樘珣辛艘稽c(diǎn).
yii 源碼閱讀指南
這里有一份 yii 源碼閱讀過(guò)程中制作的 百度腦圖 - yii 源碼解析, 方便查看類的依賴關(guān)系, yii 源碼的層次結(jié)構(gòu). 有種說(shuō)法是一圖勝千言, 希望能起到這樣的效果.
yii 框架的源碼很簡(jiǎn)單, 層次很清晰:
yii\base\BaseObject: 基類, 幾乎所有類都繼承自這個(gè)類, 使用 __get()/__set() 等魔術(shù)方法, 方便操作類屬性等
class BaseObject implements Configurable
{
public static function className()
{
return get_called_class(); // 等同于 static::CLASS, 區(qū)別與 get_class()
}
public function __get($name){};
public function __set($name, $value)
}
這里實(shí)現(xiàn)了 Configurable 接口, 給框架了帶來(lái)了 基于配置 的超強(qiáng)靈活性, 后面會(huì)有具體代碼講到
yii\base\Component: 組件, 繼承自 BaseObject, yii 框架提供的所有功能, 幾乎都是 component, 這樣就可以 \Yii:$db 這樣的形式來(lái)調(diào)用
class Component extends BaseObject
{
private $_events = [];
private $_behaviors;
}
Component 擴(kuò)展了 BaseObject, 并為所有組件定義了特性: property, event and behavior
\yii\di\Container: 容器, 這個(gè)概念就不再啰嗦
\BaseYii: yii 框架主體, 還定義了部分框架運(yùn)行時(shí)的輔助功能, log, profile 等
\Yii: 實(shí)例化 BaseYii, 這種方式 yii 中隨處可見(jiàn), Base 定義基礎(chǔ)功能, 具體使用時(shí)繼承基類并自己按需擴(kuò)展. \Yii 會(huì)同時(shí)啟動(dòng)一個(gè) Container.
class Yii extends \yii\BaseYii
{
}
spl_autoload_register(['Yii', 'autoload'], true, true);
Yii::$classMap = require __DIR__ . '/classes.php';
Yii::$container = new yii\di\Container();
這里使用 classMap 的方式來(lái)注冊(cè)框架核心類, 性能會(huì)比 composer 的 psr-4 稍高, 但是也導(dǎo)致了你有 2 種方式來(lái)管理依賴, 這點(diǎn)我是持 消極 態(tài)度的.
web\index.php: 入口腳本, 加載配置和 Yii, 實(shí)例化 application, 來(lái)完成請(qǐng)求
defined('YII_DEBUG') or define('YII_DEBUG', true);
defined('YII_ENV') or define('YII_ENV', 'dev');
require(__DIR__ . '/../vendor/autoload.php');
require(__DIR__ . '/../../vendor/yiisoft/yii2/Yii.php');
$config = require(__DIR__ . '/../config/web.php');
(new yii\web\Application($config))->run();
得益于 BaseObject 和 Component, 幾乎所有特性, 都可以通過(guò)這里的 $config 進(jìn)行配置.
之后的功能, 就是一個(gè)一個(gè) Component 了, 這里不贅述了, 通過(guò) 百度腦圖 - yii 源碼解析 非常方便的查看. 以前 model module 傻傻分不清楚的情況, 是不是迎刃而解了?
關(guān)于鏈?zhǔn)秸{(diào)用
這次閱讀源碼的過(guò)程中, 在使用 yii\widgets\DetailView 卡了一小會(huì), 被自己之前關(guān)于鏈?zhǔn)秸{(diào)用的理解給繞進(jìn)去了. 首先看第一種方式 $this:
class a {
public $b = 0;
function aa() {
$this->b += 1;
return $this;
}
function bb() {
$this->b += 2;
return $this;
}
}
$a = new a();
$a->aa()->bb()->aa();
echo $a->b;
通過(guò)在類方法中返回 $this, 從而實(shí)現(xiàn)鏈?zhǔn)秸{(diào)用, 這樣的寫(xiě)法, 可以參考 yii\db\Query 的源碼, 使用鏈?zhǔn)秸{(diào)用來(lái)構(gòu)建 sql 語(yǔ)句.
因?yàn)閷?duì) $this 這種方式 印象太深, 導(dǎo)致忽略了下面這種更常見(jiàn)的方式:
class A {
public function b()
{
$b = new b();
return $b; // 返回 b 對(duì)象
}
}
class B {
public function c() {
echo 'czl';
}
}
$a = new A();
$a->b()->c();
使用 其他對(duì)象作為自己的屬性或者函數(shù)返回值, 這是更常見(jiàn)的鏈?zhǔn)秸{(diào)用, 而在 yii 中, 這種方式更是隨處可見(jiàn), 這里用 \yii\widgets\DetailView 中使用 \yii\i18n\Formatter 來(lái)展示一下 基于配置 的超強(qiáng)靈活性:
DetailView::widget([
'model' => $model, // 和 Model 類無(wú)縫配合
'attributes' => [
'id',
'title',
'content:ntext',
'tags:ntext',
'create_time:datetime',
'update_time:datetime',
[
'attribute' => 'author_id',
'value' => $model->author->nickname,
],
],
'template' => '
{label}{value}','formatter' => [
'class' => \yii\i18n\Formatter::class,
'datetimeFormat' => 'short',
]
]);
查看 api 文檔, 會(huì)發(fā)現(xiàn)這里的 attribute 非常的強(qiáng)大:
這里的 attribute 屬性, 可以和 Model 中的 attribute 屬性一一對(duì)應(yīng)
這里的 attribute 屬性, 可以使用 attribute:format:label 格式, 其中的 format 就是對(duì)應(yīng)的
\yii\i18n\Formatter, 大部分常用的格式化方法, 這里都有定義, 比如這里的 create_time:datetime 表示使用 \yii\i18n\Formatter 中的 asDatetime() 進(jìn)行格式化
你以為到這里就結(jié)束了么:
template: 直接可以配置頁(yè)面的 html
formatter: 不止可以用 \yii\i18n\Formatter, 還可以配置
還沒(méi)完, 我們?cè)谌忠彩强梢耘渲玫?config/web.php:
$config = [
'id' => 'myYii',
...
'components' => [
'formatter' => [
'datetimeFormat' => 'Y-m-d H:i:s',
]
],
];
當(dāng)然, 全局的配置, 會(huì)被這里具體使用的地方給覆蓋掉.
另外還有 \yii\widgets\ActiveForm 和 \yii\widgets\ActiveField 的源碼, 也是這樣的方式提供超強(qiáng)的靈活性
非常推薦大家閱讀一下這塊的代碼, 嘗試動(dòng)手改改, 只要這里理解清楚了, 對(duì)框架的整體理解基本沒(méi)問(wèn)題了.
PS: 我之前表達(dá)過(guò)觀點(diǎn), 前后端分離是大勢(shì), phper 應(yīng)該更關(guān)注 后端, 關(guān)注寫(xiě)出更好的 api. 但是 yii 這種前后端無(wú)縫對(duì)接高可配置的方式, 還是把我驚艷到了. 但是我的觀點(diǎn)還是沒(méi)有變, phper 還是應(yīng)該更關(guān)注后端, 我傾向于把 yii 應(yīng)用到不需要 設(shè)計(jì) 的場(chǎng)合, 比如管理后臺(tái).
關(guān)于 db
很多初級(jí) phper 會(huì)感覺(jué) db 這塊的內(nèi)容 很多, 一方面是數(shù)據(jù)庫(kù)相關(guān)的基礎(chǔ)知識(shí)就很多(基礎(chǔ)的增刪改查并不是難度好不好), 然后 php 和數(shù)據(jù)庫(kù)聯(lián)動(dòng)的過(guò)程, 又增加了一層抽象. 我之前的 blog - hyperframework WebClient 源碼解讀 也提過(guò)這樣一個(gè)觀點(diǎn):
層出不窮的工具, 目的就是對(duì)現(xiàn)有問(wèn)題作出更 易用 的抽象. 但是伴隨抽象的不斷增多, 基礎(chǔ)部分的更加不可見(jiàn), 導(dǎo)致越來(lái)越容易 摸不著頭腦. 所以我希望我寫(xiě)的東西, 能在一開(kāi)始就給大家劃定出一個(gè)核心的范圍, 而不是又一個(gè)工具的堆砌.
先來(lái)聊 db 的第一個(gè)話題, php 使用 db 的三種方式.
3 種 db 訪問(wèn)方式
數(shù)據(jù)庫(kù)作為一個(gè)服務(wù), 其實(shí) php 是作為 client 端來(lái)訪問(wèn). 數(shù)據(jù)庫(kù)的架構(gòu)通常是分層結(jié)構(gòu), 最外層的和我們平時(shí)寫(xiě)的 接口 網(wǎng)關(guān) 其實(shí)是一樣的 -- 通過(guò)暴露 api 來(lái)提供服務(wù). 只是我們最終提供的 web 服務(wù), 走的是 http 協(xié)議, 而數(shù)據(jù)庫(kù)走的數(shù)據(jù)庫(kù)的協(xié)議, 比如和 mysql 通信需要實(shí)現(xiàn) mysql 協(xié)議. 嗯, 這個(gè)比較底層了, 協(xié)議的細(xì)節(jié)被抽象掉了, 最終暴露給我們的, 其實(shí)就是 sql.
這就是我劃定的核心范圍, 說(shuō)是 3 種方式, 本質(zhì)還是執(zhí)行 sql 語(yǔ)句而已.
// 直接執(zhí)行 sql 語(yǔ)句
$postStatus = \Yii::$app->db->createCommand('SELECT id,`name` FROM poststatus')->queryAll();
$postStatus = array_column($postStatus, 'name', 'id');
// 使用查詢構(gòu)造器
$postStatus = (new \yii\db\Query())
->select(['name', 'id'])
->from('poststatus')
->indexBy('id')
->column();
// 使用 ActiveRecord
$postStatus = \app\models\Poststatus::find()
->select(['name', 'id'])
->indexBy('id')
->column();
三種方式的關(guān)系也很簡(jiǎn)單:
ActiveRecord 調(diào)用 find() 后, @return ActiveQuery the newly created [[ActiveQuery]] instance, 其實(shí)就是返回一個(gè)拼上表名的 ActiveQuery 實(shí)例
ActiveQuery 通過(guò)鏈?zhǔn)秸{(diào)用, 拼接出一個(gè)完整的 sql
最終和 \Yii::$app->db->createCommand() 執(zhí)行沒(méi)啥區(qū)別, 只是 ActiveQuery 又提供了一些方法, 對(duì)查詢到的結(jié)果集做一些處理
這也是目前大部分框架采用的方式 -- 提供三種方式給大家使用. 這里還是發(fā)表一下我個(gè)人的觀點(diǎn), 我們的 hyperframework 中是不提供 ActiveQuery 這樣的實(shí)現(xiàn)的, 因?yàn)槲覀兿嘈? 大部分情況下, sql 是更好的選擇:
實(shí)現(xiàn)一個(gè) ActiveQuery 類并不難, 用起來(lái)也不難, 但是 sql 是必須要掌握的, 掌握了 sql 之后其實(shí)就可以用第一種方法解決問(wèn)題了
ActiveQuery 在復(fù)雜 sql 下面非常難寫(xiě), 甚至不能 -- 來(lái)自游戲數(shù)據(jù)統(tǒng)計(jì)的血淚史
當(dāng)然, ActiveQuery 也有優(yōu)點(diǎn)和合適的場(chǎng)景, 比如代碼提示和條件查詢:
$query = $db->select('xxx');
if (!empty($search['a'])) {
$query = $query->where('a', $search['a']);
}
關(guān)聯(lián)查詢
上一節(jié)只是 淺嘗輒止 的提到 ActiveRecord, 這里詳細(xì)講講, 然后再深入一點(diǎn). 先提個(gè)醒: 設(shè)計(jì)出 ActiveRecord 這樣的抽象, 真的非常厲害.
ActiveRecord, 中文翻譯為活動(dòng)記錄, 對(duì)應(yīng)于 MVC 中的 Model 這一層, 但是它是和數(shù)據(jù)庫(kù)結(jié)合最緊密的地方. 一個(gè) ActiveRecord 類, 用來(lái)對(duì)應(yīng)數(shù)據(jù)庫(kù)里的一張表, 一個(gè) ActiveRecord 實(shí)例化對(duì)象, 用來(lái)對(duì)應(yīng)這張表里面的一條記錄, 進(jìn)而通過(guò)對(duì)象的 新建/屬性修改/方法調(diào)用, 實(shí)現(xiàn)數(shù)據(jù)庫(kù)的增刪改查.
// 增
$post = new Post();
$post->title = 'daydaygo';
$post->save();
// 查
$post = Post::find(1);
// 刪
$post->delete();
// 改
$post->title = 'czl';
$post->save();
你看這樣的代碼, 是不是感受不到 sql 的存在, 但是你卻輕松實(shí)現(xiàn)了需要的功能. 這就是我認(rèn)為 厲害的地方.
再來(lái)看更厲害的 -- 關(guān)聯(lián)查詢:
// Post 中定義和 author 的關(guān)聯(lián)
public function getAuthor()
{
return $this->hasOne(Adminuser::className(), ['id' => 'author_id']);
}
// 這樣訪問(wèn) author 就簡(jiǎn)單了
$post->author;
這里先解釋一下, $post->author 會(huì)去尋找 Post 中的 getAuthor() 方法, 然后根據(jù)這里定義的關(guān)聯(lián)關(guān)系, 執(zhí)行查詢, 并將查詢到 author 記錄, 賦值給 $post->author 屬性. 這里有 2 個(gè)細(xì)節(jié):
author -> getAuthor() 其實(shí)是通過(guò) yii\db\BaseActiveRecord 中的 __get() 魔術(shù)方法實(shí)現(xiàn)的, 這也是 yii 核心的設(shè)計(jì)理念之一, 通過(guò)實(shí)現(xiàn) __get() 等魔術(shù)方法, 讓 類 更好用
Post 的注釋中有這樣一句 @property Adminuser $author, 這樣使用 $post->author 就有酸爽的代碼提示了
關(guān)于關(guān)聯(lián)查詢, 這里還有 2 個(gè)細(xì)節(jié):
查詢緩存, 這也是 yii 為什么性能這么高的原因. 一點(diǎn)題外話, 在看源碼的過(guò)程中, 有函數(shù)被標(biāo)記不推薦使用, 點(diǎn)進(jìn)入發(fā)現(xiàn)是使用緩存的姿勢(shì)不夠優(yōu)雅, 強(qiáng)耦合
// 關(guān)聯(lián)查詢
$user = User::findOne();
$orders = $user->orders; // 執(zhí)行關(guān)聯(lián)查詢, 結(jié)果被緩存
unset($user->orders); // 清楚緩存, 重新查詢
$orders2 = $user->orders;
多對(duì)多的查詢, 需要注意查詢上的優(yōu)化:
// 多次查詢
$users = User::find()->all(); // 查詢 user
foreach($users as $user){
$oders = $user->orders; // 查詢 order
}
$users = User::find()->with('orders')->all(); // 2次查詢, 一次 user, 一次 order
foreach($users as $user){
$oders = $user->orders; // 此處不會(huì)執(zhí)行數(shù)據(jù)庫(kù)查詢
}
關(guān)于鎖
基礎(chǔ)稍差的話, 可能對(duì)鎖的概念會(huì)有些陌生. 簡(jiǎn)單的解釋是: 在多進(jìn)程或者多線程編程的情況下, 同時(shí)訪問(wèn)同一個(gè)資源導(dǎo)致程序的最終結(jié)果不可控.
首選需要區(qū)分 2 個(gè)概念: 并發(fā) vs 并行
并發(fā) Concurrent: 多線程多進(jìn)程場(chǎng)景下, 微觀上 cpu 進(jìn)行調(diào)度切換, 快到人類無(wú)法直觀感知極限(0.1s), 所以宏觀上看起來(lái)是 同時(shí) 運(yùn)行
并行 parallel: 真正的 同時(shí) 運(yùn)行, 必須要都多 cpu 支持
再來(lái)一個(gè)概念: 競(jìng)態(tài)資源
在某個(gè)資源上產(chǎn)生了并發(fā)訪問(wèn), 導(dǎo)致程序執(zhí)行后沒(méi)有達(dá)到預(yù)期, 那么這個(gè)資源就是競(jìng)態(tài)資源
套用一下數(shù)據(jù)事務(wù)的例子: 2 個(gè)賬戶間轉(zhuǎn)賬, 必須加事務(wù), 只有一個(gè)賬戶上錢(qián)扣了, 另一個(gè)賬戶上錢(qián)增加了, 才算完成, 這時(shí)候去取到的 2 個(gè)賬戶的余額才是準(zhǔn)確的.
好了, 前戲差不多了, 這里來(lái)講講 yii 中用到的 2 個(gè)鎖.
mutex 互斥鎖
yii 中特地添加了 yii\mutex\Mutex, 并且提供了不同驅(qū)動(dòng)下(file, 不同 db)的實(shí)現(xiàn)
互斥鎖的理念非常簡(jiǎn)單: 保證當(dāng)前只有一個(gè)進(jìn)程(或線程)訪問(wèn)當(dāng)前資源
實(shí)現(xiàn)也非常簡(jiǎn)單, 就 2 個(gè)方法:
acquire(): 使用前請(qǐng)求鎖, 請(qǐng)求成功就繼續(xù)執(zhí)行業(yè)務(wù)邏輯, 失敗就退出
release(): 使用后釋放鎖
function lock($lockName = NULL) {
if (empty($lockName)) {
$backtrace = debug_backtrace(null, 2);
$class = $backtrace[1]['class'];
$func = $backtrace[1]['function'];
$args = implode('_', $backtrace[1]['args']);
$lockName = base64_encode($class . $func . $args);
}
$lock = \Yii::$app->mutex->acquire( $lockName ); // 請(qǐng)求鎖
if (!$lock) {
$err = "cannot get lock {$lockName}.";
throw new \Exception($err);
}
register_shutdown_function(function() use($lockName) {
return \Yii::$app->mutex->release($lockName); // 釋放鎖
});
return TRUE;
}
db optimisticLock() 樂(lè)觀鎖
這個(gè)就隱藏的比較深了. 因?yàn)橐呀?jīng)養(yǎng)成數(shù)據(jù)庫(kù)中使用自動(dòng)更新的 create_time / update_time 字段, 所以深入 ActiveRecord 的 update() 源碼進(jìn)去, 然后就發(fā)現(xiàn)了這家伙. 詳細(xì)的解釋可以看這里 百度百科 - 樂(lè)觀鎖
/**
* @see update()
* @param array $attributes attributes to update
* @return int|false the number of rows affected, or false if [[beforeSave()]] stops the updating process.
* @throws StaleObjectException
*/
protected function updateInternal($attributes = null)
{
if (!$this->beforeSave(false)) {
return false;
}
$values = $this->getDirtyAttributes($attributes);
if (empty($values)) {
$this->afterSave(false, $values);
return 0;
}
$condition = $this->getOldPrimaryKey(true);
$lock = $this->optimisticLock(); // 開(kāi)始使用樂(lè)觀鎖
if ($lock !== null) {
$values[$lock] = $this->$lock + 1;
$condition[$lock] = $this->$lock;
}
// We do not check the return value of updateAll() because it's possible
// that the UPDATE statement doesn't change anything and thus returns 0.
$rows = static::updateAll($values, $condition);
if ($lock !== null && !$rows) {
throw new StaleObjectException('The object being updated is outdated.');
}
if (isset($values[$lock])) {
$this->$lock = $values[$lock];
}
$changedAttributes = [];
foreach ($values as $name => $value) {
$changedAttributes[$name] = isset($this->_oldAttributes[$name]) ? $this->_oldAttributes[$name] : null;
$this->_oldAttributes[$name] = $value;
}
$this->afterSave(false, $changedAttributes);
return $rows;
}
關(guān)于 log & error handler
寫(xiě)代碼到一定程度, 就會(huì)開(kāi)始意識(shí)到 log & error handler 的重要性, 然而在小白程序員升級(jí)打怪的過(guò)程中, 一直在寫(xiě)業(yè)務(wù), 這 2 塊關(guān)注太少以致有些 蒼白. 并且這塊也是我比較薄弱的地方, 幾個(gè)月前在添加 Exception 的時(shí)候卡住了.
知道短處, 補(bǔ)補(bǔ)就好了.
在聊這 2 塊之前, 先補(bǔ)一下關(guān)于 回調(diào) 的基礎(chǔ)知識(shí):
平時(shí) phper 可能這樣寫(xiě)代碼的情況不多, 不過(guò)如果接觸過(guò) swoole, 寫(xiě)過(guò)一段時(shí)間的 異步編程, 這個(gè)知識(shí)點(diǎn)就再熟悉不過(guò)了, 在 swoole 的 wiki 中也特意提到過(guò), 里面列舉了 4 種, 官方文檔這里列舉了 5 種.
log 模塊
先看整體結(jié)構(gòu):
├── Logger.php
├── Dispatcher.php
└── Target.php
├── DbTarget.php
├── EmailTarget.php
├── FileTarget.php
└── SyslogTarget.php
由 Logger - Dispatcher - Target 的 3 層結(jié)構(gòu):
Logger: 日志 入口(生產(chǎn)者)
Dispatcher: 日志的 分發(fā)(通道)
Target: 日志 處理(消費(fèi)者)
其實(shí)日志系統(tǒng)的設(shè)計(jì)已經(jīng)相當(dāng)成熟了, 幾乎都采用 消息隊(duì)列 的設(shè)計(jì)模式:
生產(chǎn)者 - 消費(fèi)者 模型.
這里看一點(diǎn)代碼細(xì)節(jié):
yii 框架中的 profile 功能, 可能大家有用過(guò), 也是通過(guò) Logger 實(shí)現(xiàn)的
// 使用
\Yii::beginProfile('block1');
// some code to be profiled
\Yii::beginProfile('block2');
// some other code to be profiled
\Yii::endProfile('block2');
\Yii::endProfile('block1');
// 實(shí)現(xiàn)
public static function beginProfile($token, $category = 'application')
{
static::getLogger()->log($token, Logger::LEVEL_PROFILE_BEGIN, $category);
}
// yii\log\Logger
const LEVEL_PROFILE_BEGIN = 0x50
const LEVEL_PROFILE_END = 0x60
使用 flush: 先 緩存 一下, 然后再一起落地, 性能要比直接寫(xiě)直接落地高一些
public function log($message, $level, $category = 'application')
{
$time = microtime(true);
$traces = [];
// ...
$this->messages[] = [$message, $level, $category, $time, $traces, memory_get_usage()]; // 暫時(shí)緩存到這里
if ($this->flushInterval > 0 && count($this->messages) >= $this->flushInterval) {
$this->flush();
}
}
回調(diào)終于登場(chǎng)了, register_shutdown_function() 函數(shù)下面還會(huì)看到
public function init()
{
parent::init();
register_shutdown_function(function () {
// make regular flush before other shutdown functions, which allows session data collection and so on
$this->flush();
// make sure log entries written by shutdown functions are also flushed
// ensure "flush()" is called last when there are multiple shutdown functions
register_shutdown_function([$this, 'flush'], true);
});
}
日志模塊的代碼還是很簡(jiǎn)單的. 實(shí)現(xiàn)日志模塊其實(shí)并不難, 但是新手想用好日志卻感覺(jué)有點(diǎn) 經(jīng)驗(yàn)積累 的意思, 特別是遇到的問(wèn)題的時(shí)候發(fā)現(xiàn)沒(méi)有日志輔助定位問(wèn)題. 我的建議也很簡(jiǎn)單:
多打日志, 多用日志.
error handler 模塊
如果說(shuō)日志大部分時(shí)候只用 Logger::info() 這樣調(diào)用一下就好了, Exception 天生就要復(fù)雜一點(diǎn)了, 因?yàn)橥暾倪^(guò)程是這樣的:
try {
// do something
throw new \Exception("Error Processing Request", 1);
} catch (\Exception $e) {
// handle error
}
但是, 其實(shí)只要記住這個(gè)基本 骨架, 任何地方都是同樣的. 如果這塊比較薄弱, 可以 參考官方手冊(cè) - Exception 多看一看.
yii 框架中 Exception 的使用很多, 所以看起來(lái)會(huì)比較凌亂, 但其實(shí)層次很清晰:
首先是 base, 這基本確定了 Exception 的分類:
\yii\base\Exception: 異常基類, 統(tǒng)一添加 getName() 方法給異常添加標(biāo)識(shí)
\yii\base\ErrorException: 處理未捕獲的 php 錯(cuò)誤和異常, 下面會(huì)著重講一下 register() 方法
\yii\base\UserException: 用戶可見(jiàn)異常基類, 這個(gè)很重要, 添加了一個(gè)明顯分類
\yii\base\XxxException: 其他異常
然后就是根據(jù)應(yīng)用不同:
\yii\web\XxxException: web 應(yīng)用下的異常
\yii\console\XxxException: console 應(yīng)用下的異常
好了, 再來(lái)看點(diǎn)源碼:
\yii\base\ErrorException 中的 register() 方法: 注冊(cè)函數(shù)回調(diào); 兼容 HHVM
public function register()
{
ini_set('display_errors', false);
set_exception_handler([$this, 'handleException']);
if (defined('HHVM_VERSION')) {
set_error_handler([$this, 'handleHhvmError']);
} else {
set_error_handler([$this, 'handleError']);
}
if ($this->memoryReserveSize > 0) {
$this->_memoryReserve = str_repeat('x', $this->memoryReserveSize);
}
register_shutdown_function([$this, 'handleFatalError']);
}
\yii\base\UserException
/**
* UserException is the base class for exceptions that are meant to be shown to end users.
* Such exceptions are often caused by mistakes of end users.
*/
class UserException extends Exception
{
}
一個(gè)明顯的場(chǎng)景, 就是 http 的 4xx 錯(cuò)誤:
class HttpException extends UserException
{
/**
* @var int HTTP status code, such as 403, 404, 500, etc.
*/
public $statusCode;
}
還有一個(gè)常用的方式(套路), 將應(yīng)用整個(gè)包在 try-catch 中, 統(tǒng)一捕獲異常
// 入口腳本: web/index.php
require(__DIR__ . '/../../vendor/yiisoft/yii2/Yii.php');
$config = require(__DIR__ . '/../config/web.php');
(new yii\web\Application($config))->run();
// \yii\base\Application
public function run()
{
try {
$this->state = self::STATE_BEFORE_REQUEST;
$this->trigger(self::EVENT_BEFORE_REQUEST);
$this->state = self::STATE_HANDLING_REQUEST;
$response = $this->handleRequest($this->getRequest());
$this->state = self::STATE_AFTER_REQUEST;
$this->trigger(self::EVENT_AFTER_REQUEST);
$this->state = self::STATE_SENDING_RESPONSE;
$response->send();
$this->state = self::STATE_END;
return $response->exitStatus;
} catch (ExitException $e) {
$this->end($e->statusCode, isset($response) ? $response : null);
return $e->statusCode;
}
}
寫(xiě)在最后
聊了這么多, 內(nèi)容多了之后, 也會(huì)有些 雜亂, 而且也無(wú)法深入到太多的細(xì)節(jié). 我比較滿意的是, 在一開(kāi)始我就計(jì)劃好使用腦圖, 嘗試整體的理解架構(gòu), 那些記下的細(xì)節(jié), 反而有點(diǎn)像 意外之喜.
大型開(kāi)源項(xiàng)目的源碼是一座寶礦. 編程也是一項(xiàng)技藝, 如同江湖中對(duì)武功的崇拜一樣, 程序員也會(huì)對(duì)自己的一技之長(zhǎng)產(chǎn)生驕傲.
也許確實(shí)沒(méi)有大段的時(shí)間去閱讀源碼, 但是使用方法時(shí), 多點(diǎn)進(jìn)去看看, 也經(jīng)常會(huì)有所收獲, 比如 yii 中 cache 相關(guān)的方法:
// 平時(shí)使用
Yii::$app->cache->set('key', 'value');
// 進(jìn)入會(huì)發(fā)現(xiàn), 可以設(shè)置 過(guò)期時(shí)間 + 緩存依賴
public function set($key, $value, $duration = null, $dependency = null)
總結(jié)
以上是生活随笔為你收集整理的php yii框架源码,yii 源码解读的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 预计这个价!曝vivo V27系列将于3
- 下一篇: 动态规划算法php,php算法学习之动态