form objectでupdate機能を実装した話

## 何をしたか
インスタグラムを模したアプリを作っています。
下記のように、ユーザーに紐づく投稿があり、それがさらに複数のimageを持っている構造になっています。

[![Image from Gyazo](https://i.gyazo.com/3ff5532b162755ce3983340414e8fc9a.png)](https://gyazo.com/3ff5532b162755ce3983340414e8fc9a)

こちらの構造で、`form object`を使って投稿の新規作成フォームを作ったのはよかったのですが、

▼こちらの記事で実装しています
[form objectを使ってネストしたフォームにcarrierwaveで画像を保存した話。](https://qiita.com/tanutanu/items/81c96d4fe9befef19fd0)

そこからさらに、`update`のフォームを作成するのにかなり苦労をしてしまったので、書いたコードをまとめておこうと思います。

なお、実行環境は下記の通りです。

- `Rails 5.2.3`
- `Ruby 2.6.0`

また、画像の投稿には`carrier wave`を使用しています。

## 書いたコード
実際に書いたコードを紹介して、そこに説明を加えていこうと思います。

### controller

`form object`で生成したインスタンスを、@post_formの形でビューに渡しています。

```ruby:controllers/posts_controller.rb
class PostsController < ApplicationController
  before_action :require_login, only: %i(new edit)
  before_action :set_post, only: %i(edit update)

  def new
    @post_form = PostForm.new(current_user)
  end

  def create
    @post_form = PostForm.new(current_user, post_params, post: Post.new)
    if @post_form.save!
      redirect_to root_path
      flash[:success] = "投稿しました"
    else
      flash.now[:danger] = "投稿に失敗しました"
      render :new
    end
  end

  def edit
    @post_form = PostForm.new(current_user, post: @post)
  end

  def update
    @post_form = PostForm.new(current_user, post_params, post: @post)
    if @post_form.save!
      redirect_to root_path
      flash[:success] = "投稿を編集しました"
    else
      flash.now[:danger] = "投稿の編集に失敗しました"
      render :edit
    end
  end

  private

  def post_params
    params.require(:post).permit(:body, photoes: [])
  end

  def set_post
    @post = Post.find(params[:id])
  end
end
```

### view

ビューでは、`model: @post_form`の形でインスタンスを受け取ります。（実際には`new`と`edit`で同じパーシャルを使いまわしていたため、`@post_form`は`form`というローカル変数にしました。）

```Ruby:views/posts/_post_form.html.slim
= form_with model: @post_form, local: true do |f|
  .form-group
    = f.label :photoes, t('activerecord.models.images.photo'), class: 'bmd-label-floating"'
    = f.file_field :photoes, multiple: true, class: 'form-control mb-1'
  .form-group
    = f.label :body, t('activerecord.models.posts.body'), class: 'bmd-label-floating"'
    = f.text_field :body, class: 'form-control'
  = f.submit '登録する', class: 'btn btn-raised btn-success'
```

### form object

```ruby:forms/post_form.rb
class PostForm
  include ActiveModel::Model

  attr_accessor :body, :photoes, :user
  validates :body, presence: true

  def initialize(user, params = {}, post: '')
    @post ||= Post.new
    @post.assign_attributes({user: user, body: params[:body]})
    super(params)
  end

  def to_model # 解説します
    @post
  end

  def save!
    return false if invalid?
    if photoes
      photoes.each do |photo|
        @post.images.build(photo: photo).save!
      end
    end

    @post.save! ? true : false
  end
end
```

ポイントは、`to_model`で`form object`にモデルのような振る舞いをさせているところです。どういうことかというと、`form object`はActiveRecordのモデルとは異なる「ただのクラス」であるため、Railsが新規作成・編集フォームでデフォルトで推測してくれている、ルーティングのパスがうまく適用されません。

そのため`to_model`で`form object`があたかもモデルのように思い込ませることで、Railsのデフォルトのルーティングが`form object`でも使える様にします。

この辺りの実装は、この記事を大変参考にさせていただきました。

[Rails: Form Objectと`#to_model`を使ってバリデーションをモデルから分離する（翻訳）](https://techracho.bpsinc.jp/hachi8833/2018_03_02/51350)


### [未解決]updateアクションに起こっていた課題。

ところが、この実装をしても`update`アクションに関しては、`POST 'posts/:id'`にアクセスしようとしてしまい、うまくルーティングをたどれませんでした。

それに対し、ネットでは以下の２つの解決策を見つけることができました。

- [`form`のURLを上書き](https://stackoverflow.com/questions/18410021/how-can-i-use-a-form-object-for-the-edit-update-routines/28657098)する。
- [`delegate`を使って適用させるアクションを変更](https://product-development.io/posts/rails-design-pattern-form-objects)する。

私の場合、前者はコードが冗長になりすぎるのと、後者はうまく動作させることができなかったので、仕方なしですが`routes.rb`を下記のように書き換えて対応しました。

```ruby:config/routes.rb
post '/posts/:id', to: 'posts#update'
```

モンキーパッチ感が否めないですが、今回はこれで良しとします...。もう少し実力が上がったらリファクタリングしてみたいです。

## 感想など

仕事の実装では、`form object`のupdateメソッドは実装の時間が取れず諦めていたのですが、これで実装に向けた糸口が見つかりました:relaxed:
社内でも共有できるぐらい知見を高めていきたいです:sparkles: