作業日誌2

前書き

今回やりたかったことはstaffの新規登録をしようとするとログイン認証がかかる→companyのパスワードでログイン→ログインしたcompanyを外部キーとして新規staffをcreateという流れ。その過程で学習したこととハマったところをまとめる。話題が飛んだりエラーの解決法はわかったけど、原因がわからなかったりする。わかったら追記する。

学習ポイント

フィルタとログイン認証

ルートでstaff/newをリクエストするとstaffコントローラのnewメソッドに飛ぶが

before_action :check_logined, only: [:new, :edit, :update, :destroy]

のようにフィルタをかけているので、check_loginedの処理に入る。check_loginedの部分(フィルタのメソッド)は通常privateメソッドとして定義する。before_actionメソッドでオプションonly: で指定したメソッドに先立ってcheck_loginedを実行させることができる。

    # app/controllers/staff_controller.rb
    
    def check_logined
    
          # sessionに:companyがあるかどうかで条件分岐
    
          if session[:company] then
    
            # begin...endで囲むと...の部分が最低一回は繰り返される。
    
            begin
    
              # 新たにインスタンス変数(テンプレート変数)を作る
    
              @company = Company.find(session[:company])
    
            # rescueで例外処理
    
            rescue ActiveRecord::RecordNotFound
    
              reset_session
    
            end
              
          end
          # companyがなければ、、
          unless @company
            # flashメソッドを使って現在地へのパスをredirect_toの先に渡している。
            flash[:referer] = request.fullpath
            # redirect_toメソッドで引数urlの位置に処理を飛ばすことができる。
            # ここではloginコントローラのindexを指定している。
            # もしコントローラクラスにindexがあればその処理を行うが
            # ない場合はviews/login/index.html.erbを表示。
            redirect_to controller: :login, action: :index
          end
        end

予めcompanyログインをしてない場合、unless以下の処理に入る。redirect_toは 処理を引数のurlに移動させるメソッド。引数はurlぽくなくてハッシュにしている。これはurl_forと同じ指定方法。

url_forメソッド

ルート定義から動的にurlを生成する。これ自体を使用する機会は多くはないが、引数やオプションの指定の仕方はredirect_toやform_tagでのurlのして方法とも共通するので理解を深めておくと便利。いずれもurlを動的に生成するための指定の仕方。

オプション 概要
controller コントローラー
action アクション名
host ホスト名(現在のホストを上書き)
protocol プロトコル名(現在のプロトコルを上書き)
anchor アンカー名
only_path 相対パスを返すかどうか。hostの指定がない場合、defaultはtrue
trailing_slash 末尾にスラッシュをつけるかどうか。
user http認証に使用するユーザ名
password http認証に使用するパスワード

上のredirect_toではこのようにした。

    redirect_to controller: :login, action: :index

この場合はloginコントローラのindexアクションを指定している。生成されるurlは/login/index。なのでroutes.rbにもget 'login/index'が必要。この場合は現在地がstaffコントローラの中にいるのでcontroller: :loginを明示的に指定する必要がある。redirect_toの先をみてみる。

    # app/views/login/index.html.erb
    <p style="color: Red"><%= @error %></p>
    # 
    <%= form_tag action: :auth do %>
      <div class="field">
        <%= label_tag :name, '企業名' %><br />
        <%= text_field_tag :name, '', size: 20 %>
      </div>
      <div class="field">
        <%= label_tag :password, '企業パスワード' %><br />
        <%= password_field_tag :password, '', size: 20 %>
      </div>
      <%= hidden_field_tag :referer, flash[:referer] %>
      <%= submit_tag 'ログイン' %>
    <% end %>

対して↑のform_tagのアクションのpost先は

    form_tag action: :auth

のようにアクションパラメータのみ指定している。こうするとまず各メソッドが現在地を基点にurlを生成するためコントローラはデフォルトでloginが指定される(loginディレクトリのviewだから)。具合的に生成されるurlは/login/authとなる。form_tagはデフォルトでpostメソッドでパラメータをurl先に渡すためroutes.rbにはpost 'login/auth'の定義がないといけない。次にloginコントローラのauth。

     def auth
      # paramsで受け取った:nameでCompanyインスタンスを作る。
       company = Company.find_by(name: params[:name])
      # companyインスタンスが作られていて、かつauthenticateできれば
       if company && company.authenticate(params[:password]) then
         #reset_session
         #セッションにcompanyの主キーを入れる。
         session[:company] = company.id
        # :refererにはflash[:referer]が入っているためnewの画面に戻れる。
         redirect_to params[:referer]
       else
         flash.now[:referer] = params[:referer]
         @error = 'ユーザ名/パスワードが間違っています。'
         render 'index'
       end
     end

    redirect_to params[:referer]

