控制器
介绍
控制器是组件结构,实现一个或多个处理器。它们是表示层(HTTP/REST)的一部分。它们的职责是:
- 从请求中获取数据并正确格式化:原始请求数据被转换为 DTO。
- 将其发送给处理业务逻辑并返回 DTO 的服务。
- 使用服务返回的结果构建 HTTP 响应。
因此,控制器本身不应实现业务逻辑。相反,它们依赖于服务。
每个功能或资源应有自己的包。例如,如果您有一个处理用户注册、用户档案等的控制器,您应该创建一个 http/controller/user
包。
INFO
因为控制器是组件,它们通过组合 goyave.Component
可以访问服务器的所有基本资源。
以下是一个简单 CRUD 控制器的完整示例。示例中不包含服务实现、DTO 结构和验证规则集。
go
// http/controller/product/product.go
package product
import (
"context"
"net/http"
"strconv"
"goyave.dev/goyave/v5"
"goyave.dev/goyave/v5/database"
"goyave.dev/goyave/v5/util/typeutil"
"my-project/dto"
"my-project/service"
)
type Service interface {
GetByID(ctx context.Context, id int64) (*dto.Product, error)
Paginate(ctx context.Context, page int, pageSize int) (*database.PaginatorDTO[*dto.User], error)
Create(ctx context.Context, createDTO *dto.CreateProduct) (*dto.Product, error)
Update(ctx context.Context, id int64, updateDTO *dto.UpdateProduct) error
Delete(ctx context.Context, id int64) error
}
type Controller struct {
goyave.Component
ProductService Service
}
func (ctrl *Controller) Init(server *goyave.Server) {
ctrl.ProductService = server.Service(service.Product).(Service)
ctrl.Component.Init(server)
}
func (ctrl *Controller) RegisterRoutes(router *goyave.Router) {
subrouter := router.Subrouter("/products")
subrouter.Get("/", ctrl.Index).ValidateQuery(IndexRequest)
subrouter.Post("/", ctrl.Create).ValidateBody(CreateRequest)
subrouter.Get("/{productID:[0-9+]}", ctrl.Show)
subrouter.Patch("/{productID:[0-9+]}", ctrl.Update).ValidateBody(UpdateRequest)
subrouter.Delete("/{productID:[0-9+]}", ctrl.Delete)
}
func (ctrl *Controller) Index(response *goyave.Response, request *goyave.Request) {
query := typeutil.MustConvert[*dto.Index](request.Query)
paginator, err := ctrl.ProductService.Paginate(request.Context(), query.Page.Default(1), query.PerPage.Default(20))
if response.WriteDBError(err) {
return
}
response.JSON(http.StatusOK, paginator)
}
func (ctrl *Controller) Show(response *goyave.Response, request *goyave.Request) {
productID, err := strconv.ParseInt(request.RouteParams["productID"], 10, 64)
if err != nil {
response.Status(http.StatusNotFound)
return
}
user, err := ctrl.ProductService.GetByID(request.Context(), productID)
if response.WriteDBError(err) {
return
}
response.JSON(http.StatusOK, user)
}
func (ctrl *Controller) Create(response *goyave.Response, request *goyave.Request) {
createDTO := typeutil.MustConvert[*dto.CreateProduct](request.Data)
product, err := ctrl.ProductService.Create(request.Context(), createDTO)
if response.WriteDBError(err) {
return
}
response.JSON(http.StatusCreated, map[string]int64{"id": product.ID})
}
func (ctrl *Controller) Update(response *goyave.Response, request *goyave.Request) {
productID, err := strconv.ParseInt(request.RouteParams["productID"], 10, 64)
if err != nil {
response.Status(http.StatusNotFound)
return
}
updateDTO := typeutil.MustConvert[*dto.UpdateProduct](request.Data)
err = ctrl.ProductService.Update(request.Context(), productID, updateDTO)
response.WriteDBError(err)
}
func (ctrl *Controller) Delete(response *goyave.Response, request *goyave.Request) {
productID, err := strconv.ParseInt(request.RouteParams["productID"], 10, 64)
if err != nil {
response.Status(http.StatusNotFound)
return
}
err = ctrl.ProductService.Delete(request.Context(), productID)
response.WriteDBError(err)
}
- 首先,我们定义
Service
接口,它代表控制器的依赖项。 - 在
Init()
中,我们从服务器的服务容器中获取它:server.Service(service.Product).(Service)
。Init()
是使用router.Controller()
时由框架自动调用的函数。还有另一种注入依赖的方法,在下面解释。了解更多关于服务的信息请点击这里。 - 在
Update
和Delete
的末尾不需要添加response.Status(http.StatusNoContent)
,因为如果响应体为空且未设置状态,框架会自动将响应状态设置为204
。 - 设置
Content-Type
标头不是必需的。response.Write
会自动检测内容类型并相应设置标头,如果后者尚未定义。
注意
为了使用包含已解析请求体的 request.Data
,您需要添加解析中间件。
go
import "goyave.dev/goyave/v5/middleware/parse"
router.GlobalMiddleware(&parse.Middleware{})
命名约定
- 控制器包根据它们主要使用的资源命名,使用单数形式。例如,
Product
模型的控制器将称为http/controller/product
。如果控制器与资源无关,则给它一个表达性强的名称。 - 为了避免重复,控制器结构总是命名为
Controller
。这样,从主路由注册器创建控制器是清晰且易于阅读的:&product.Controller{}
。 - 控制器处理器总是导出的。所有不是处理器的函数必须是非导出的。
- CRUD 操作命名和路由:
方法 | URI | 处理器名称 | 描述 |
---|---|---|---|
GET | /product | Index() | 获取产品列表 |
POST | /product | Create() | 创建产品 |
GET | /product/{id} | Show() | 显示产品 |
PATCH | /product/{id} | Update() | 更新产品 |
PUT | /product/{id} | Upsert() | 创建或替换产品 |
DELETE | /product/{id} | Delete() | 删除产品 |
依赖注入
有两种方法可以将依赖项注入控制器。对于这两种方法,建议使用接口来定义依赖项,而不是服务类型本身。这消除了直接的代码依赖,并有助于编写测试。
使用服务容器
此方法非常适合与 controller.Init()
、controller.RegisterRoutes()
和 router.Controller()
一起使用。控制器将从框架提供的服务容器中获取其依赖项。
go
// http/controller/product/product.go
type Controller struct {
goyave.Component
ProductService Service
}
func (ctrl *Controller) Init(server *goyave.Server) {
ctrl.ProductService = server.Service(service.Product).(Service)
ctrl.Component.Init(server)
}
go
// http/route/route.go
func Register(server *goyave.Server, router *goyave.Router) {
router.Controller(&product.Controller{})
}
TIP
- 注意我们使用接口进行类型断言,而不是实际的服务类型。
- 了解更多关于服务以及如何注册它们的信息,请参阅专用部分。
使用构造函数
如果您不想使用 controller.Init()
、router.Controller()
,并且您更喜欢在主路由注册器内部注册路由,而不是在控制器旁边,为您的控制器定义一个构造函数,该函数将其所有依赖项作为参数:
go
// http/controller/product/product.go
type Controller struct {
goyave.Component
ProductService Service
}
func NewController(server *goyave.Server, productService Service) *Controller {
ctrl := &Controller{
ProductService: productService,
}
ctrl.Init(server)
return ctrl
}
go
// http/route/route.go
func Register(server *goyave.Server, router *goyave.Router) {
//...
{
ctrl := product.NewController(server, productService)
productRouter := router.Subrouter("/products")
productRouter.Get("/{productID:[0-9+]}", ctrl.Show)
//...
}
}