Doctrine\ORM\QueryBuilder 源碼解析之 where

背景

最近有需求實現類似于 QueryBuilder 的謂詞語句阿纤,就去翻看了它的源碼。先看兩個例子
例子1

$qb = $em->createQueryBuilder();
$qb->select('*')->from('User', 'u')->where('u.id = 1');
echo $qb->getDQL();

例子2

$qb = $em->createQueryBuilder();
$qb->select('*')
            ->from('User', 'u')
            ->where('u.id = 1')
            ->andWhere('u.score >= 90')
            ->orWhere('u.score <= 100');
 echo $qb->getDQL();

是不是有點懵逼夷陋,希望看完我的文章欠拾,希望你有所收獲。
接下來會以例子2講解骗绕,分別解釋 where藐窄、andWhereorWhere 方法酬土,圖文并茂荆忍,一步步教你理解上述PHP代碼轉換為sql語句的原理。

代碼不難,建議大家配合源碼食用刹枉。[相關源碼在此]https://github.com/doctrine/orm/blob/master/lib/Doctrine/ORM/QueryBuilder.php(https://github.com/doctrine/orm/blob/master/lib/Doctrine/ORM/QueryBuilder.php
)

添加謂詞語句

先看 QueryBuilder::where方法叽唱,生成了一個Andx謂詞對象 $predicates

public function where($predicates)
   {
       if ( ! (func_num_args() == 1 && $predicates instanceof Expr\Composite)) {
           $predicates = new Expr\Andx(func_get_args());
       }

       return $this->add('where', $predicates);
   }

執(zhí)行 var_export($predicates)后查看成員變量如下嘶卧,separator 是 where 子句條件之間的連接符尔觉,有 AND 和 OR。parts 是由條件組成的數組芥吟。

// var_export($predicates)的輸出
Doctrine\ORM\Query\Expr\Andx::__set_state(array(
   'separator' => ' AND ',
   'allowedClasses' => 
  array (
    0 => 'Doctrine\\ORM\\Query\\Expr\\Comparison',
    1 => 'Doctrine\\ORM\\Query\\Expr\\Func',
    2 => 'Doctrine\\ORM\\Query\\Expr\\Orx',
    3 => 'Doctrine\\ORM\\Query\\Expr\\Andx',
  ),
   'preSeparator' => '(',
   'postSeparator' => ')',
   'parts' => 
  array (
    0 => 'u.id = 1',
  ),
))

接下來執(zhí)行的return $this->add('where', $predicates);函數很簡單侦铜,把謂詞對象作為QueryBuilder::_dqlParts中的key為where的value吉懊,打印QueryBuilder對象如下

// 此函數有刪減蕉鸳,完整請看官方源碼
// QueryBuilder的add方法
public function add($dqlPartName, $dqlPart, $append = false)
    {
        $isMultiple = is_array($this->_dqlParts[$dqlPartName])
            && !($dqlPartName == 'join' && !$append);
        // $isMultiple 這里的值為 false
        $this->_dqlParts[$dqlPartName] = ($isMultiple) ? [$dqlPart] : $dqlPart;
        $this->_state = self::STATE_DIRTY;

        return $this;
    }

