Eloquent CRUD:save

更新一個entity,就跟儲存一個新entity一樣,需要使用save方法:

$user = User::find(1);

$user->email = '[email protected]';

$user->save();

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

	/**
	 * Save the model to the database.
	 *
	 * @param  array  $options
	 * @return bool
	 */
	public function save(array $options = array())
	{
		$query = $this->newQueryWithoutScopes();

		// If the "saving" event returns false we'll bail out of the save and return
		// false, indicating that the save failed. This gives an opportunities to
		// listeners to cancel save operations if validations fail or whatever.
		if ($this->fireModelEvent('saving') === false)
		{
			return false;
		}

		// If the model already exists in the database we can just update our record
		// that is already in this database using the current IDs in this "where"
		// clause to only update this model. Otherwise, we'll just insert them.
		if ($this->exists)
		{
			$saved = $this->performUpdate($query, $options);
		}

		// If the model is brand new, we'll insert it into our database and set the
		// ID attribute on the model to the value of the newly inserted row's ID
		// which is typically an auto-increment value managed by the database.
		else
		{
			$saved = $this->performInsert($query, $options);
		}

		if ($saved) $this->finishSave($options);

		return $saved;
	}

在前一篇Eloquent CRUD:find我們看到了Eloquent\Model大量運用到Eloquent\Builder和Query\Builder類別。
newQueryWithoutScopes從命名可猜想是在製作Query\Builder實體。
我們把重點放在performUpdate和performInsert吧

	/**
	 * Perform a model update operation.
	 *
	 * @param  \Illuminate\Database\Eloquent\Builder  $query
	 * @return bool|null
	 */
	protected function performUpdate(Builder $query, array $options)
	{
		$dirty = $this->getDirty();

		if (count($dirty) > 0)
		{
			// If the updating event returns false, we will cancel the update operation so
			// developers can hook Validation systems into their models and cancel this
			// operation if the model does not pass validation. Otherwise, we update.
			if ($this->fireModelEvent('updating') === false)
			{
				return false;
			}

			// First we need to create a fresh query instance and touch the creation and
			// update timestamp on the model which are maintained by us for developer
			// convenience. Then we will just continue saving the model instances.
			if ($this->timestamps && array_get($options, 'timestamps', true))
			{
				$this->updateTimestamps();
			}

			// Once we have run the update operation, we will fire the "updated" event for
			// this model instance. This will allow developers to hook into these after
			// models are updated, giving them a chance to do any special processing.
			$dirty = $this->getDirty();

			if (count($dirty) > 0)
			{
				$this->setKeysForSaveQuery($query)->update($dirty);

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

		return true;
	}

首先利用getDirty得知哪些屬性被更動過、需要更新:

	/**
	 * Get the attributes that have been changed since last sync.
	 *
	 * @return array
	 */
	public function getDirty()
	{
		$dirty = array();

		foreach ($this->attributes as $key => $value)
		{
			if ( ! array_key_exists($key, $this->original))
			{
				$dirty[$key] = $value;
			}
			elseif ($value !== $this->original[$key] &&
                                 ! $this->originalIsNumericallyEquivalent($key))
			{
				$dirty[$key] = $value;
			}
		}

		return $dirty;
	}

看起來是在比較attributes跟original陣列的內容,但是我們設定value時並沒有碰到它們呀?

$user->email = '[email protected]';

我們並不是

$user->attributes['email'] = '[email protected]';

getDirty怎麼會比較那兩個陣列呢?
原來是magic method __set

	/**
	 * Dynamically set attributes on the model.
	 *
	 * @param  string  $key
	 * @param  mixed   $value
	 * @return void
	 */
	public function __set($key, $value)
	{
		$this->setAttribute($key, $value);
	}

除此之外,performUpdate的其他部份看起來都很直觀。

接著看performInsert:

	/**
	 * Perform a model insert operation.
	 *
	 * @param  \Illuminate\Database\Eloquent\Builder  $query
	 * @return bool
	 */
	protected function performInsert(Builder $query, array $options)
	{
		if ($this->fireModelEvent('creating') === false) return false;

		// First we'll need to create a fresh query instance and touch the creation and
		// update timestamps on this model, which are maintained by us for developer
		// convenience. After, we will just continue saving these model instances.
		if ($this->timestamps && array_get($options, 'timestamps', true))
		{
			$this->updateTimestamps();
		}

		// If the model has an incrementing key, we can use the "insertGetId" method on
		// the query builder, which will give us back the final inserted ID for this
		// table from the database. Not all tables have to be incrementing though.
		$attributes = $this->attributes;

		if ($this->incrementing)
		{
			$this->insertAndSetId($query, $attributes);
		}

		// If the table is not incrementing we'll simply insert this attributes as they
		// are, as this attributes arrays must contain an "id" column already placed
		// there by the developer as the manually determined key for these models.
		else
		{
			$query->insert($attributes);
		}

		// We will go ahead and set the exists property to true, so that it is set when
		// the created event is fired, just in case the developer tries to update it
		// during the event. This will allow them to do so and run an update here.
		$this->exists = true;

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

		return true;
	}

by 阿川先生