入力フォームの実装で結構苦労した話

# 自己紹介
職業プログラマ歴約半年、PHP歴約半年、Laravelの存在は入社直前に知った、ひよっこプログラマです。Qiitaへの投稿はこれが初めてです。暖かい目で見てただければ幸いです。

# 背景
会社のサービスで使うフレームワークをLaravelにするということで、とある入力フォームの移植をすることになった。「項目数も少ないしそんなに時間かからないだろう」と思いながらやっていたところ、意外と厄介なことに気づいて、最終的に色々な手法を組み合わせて無理やり仕上げることになった。
この記事はそのダイジェスト版である。(実際のところもっと苦戦したり頓珍漢な実装になりかけたりしている)

# 仕様

- DB
    - tableX：ユーザーアカウントと一対一関係にある**が、存在しないことがある**
- 入力ページ
    - ユーザーアカウントでログインしている時のみアクセスできる = アクセスされる時、アクセス者に対応するユーザーアカウントが必ず存在している
    - ユーザーアカウントに紐づくtableXの情報が存在していれば規定値が入力されたフォームを表示する
    - ユーザーアカウントに紐づくtableXの情報が存在しなければ空のフォームを表示する
    - 確認ページに向かうボタンがある
    - 確認ページから戻ってきた際入力値が保持されている
- 確認ページ
    - 入力ページのフォームの内容が表示される
    - 入力ページに戻るボタン・内容を確定するボタンがある
    - **`<input type="hidden">`で内容を保持するのは禁止**(改竄対策などの為)

# どう実装したか

## tableXの情報を取得する

まずは以下のような感じに書いた。(ルーティングやミドルウェアについては省略)

```App\User.php
// ...
    public function tableX(): HasOne
    {
        return $this->hasOne('App\TableX', 'userno')
    }
// ...
```

```App\Http\Controllers\SomeformController.php
Class SomeformController extends Controller
{
    public function edit()
    {
        $tableX = Auth::user()->tableX();
        return view('someform.edit', ['tableX' => $tableX]);
    }
}
```

```html:resources\views\someform\edit.blade.php
{{-- ... --}}
<form method="..." action="...">
  @csrf
  <div>
    <h4>項目A</h4>
    <input name="item_a" value="{{ $tableX->item_a }}" />
  </div>
  {{-- ... --}}
  <input type="submit" name="confirm" value="確認する" />
</form>
{{-- ... --}}
```

このように実装した結果……

```
ErrorException (E_ERROR)
Trying to get property 'item_a' of non-object
```

無事:innocent:
原因は上にも書いた**`…が、存在しないこともある`**である。次のように書き換えて解消した。

```html:resources\views\someform\edit.blade.php
  {{-- ... --}}
    <input name="item_a" value="{{ $tableX->item_a ?? '' }}"/>
  {{-- ... --}}
```

:innocent:な時`$tableX`は`null`で、通常の箇所で`$tableX->item_a`と書くと`Trying to get property 'b' of non-object`と出てくるところだが、null合体演算子`??`の左辺に書いた場合は`isset`の時のように上手く処理してくれるようだった。

## 確認画面に入力値を表示させる

かなり苦戦した覚えがあるが、最終的に次のように書いたところ動作するようになった。

```App\Http\Controllers\SomeformController.php
// ...
    public function confirm(Request $request)
    {
        $request->flash();
        return view('someform.confirm');
    }
// ...
```

```html:resources\views\someform\confirm.blade.php
<div>
  <div>
    <h4>項目A</h4>
    <div>{{ old('item_a') }}</div>
  </div>
  {{-- ... --}}
  <form method="..." action="...">
    @csrf
    <input type="submit" name="submit" value="保存"/>
  </form>
  <form method="..." action="...">
    <input type="submit" name="back" value="戻る">
  </form>
</div>
```

`old()`と`$request->flash()`の使い方を何となく理解できたようなできていないような……。

## 確認画面から戻った時に入力値がクリアされないようにする

ここまでのコードだと、戻るボタンを押した時に入力値がリセットされてしまう:innocent:
ここも最適解を見つけるまでに結構な時間が掛かったような覚えがあるが、最終的に次のように書くことで解決できた。

```html:resources\views\someform\edit.blade.php
{{-- ... --}}
    <input name="item_a" value="{{ old('item_a', $tableX->item_a ?? '') }}"/>
{{-- ... --}}
```

## 保存処理を実装する

ここまで書いてしまえばあとは保存処理を実装するだけ、楽勝！

```App\Http\Controllers\SomeformController.php
// ...
    public function submit()
    {
        $tableX = Auth::user()->tableX;
        $tableX->fill(old())->save();
        return redirect('...');
    }
// ...
```

```
Symfony \ Component \ Debug \ Exception \ FatalThrowableError (E_ERROR)
Call to a member function fill() on null
```

:innocent: :innocent: :innocent:

原因は最初にも出てきた**`…が、存在しないこともある`**だった。
最終的に次のように書いて解決した。

```App\Http\Controllers\SomeformController.php
// ...
    public function submit()
    {
        $tableX = TableX::firstOrNew(['userno' => Auth::user()->id]);
        $tableX->fill(old())->save();
        return redirect('...');
    }
// ...
```

「`TableX`テーブルと`User`テーブルは関連付いているから`User`モデルのオブジェクトから`TableX`オブジェクトを……」という考えに固執していると無意味に複雑なコードになってしまう。(実際なってしまっていた。)

# おわりに
本文中にもある通り、完成までにかなり苦戦しました。完成に至るまでに無意味に複雑で長いコードになって、「Laravelは厄介だ」と思うこともありました。しかし記事に起こしていると、`firstOrNew`関数など、煩雑な記述を解消する為のツールがLaravelによって既に用意されていることも分かってきました。そういったツールを使いこなせるよう、これからも勉強していきたい次第です。

ここまでお読みくださり、ありがとうございました。