controller-patterns について
Ruby on Rails のコントローラーを生成・レビューするスキルです。RESTful なエンドポイントを実装する開発者向けに、認可、ネストしたリソース、状態遷移、一括操作、パラメーター処理、レスポンスのパターンを示します。
主な機能
- 01RESTful CRUD コントローラーのパターン
- 02Pundit による認可の指針
- 03ネストしたリソースと状態遷移のコントローラー
- 04一括操作のパターン
- 05Strong Parameters とステータスコード
活用シーン
- 01標準的な CRUD 操作用コントローラーの作成
- 02リソース操作への認可の追加
- 03承認や有効化の状態遷移の実装
- 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_entity | render :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] }を使用します
エージェントへの指示
新しいコントローラーの生成
-
コントローラーの種類を特定します:
- 標準的な CRUD → Simple CRUD パターンを使用
- 状態遷移 → Namespaced State パターンを使用
- 一括操作 → Bulk Operation パターンを使用
- 1つの親の下にあるネストされたリソース → Nested Resource パターンを使用
- 複数の親を持つ可能性がある子リソース →
polymorphic-parent-resourcesスキルを確認
-
パターンを適用します:
- 適切なテンプレートから始めます
- 認可(
authorize、policy_scope)を追加します - strong parameters を定義します
- 明示的にスコープを指定した before_actions を追加します
- 正しいステータスコードとフラッシュメッセージを使用します
-
規約に従います:
- 名前:
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 パターン → ステータスコード → メッセージ