LaravelのEloquentモデルのsaveの戻り値falseはエラーではない

# はじめに
割とよく以下のようなコードを見かけるのですがエラーは例外になるはずなのでfalse返すのはどんなとき?と疑問を持ったので調べてみました。

```php
if (!$model->save()) {
    throw new Exception("データの保存に失敗しました");
}
```

# 結論

falseを返すのはsaving, creating, updatingイベントのハンドラがfalseを返すことで保存がキャンセルされたとき。
それ以外の時はtrueが返る。

DBへの書き込み失敗はそもそも例外になるのでsave()が値を返すことはない。

# コードを読む
save()関数は以下のようになっていました。
```php
public function save(array $options = [])
    {
        $this->mergeAttributesFromCachedCasts();

        $query = $this->newModelQuery();

        // If the "saving" event returns false we'll bail out of the save and return
        // false, indicating that the save failed. This provides a chance for any
        // 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->isDirty() ?
                $this->performUpdate($query) : true;
        }

        // 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);

            if (! $this->getConnectionName() &&
                $connection = $query->getConnection()) {
                $this->setConnection($connection->getName());
            }
        }

        // If the model is successfully saved, we need to do a few more things once
        // that is done. We will call the "saved" method here to run any actions
        // we need to happen after a model gets successfully saved right here.
        if ($saved) {
            $this->finishSave($options);
        }

        return $saved;
    }
```

この中にreturn文は2つあります。

ひとつめはsavingモデルイベントがfalseを返した時です。
```php
        if ($this->fireModelEvent('saving') === false) {
            return false;
        }
```
savingイベントはモデルを保存しようとしたときに呼ばれるイベントです。マニュアルには書いているところが見つけられませんでしたが、コメントによればfalseを返すと保存をキャンセルできるということでした。キャンセルした場合はsave()は保存を行わずにfalseを返します。

もう一つは末尾の
```php
        return $saved;
```
です。$savedの値を設定しているところは2つあって、モデルが新規に作られてdb上に存在しないときと、dbから読み込まれて作られた場合とで分岐しています。
## DB上に存在するとき
```php
            $saved = $this->isDirty() ?
                $this->performUpdate($query) : true;
```
読み込んだ時と値が変更されているかどうかで違って、変更されていればperformUpdate()の戻り値、変更されていなければ固定でtrueになります。

performUpdateは以下のようになっています。
```php
 protected function performUpdate(Builder $query)
    {
        // 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->usesTimestamps()) {
            $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->syncChanges();

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

        return true;
    }
```
updatingイベントがfalseを返した時は書き込みをキャンセルしてfalseを、それ以外の時はupdateを行ってtrueを返します。

## 新規の場合
```php
 $saved = $this->performInsert($query);
```
performInsertの戻り値がそのまま$savedに入ります。
performInsertは以下のようになっています。
```php
    protected function performInsert(Builder $query)
    {
        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->usesTimestamps()) {
            $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->getAttributesForInsert();

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

        // If the table isn't incrementing we'll simply insert these attributes as they
        // are. These attribute arrays must contain an "id" column previously placed
        // there by the developer as the manually determined key for these models.
        else {
            if (empty($attributes)) {
                return true;
            }

            $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->wasRecentlyCreated = true;

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

        return true;
    }
```
例によってcreatingイベントの戻り値がfalseだと保存がキャンセルされてfalseを返します。
それ以外はtrueです。

ただ注意すべき点がひとつあって、idが自動インクリメントでないときは、保存すべき$attributeが空の時はinsertを行わないようになっています。idになるカラムの値は必ずあるはずだから空になることはあり得ないのでというようなことがコメントには書いてあります。しかしidになるカラム以外のカラムに値があった場合はidがなくても通しなので、なんとなく片手落ちのような? Eloquentモデルを使う場合PKは必要で、PKはnullを許容しないので値がなければどうせエラーになるから特別扱いしなくていいんじゃないかと思うのですが、どうしてこうしているんだろう?