mcpField
C
controller-patterns

controller-patterns

github.com/rolemodel开发743 次安装1 天前更新
代码仓库

关于 controller-patterns

用于生成和审查 Ruby on Rails 控制器的技能,面向构建 RESTful 应用端点的开发者。内容涵盖授权、嵌套资源、状态转换、批量操作、参数处理和响应规范。

主要功能

  1. 01RESTful CRUD 控制器模式
  2. 02Pundit 授权指导
  3. 03嵌套资源与状态转换控制器
  4. 04批量操作模式
  5. 05强参数与状态码规范

适用场景

  1. 01创建标准 CRUD 操作控制器
  2. 02为资源操作添加授权
  3. 03实现审批或激活状态转换
  4. 04审查嵌套资源和批量操作控制器

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/rolemodel/rolemodel-skills --skill controller-patterns

安装后在 Claude Code 中输入/controller-patterns确认已加载。

详细介绍

Rails 控制器模式

快速参考

何时使用此技能
  • 生成新的 Rails 控制器
  • 根据最佳实践审查现有控制器
  • 实现 RESTful 操作(index、show、new、create、edit、update、destroy)
  • 使用 Pundit 添加授权
  • 处理嵌套资源
  • 实现状态转换(提交、审批、激活)
  • 对资源执行批量操作
核心模式一览

标准 CRUD 控制器:

class ResourcesController < ApplicationController
  before_action :set_resource, only: %i[show edit update destroy]

  def index
    @resources = policy_scope(Resource)
  end

  def show; end

  def new
    @resource = authorize Resource.new
  end

  def create
    @resource = authorize Resource.new(resource_params)
    @resource.save ? redirect_to(@resource, notice: 'Successfully Created Resource') : render('new', status: :unprocessable_content)
  end

  def edit; end

  def update
    @resource.update(resource_params) ? redirect_to(@resource, notice: 'Successfully Updated Resource') : render('edit', status: :unprocessable_content)
  end

  def destroy
    @resource.destroy
    redirect_to resources_url, notice: 'Successfully Deleted Resource'
  end

  private

  def set_resource
    @resource = authorize Resource.find(params[:id])
  end

  def resource_params
    params.expect(resource: %i[attr1 attr2])
  end
end

命名空间状态控制器:

class Resources::StatesController < ApplicationController
  before_action :set_resource
  before_action :ensure_valid_state, only: :create

  def create
    @resource.activate!
    redirect_to resources_path, notice: 'Resource activated.'
  end

  def destroy
    @resource.deactivate!
    redirect_to resources_path, notice: 'Resource deactivated.'
  end

  private

  def set_resource
    @resource = current_user.resources.find(params[:resource_id])
  end

  def ensure_valid_state
    redirect_to(resources_path, alert: 'Invalid state') unless @resource.can_activate?
  end
end

决策树

Need to add controller functionality?
│
├─ Standard CRUD operations (list, view, create, edit, delete)?
│  └─ Use: Standard RESTful Controller Pattern
│
├─ State transitions (submit, approve, activate, publish)?
│  └─ Use: Namespaced State Controller (create/destroy actions)
│
├─ Bulk operations (bulk submit, bulk delete)?
│  └─ Use: Namespaced Bulk Controller (create action only)
│
├─ Nested under a single parent resource?
│  └─ Use: Nested RESTful Controller Pattern
│
├─ Child resource that hangs off several different parents?
│  (comments, reports, duplications, attachments)
│  └─ Use: polymorphic-parent-resources skill
│     (one route concern + one controller, not one controller per parent)
│
└─ Complex authorization rules?
   └─ Add: Policy scopes and explicit authorization checks

基本模式

1. 授权(Pundit)

模式: 使用 authorize 或 policy_scope 授权所有资源交互。

# Collections - use policy_scope
def index
  @products = policy_scope(Product)
end

# New instances - authorize the class
def new
  @product = authorize Product.new
end

# Existing instances - authorize in set method
def set_product
  @product = authorize Product.find(params[:id])
end

规则:

  • 对集合(index)使用 policy_scope()
  • 对新记录(new、create)使用 authorize ClassName.new()
  • 在 set_* 方法中对现有记录调用 authorize
  • 绝不要跳过资源操作的授权
