撈出一筆資料時,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有點炫技啦,多寫幾行清楚得多。