Eloquent CRUD:__construct

假設我們在寫一個電子商務網站。建立訂單的第一步大概長這樣:

$order = new Order();

或是

$order = new Order(array('amount'=> 500, 'serialNumber' => '201501300001'));

背後發生了什麼事呢?讓我們一一來看。

首先當然是建構式:

	/**
	 * Create a new Eloquent model instance.
	 *
	 * @param  array  $attributes
	 * @return void
	 */
	public function __construct(array $attributes = array())
	{
		$this->bootIfNotBooted();

		$this->syncOriginal();

		$this->fill($attributes);
	}

第一步先檢查是否需要「啟動」:

	/**
	 * Check if the model needs to be booted and if so, do it.
	 *
	 * @return void
	 */
	protected function bootIfNotBooted()
	{
		$class = get_class($this);

		if ( ! isset(static::$booted[$class]))
		{
			static::$booted[$class] = true;

			$this->fireModelEvent('booting', false);

			static::boot();

			$this->fireModelEvent('booted', false);
		}
	}

利用一個類別的靜態陣列紀錄是否已啟動,並且觸發啟動前與啟動後的事件。
看來一個類別在一次request內只會被啟動一次。啟動在做什麼呢?

	/**
	 * The "booting" method of the model.
	 *
	 * @return void
	 */
	protected static function boot()
	{
		$class = get_called_class();

		static::$mutatorCache[$class] = array();

		// Here we will extract all of the mutated attributes so that we can quickly
		// spin through them after we export models to their array form, which we
		// need to be fast. This will let us always know the attributes mutate.
		foreach (get_class_methods($class) as $method)
		{
			if (preg_match('/^get(.+)Attribute$/', $method, $matches))
			{
				if (static::$snakeAttributes) $matches[1] = snake_case($matches[1]);

				static::$mutatorCache[$class][] = lcfirst($matches[1]);
			}
		}

		static::bootTraits();
	}


文件中有提到Accessors & Mutators
原來,啟動是為了支援客製化的屬性讀取、寫入功能。
最後一句跟PHP的traits功能有關,你需要先了解PHP traits。
bootTraits會檢查所有traits、看是否有需要「啟動」的部份。舉例來說,Soft Deleting就是利用了traits。

	/**
	 * Boot all of the bootable traits on the model.
	 *
	 * @return void
	 */
	protected static function bootTraits()
	{
		foreach (class_uses_recursive(get_called_class()) as $trait)
		{
			if (method_exists(get_called_class(), $method = 'boot'.class_basename($trait)))
			{
				forward_static_call([get_called_class(), $method]);
			}
		}
	}

注意class_uses_recursive並不是PHP內建函數,是Larave自定義的helper輔助函數。
再來是建構式第二句的方法:

	/**
	 * Sync the original attributes with the current.
	 *
	 * @return $this
	 */
	public function syncOriginal()
	{
		$this->original = $this->attributes;

		return $this;
	}

你會發現original跟attributes都還只是空陣列。在建構式執行這句,乍看之下沒有意義?
我詢問過Laravel原作者,這是他的答覆
翻閱文件,會發現其實可以override attributes陣列。

建構式的最後一句,也是唯一有對傳入建構式的參數做處理的方法來了:

	/**
	 * Fill the model with an array of attributes.
	 *
	 * @param  array  $attributes
	 * @return $this
	 *
	 * @throws \Illuminate\Database\Eloquent\MassAssignmentException
	 */
	public function fill(array $attributes)
	{
		$totallyGuarded = $this->totallyGuarded();

		foreach ($this->fillableFromArray($attributes) as $key => $value)
		{
			$key = $this->removeTableFromKey($key);

			// The developers may choose to place some attributes in the "fillable"
			// array, which means only those attributes may be set through mass
			// assignment to the model, and all others will just be ignored.
			if ($this->isFillable($key))
			{
				$this->setAttribute($key, $value);
			}
			elseif ($totallyGuarded)
			{
				throw new MassAssignmentException($key);
			}
		}

		return $this;
	}