2. 前置操作

模式: 使用明确的操作范围提取通用的设置逻辑。

# Resource loading (most common)
before_action :set_product, only: %i[show edit update destroy]

# Parent resource loading (nested)
before_action :set_company
before_action :set_employee, only: %i[show edit update destroy]

# State validation
before_action :ensure_pending, only: :create
before_action :ensure_stopped, only: :create

规则:

  • 始终使用 only: 或 except:
  • 使用描述性名称:set_[resource]、ensure_[state]、require_[permission]
  • 顺序很重要——按声明顺序执行
  • 每个方法只负责一项职责

状态验证示例:

def ensure_pending
  return if @resource.pending?
  redirect_to resources_path, alert: 'Must be pending.'
end
3. RESTful 操作模式

Index - 列出所有资源:

def index
  @resources = policy_scope(Resource)
end

Show - 显示单个资源:

def show
  # Resource set via before_action
  # Load scoped associations if needed
  @related = policy_scope(@resource.related_items)
end

New - 新建资源的表单:

def new
  @resource = authorize Resource.new
end

Create - 保存新资源:

def create
  @resource = authorize Resource.new(resource_params)
  if @resource.save
    redirect_to @resource, notice: 'Successfully Created Resource'
  else
    render :new, status: :unprocessable_content
  end
end

Edit - 编辑现有资源的表单:

def edit
  # Resource set via before_action
end

Update - 保存对资源的更改:

def update
  if @resource.update(resource_params)
    redirect_to @resource, notice: 'Successfully Updated Resource'
  else
    render :edit, status: :unprocessable_content
  end
end

Destroy - 删除资源:

def destroy
  @resource.destroy
  redirect_to resources_url, notice: 'Successfully Deleted Resource'
end
4. 强参数

模式: 在私有方法中定义允许的属性。

def resource_params
  params.expect(
    resource: [
      :simple_attr,
      :another_attr,
      nested_attrs: %i[id attr1 attr2 _destroy],
      array_attrs: [],
      multiple_ids: []
    ]
  )
end

规则:

  • 使用 params.expect(model: [...])
  • 嵌套属性:{nested_attrs: %i[id attr _destroy]}
  • 数组:{array_attr: []}
  • 对于更新,在嵌套属性中包含 :id;对于删除,包含 _destroy
5. HTTP 状态码
# Success - redirects (default 302, no status needed)
redirect_to @resource, notice: 'Success'

# Validation failure - render with unprocessable_content
render :new, status: :unprocessable_content    # 422
render :edit, status: :unprocessable_content   # 422

# Other statuses (rare in controllers)
head :no_content                               # 204
head :forbidden                                # 403
head :not_found                                # 404

规则:

  • 重定向无需显式指定状态码
  • 验证失败::unprocessable_content(422)
  • Turbo 需要正确的状态码来处理错误
6. Flash 消息

模式: 提供一致、友好的用户提示。

# Success (notice:)
redirect_to @product, notice: 'Successfully Created Product'
redirect_to @product, notice: 'Successfully Updated Product'
redirect_to products_url, notice: 'Successfully Deleted Product'

# Errors (alert:)
redirect_to products_path, alert: 'Must be pending to submit.'
redirect_to products_path, alert: 'Cannot delete active product.'

规则:

  • 格式:Successfully [Action] [Resource]
  • 成功时使用 notice:
  • 出现错误或警告时使用 alert:
  • 保持简洁,并以操作为中心
  • 资源名称使用标题式大小写
7. 命名约定
# Controllers
ProductsController < ApplicationController
Admin::ProductsController < Admin::BaseController
Products::SubmissionsController < ApplicationController

# Instance variables
@product, @user         # Singular for one resource
@products, @users       # Plural for collections

# Private methods
def set_product         # Resource loading
def product_params      # Strong parameters
def ensure_pending      # State validation
def require_admin       # Authorization check

完整示例

