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. 05Strong Parameters とステータスコード

活用シーン

  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. Before アクション

パターン: 共通のセットアップロジックを、対象アクションを明示して切り出します。

# 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 - 1 つのリソースを表示:

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. Strong Parameters

パターン: 許可する属性を private メソッドで定義します。

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
ネストされたリソースのコントローラー

子リソースの親が 1 種類だけの場合に使います。子リソースが複数の異なる親に紐づく可能性がある場合(コメント、レポート、複製、添付ファイルなど)は、親ごとにこれをコピーしないでください。代わりに 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 に抽出
  • 1つのコントローラーで両方の遷移に対応
  • テストが容易
一括操作コントローラー

使用する場面: 複数のレコードを一度に操作する場合(一括送信、一括削除、一括アーカイブ)です。

パターン: 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] } を使用します

エージェントへの指示

新しいコントローラーの生成
  1. コントローラーの種類を特定します:

    • 標準的な CRUD → Simple CRUD パターンを使用
    • 状態遷移 → Namespaced State パターンを使用
    • 一括操作 → Bulk Operation パターンを使用
    • 1つの親の下にあるネストされたリソース → Nested Resource パターンを使用
    • 複数の親を持つ可能性がある子リソース → polymorphic-parent-resources スキルを確認
  2. パターンを適用します:

    • 適切なテンプレートから始めます
    • 認可(authorize、policy_scope)を追加します
    • strong parameters を定義します
    • 明示的にスコープを指定した before_actions を追加します
    • 正しいステータスコードとフラッシュメッセージを使用します
  3. 規約に従います:

    • 名前:ResourcesController または Resources::StatesController
    • 継承元:ApplicationController
    • インスタンス変数:@resource(単数形)、@resources(複数形)
    • プライベートメソッド:set_resource、resource_params
既存のコントローラーのレビュー

確認項目:

  • すべてのリソース操作に認可がある
  • only:/except: を指定した before_action がある
  • strong parameters を使用している(params[] に直接アクセスしない)
  • バリデーション失敗時のステータスが :unprocessable_content になっている
  • フラッシュメッセージに一貫性がある:Successfully [Action] [Resource]
  • 適切な命名規約に従っている
  • 状態のバリデーションがメインアクションではなく before_actions にある
  • 読み込む親だけが異なる兄弟コントローラーがない(polymorphic-parent-resources を参照)

優先順位: セキュリティ(認可、パラメーター)→ RESTful パターン → ステータスコード → メッセージ

コメント