clails の Controller は Ruby on Rails の Controller を参考にした HTTP リクエストハンドラーです。 Controller は HTTP リクエストを受け取り、Model を使ってデータを処理し、View にデータを渡したり、JSON レスポンスを返したりします。
- Controller は HTTP メソッド(GET、POST、PUT、DELETE)ごとに処理を定義します
- Web アプリケーション用の
<web-controller>と REST API 用の<rest-controller>があります - ルーティングテーブルで URL パスと Controller を紐付けます
- URL パラメータは自動的に抽出され、Controller からアクセスできます
clails には3種類の Controller クラスがあります。
すべての Controller の基底クラスです。HTTP リクエストの基本的な処理を提供します。
(defclass <my-controller> (<base-controller>)
())HTML ビューをレンダリングする Web アプリケーション用の Controller です。
(defclass <my-web-controller> (<web-controller>)
())JSON などの構造化データを返す REST API 用の Controller です。
(defclass <my-api-controller> (<rest-controller>)
())(in-package #:your-app/controller)
(defclass <users-controller> (<web-controller>)
()
(:documentation "Users controller for managing user resources"))各 HTTP メソッドに対応するメソッドをオーバーライドします。
;; GET リクエストの処理
(defmethod do-get ((controller <users-controller>))
(let ((users (get-all-users)))
(set-view controller "users/index.html"
`(:users ,users))))
;; POST リクエストの処理
(defmethod do-post ((controller <users-controller>))
(let* ((name (param controller "name"))
(email (param controller "email"))
(user (create-user name email)))
(set-redirect controller "/users")))
;; PUT リクエストの処理
(defmethod do-put ((controller <users-controller>))
(let* ((id (param controller "id"))
(name (param controller "name"))
(user (update-user id name)))
(set-view controller "users/show.html"
`(:user ,user))))
;; DELETE リクエストの処理
(defmethod do-delete ((controller <users-controller>))
(let ((id (param controller "id")))
(delete-user id)
(set-redirect controller "/users")))HTML の <form> タグは GET と POST メソッドしかサポートしていません。
PUT や DELETE リクエストを HTML フォームから送信するには、_method パラメータを使用します。
PUT リクエストの例:
<form action="/users/123" method="POST">
<input type="hidden" name="_method" value="PUT">
<input type="text" name="name" value="John Doe">
<button type="submit">更新</button>
</form>DELETE リクエストの例:
<form action="/users/123" method="POST">
<input type="hidden" name="_method" value="DELETE">
<button type="submit">削除</button>
</form>clails は POST リクエスト内の _method パラメータをチェックし、以下のようにルーティングします:
_methodが"PUT"の場合 →do-putメソッドを呼び出し_methodが"DELETE"の場合 →do-deleteメソッドを呼び出し_methodが指定されていない場合 →do-postメソッドを呼び出し
これにより、HTML フォームから REST API のような操作が可能になります。
ルーティングテーブルで URL パスと Controller を紐付けます。
config/routes.lisp などでルーティングを定義します。
(in-package #:your-app/config)
(setf clails/environment:*routing-tables*
'(;; トップページ
(:path "/"
:controller "your-app/controller::<top-controller>")
;; ユーザー一覧・作成
(:path "/users"
:controller "your-app/controller::<users-controller>")
;; ユーザー詳細・更新・削除
(:path "/users/:id"
:controller "your-app/controller::<user-controller>")
;; ネストしたリソース
(:path "/users/:user-id/posts/:post-id"
:controller "your-app/controller::<user-posts-controller>")))
;; アプリケーション起動時に初期化
(clails/controller/base-controller:initialize-routing-tables)URL パス内の :parameter-name は自動的に抽出され、param 関数でアクセスできます。
;; ルート定義: "/users/:user-id/posts/:post-id"
;; アクセス例: GET /users/123/posts/456
(defmethod do-get ((controller <user-posts-controller>))
(let ((user-id (param controller "user-id")) ; => "123"
(post-id (param controller "post-id"))) ; => "456"
;; 処理...
))リクエストパラメータ(クエリパラメータ、POST データ、URL パラメータ)を取得します。
(defmethod do-get ((controller <search-controller>))
(let ((query (param controller "q"))
(page (param controller "page")))
;; 検索処理...
))POST リクエストのフォームデータも同様に取得できます。
(defmethod do-post ((controller <users-controller>))
(let ((name (param controller "name"))
(email (param controller "email"))
(age (parse-integer (param controller "age"))))
;; ユーザー作成処理...
))View テンプレートとデータを指定してレンダリングします。
(defmethod do-get ((controller <users-controller>))
(let ((users (execute-query
(query <user>
:as :user
:order-by ((:user :created-at :desc)))
'())))
;; View とデータを設定
(set-view controller "users/index.html"
`(:users ,users
:title "ユーザー一覧"))))View ファイルは app/views/ ディレクトリからの相対パスで指定します。
;; app/views/users/index.html を使用
(set-view controller "users/index.html" data)
;; app/views/admin/users/show.html を使用
(set-view controller "admin/users/show.html" data)View パスから自動的にパッケージ名が解決されます。
"index.html"→:your-app/views/package"users/show.html"→:your-app/views/users/package"admin/users/list.html"→:your-app/views/admin/users/package
指定したパスにリダイレクトします。
(defmethod do-post ((controller <users-controller>))
(let ((user (create-user "Taro" "taro@example.com")))
;; ユーザー作成後、一覧ページにリダイレクト
(set-redirect controller "/users")))絶対 URL(http:// または https:// で始まる)も指定できます。
(defmethod do-get ((controller <external-controller>))
;; 外部サイトにリダイレクト
(set-redirect controller "https://example.com/"))- HTTP ステータスコードは 302(Found)
- Location ヘッダーが自動的に設定されます
- 相対パスの場合は、リクエストのスキーム、ホスト、ポートから完全な URL が構築されます
(defclass <api-users-controller> (<rest-controller>)
()
(:documentation "REST API for user resources"))set-response メソッドで連想リストを設定します。
(defmethod do-get ((controller <api-users-controller>))
(let ((users (get-all-users)))
(set-response controller
`((:status . "success")
(:data . ,(mapcar #'user-to-alist users))))))
(defun user-to-alist (user)
`((:id . ,(ref user :id))
(:name . ,(ref user :name))
(:email . ,(ref user :email))))エラー時は適切なステータスコードとメッセージを返します。
(defmethod do-get ((controller <api-user-controller>))
(let* ((id (param controller "id"))
(user (find-user-by-id id)))
(if user
(set-response controller
`((:status . "success")
(:data . ,(user-to-alist user))))
(progn
(setf (slot-value controller 'code) 404)
(set-response controller
`((:status . "error")
(:message . "User not found")))))))(defmethod do-post ((controller <users-controller>))
(let ((user (create-user "Taro" "taro@example.com")))
;; 201 Created を設定
(setf (slot-value controller 'code) 201)
(set-response controller
`((:status . "success")
(:data . ,(user-to-alist user))))))(defmethod do-get ((controller <download-controller>))
(let ((file-content (read-file-content)))
;; カスタムヘッダーを設定
(setf (slot-value controller 'header)
`(:content-type "application/octet-stream"
:content-disposition "attachment; filename=\"data.csv\""))
;; レスポンスを設定
...))200- OK(デフォルト)201- Created(リソース作成成功)204- No Content(レスポンスボディなし)302- Found(リダイレクト)400- Bad Request(不正なリクエスト)404- Not Found(リソースが見つからない)500- Internal Server Error(サーバーエラー)
- ルーティング: URL パスから対応する Controller を検索
- インスタンス化: Controller のインスタンスを作成
- パラメータ設定: URL パラメータ、クエリパラメータ、POST データを設定
- メソッド呼び出し: HTTP メソッドに応じて
do-get、do-postなどを呼び出し - レスポンス生成: View のレンダリング、またはレスポンスデータの返却
Controller インスタンスには以下のスロットがあります。
request- HTTP リクエストオブジェクトenv- 環境変数(lack 環境)code- HTTP ステータスコード(デフォルト: 200)header- HTTP レスポンスヘッダー(plist)params- リクエストパラメータ(ハッシュテーブル)
view- View テンプレートのパス名view-data- View に渡すデータview-package- View レンダリング用のパッケージ名
response- レスポンスデータ(連想リスト)
(defclass <users-controller> (<web-controller>)
())
;; 一覧表示 (GET /users)
(defmethod do-get ((controller <users-controller>))
(let ((users (execute-query
(query <user>
:as :user
:order-by ((:user :name)))
'())))
(set-view controller "users/index.html"
`(:users ,users
:title "ユーザー一覧"))))
;; 作成 (POST /users)
(defmethod do-post ((controller <users-controller>))
(let* ((name (param controller "name"))
(email (param controller "email"))
(user (make-record '<user> :name name :email email)))
(if (save user)
(set-redirect controller (format nil "/users/~A" (ref user :id)))
(set-view controller "users/new.html"
`(:user ,user
:errors ,(get-errors user))))))
(defclass <user-controller> (<web-controller>)
())
;; 詳細表示 (GET /users/:id)
(defmethod do-get ((controller <user-controller>))
(let* ((id (param controller "id"))
(user (first (execute-query
(query <user>
:as :user
:where (:= (:user :id) :id))
`(:id ,id)))))
(if user
(set-view controller "users/show.html"
`(:user ,user))
(error '404/not-found))))
;; 更新 (PUT /users/:id)
(defmethod do-put ((controller <user-controller>))
(let* ((id (param controller "id"))
(user (first (execute-query
(query <user>
:as :user
:where (:= (:user :id) :id))
`(:id ,id))))
(name (param controller "name")))
(when user
(setf (ref user :name) name)
(save user))
(set-redirect controller (format nil "/users/~A" id))))
;; 削除 (DELETE /users/:id)
(defmethod do-delete ((controller <user-controller>))
(let* ((id (param controller "id"))
(user (first (execute-query
(query <user>
:as :user
:where (:= (:user :id) :id))
`(:id ,id)))))
(when user
(destroy user))
(set-redirect controller "/users")))(defmethod do-post ((controller <order-controller>))
(clails/model/transaction:with-transaction
(let* ((user-id (param controller "user-id"))
(product-id (param controller "product-id"))
(quantity (parse-integer (param controller "quantity")))
(product (find-product product-id)))
;; 在庫チェック
(unless (>= (ref product :stock) quantity)
(error "Not enough stock"))
;; 注文作成
(let ((order (make-record '<order>
:user-id user-id
:product-id product-id
:quantity quantity
:total-price (* (ref product :price) quantity))))
(save order)
;; 在庫更新
(setf (ref product :stock) (- (ref product :stock) quantity))
(save product)
;; 成功時のリダイレクト
(set-redirect controller (format nil "/orders/~A" (ref order :id)))))))(defclass <api-users-controller> (<rest-controller>)
())
;; 一覧取得 (GET /api/users)
(defmethod do-get ((controller <api-users-controller>))
(let ((users (get-all-users)))
(set-response controller
`((:status . "success")
(:count . ,(length users))
(:data . ,(mapcar #'user-to-json users))))))
;; 作成 (POST /api/users)
(defmethod do-post ((controller <api-users-controller>))
(let* ((name (param controller "name"))
(email (param controller "email"))
(user (make-record '<user> :name name :email email)))
(if (save user)
(progn
(setf (slot-value controller 'code) 201)
(set-response controller
`((:status . "success")
(:data . ,(user-to-json user)))))
(progn
(setf (slot-value controller 'code) 400)
(set-response controller
`((:status . "error")
(:errors . ,(get-errors-json user))))))))
(defun user-to-json (user)
`((:id . ,(ref user :id))
(:name . ,(ref user :name))
(:email . ,(ref user :email))
(:created-at . ,(format-datetime (ref user :created-at)))))(defmethod do-get ((controller <users-controller>))
(let* ((page (or (parse-integer (param controller "page") :junk-allowed t) 1))
(per-page 20)
(offset (* (1- page) per-page))
(users (execute-query
(query <user>
:as :user
:order-by ((:user :created-at :desc))
:limit per-page
:offset offset)
'()))
(total-count (count-users)))
(set-view controller "users/index.html"
`(:users ,users
:page ,page
:per-page ,per-page
:total-count ,total-count
:total-pages ,(ceiling total-count per-page)))))デフォルトの実装では、未定義の HTTP メソッドは 404/not-found エラーを発生させます。
;; do-get を実装しない場合、自動的に 404 エラー
(defclass <my-controller> (<base-controller>)
())
;; カスタム 404 エラー
(defmethod do-get ((controller <my-controller>))
(error '404/not-found :path (getf (env controller) :path-info)))エラーページを表示する専用の Controller を作成できます。
(defclass <error-controller> (<web-controller>)
())
(defmethod do-get ((controller <error-controller>))
(setf (slot-value controller 'code) 500)
(set-view controller "errors/500.html"
`(:message "Internal Server Error")))Controller は以下の責務のみを持つべきです。
- リクエストの検証: パラメータの存在チェック、型チェック
- Model の呼び出し: ビジネスロジックは Model に委譲
- レスポンスの構築: View やレスポンスデータの設定
複雑なビジネスロジックは Model や Service クラスに切り出します。
;; 悪い例: Controller にビジネスロジックを書く
(defmethod do-post ((controller <order-controller>))
(let ((product (find-product (param controller "product-id"))))
;; 複雑な計算や検証...
))
;; 良い例: Service クラスに切り出す
(defmethod do-post ((controller <order-controller>))
(let ((order-service (make-instance '<order-service>)))
(create-order order-service
:user-id (param controller "user-id")
:product-id (param controller "product-id")
:quantity (param controller "quantity"))))パラメータは必ず検証します。
(defmethod do-post ((controller <users-controller>))
(let ((name (param controller "name"))
(email (param controller "email")))
;; バリデーション
(unless (and name email)
(setf (slot-value controller 'code) 400)
(set-response controller
`((:status . "error")
(:message . "Name and email are required")))
(return-from do-post))
;; 処理続行...
))REST API は RESTful な設計原則に従います。
GET /users- 一覧取得GET /users/:id- 詳細取得POST /users- 作成PUT /users/:id- 更新DELETE /users/:id- 削除
clails の Controller は以下の特徴を持ちます。
- シンプルな設計: HTTP メソッドごとにメソッドを定義するだけ
- 柔軟なルーティング: URL パラメータの自動抽出とパターンマッチング
- View の統合:
set-viewによる簡単な View レンダリング - REST API サポート:
<rest-controller>による JSON レスポンスの返却 - トランザクション対応: Model と連携したトランザクション管理
詳細な API リファレンスについては、各関数の docstring を参照してください。