Let's Go پردازش فرم‌ها › تجزیه خودکار فرم
قبلی · فهرست · بعدی
فصل ۷.۶.

تجزیه خودکار فرم

می‌توانیم handler snippetCreatePost خود را بیشتر با استفاده از یک بسته شخص ثالث مانند go-playground/form یا gorilla/schema برای تجزیه خودکار داده‌های فرم به ساختار snippetCreateForm ساده کنیم. استفاده از یک دیکدر خودکار کاملاً اختیاری است، اما می‌تواند به صرفه‌جویی در وقت و تایپ شما کمک کند — به خصوص اگر برنامه شما فرم‌های زیادی دارد، یا نیاز به پردازش یک فرم بسیار بزرگ دارید.

در این فصل نحوه استفاده از بسته go-playground/form را بررسی می‌کنیم. اگر همراه می‌آیید، لطفاً پیش بروید و آن را به این صورت نصب کنید:

$ go get github.com/go-playground/form/v4@v4
go get: added github.com/go-playground/form/v4 v4.2.1

استفاده از دیکدر فرم

برای کار کردن این، اولین کاری که باید انجام دهیم، مقداردهی اولیه یک نمونه جدید *form.Decoder در فایل main.go خود و در دسترس قرار دادن آن برای handlerهای ما به عنوان یک وابستگی است. به این صورت:

File: cmd/web/main.go
package main

import (
    "database/sql"
    "flag"
    "html/template"
    "log/slog"
    "net/http"
    "os"

    "snippetbox.alexedwards.net/internal/models"

    "github.com/go-playground/form/v4" // New import
    _ "github.com/go-sql-driver/mysql"
)

// Add a formDecoder field to hold a pointer to a form.Decoder instance.
type application struct {
    logger        *slog.Logger
    snippets      *models.SnippetModel
    templateCache map[string]*template.Template
    formDecoder   *form.Decoder
}

func main() {
    addr := flag.String("addr", ":4000", "HTTP network address")
    dsn := flag.String("dsn", "web:pass@/snippetbox?parseTime=true", "MySQL data source name")
    flag.Parse()

    logger := slog.New(slog.NewTextHandler(os.Stdout, nil))

    db, err := openDB(*dsn)
    if err != nil {
        logger.Error(err.Error())
        os.Exit(1)
    }
    defer db.Close()

    templateCache, err := newTemplateCache()
    if err != nil {
        logger.Error(err.Error())
        os.Exit(1)
    }

    // Initialize a decoder instance...
    formDecoder := form.NewDecoder()

    // And add it to the application dependencies.
    app := &application{
        logger:        logger,
        snippets:      &models.SnippetModel{DB: db},
        templateCache: templateCache,
        formDecoder:   formDecoder,
    }

    logger.Info("starting server", "addr", *addr)

    err = http.ListenAndServe(*addr, app.routes())
    logger.Error(err.Error())
    os.Exit(1)
}

...

بعد بیایید به فایل cmd/web/handlers.go خود برویم و آن را برای استفاده از این دیکدر جدید به‌روزرسانی کنیم، به این صورت:

File: cmd/web/handlers.go
package main

...

// Update our snippetCreateForm struct to include struct tags which tell the
// decoder how to map HTML form values into the different struct fields. So, for
// example, here we're telling the decoder to store the value from the HTML form
// input with the name "title" in the Title field. The struct tag `form:"-"` 
// tells the decoder to completely ignore a field during decoding.
type snippetCreateForm struct {
    Title               string `form:"title"`
    Content             string `form:"content"`
    Expires             int    `form:"expires"`
    validator.Validator `form:"-"`
}

func (app *application) snippetCreatePost(w http.ResponseWriter, r *http.Request) {
    err := r.ParseForm()
    if err != nil {
        app.clientError(w, http.StatusBadRequest)
        return
    }

    // Declare a new empty instance of the snippetCreateForm struct.
    var form snippetCreateForm

    // Call the Decode() method of the form decoder, passing in the current
    // request and *a pointer* to our snippetCreateForm struct. This will
    // essentially fill our struct with the relevant values from the HTML form.
    // If there is a problem, we return a 400 Bad Request response to the client.
    err = app.formDecoder.Decode(&form, r.PostForm)
    if err != nil {
        app.clientError(w, http.StatusBadRequest)
        return
    }

    // Then validate and use the data as normal...
    form.CheckField(validator.NotBlank(form.Title), "title", "This field cannot be blank")
    form.CheckField(validator.MaxChars(form.Title, 100), "title", "This field cannot be more than 100 characters long")
    form.CheckField(validator.NotBlank(form.Content), "content", "This field cannot be blank")
    form.CheckField(validator.PermittedValue(form.Expires, 1, 7, 365), "expires", "This field must equal 1, 7 or 365")

    if !form.Valid() {
        data := app.newTemplateData(r)
        data.Form = form
        app.render(w, r, http.StatusUnprocessableEntity, "create.tmpl", data)
        return
    }

    id, err := app.snippets.Insert(form.Title, form.Content, form.Expires)
    if err != nil {
        app.serverError(w, r, err)
        return
    }

    http.Redirect(w, r, fmt.Sprintf("/snippet/view/%d", id), http.StatusSeeOther)
}

