13.3 Designing controllers

Most traditional MVC frameworks are based on suffix action mapping. Nowadays, the REST style web architecture is becoming increasingly popular. One can implement REST-style URLs by filtering or rewriting them, but why not just design a new REST-style MVC framework instead? This section is based on this idea, and focuses on designing and implementing a controller based, REST-style MVC framework from scratch. Our goal is to simplify the development of web applications, perhaps even allowing us to write a single line of code capable of serving "Hello, world".

The controller's role

The MVC design pattern is currently the most used framework model for web applications. By keeping Models, Views and Controllers separated, we can keep our web applications modular, maintainable, testable and extensible. A model encapsulates data and any of the business logic that governs that data, such as accessibility rules, persistence, validation, etc. Views serve as the data's representation and in the case of web applications, they usually live as templates which are then rendered into HTML and served. Controllers serve as the "glue" logic between Models and Views and typically have methods for handling different URLs. As described in the previous section, when a URL request is forwarded to a controller by the router, the controller delegates commands to the Model to perform some action, then notifies the View of any changes. In certain cases, there is no need for models to perform any kind of logical or data processing, or for any views to be rendered. For instance, in the case of an HTTP 302 redirect, no view needs to be rendered and no processing needs to be performed by the Model, however the Controller's job is still essential.

RESTful design in Beego

The previous section describes registering route handlers with RESTful structs. Now, we need to design the base class for a logic controller that will be composed of two parts: a struct and interface type.

type Controller struct {
    Ct        *Context
    Tpl       *template.Template
    Data      map[interface{}]interface{}
    ChildName string
    TplNames  string
    Layout    []string
    TplExt    string
}

type ControllerInterface interface {
    Init(ct *Context, cn string) //Initialize the context and subclass name
    Prepare()                    //some processing before execution begins
    Get()                        //method = GET processing
    Post()                       //method = POST processing
    Delete()                     //method = DELETE processing
    Put()                        //method = PUT handling
    Head()                       //method = HEAD processing
    Patch()                      //method = PATCH treatment
    Options()                    //method = OPTIONS processing
    Finish()                     //executed after completion of treatment
    Render() error               //method executed after the corresponding method to render the page
}

Then add the route handling function described earlier in this chapter. When a route is defined to be a ControllerInterface type, so long as we can implement this interface, we can have access to the following methods of our base class controller.

func (c *Controller) Init(ct *Context, cn string) {
    c.Data = make(map[interface{}]interface{})
    c.Layout = make([]string, 0)
    c.TplNames = ""
    c.ChildName = cn
    c.Ct = ct
    c.TplExt = "tpl"
}

func (c *Controller) Prepare() {

}

func (c *Controller) Finish() {

}

func (c *Controller) Get() {
    http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405)
}

func (c *Controller) Post() {
    http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405)
}

func (c *Controller) Delete() {
    http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405)
}

func (c *Controller) Put() {
    http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405)
}

func (c *Controller) Head() {
    http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405)
}

func (c *Controller) Patch() {
    http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405)
}

func (c *Controller) Options() {
    http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405)
}

func (c *Controller) Render() error {
    if len(c.Layout) > 0 {
        var filenames []string
        for _, file := range c.Layout {
            filenames = append(filenames, path.Join(ViewsPath, file))
        }
        t, err := template.ParseFiles(filenames...)
        if err != nil {
            Trace("template ParseFiles err:", err)
        }
        err = t.ExecuteTemplate(c.Ct.ResponseWriter, c.TplNames, c.Data)
        if err != nil {
            Trace("template Execute err:", err)
        }
    } else {
        if c.TplNames == "" {
            c.TplNames = c.ChildName + "/" + c.Ct.Request.Method + "." + c.TplExt
        }
        t, err := template.ParseFiles(path.Join(ViewsPath, c.TplNames))
        if err != nil {
            Trace("template ParseFiles err:", err)
        }
        err = t.Execute(c.Ct.ResponseWriter, c.Data)
        if err != nil {
            Trace("template Execute err:", err)
        }
    }
    return nil
}

func (c *Controller) Redirect(url string, code int) {
    c.Ct.Redirect(code, url)
}    

Above, the controller base class already implements the functions defined in the interface. Through our routing rules, the request will be routed to the appropriate controller which will in turn execute the following methods:

Init() initialization routine 
Prepare() pre-initialization routine; each inheriting subclass may implement this function
method() depending on the request method, perform different functions: GET, POST, PUT, HEAD, etc. Subclasses should implement these functions; if not implemented, then the default is 403
Render() optional method. Determine whether or not to execute according to the global variable "AutoRender"  
Finish() is executed after the action been completed. Each inheriting subclass may implement this function 

Application guide

Above, we've just finished discussing Beego's implementation of the base controller class. We can now use this information to design our request handling, inheriting from the base class and implementing the necessary methods in our own controller.

package controllers

import (
    "github.com/astaxie/beego"
)

type MainController struct {
    beego.Controller
}

func (this *MainController) Get() {
    this.Data["Username"] = "astaxie"
    this.Data["Email"] = "[email protected]"
    this.TplNames = "index.tpl"
}

In the code above, we've implemented a subclass of Controller called MainController which only implements the Get() method. If a user tries to access the resource using any of the other HTTP methods (POST, HEAD, etc), a 403 Forbidden will be returned. However, if a user submits a GET request to the resource and we have the AutoRender variable set to true, the resource's controller will automatically call its Render() function, rendering the corresponding template and responding with the following:

The index.tpl code can be seen below; as you can see, parsing model data into a template is quite simple:

<!DOCTYPE html>
<html>
  <head>
    <title>beego welcome template</title>
  </head>
  <body>
    <h1>Hello, world!{{.Username}},{{.Email}}</h1>
  </body>
</html>

results matching ""

    No results matching ""