define a helper method like devise's current_user

定义一个 helper method。

背景

在工作中遇到的一个问题:

navbar的item根据是否存在项目来决定是否显示,也就是写一个类似current_user的helper method current_project,要求当current_project存在时,navbar中的dropdown组件【以project为item】显示current_project的name,navbar的item则显示该project下的plans,issues等等,当current_project为nil时,则不显示project的任何信息。

其中,project与plan,issue存在一对多的关系:

#project.rb
class Project < ApplicationRecord
  has_many :plans, dependent: :destroy
  has_many :issues, dependent: :destroy
end

#plan.rb
class Plan < ApplicationRecord
  belongs_to :project
end

#issue.rb
class Issue < ApplicationRecord
  belongs_to :project
end

我最初在module ApplicationHelper中定义了一个helper method,算是勉强实现了功能,但是写的极为难看,是通过request.original_fullpath()来判断请求中是否存在project, 如果存在,则找到current_project。而且这个helper method因为定义在ApplicationHelper中,便只能在views中使用。后面老大教了个好方法,真心觉得很赞,记录下大致的解决方法。

解决

devise的current_user的定义,较为复杂,在它的源代码define_helpers中可以看到:

def current_#{mapping}
  @current_#{mapping} ||= warden.authenticate(scope: :#{mapping})
end

以我目前的水平,真心看的不是很明白,在stack overflow看到了一个比较简单的定义:

#application_controller.rb
def current_user
  @current_user ||= User.find_by_id!(session[:user_id])
end
helper_method :current_user

参考这个,我们可以定义一下current_project:

#application_controller.rb
def current_project
  @current_project ||= Project.find(params[:project_id])
end
helper_method :current_project

这样看着,貌似很简单就解决了,但是有一个问题,那就是所有的request中,project_id并不一定存在,这样使用Project.find(params[:project_id])就有可能报错,显示params中找不到project_id。

老大教的方法是这样的,重新定义了一个controller,然后通过继承,覆写current_project。具体如下:

Step1、在application_controller.rb定义current_project:

#application_controller.rb
def current_project
  nil
end
helper_method :current_project

注意,这里的current_project返回的是nil值,这样所有继承自ApplicationController的Controller,其current_project默认为nil。

Step2、新建一个BaseProjectController, 继承自ApplicationController,重新定义current_project,覆盖掉父类的method:

#base_project_controller.rb
class BaseProjectController < ApplicationController
  def current_project
    @current_project ||= Project.find(params[:project_id])
  end
end

Step3、修改IssuesController和PlansController,让它们继承自BaseProjectController,拥有current_project方法:【以IssuesController为例,PlansController以此类推】

## 修改前:
class IssuesController < BaseProjectController
  load_and_authorize_resource :project
  .....
end

## 修改后:
class IssuesController < BaseProjectController
  before_action -> { @project = current_project }
  authorize_resource :project
  .....
end

这里删除了cancancan的load_authorize,改用before_action的方式,将current_project赋给了@project。

对于没有继承自BaseProjectController的Controller,其current_project则为nil,不得不说,这样方式真的是漂亮。

OK!

参考

devise

What do helper and helper_method do?