امیدوارم بتوانید مزیت این الگو را ببینید. می‌توانیم از تگ‌های ساختار ساده برای تعریف یک نگاشت بین فرم HTML ما و فیلدهای ساختار 'مقصد' استفاده کنیم، و باز کردن بسته داده‌های فرم به مقصد حالا فقط نیاز به نوشتن چند خط کد دارد — صرف نظر از اینکه فرم چقدر بزرگ است.

مهم است که تبدیل‌های نوع نیز به طور خودکار مدیریت می‌شوند. می‌توانیم این را در کد بالا ببینیم، جایی که مقدار expires به طور خودکار به یک نوع داده int نگاشت می‌شود.

پس این واقعاً خوب است. اما یک مشکل وجود دارد.

وقتی app.formDecoder.Decode() را فراخوانی می‌کنیم، به یک اشاره‌گر غیر nil به عنوان مقصد تجزیه هدف نیاز دارد. اگر سعی کنیم چیزی که یک اشاره‌گر غیر nil نیست را بدهیم، آنگاه Decode() یک خطای form.InvalidDecoderError برمی‌گرداند.

اگر این هرگز اتفاق بیفتد، یک مشکل بحرانی با کد برنامه ما است (به جای یک خطای کلاینت به دلیل ورودی بد). بنابراین باید این خطا را به طور خاص بررسی کنیم و آن را به عنوان یک مورد خاص مدیریت کنیم، به جای اینکه فقط یک پاسخ 400 Bad Request برگردانیم.

ایجاد یک کمک‌کننده decodePostForm

برای کمک به این، بیایید یک کمک‌کننده جدید decodePostForm() ایجاد کنیم که سه کار انجام می‌دهد:

اگر همراه می‌آیید، لطفاً پیش بروید و این را به فایل cmd/web/helpers.go خود به این صورت اضافه کنید:

File: cmd/web/helpers.go
package main

import (
    "bytes"
    "errors" // New import
    "fmt"
    "net/http"
    "time"

    "github.com/go-playground/form/v4" // New import
)

...

// Create a new decodePostForm() helper method. The second parameter here, dst,
// is the target destination that we want to decode the form data into.
func (app *application) decodePostForm(r *http.Request, dst any) error {
    // Call ParseForm() on the request, in the same way that we did in our
    // snippetCreatePost handler.
    err := r.ParseForm()
    if err != nil {
        return err
    }

    // Call Decode() on our decoder instance, passing the target destination as
    // the first parameter.
    err = app.formDecoder.Decode(dst, r.PostForm)
    if err != nil {
        // If we try to use an invalid target destination, the Decode() method
        // will return an error with the type *form.InvalidDecoderError.We use 
        // errors.As() to check for this and raise a panic rather than returning
        // the error.
        var invalidDecoderError *form.InvalidDecoderError
        
        if errors.As(err, &invalidDecoderError) {
            panic(err)
        }

        // For all other errors, we return them as normal.
        return err
    }

    return nil
}

و با انجام این کار، می‌توانیم ساده‌سازی نهایی را در handler snippetCreatePost خود انجام دهیم. پیش بروید و آن را برای استفاده از کمک‌کننده decodePostForm() به‌روزرسانی کنید و فراخوانی r.ParseForm() را حذف کنید، به طوری که کد به این صورت باشد:

File: cmd/web/handlers.go
package main

...

func (app *application) snippetCreatePost(w http.ResponseWriter, r *http.Request) {
    var form snippetCreateForm

    err := app.decodePostForm(r, &form)
    if err != nil {
        app.clientError(w, http.StatusBadRequest)
        return
    }

    form.CheckField(validator.NotBlank(form.Title), "title", "This field cannot be blank")
    form.CheckField(validator.MaxChars(form.Title, 100), "title", "This field cannot be more than 100 characters long")
    form.CheckField(validator.NotBlank(form.Content), "content", "This field cannot be blank")
    form.CheckField(validator.PermittedValue(form.Expires, 1, 7, 365), "expires", "This field must equal 1, 7 or 365")

    if !form.Valid() {
        data := app.newTemplateData(r)
        data.Form = form
        app.render(w, r, http.StatusUnprocessableEntity, "create.tmpl", data)
        return
    }

    id, err := app.snippets.Insert(form.Title, form.Content, form.Expires)
    if err != nil {
        app.serverError(w, r, err)
        return
    }

    http.Redirect(w, r, fmt.Sprintf("/snippet/view/%d", id), http.StatusSeeOther)
}

این واقعاً خوب به نظر می‌رسد.

کد handler ما حالا خوب و مختصر است، اما هنوز از نظر رفتار و کاری که انجام می‌دهد بسیار واضح است. و یک الگوی کلی برای پردازش و اعتبارسنجی فرم داریم که می‌توانیم به راحتی روی فرم‌های دیگر در پروژه خود دوباره استفاده کنیم — مانند فرم‌های ثبت‌نام و ورود کاربر که به زودی می‌سازیم.