假設我們在寫一個電子商務網站。建立訂單的第一步大概長這樣:
$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子類別在建構式中會做的事情。是不是比想像中複雜地多呢:)