撈出一筆資料時,Eloquent是如何將資料mapping成物件的呢?
$user = User::find(1);
資料庫撈出來的應該是一大堆rows,轉成PHP的陣列還算容易想像,但怎麼轉成Eloquent實體呢?
我們來看看find方法背後做了什麼。
// Illuminate\Database\Eloquent\Model.php /** * Find a model by its primary key. * * @param mixed $id * @param array $columns * @return \Illuminate\Support\Collection|static */ public static function find($id, $columns = array('*')) { if (is_array($id) && empty($id)) return new Collection; $instance = new static; return $instance->newQuery()->find($id, $columns); }
別被new static嚇到了,其實就是new static(),也就是呼叫當前類別的建構式而已。
接著看看newQuery方法。
// Illuminate\Database\Eloquent\Model.php /** * Get a new query builder for the model's table. * * @return \Illuminate\Database\Eloquent\Builder */ public function newQuery() { $builder = $this->newEloquentBuilder( $this->newBaseQueryBuilder() ); // Once we have the query builders, we will set the model instances so the // builder can easily access any information it may need from the model // while it is constructing and executing various queries against it. $builder->setModel($this)->with($this->with); return $this->applyGlobalScopes($builder); }
newBaseQueryBuilder會回傳\Illuminate\Database\Query\Builder實體。
這段重點就是拿\Illuminate\Database\Query\Builder當參數傳入,製作出\Illuminate\Database\Eloquent\Builder實體。
一個專門製造Query,一個專門製作Eloquent,在想像它們的時候是分開的。
接著要呼叫這Eloquent\Builder的find方法。
// \Illuminate\Database\Eloquent\Builder.php /** * Find a model by its primary key. * * @param mixed $id * @param array $columns * @return \Illuminate\Database\Eloquent\Model|static|null */ public function find($id, $columns = array('*')) { if (is_array($id)) { return $this->findMany($id, $columns); } $this->query->where($this->model->getQualifiedKeyName(), '=', $id); return $this->first($columns); }
我不喜歡第一段的防禦性編程。它若發現傳入的是陣列會去呼叫findMany…。這會導致其他Laravel框架開發者濫用find方法、不去分清楚find跟findMany的差別。這是在迴避溝通不良的問題。
參閱:Why Defensive Programming is Rubbish
接著會呼叫Database\Query實體(它在前面的方法中被產生,我省略了這部份)的where方法。
用getQualifiedKeyName產生像是’users.id’的字串給where方法。
其實就是用OOP的方式以Database\Query去寫SQL。
最後,呼叫first方法。
// Illuminate\Database\Eloquent\Builder.php /** * Execute the query and get the first result. * * @param array $columns * @return \Illuminate\Database\Eloquent\Model|static|null */ public function first($columns = array('*')) { return $this->take(1)->get($columns)->first(); }
呼叫了take方法…但是source code沒有定義take方法。它到底是什麼?
嘿嘿,別忘了PHP有__call這個magic function。
// Illuminate\Database\Eloquent\Builder.php /** * Dynamically handle calls into the query instance. * * @param string $method * @param array $parameters * @return mixed */ public function __call($method, $parameters) { if (isset($this->macros[$method])) { array_unshift($parameters, $this); return call_user_func_array($this->macros[$method], $parameters); } elseif (method_exists($this->model, $scope = 'scope'.ucfirst($method))) { return $this->callScope($scope, $parameters); } $result = call_user_func_array(array($this->query, $method), $parameters); return in_array($method, $this->passthru) ? $result : $this; }
真正的take出現在倒數第二行。也就是Database\Query的take。
它只是要加入SQL語法的LIMIT而已。
注意最後一行。只有當方法在passthru陣列裡面才會回傳方法結果,不然都是傳回自己。
take, get, first三個方法Database\Query類別都有,導致這段看似一般的Method Chaining,其實完全不是。
take是Query\Builder的、get是Eloquent\Builder的、first是Eloquent\Collection的。
所以這行:
return $this->take(1)->get($columns)->first();
其實等於:
$this->query->take(1); $models = $this->get($columns); return $models->first();
如您所見,這就是magic method會被人詬病的地方。這段code變得超級難trace,不知道到底在呼叫什麼。
這三句,第一句就是用$query設定數量(MySQL的LIMIT),第二句是得到包住多個Eloquent實體的Collection,第三句則是拿出Collection的第一個。
第二句的get就是你常用到的那個get:
$users = User::where('votes', '>', 100)->take(10)->get();
來看看它的長相吧。
// Illuminate\Database\Eloquent\Builder.php /** * Execute the query as a "select" statement. * * @param array $columns * @return \Illuminate\Database\Eloquent\Collection|static[] */ public function get($columns = array('*')) { $models = $this->getModels($columns); // If we actually found models we will also eager load any relationships that // have been specified as needing to be eager loaded, which will solve the // n+1 query issue for the developers to avoid running a lot of queries. if (count($models) > 0) { $models = $this->eagerLoadRelations($models); } return $this->model->newCollection($models); }
重點來了。終於能回答一開始的問題
「資料庫撈出來的應該是一大堆rows,轉成PHP的陣列還算容易想像,但怎麼轉成Eloquent實體呢?」
來看第一句的getModels:
// Illuminate\Database\Eloquent\Builder.php /** * Get the hydrated models without eager loading. * * @param array $columns * @return \Illuminate\Database\Eloquent\Model[] */ public function getModels($columns = array('*')) { // First, we will simply get the raw results from the query builders which we // can use to populate an array with Eloquent models. We will pass columns // that should be selected as well, which are typically just everything. $results = $this->query->get($columns); $connection = $this->model->getConnectionName(); $models = array(); // Once we have the results, we can spin through them and instantiate a fresh // model instance for each records we retrieved from the database. We will // also set the proper connection name for the model after we create it. foreach ($results as $result) { $models[] = $model = $this->model->newFromBuilder($result); $model->setConnection($connection); } return $models; }
解答看起來不遠了!回到Eloquent\Model看這個方法
// Illuminate\Database\Eloquent\Model.php /** * Create a new model instance that is existing. * * @param array $attributes * @return static */ public function newFromBuilder($attributes = array()) { $instance = $this->newInstance(array(), true); $instance->setRawAttributes((array) $attributes, true); return $instance; }
原來在這裡!終於找到答案了!腦袋都快燒掉了!
複習
好,最後我們來複習一遍。
Eloquent的find依序執行:
一、做出Eloquent\Builder物件,請它根據primary key做出Eloquent\Model實體
二、Eloquent\Builder會拿出Query\Builder實體幫忙,告訴它where敘述
三、Eloquent\Builder接著告訴它limit敘述、拿出rows做成物件、把第一個回傳
以上大致就是一個Eloquent子類別在find時會做的事情。是不是比想像中複雜地多呢:)
我是覺得contributors有點炫技啦,多寫幾行清楚得多。