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

تجزیه خودکار فرم (Automatic Form Parsing)

در این بخش، نحوه تجزیه خودکار فرم (Automatic Form Parsing) را بررسی می‌کنیم. این روش به ما کمک می‌کند تا داده‌های فرم (Form Data) را به صورت خودکار به ساختارهای داده (Data Structures) تبدیل کنیم.

برای شروع، بیایید یک نوع فرم (Form Type) جدید برای قطعه‌های کد (Code Snippets) ایجاد کنیم که شامل قوانین اعتبارسنجی (Validation Rules) باشد:

ما می‌توانیم هندلر 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 خود مقداردهی کنیم و آن را به عنوان یک وابستگی در دسترس هندلرهای خود قرار دهیم. به این صورت:

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() را فراخوانی می‌کنیم، نیاز به یک اشاره‌گر غیر تهی به عنوان مقصد رمزگشایی دارد. اگر سعی کنیم چیزی که نیست یک اشاره‌گر غیر تهی را پاس دهیم، 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
}

و با انجام این کار، می‌توانیم ساده‌سازی نهایی را به هندلر snippeCreatePost خود انجام دهیم. بروید و آن را به‌روزرسانی کنید تا از کمک‌کننده 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)
}

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

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

واژه‌نامه اصطلاحات فنی

اصطلاح فارسی معادل انگلیسی توضیح
تجزیه خودکار فرم Automatic Form Parsing تبدیل خودکار داده‌های فرم به ساختارهای داده
داده‌های فرم Form Data اطلاعات وارد شده در فرم
ساختارهای داده Data Structures الگوهای سازماندهی داده‌ها در برنامه
نوع فرم Form Type ساختار داده برای نگهداری اطلاعات فرم
قطعه‌های کد Code Snippets بخش‌های کوچک و مستقل کد
قوانین اعتبارسنجی Validation Rules شرایط لازم برای معتبر بودن داده‌ها
تگ‌های ساختاری Struct Tags متادیتای اضافه شده به فیلدهای ساختار
پردازش خودکار Automatic Processing انجام عملیات بدون دخالت دستی
تبدیل داده Data Conversion تغییر شکل داده از یک فرمت به فرمت دیگر
اعتبارسنجی خودکار Automatic Validation بررسی خودکار صحت داده‌ها