yujiro's blog

エンジニアリング全般の事書きます

【Rails】Form Object を作って Fat Model 解消に近づく

題名の通り、Form Object ですが、こんな感じで実装してるよってのを紹介したいと思います。

下記の例では GeneralUser モデルというユーザー名属性を持ったモデルと(他の属性は下記のForm Object では扱いません。)

GeneralUserAccount という銀行口座情報を格納するためのモデルを用いて説明します。

general_user が general_user_account を has_one している状態です。

まず Form Object です。

class AccountForm
  include Virtus.model
  include ActiveModel::Model

  attr_accessor :general_user, :general_user_account, :username

  def initialize(general_user)
    @general_user = general_user
    @general_user.build_general_user_account unless @general_user.general_user_account
    self.attributes = @general_user.attributes
    self.general_user_account = @general_user.general_user_account if @general_user.general_user_account
  end

  def assign_attributes(params = {})
    @params = params
    general_user.assign_attributes(general_user_params)
    general_user.general_user_account.assign_attributes(general_user_account_params)
  end

  def save
    if !general_user.save
      add_errors(general_user)
      return false
    end
    true
  end

  private

  def general_user_params
    @params.require(:account_form).permit(:username)
  end

  def general_user_account_params
    @params.require(:account_form).require(:general_user_account).permit(:financial_institution_name, :branch_name, :branch_code,  :account_number, :account_title, :account_type)
  end

  def add_errors(obj) 
    obj.errors.messages.keys.each do |key, value|
      obj.errors.messages[key].each do |msg|
        errors.add(key, msg)
      end
    end
  end

end

ポイントは何点かあるんだけど、まず

self.attributes = @general_user.attributes

の部分。

これはVirtus の機能なんだけど AccountForm クラスのインスタンスに GeneralUserモデルの属性をぶちこみます。

つまり

account_form = AccountForm.new

としたら

account_form.username

でGeneralUserモデルの属性にアクセスできる状態になる。

has_one している GeneralUserAccount モデルのほうは

 self.general_user_account = @general_user.general_user_account if @general_user.general_user_account

という風に直接ぶちこむ。 こうすることで

普通にModelをnewしてform_for や form_fields に渡すのと同じように扱い、値を展開できる。

コントローラは以下の感じ。

  def edit
    @account_form = AccountForm.new(current_user)
  end

  def update
    @account_form = AccountForm.new(current_user)
    @account_form.assign_attributes(params)
    if !@account_form.save
      flash.now[:alert] =  '編集に失敗しました。'
      render action: 'edit'
      return
    end
    redirect_to ({:action => 'edit'}), :notice => '編集しました。'
  end

普通にmodel を使うような形でキレイに書けます。

それで View は以下

= form_for @account_form, url: {action: 'update'} do |f|
  = f.fields_for :general_user_account, f.object.general_user_account do |ch_f|
    = f.text_field :username
      - if @account_form.errors.messages[:username].present?
        - @account_form.errors.messages[:username].each do |m|
          = m
    = ch_f.text_field :financial_institution_name
      - if @account_form.errors.messages[:"general_user_account.financial_institution_name"].present?
        - @account_form.errors.messages[:"general_user_account.financial_institution_name"].each do |m|
          = m
...

ちなみに 「実践Ruby-Rails-4-現場のプロから学ぶ本格Webプログラミング」 のコードでは Virtus のようなものは使わずに form_for のしたに fields_for を使って実装してた

つまり同じことを上記の例ですると

= form_for @account_form, url: {action: 'update'} do |f|
  = f.fields_for :general_user_account, f.object.general_user do |ff|
    = ff.fields_for :general_user_account, ff.object.general_user_account do |fff|
      = ff.text_field :username
..
      = fff.text_field :financial_institution_name

..

みたいな感じになります。