ユーザが選択した値を保持するoptionタグ作成方法 - foreach / old() / Null合体演算子 -【Laravel】

#まえがき

・先日、検索機能実装のお仕事を頂いたのですが、その中で結構時間がかかった部分を紹介したいと思います。
**「セレクトボックス（optionタグ）で選択した値を、ページ遷移先でも選択状態にする」**

「え？なにが難しいの？」
そう思う方もいらっしゃるでしょう…

「以下のようなコードで実現できますよ！！」

なんてうたってるサイト上に数あるコード、片っ端から全て試しましたが出来ませんでした。
問題となったコード3つと解決したコード１つの計4つのコードで発生した問題と解決方法を解説していきたいと思います。

#目次

- *環境*
- *要件定義*
- *発生した問題点*
- *問題となったコード(時系列順)*
- *解決したコード*
- *解説*
- *参考文献*


#環境

- PHP 7.4.15
- Laravel 6.20.16


#要件定義

**以下の要件定義を実現する上で詰まりました**

- configディレクトリに定数用のディレクトリ・ファイルを作成、そこにセレクトボックス用のデータ配列を定義
    - 参考：*[Laravelで定数をつかうよ](https://qiita.com/clustfe/items/4fa169c6d5114a9b6f4e)*
- 検索結果を表示した時に、検索条件を保持し、選択されたセレクトボックスの文字列を保持
- ページネーションで他ページに遷移した場合も検索条件、検索結果を保持
    - その他
        - 未選択時は null
            - Undefined variable エラーを回避

#発生した問題点

**1. ページ遷移時に選択した値が保持されず空欄になる**
**2. 空欄ではないが選択した値とは異なる値が表示される**
**3. 表示に成功したがユーザが選択した値を if 文の条件に使用しているため、値の未選択時に Undefined variable エラー**




#問題となったコード（時系列順）

**1. ページ遷移時に選択した値が保持されず空欄になる**

```php:blade.php
<select name="term">
    @foreach (config('const.term') as $termNumber)
        <option value="{{ $termNumber }}" @if(old('term') == $termNumber) selected @endif>{{ $termNumber }}</option>
    @endforeach
</select>
```

**2. 空欄ではないが選択した値とは異なる値が表示される**

```php:blade.php
<select name="term">
    @foreach (config('const.term') as $index => $termNumber)
        <option value="{{ $termNumber }}" @if(old('term', $index) == $termNumber) selected @endif>{{ $termNumber }}</option>
    @endforeach
</select>

```

**3. 表示に成功したが、ユーザが選択した値を if 文の条件に使用しているために未選択時に Undefined variable エラー**

```php:blade.php
<select name="term" id="term" class="mr-2">
    @foreach (config('const.term') as $termNumber)
        <option value="{{ $termNumber }}" @if(old('term', $inputTerm) == $termNumber) selected @endif>{{ $termNumber }}</option>
    @endforeach
</select>
```


`@foreach (config('const.term') as $index)`で使用している
`config('const.term')`は下記のコードです。

```php:config/const.php
<?php
    return [
        'term' => [
            '' => '', 
            '1' => '1', 
            '2' => '2', 
            '3' => '3', 
            '4' => '4', 
            '5' => '5', 
            '6' => '6', 
            '7' => '7', 
            '8' => '8', 
            '9' => '9', 
            '10' => '10', 
            '11' => '11', 
            '12' => '12', 
            '13' => '13', 
            '14' => '14', 
            '15' => '15', 
            '16' => '16', 
            '17' => '17', 
            '18' => '18', 
            '19' => '19', 
            '20' => '20'
        ],
    ];
```

`@if(old('term', $inputTerm)`の`$inputTerm`はセレクトボックスで選択した`term`の値を request オブジェクト内から取得して変数に代入したものです。以下の controller 内のメソッドにて定義し blade に渡しています。

```php:controller
    /**
     *  検索結果画面表示
     * 
     * @param  App\Article  $article
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function search(Article $article, Request $request)
    {
        $inputTerm = $request->term;
        $inputCategory = $request->category;
        $inputWord = $request->word;
        〜中略〜
        $articles = $article->getBySearchParameters($inputTerm, $inputCategory, $inputWord);
        return view('articles.index', compact('inputTerm', 'inputCategory', 'inputWord', 'articles'));
    }
```


#解決したコード

```php:blade.php
<select name="term" id="term" class="mr-2">
    @foreach (config('const.term') as $termNumber)
        <option value="{{ $termNumber }}" @if(old('term', $inputTerm ?? '') == $termNumber) selected @endif>{{ $termNumber }}</option>
    @endforeach
</select>
```
コードの変遷と変更箇所（２箇所）は以下

```php:blade.php

1. ページ遷移時に選択した値が保持されず空欄になる
@foreach (config('const.term') as $termNumber)
2. 空欄ではないが選択した値とは異なる値が表示される
@foreach (config('const.term') as $index => $termNumber)
3. 表示に成功したがユーザが選択した値を if 文の条件に使用しているため、未選択時に Undefined variable エラー
4. 解決したコード
@foreach (config('const.term') as $termNumber)


1. ページ遷移時に選択した値が保持されず空欄になる
<option value="{{ $termNumber }}" @if(old('term') == $termNumber) selected @endif>{{ $termNumber }}</option>
2. 空欄ではないが選択した値とは異なる値が表示される
<option value="{{ $termNumber }}" @if(old('term', $index) == $termNumber) selected @endif>{{ $termNumber }}</option>
3. 表示に成功したがユーザが選択した値を if 文の条件に使用しているため、未選択時に Undefined variable エラー
<option value="{{ $termNumber }}" @if(old('term', $inputTerm) == $termNumber) selected @endif>{{ $termNumber }}</option>
4. 解決したコード
<option value="{{ $termNumber }}" @if(old('term', $inputTerm ?? '') == $termNumber) selected @endif>{{ $termNumber }}</option>
```


#解説

**1. ページ遷移時に選択した値が保持されず空欄になる**

option 毎に
「この選択肢を選んでいれば、それをselectedにする」
という if 文条件を記述していますが、結果は空欄。
繰り返し処理で出力された値とユーザが選択した値が一致することで、その値を selected に出来ると思ったのですが、上手いこと一致しませんでした。

```php:blade.php
<select name="term">
    @foreach (config('const.term') as $termNumber)
        <option value="{{ $termNumber }}" @if(old('term') == $termNumber) selected @endif>{{ $termNumber }}</option>
    @endforeach
</select>
```

**2. 空欄ではないが選択した値とは異なる値が表示される**

[Laravel 5.8 ヘルパ old()](https://readouble.com/laravel/5.8/ja/helpers.html#method-old)
>old()
old関数はセッションにフラッシュデーターとして保存されている直前の入力値を取得します。

old関数は第2引数に記述した値を、初期値として設定できます。つまり、
name 属性に設定された値が old() に存在すれば value に入力され、nullの場合は第2引数に記述した値がされます。これでいける気がするのですが、保持される値は、何を選んでも「２０」でした。正直どんな処理でこの結果になるのか未だによく分かりません…

```php:blade.php
<select name="term">
    @foreach (config('const.term') as $index => $termNumber)
        <option value="{{ $termNumber }}" @if(old('term', $index) == $termNumber) selected @endif>{{ $termNumber }}</option>
    @endforeach
</select>

```

**3. 表示に成功したがユーザが選択した値を if 文の条件に使用しているために未選択時に Undefined variable エラー**

ユーザが選択した値を request オブジェクトから取得し old関数の第２引数に設定します。
これこそユーザが選択した値です。
ユーザが選択した値と繰り返し処理で順番に巡ってきた値とが == で true になればユーザが選択した値がselected されます！
やったー！！できたー！！と思いましたがまだ不完全です。

「ユーザが選択した値がない場合」
つまり
「ユーザが値を選択しなかった場合」

では`$inputTerm`は 未定義で null ですらありません。Undefined variable 、いわゆる未定義エラーが発生します。
どういった場面かというと、サイト接続時の初回画面表示時です。
初めて訪問するページでは、old関数で初期値に設定した値が未定義状態になるコードです。


```php:blade.php
<select name="term" id="term" class="mr-2">
    @foreach (config('const.term') as $termNumber)
        <option value="{{ $termNumber }}" @if(old('term', $inputTerm) == $termNumber) selected @endif>{{ $termNumber }}</option>
    @endforeach
</select>
```

**4. 解決したコード**

上記で発生した Undefined variabl エラーを解消するためには、初期値として設定した`$inputTerm`が未定義の際、
代わりに null になるようにすれば良いのです。
これを実現したのが

**「null合体演算子」**

です。

`$inputTerm`が falsy (null or 0 or undefined or false) な値の場合、`??`以降の値にしてくれます。
ここは三項（エルビス）演算子では対応できません。Undefined variable が発生します
また、以下のような特徴もあります。

- `A ?? B`の A の存在チェックもしてくれます。`isset`いらなくなるのかな？
- A が falsy の場合のみBを参照するので、`??`は falsy なものだけをフィルタリングしてくれる様なイメージです。
- ネストすることも可能。1つ目がなければ2つ目、2つ目がなければ3つ目のような記述
    - ` (old('term', $inputTerm ?? '$inputWord' ?? '$inputCategory' ?? '')`

参考はこちら：*[三項演算子](http://php.net/manual/ja/language.operators.comparison.php#language.operators.comparison.ternary)　[Null 合体演算子](http://php.net/manual/ja/language.operators.comparison.php#language.operators.comparison.coalesce)*

```php:blade.php
<select name="term" id="term" class="mr-2">
    @foreach (config('const.term') as $termNumber)
        <option value="{{ $termNumber }}" @if(old('term', $inputTerm ?? '') == $termNumber) selected @endif>{{ $termNumber }}</option>
    @endforeach
</select>
```

これで要件定義を満たすことが出来ました！



#参考文献

- [三項演算子](http://php.net/manual/ja/language.operators.comparison.php#language.operators.comparison.ternary)
- [Null 合体演算子](http://php.net/manual/ja/language.operators.comparison.php#language.operators.comparison.coalesce)
- [似てるようで違う、PHPのエルビス演算子とNull合体演算子](https://qiita.com/jay-es/items/3b8734bc02070d074a3e)
- [【PHP】知らなかった比較演算子【null合体演算子】](https://se-tomo.com/2019/03/02/%E3%80%90php%E3%80%91%E7%9F%A5%E3%82%89%E3%81%AA%E3%81%8B%E3%81%A3%E3%81%9F%E6%AF%94%E8%BC%83%E6%BC%94%E7%AE%97%E5%AD%90%E3%80%90null%E5%90%88%E4%BD%93%E6%BC%94%E7%AE%97%E5%AD%90%E3%80%91/#_ltgt)
- [PHPのNull合体演算子でUndefined indexを防ぐ](https://zenn.dev/suzu819/articles/d7312a36ae868b)
- [Laravelで定数をつかうよ](https://qiita.com/clustfe/items/4fa169c6d5114a9b6f4e)
- [Laravelで定数を使うときにConfigを使う腰抜けはもう死んだ](https://zenn.dev/aoi_avant/articles/19ca83d8ae67d7)
- [【Laravel】 都道府県のプルダウン作る](https://zakkuri.life/laravel%E3%81%A7%E9%83%BD%E9%81%93%E5%BA%9C%E7%9C%8C%E3%81%AE%E3%83%97%E3%83%AB%E3%83%80%E3%82%A6%E3%83%B3%E4%BD%9C%E3%82%8B/)
- [Laravelで都道府県のプルダウンメニューを作ってみる](https://qiita.com/sakuraya/items/42fffe0f171d49ee74a0)
- [配列を使ったプルダウンの選択肢の表示の理屈について](https://teratail.com/questions/51230)
- [Laravelのoldヘルパーが空の場合の初期値...](https://qiita.com/namizatork/items/f94180e38c63071e02d8)
- [bladeのフォーム欄で使うold関数 | laravel](https://mutimutisan.com/old-laravel)
- [oldヘルパー関数の初期値](http://ktykwsk.com/old%E3%83%98%E3%83%AB%E3%83%8F%E3%82%9A%E3%83%BC%E9%96%A2%E6%95%B0%E3%81%AE%E5%88%9D%E6%9C%9F%E5%80%A4/)
- [Laravel 5.8 ヘルパ old()](https://readouble.com/laravel/5.8/ja/helpers.html#method-old)
- [Laravelのold()で配列を扱う場合のインデックスの表現方法](https://itneko.com/laravel-old/)