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