首先檢查所有屬性是否都被「防護」。這是避免使用者直接把瀏覽器丟過來的value,直接塞給Eloquent當屬性、造成資安漏洞的設計。參閱Mass Assignment

	/**
	 * Determine if the model is totally guarded.
	 *
	 * @return bool
	 */
	public function totallyGuarded()
	{
		return count($this->fillable) == 0 && $this->guarded == array('*');
	}

接著fillableFromArray會檢查參數陣列中,有哪些是可以寫入的屬性:

	/**
	 * Get the fillable attributes of a given array.
	 *
	 * @param  array  $attributes
	 * @return array
	 */
	protected function fillableFromArray(array $attributes)
	{
		if (count($this->fillable) > 0 && ! static::$unguarded)
		{
			return array_intersect_key($attributes, array_flip($this->fillable));
		}

		return $attributes;
	}

你會發現有unguarded變數可以關閉防護機制。原始碼中有三個方法可以直接操作這個變數,官方文件沒有寫進去。
array_intersect_key跟array_flip的巧妙搭配運用,找到了可寫入的屬性。

接著若通過isFillable,就setAttribute。
為什麼要再次檢查isFillable呢?不是都用fillableFromArray只找出可寫入的屬性了嗎?

	/**
	 * Determine if the given attribute may be mass assigned.
	 *
	 * @param  string  $key
	 * @return bool
	 */
	public function isFillable($key)
	{
		if (static::$unguarded) return true;

		// If the key is in the "fillable" array, we can of course assume that it's
		// a fillable attribute. Otherwise, we will check the guarded array when
		// we need to determine if the attribute is black-listed on the model.
		if (in_array($key, $this->fillable)) return true;

		if ($this->isGuarded($key)) return false;

		return empty($this->fillable) && ! starts_with($key, '_');
	}

原來是要檢查有沒有在guarded陣列裡面。

再來,終於要寫入屬性了:

	/**
	 * Set a given attribute on the model.
	 *
	 * @param  string  $key
	 * @param  mixed   $value
	 * @return void
	 */
	public function setAttribute($key, $value)
	{
		// First we will check for the presence of a mutator for the set operation
		// which simply lets the developers tweak the attribute as it is set on
		// the model, such as "json_encoding" an listing of data for storage.
		if ($this->hasSetMutator($key))
		{
			$method = 'set'.studly_case($key).'Attribute';

			return $this->{$method}($value);
		}

		// If an attribute is listed as a "date", we'll convert it from a DateTime
		// instance into a form proper for storage on the database tables using
		// the connection grammar's date format. We will auto set the values.
		elseif (in_array($key, $this->getDates()) && $value)
		{
			$value = $this->fromDateTime($value);
		}

		$this->attributes[$key] = $value;
	}

一開始的Mutator檢查…嗯我還沒搞懂,先略過。
接著是檢查是否為日期屬性、轉成字串。就是Date Mutator這段的描述。
沒問題的話,就存進attributes陣列裡頭。
咦?怎麼是存進attributes陣列、不是直接設成物件屬性呀?
那我取出屬性時不就要透過attributes陣列?
別擔心,別忘了PHP有__get這種神奇函數。

	/**
	 * Dynamically retrieve attributes on the model.
	 *
	 * @param  string  $key
	 * @return mixed
	 */
	public function __get($key)
	{
		return $this->getAttribute($key);
	}

複習

好,最後我們來複習一遍。
Eloquent建構式會依序執行三個方法。
一、執行啟動程序(支援客製化屬性讀寫、執行所有traits的啟動程序)
二、載入使用者自定義屬性(使用者定義的$this->attributes陣列)
三、將傳入建構式的參數寫入為屬性

以上大致就是一個Eloquent子類別在建構式中會做的事情。是不是比想像中複雜地多呢:)

by 阿川先生