の:refererはapp/controllers/staff_controller.rbで flash[:referer] = request.fullpathとしてflashメソッドで定義しているものが次のapp/views/login/index.html.erbで

    <%= hidden_field_tag :referer, flash[:referer] %>

で:refererキーでpostされているものを受け取っている。なので中身はrequest.fullpathということになるがこれはその地点のルートからのパスが戻り値になるので、実際には/staff/newが入っていることになる。つまり、ログイン認証で入れなかった場所にとぶということ。その前の無事パスワード認証をクリアした時点でsession[:company]にさっき作ったcompanyインスタンスのidが格納されている。

セッション

セッションとはユーザ単位でデータを管理するための仕組み。

    session[:name] = value

のようにデータを保存する。sessionにキーを持たせてそこに値を入れる。取り出すときも

    @name = session[:name]

のようにする。セッションの寿命はデフォルトではブラウザを閉じるまでである。

    session[:name] = nil  #特定のキーのセッションを消去
    reset_session         #全てのsession情報を削除

のようにして記録を消すことができる。

テンプレート変数と部分テンプレート

/staff/newでコントローラに行くと改めてフィルターであるcheck_loginedがかけられる。今度はsession[:compaby]を持っているのでそこに格納されている主キーを元にテンプレート変数@companyが作られる。 viewメソッドでは@staff = Staff.newでstaffインスタンスが初期化され、staffs/new.htmlに行き、そこで部分テンプレートの_formが呼び出される。ここが超ハマったところ。

    <%= render 'form', staff: @staff, company: @company %>

ここでとても 重要なのは@company(や@staff)を変数経由で部分テンプレートに渡すということ。

例えば部分テンプレートが

    # app/views/staffs/_form.html.erb
    <%= form_with(model: staff, local: true) do |form| %>
    <div class="field">
    <%= form.hidden_field :company_id, :value => company.id %>
    </div>
    <div class="actions">
        <%= form.submit %>
    </div>
    <% end %>

となっている場合、company.idがいつでも参照できる。もし仮にcompany: @company の定義をせずに部分テンプレートで

    :value => @company.id

のように直接 @companyを参照しようとすると、staffs_controllerのcheck_loginedで@companyを作ってからそのままメインのテンプレートを経由して部分テンプレートまできた場合にはテンプレート変数@companyが生きている。しかしそれをpostした時に@companyの値がnilになってしまう。どのルートを経てもその値を保持するためにはメインのテンプレートで引数に入れておくことが重要になる。なぜnilになるのかはわからない。インスタンス変数を経由してインスタンスの値にアクセスできるのが最初にてviewを描画したときだけ、ということ何だと思うが逆にcompany: @companyならおっけーなの何で?renderが何かしているのかもしれない。

2018/08/19 追記
結局コントローラでsaveした時にformの値を見ることになるようでこの時にsaveするコントローラは@companyの出どころを確認しない。なのでcompanyパラメータを経由して@companyを参照できるようにしておくと良いということだと思う。formを描画するときとpostを受け取る時はそれを扱うコントローラが違うので変数の扱いも違うということ。

参考:Railsの部分テンプレートからインスタンス変数を参照するのはやめよう。 | CreativeStyle


submitするときは

    <%= form_with(model: staff, local: true) do |form| %>

のform_withメソッドによりURLとスコープが自動推測される。この場合はurlとして

    action="/staffs"

が自動生成され、formの元として渡しているモデルがメインテンプレートのstaff: @staffによって@staffになっており、@staffはstaff/newを通った時に@staff = Staff.newで初期化されているのでform_withによってcreateアクションが要求される。すでにこの時にはパーシャルに@companyを入れた場合それがnilになっているので考えられる仮説としてはsubmitした後に再度テンプレートが描画されるタイミングがあって、その際に@companyを格納している変数を参照することはできるが、コントローラーで定義したテンプレート変数は参照することができない、ということが起こっているのかもしれない。よくわからないが、コントローラで生成したテンプレート変数はメインのテンプレート上で別の変数に格納した上で部分テンプレートに渡すのが重要だということがわかった。

感想

やっぱりコントローラーのことが全然わかってない。やっぱり『Ruby on Rails5 アプリケーションプログラミング』の第6章早くやるべき!