// _dqlParts 結構
array (
...
  'where' => 
  Doctrine\ORM\Query\Expr\Andx::__set_state(array(
     'separator' => ' AND ',
     'allowedClasses' => 
    array (
      0 => 'Doctrine\\ORM\\Query\\Expr\\Comparison',
      1 => 'Doctrine\\ORM\\Query\\Expr\\Func',
      2 => 'Doctrine\\ORM\\Query\\Expr\\Orx',
      3 => 'Doctrine\\ORM\\Query\\Expr\\Andx',
    ),
     'preSeparator' => '(',
     'postSeparator' => ')',
     'parts' => 
    array (
      0 => 'u.id = 1',
    ),
  ))
...

接下來具體看 QueryBuilder::andWhere方法严嗜,
getDQLPart取出的是剛才設置的Andx對象返帕,接著執(zhí)行Andx的addMultiple方法羡疗,最終調用的是Andx::add方法串远,這個方法最終是把'u.score >= 90'加入到Andx::parts數組中



// QueryBuilder
public function andWhere()
    {
        $args  = func_get_args();
        $where = $this->getDQLPart('where');

        if ($where instanceof Expr\Andx) {
            $where->addMultiple($args);
        } else {
            array_unshift($args, $where);
            $where = new Expr\Andx($args);
        }

        return $this->add('where', $where);
    }

// Andx
public function add($arg)
    {
        if ( $arg !== null && (!$arg instanceof self || $arg->count() > 0) ) {
            // If we decide to keep Expr\Base instances, we can use this check
            if ( ! is_string($arg)) {
                $class = get_class($arg);

                if ( ! in_array($class, $this->allowedClasses)) {
                    throw new \InvalidArgumentException("Expression of type '$class' not allowed in this context.");
                }
            }

            $this->parts[] = $arg;
        }

        return $this;
    }

所以此時Andx::parts數組中有了兩個元素

Doctrine\ORM\Query\Expr\Andx::__set_state(array(
   'separator' => ' AND ',
   'allowedClasses' => 
  array (
    0 => 'Doctrine\\ORM\\Query\\Expr\\Comparison',
    1 => 'Doctrine\\ORM\\Query\\Expr\\Func',
    2 => 'Doctrine\\ORM\\Query\\Expr\\Orx',
    3 => 'Doctrine\\ORM\\Query\\Expr\\Andx',
  ),
   'preSeparator' => '(',
   'postSeparator' => ')',
   'parts' => 
  array (
    0 => 'u.id = 1',
    1 => 'u.score >= 90',
  ),
))

繼續(xù)看QueryBuilder::orWhere方法汹族,取出的 $where 是剛剛andWhere 執(zhí)行后設置的 Andx 對象锚国,執(zhí)行 array_unshift($args, $where)語句后蒙袍,形成的 $args 由一個 Andx 對象和一個字符串 'u.score <= 100'組成俊卤。

public function orWhere()
    {
        $args  = func_get_args();
        $where = $this->getDQLPart('where');

        if ($where instanceof Expr\Orx) {
            $where->addMultiple($args);
        } else {
            array_unshift($args, $where);
            $where = new Expr\Orx($args);
        }

        return $this->add('where', $where);
    }

將包含 Andx 對象和字符串 u.score <= 100$args 數組作為 Orx 對象的構造方法,Orx 構造函數的內部實現是 addMultiple方法害幅,最終調用 Orx::add 方法將 $args 中的元素全部都加到 Orx對象 的 $parts對象中消恍,最終 Orx 對象的 parts 內容的示意圖的階段 3 所示

image.png

我整理了一下添加邏輯如下所示


image.png

解析謂詞語句

謂詞對象轉換成謂詞語句其實就是一句話,

$queryPart = $this->getDQLPart($queryPartName);
echo $queryPart;

不要覺得奇怪以现,對象也可以當作字符串用狠怨,引用PHP手冊上的原話

__toString() 方法用于一個類被當成字符串時應怎樣回應。

謂詞對象的__toString的實現在Doctrine\ORM\Query\Expr\Composite邑遏,一起來看看

public function __toString()
    {
        if ($this->count() === 1) {
            return (string) $this->parts[0];
        }

        $components = [];

        foreach ($this->parts as $part) {
            $components[] = $this->processQueryPart($part);
        }

        return implode($this->separator, $components);
    }

private function processQueryPart($part)
    {
        $queryPart = (string) $part;

        if (is_object($part) && $part instanceof self && $part->count() > 1) {
            return $this->preSeparator . $queryPart . $this->postSeparator;
        }

        if (stripos($queryPart, ' OR ') !== false || stripos($queryPart, ' AND ') !== false) {
            return $this->preSeparator . $queryPart . $this->postSeparator;
        }

        return $queryPart;
    }

帶入之前組合好的 Orx 對象佣赖,一起來分析下。Orx 對象parts屬性的兩個元素分別會被帶入processQueryPart執(zhí)行记盒。
Andx 你先來憎蛤,走到$queryPart = (string) $part,我們希望$part被當作字符串處理,繼續(xù)回到__toString纪吮,這里是個遞歸蹂午。
Andx對象parts屬性的兩個字符串元素繼續(xù)帶入processQueryPart執(zhí)行。這兩個字符串經過處理會作為Andx對象的$components的元素彬碱,最終經過implode($this->separator, $components)返回字符串 u.id=1 and u.score >= 90 豆胸,此時的值會被返回到 $queryPart

接下來執(zhí)行到的是 return $this->preSeparator . $queryPart . $this->postSeparator; 到 Orx 對象的 $components的數組中去巷疼。

// $part 是 Andx 對象
// Andx 對象 經過字符串化后成了 u.id=1 and u.score >= 90晚胡,賦值給 $queryPart
$queryPart = (string) $part;

// 因為Andx對象有兩個條件灵奖,所以左右兩邊會被加上括號,最終返回 (u.id = 1 AND u.score >= 90)
if (is_object($part) && $part instanceof self && $part->count() > 1) {
  return $this->preSeparator . $queryPart . $this->postSeparator;
}

Orx 對象parts屬性的第一個元素已經處理完畢估盘,接下來是第二個元素u.score <= 100瓷患,字符串就很簡單了,直接返回到 Orx 對象的 $components 中去遣妥!
現在來看看Orx 對象的 $components中間有啥了

array (
  0 => '(u.id = 1 AND u.score >= 90)',
  1 => 'u.score <= 100',
)

再用implode切割成字符串 結果就是出來了(u.id = 1 AND u.score >= 90) OR u.score <= 100,解析完畢擅编!看不懂的同學看我整理的流程圖。

image.png

總結

具體細節(jié)大家可以使用 phpStorm + xdebug 單步調試研究箫踩。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末爱态,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子境钟,更是在濱河造成了極大的恐慌锦担,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,602評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件慨削,死亡現場離奇詭異洞渔,居然都是意外死亡,警方通過查閱死者的電腦和手機缚态,發(fā)現死者居然都...
    沈念sama閱讀 88,442評論 2 382
  • 文/潘曉璐 我一進店門磁椒,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人玫芦,你說我怎么就攤上這事衷快。” “怎么了姨俩?”我有些...
    開封第一講書人閱讀 152,878評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長师郑。 經常有香客問我环葵,道長,這世上最難降的妖魔是什么宝冕? 我笑而不...
    開封第一講書人閱讀 55,306評論 1 279
  • 正文 為了忘掉前任张遭,我火速辦了婚禮,結果婚禮上地梨,老公的妹妹穿的比我還像新娘菊卷。我一直安慰自己,他們只是感情好宝剖,可當我...
    茶點故事閱讀 64,330評論 5 373
  • 文/花漫 我一把揭開白布洁闰。 她就那樣靜靜地躺著,像睡著了一般万细。 火紅的嫁衣襯著肌膚如雪扑眉。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,071評論 1 285
  • 那天,我揣著相機與錄音腰素,去河邊找鬼聘裁。 笑死,一個胖子當著我的面吹牛弓千,可吹牛的內容都是我干的衡便。 我是一名探鬼主播,決...
    沈念sama閱讀 38,382評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼洋访,長吁一口氣:“原來是場噩夢啊……” “哼镣陕!你這毒婦竟也來了?” 一聲冷哼從身側響起捌显,我...
    開封第一講書人閱讀 37,006評論 0 259
  • 序言:老撾萬榮一對情侶失蹤茁彭,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后扶歪,有當地人在樹林里發(fā)現了一具尸體理肺,經...
    沈念sama閱讀 43,512評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 35,965評論 2 325
  • 正文 我和宋清朗相戀三年善镰,在試婚紗的時候發(fā)現自己被綠了妹萨。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,094評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡炫欺,死狀恐怖乎完,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情品洛,我是刑警寧澤树姨,帶...
    沈念sama閱讀 33,732評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站桥状,受9級特大地震影響帽揪,放射性物質發(fā)生泄漏。R本人自食惡果不足惜辅斟,卻給世界環(huán)境...
    茶點故事閱讀 39,283評論 3 307
  • 文/蒙蒙 一转晰、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧士飒,春花似錦查邢、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,286評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至芳撒,卻和暖如春实胸,著一層夾襖步出監(jiān)牢的瞬間他嫡,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,512評論 1 262
  • 我被黑心中介騙來泰國打工庐完, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留钢属,地道東北人。 一個月前我還...
    沈念sama閱讀 45,536評論 2 354
  • 正文 我出身青樓门躯,卻偏偏與公主長得像淆党,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子讶凉,可洞房花燭夜當晚...
    茶點故事閱讀 42,828評論 2 345

推薦閱讀更多精彩內容