Laravelの中間テーブル値を更新する

# 目的
ユーザーがグループに所属する際に役割（role）を任意で設定できる
※roleはマスターでよくね？はひとまず放っておいて…

実行バージョンは **Laravel 8.57.0**

ドキュメントにもなかったので、書置きです！

## 構成例

Userテーブル

| id | email | name |
|:-:|:-:|:-:|
| 1 | a@example.com | tanaka |
| 2 | b@example.com | saitou |
| 3 | a2@example.com | chiba |

Groupテーブル

| id | name |
|:-:|:-:|
| 1 | Aグループ |
| 2 | Bグループ |

中間テーブル

| id | group_id | user_id | role |
|:-:|:-:|:-:|:-:|
| 1 | 1 | 1 | リーダー |
| 2 | 2 | 2 | グループ長 |


## モデル
```php:App\Models\User.php
use App\Models\Group;

class User extends Authenticatable
{

  ~諸々略~

  public function groups()
  {
    return $this->belongsToMany(Group::class);
  }
}

```
Groupモデルは割愛します。

---

結果だけで良い！って人は[中間テーブルの値を更新（本題！）](#中間テーブルの値を更新（本題！）)へ！

上記のような構成の場合、まず

## 中間テーブルへの追加（紐づける）
`chiba`さんを`Aグループ`の`サポーター`にする時

```php
$user_id = 3; // chiba
$group_id = 1; // Aグループ

User::find(3)->groups()->attach($group_id, ['role' => 'サポーター']);
```

で、この`tanaka`さん`Aグループ`で`サポーター`という役割につくことができました。
昇格の時、経歴として考え、再度`attach`メソッドでも良いかもですが、今回は更新します。

## updateExistingPivotメソッド

公式だと、
`updateExistingPivot`メソッドがぱっと目に入りました。
[Eloquent:リレーション 8.x Laravel 中間テーブルのレコード更新](https://readouble.com/laravel/8.x/ja/eloquent-relationships.html?header=%25E4%25B8%25AD%25E9%2596%2593%25E3%2583%2586%25E3%2583%25BC%25E3%2583%2596%25E3%2583%25AB%25E3%2581%25AE%25E3%2583%25AC%25E3%2582%25B3%25E3%2583%25BC%25E3%2583%2589%25E6%259B%25B4%25E6%2596%25B0)
でも、**既存の**って書いてる…

**横着したい…中間テーブル存在してなくても、作成して、存在してれば更新してほしい！！**
ってので、見つけたのが

## syncWithPivotValuesメソッド
[Eloquent:リレーション 8.x Laravel 関連の同期](https://readouble.com/laravel/8.x/ja/eloquent-relationships.html?header=%25E9%2596%25A2%25E9%2580%25A3%25E3%2581%25AE%25E5%2590%258C%25E6%259C%259F)
にある、`syncWithPivotValues`メソッド
>同期したモデルIDごとに同じ中間テーブルの値を挿入したい場合は、syncWithPivotValuesメソッドを使用できます。


### 落とし穴
上記の公式ドキュメントの参考ソース

```php
$user->roles()->syncWithPivotValues([1, 2, 3], ['active' => true]);
```

この通り最初の構成を例に書き換えると

```php
$user_id = 3; // chiba
$group_id = 1; // Aグループ

User::find(3)->groups()->syncWithPivotValues([$user_id], ['role' => 'サポーター']);
```

この結果…
`tanaka`さんが消えた！
リーダーがいなくなった！~~イェーイ！~~
なんてこった　~~責任取るやつ~~　旗振りがいないなんて！
第一引数で渡していない外部キー（この場合ユーザーのID）たちは`detach`される…
同期処理の中にあるからね…**同期**だからね（ショック）
同期系メソッドでも他を残せないか`syncWithoutDetaching`メソッドみたいに！って思ったけど、
公式ドキュメントには載ってなかった…
とりあえず上記の各メソッドのソースコード実際に見てみよ…で見つけれたのが次！

※ちなみに、`syncWithoutDetaching`メソッドは外部キーのみ対応しているので、シンプルな中間テーブルの場合これで事足りるはず。


## 中間テーブルの値を更新（本題！）
他はそのままで（上の件に書いてます！）、対象のみ（今回は`chiba`さん）を
`chiba`さんを`Aグループ`の`サポーター`から`サブリーダー`に更新する時

```php
$user_id = 3; // chiba
$group_id = 1; // Aグループ

User::find(3)->groups()->syncWithPivotValues($group_id, ['role' => 'サブリーダー'], false);
```

結論だけ言うと、第3引数`false`を利用するだけでした！
ドキュメントせめてデフォルト値あること書いてくれ！ってのと、改めてフレームワークの中身見るべきだなと…

※第1引数を配列でなくしているのは、下記の該当メソッド内で、`collect`メソッドが実行されているため、単一の場合、配列からの`collection`クラスとして処理されるためです。
`project\vendor\laravel\framework\src\Illuminate\Collections\Traits\EnumeratesValues.php`の`getArrayableItems`メソッドでその記述があります。


## 該当のメソッド

以下が該当のメソッド。
デフォルト値入ってますねー

```php:\vendor\laravel\framework\src\Illuminate\Database\Eloquent\Relations\Concerns\InteractsWithPivotTable.php
    /**
     * Sync the intermediate tables with a list of IDs or collection of models with the given pivot values.
     *
     * @param  \Illuminate\Support\Collection|\Illuminate\Database\Eloquent\Model|array  $ids
     * @param  array  $values
     * @param  bool  $detaching
     * @return array
     */
    public function syncWithPivotValues($ids, array $values, bool $detaching = true)
    {
        return $this->sync(collect($this->parseIds($ids))->mapWithKeys(function ($id) use ($values) {
            return [$id => $values];
        }), $detaching);
    }
```

で、このメソッド内の`sync`はこちら
こっちでもデフォルト値入ってますねー

```php:\vendor\laravel\framework\src\Illuminate\Database\Eloquent\Relations\Concerns\InteractsWithPivotTable.php
    /**
     * Sync the intermediate tables with a list of IDs or collection of models.
     *
     * @param  \Illuminate\Support\Collection|\Illuminate\Database\Eloquent\Model|array  $ids
     * @param  bool  $detaching
     * @return array
     */
    public function sync($ids, $detaching = true)
    {
        $changes = [
            'attached' => [], 'detached' => [], 'updated' => [],
        ];

        // First we need to attach any of the associated models that are not currently
        // in this joining table. We'll spin through the given IDs, checking to see
        // if they exist in the array of current ones, and if not we will insert.
        $current = $this->getCurrentlyAttachedPivots()
                        ->pluck($this->relatedPivotKey)->all();

        $detach = array_diff($current, array_keys(
            $records = $this->formatRecordsList($this->parseIds($ids))
        ));

        // Next, we will take the differences of the currents and given IDs and detach
        // all of the entities that exist in the "current" array but are not in the
        // array of the new IDs given to the method which will complete the sync.
        if ($detaching && count($detach) > 0) {
            $this->detach($detach);

            $changes['detached'] = $this->castKeys($detach);
        }

        // Now we are finally ready to attach the new records. Note that we'll disable
        // touching until after the entire operation is complete so we don't fire a
        // ton of touch operations until we are totally done syncing the records.
        $changes = array_merge(
            $changes, $this->attachNew($records, $current, false)
        );

        // Once we have finished attaching or detaching the records, we will see if we
        // have done any attaching or detaching, and if we have we will touch these
        // relationships if they are configured to touch on any database updates.
        if (count($changes['attached']) ||
            count($changes['updated']) ||
            count($changes['detached'])) {
            $this->touchIfTouching();
        }

        return $changes;
    }
```

### 余談
`syncWithoutDetaching`メソッドは、`sync`メソッドの第2引数を`false`にして実行しているだけ

```php:\vendor\laravel\framework\src\Illuminate\Database\Eloquent\Relations\Concerns\InteractsWithPivotTable.php
    /**
     * Sync the intermediate tables with a list of IDs without detaching.
     *
     * @param  \Illuminate\Support\Collection|\Illuminate\Database\Eloquent\Model|array  $ids
     * @return array
     */
    public function syncWithoutDetaching($ids)
    {
        return $this->sync($ids, false);
    }
```