最近有需求實現相似於 QueryBuilder 的謂詞語句,就去翻看了它的源碼。先看兩個例子
例子1php
$qb = $em->createQueryBuilder(); $qb->select('*')->from('User', 'u')->where('u.id = 1'); echo $qb->getDQL();
例子2git
$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、andWhere、orWhere 方法,圖文並茂,一步步教你理解上述PHP代碼轉換爲sql語句的原理。github
代碼不難,建議你們配合源碼食用。[相關源碼在此]https://github.com/doctrine/orm/blob/master/lib/Doctrine/ORM/QueryBuilder.php(https://github.com/doctrine/orm/blob/master/lib/Doctrine/ORM/QueryBuilder.php
)sql
先看 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); }
執行 var_export($predicates)
後查當作員變量以下,separator
是 where 子句條件之間的鏈接符,有 AND 和 OR。parts
是由條件組成的數組。app
// 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', ), ))
接下來執行的return $this->add('where', $predicates);
函數很簡單,把謂詞對象做爲QueryBuilder::_dqlParts
中的key爲where的value,打印QueryBuilder對象以下ide
// 此函數有刪減,完整請看官方源碼 // 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對象,接着執行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數組中有了兩個元素:'u.id = 1' 和 'u.score >= 90'oop
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', ), ))
繼續看QueryBuilder::orWhere
方法,取出的 $where
是剛剛andWhere
執行後設置的 Andx 對象,執行 array_unshift($args, $where)
語句後,造成的 $args
由一個 Andx 對象和一個字符串 'u.score <= 100'
組成。post
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 所示
我整理了一下添加邏輯以下所示
謂詞對象轉換成謂詞語句其實就是一句話,
$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執行。
Andx 你先來,走到$queryPart = (string) $part
,咱們但願$part
被看成字符串處理,繼續回到__toString
,這裏是個遞歸。
Andx對象parts屬性的兩個字符串元素繼續帶入processQueryPart執行。這兩個字符串通過處理會做爲Andx對象的$components
的元素,最終通過implode($this->separator, $components)
返回字符串 u.id=1 and u.score >= 90
,此時的值會被返回到 $queryPart
。
接下來執行到的是 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
,解析完畢!看不懂的同窗看我整理的流程圖。
具體細節你們可使用 phpStorm + xdebug 單步調試研究。