تجزیه خودکار فرم (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 خود مقداردهی کنیم و آن را به عنوان یک وابستگی در دسترس هندلرهای خود قرار دهیم. به این صورت:
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 خود بروید و آن را بهروزرسانی کنید تا از این رمزگشا جدید استفاده کند، به این صورت:
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() ایجاد کنیم که سه کار انجام میدهد:
- فراخوانی
r.ParseForm()بر روی درخواست فعلی. - فراخوانی
app.formDecoder.Decode()برای باز کردن دادههای فرم HTML به مقصد هدف. - بررسی برای خطای
form.InvalidDecoderErrorو ایجاد یک وحشت اگر ما آن را ببینیم.
اگر در حال دنبال کردن هستید، لطفاً این را به فایل 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() را حذف کنید، به طوری که کد به این صورت باشد:
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 | بررسی خودکار صحت دادهها |