简单 CRUD 控制器
class ProductsController < ApplicationController
  before_action :set_product, only: %i[show edit update destroy]

  def index
    @products = policy_scope(Product)
  end

  def show; end

  def new
    @product = authorize Product.new
  end

  def create
    @product = authorize Product.new(product_params)
    if @product.save
      redirect_to @product, notice: 'Successfully Created Product'
    else
      render :new, status: :unprocessable_content
    end
  end

  def edit; end

  def update
    if @product.update(product_params)
      redirect_to @product, notice: 'Successfully Updated Product'
    else
      render :edit, status: :unprocessable_content
    end
  end

  def destroy
    @product.destroy
    redirect_to products_url, notice: 'Successfully Deleted Product'
  end

  private

  def set_product
    @product = authorize Product.find(params[:id])
  end

  def product_params
    params.expect(product: %i[name description price])
  end
end
嵌套资源控制器

仅当子资源恰好只有一种父资源时使用此模式。如果子资源可以归属于多种不同的父资源(评论、报告、重复项、附件),则不要为每种父资源复制此模式——请改为阅读 polymorphic-parent-resources 技能。

class OrderItemsController < ApplicationController
  before_action :set_order
  before_action :set_order_item, only: %i[show edit update destroy]

  def index
    @order_items = policy_scope(@order.order_items)
  end

  def new
    @order_item = authorize @order.order_items.build
  end

  def create
    @order_item = authorize @order.order_items.build(order_item_params)
    if @order_item.save
      redirect_to [@order, @order_item], notice: 'Successfully Created Order Item'
    else
      render :new, status: :unprocessable_content
    end
  end

  def update
    if @order_item.update(order_item_params)
      redirect_to [@order, @order_item], notice: 'Successfully Updated Order Item'
    else
      render :edit, status: :unprocessable_content
    end
  end

  def destroy
    @order_item.destroy
    redirect_to order_order_items_url(@order), notice: 'Successfully Deleted Order Item'
  end

  private

  def set_order
    @order = authorize Order.find(params[:order_id])
  end

  def set_order_item
    @order_item = authorize @order.order_items.find(params[:id])
  end

  def order_item_params
    params.expect(order_item: %i[product_id quantity price])
  end
end

常见错误

❌ 反面模式✅ 正确模式
@product = Product.new(product_params)@product = authorize Product.new(product_params)
render :new, status: :unprocessable_entityrender :new, status: :unprocessable_content
render :new(验证失败时)render :new, status: :unprocessable_content
@product = Product.new(params[:product])@product = Product.new(product_params)
params.require(:product).permit(:name)params.expect(product: %i[name])
redirect_to @product, notice: 'Product created!'
redirect_to @product, notice: 'Success!'
redirect_to @product, notice: 'Successfully Created Product'
before_action :set_product(未指定范围)before_action :set_product, only: %i[show edit update destroy]
为状态变更编写自定义操作使用带命名空间的控制器和 RESTful 操作
为每个父资源创建一个带命名空间的控制器(Estimates::CommentsController、Widgets::CommentsController)为所有父资源使用同一个控制器——参见 polymorphic-parent-resources 技能
在父控制器中添加 add_comment / create_report 操作使用单独的 RESTful 子控制器——参见 polymorphic-parent-resources 技能

高级模式

带命名空间的状态控制器

适用场景: 操作表示资源的状态转换(提交/撤回、激活/停用、批准/拒绝)。

模式: 在父资源下使用命名空间,并通过 create 和 destroy 处理状态变更。

# Controller: app/controllers/time_entries/submissions_controller.rb
class TimeEntries::SubmissionsController < ApplicationController
  before_action :set_time_entry
  before_action :ensure_valid_for_submission, only: :create
  before_action :ensure_submitted, only: :destroy

  def create
    @time_entry.update!(status: :submitted, submitted_at: Time.current)
    redirect_to time_entries_path, notice: 'Time entry submitted for approval.'
  end

  def destroy
    @time_entry.update!(status: :pending, submitted_at: nil)
    redirect_to time_entries_path, notice: 'Time entry unsubmitted.'
  end

  private

  def set_time_entry
    @time_entry = current_user.time_entries.find(params[:time_entry_id])
  end

  def ensure_valid_for_submission
    return if @time_entry.pending? && @time_entry.stopped?
    redirect_to time_entries_path, alert: 'Only stopped pending entries can be submitted.'
  end

  def ensure_submitted
    return if @time_entry.submitted?
    redirect_to time_entries_path, alert: 'Only submitted entries can be unsubmitted.'
  end
end

