Eloquent CRUD:find

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

by 阿川先生