# Routes
resources :time_entries do
  resource :submission, only: [:create, :destroy], module: :time_entries
end

# Views
button_to time_entry_submission_path(@time_entry), method: :post    # Submit
button_to time_entry_submission_path(@time_entry), method: :delete  # Unsubmit

优点:

  • 符合 RESTful 风格(使用标准的 create/destroy 操作)
  • 文件组织清晰(controllers/time_entries/submissions_controller.rb)
  • 将验证逻辑提取到 before_actions 中
  • 一个控制器即可处理两种状态转换
  • 易于测试
批量操作控制器

适用场景: 一次操作多条记录(批量提交、批量删除、批量归档)。

模式: 使用仅包含 create 操作的带命名空间控制器,并在 before_actions 中进行验证。

# Controller: app/controllers/time_entries/bulk_submissions_controller.rb
class TimeEntries::BulkSubmissionsController < ApplicationController
  before_action :set_entries
  before_action :ensure_entries_present
  before_action :ensure_entries_valid

  def create
    @entries.update_all(status: TimeEntry.statuses[:submitted], submitted_at: Time.current)
    redirect_to time_entries_path, notice: "#{@entries.count} #{'entry'.pluralize(@entries.count)} submitted."
  end

  private

  def set_entries
    ids = params[:time_entry_ids] || []
    @entries = current_user.time_entries.where(id: ids)
  end

  def ensure_entries_present
    return if @entries.any?
    redirect_to time_entries_path, alert: 'No entries selected.'
  end

  def ensure_entries_valid
    invalid = @entries.reject { |e| e.pending? && e.stopped? }
    return if invalid.empty?
    redirect_to time_entries_path, alert: 'Only stopped pending entries can be submitted.'
  end
end

# Routes
resource :bulk_submissions, only: :create, module: :time_entries

# Views
form_with url: bulk_submissions_path, method: :post do |f|
  # checkboxes for time_entry_ids[]
end
show 中的作用域集合

适用场景: 显示包含多个相关集合的资源。

def show
  @active_projects = policy_scope(@company.projects.active)
  @archived_projects = policy_scope(@company.projects.archived)
  @team_members = policy_scope(@company.users)
end
复杂的嵌套属性

适用场景: 表单接受嵌套记录(has_many 关联)。

def product_params
  params.expect(
    product: [
      :name,
      :description,
      :price,
      images_attributes: %i[id url alt_text _destroy],
      variants_attributes: %i[id sku price stock_count _destroy],
      tags: [],
      category_ids: []
    ]
  )
end

要点:

  • 更新现有嵌套记录时包含 :id
  • 通过嵌套属性删除记录时包含 _destroy
  • 简单数组使用 { array_attr: [] }
  • 嵌套属性哈希使用 { nested_attrs: %i[attr1 attr2] }

Agent 指令

生成新控制器
  1. 确定控制器类型:

    • 标准 CRUD → 使用简单 CRUD 模式
    • 状态转换 → 使用带命名空间的状态模式
    • 批量操作 → 使用批量操作模式
    • 嵌套资源仅隶属于一个父资源 → 使用嵌套资源模式
    • 可能隶属于多个父资源的子资源 → 阅读 polymorphic-parent-resources 技能
  2. 应用模式:

    • 从合适的模板开始
    • 添加授权(authorize、policy_scope)
    • 定义强参数
    • 添加范围明确的 before_actions
    • 使用正确的状态码和 flash 消息
  3. 遵循约定:

    • 命名:ResourcesController 或 Resources::StatesController
    • 继承自:ApplicationController
    • 实例变量:@resource(单数)、@resources(复数)
    • 私有方法:set_resource、resource_params
审查现有控制器

检查以下事项:

  • 对所有资源操作进行授权
  • before_action 使用 only:/except:
  • 使用强参数(不直接访问 params[])
  • 验证失败时使用状态 :unprocessable_content
  • flash 消息一致:Successfully [Action] [Resource]
  • 遵循恰当的命名约定
  • 在 before_actions 中验证状态(而非在主操作中)
  • 不存在仅因加载的父资源不同而有所区别的同级控制器(参见 polymorphic-parent-resources)

优先级顺序: 安全性(授权、参数)→ RESTful 模式 → 状态码 → 消息

评论

支持 Markdown