Tái cấu trúc ứng dụng Rails với dry-validation
Khi phát triển các ứng dụng web, chúng ta thường phải đối mặt với vấn đề có chấp nhận hay không và xác thực đầu vào của người dùng hoặc một số dữ liệu đến từ các nguồn bên ngoài. Thông thường có đề xuất cách để giải quyết vấn đề này: Đầu tiên, hạn chế các tham số biểu mẫu ở level controller, sử ...
Khi phát triển các ứng dụng web, chúng ta thường phải đối mặt với vấn đề có chấp nhận hay không và xác thực đầu vào của người dùng hoặc một số dữ liệu đến từ các nguồn bên ngoài. Thông thường có đề xuất cách để giải quyết vấn đề này:
- Đầu tiên, hạn chế các tham số biểu mẫu ở level controller, sử dụng thành phần strong_parameters
- Sau đó, xác thực các tham số được phép ở level model, sử dụng ActiveModel :: Validations
Cách tiếp cận này sau đó đã phát triển thành một phương pháp khác, cho thấy việc trích xuất các xác nhận hợp lệ từ các mô hình thành các lớp riêng biệt, tất cả các hành động xác thực hoặc thậm chí liên tục tồn tại được thực hiện. Mẫu này được gọi là Form Object và thường liên quan đến các giải pháp như: Virtus, Reform, Dry-Types, makandra/active_type.
Bây giờ chúng ta sẽ cố gắng cấu trúc lại cả hai hàm strong_parameters và xác thực mô hình bằng cách sử dụng gem dry-Validation.
Ứng dụng ví dụ của tôi cho phép một số người dùng đăng ký bằng điện thoại di động và một số nhóm người dùng khác có thể đăng ký bằng email. Logic đăng ký cũng có sự khác biệt
class User < ApplicationRecord attr_acessor :mobile_registration # validations validates :phone, presence: true, uniqueness: true, format: { with: /A+d*/, message: I18n.t('errors.messages.phone_format_is_invalid') } validates :email, uniqueness: true, case_sensitive: false, unless: proc { |u| u.mobile_registration || u.email.blank? } validates :password, length: { minimum: 8 }, on: :create, unless: proc { |u| u.mobile_registration || u.email.blank? } validates :password, confirmation: true, presence: true, if: :password_present? validates :password_confirmation, presence: true, if: :password_present? validates :full_name, presence: true end
model này chứa một loạt các xác nhận:
- phone luôn được xác thực
- email chỉ được xác thực nếu thuộc tính hiện tại và thuộc tính mobile_registration không được đặt
- mật khẩu chỉ được xác thực khi tạo hành động và chỉ khi thuộc tính hiện tại và thuộc tính mobile_registration bị bỏ đặt
- password_confirmation phải bằng mật khẩu và phải có mặt nếu có mật khẩu
- full_name luôn được xác thực
Đối với những người mới làm quen, phần code này của mô hình có vẻ quá phức tạp. Bước đầu tiên trong một cách để phá vỡ sự phức tạp này là để di chuyển xác nhận từ model và gọi chúng chỉ trong các action controller cụ thể, nơi không cần kiểm tra điều kiện.Ví dụ: nếu xác thực mật khẩu chỉ được thực hiện khi tạo, tại sao không di chuyển nó đến hành động tạo của bộ điều khiển tương ứng? Ngoài ra, rõ ràng chúng tôi có các hành động riêng biệt đăng ký người dùng thiết bị di động và người dùng thông thường, vì vậy chúng tôi sẽ không gọi xác thực email và mật khẩu cho người dùng đăng ký bằng mobie phone.
Chúng ta hãy xem xét một hành động của controller (chúng được định hướng một cách có mục đích):
# The first one used to register users via web interface: class UsersController < ApplicationController def create @user = User.new(user_params) if @user.save redirect_to profile_path(@user) else render :new end end private def user_params params.require(:user).permit(:phone, :email, :password, :password_confirmation, :full_name) end end # The second one is for mobile registrations class Mobile::UsersController < MobilesController def create @user = User.new(user_params) # note this assignment. It is needed only to bypass conditional validation @user.mobile_registration = true if @user.save redirect_to profile_path(@user) else render :new end end private def user_params params.require(:user).permit(:phone, :full_name, :mobile_registration) end end
Sử dụng dry-validation chúng ta có thể trích xuất tất cả các logic liên quan đến xác nhận hợp lệ thành các lớp riêng biệt và cũng loại bỏ các strong_parameters. Có vấn đề gì với strong_parameters? Cần nhớ, cách các phương thức liên quan đến strong_parameters đang phát triển và trở nên cồng kềnh theo thời gian.
Vậy, hãy viết một lớp mới cho User model validation, sẽ được sử dụng để xác thực người dùng đăng ký từ giao diện web:
require 'dry-validation' class UserValidator UserSchema = Dry::Validation.Schema do # regular expression for phone validations PHONE_REGEX = /A+d*/ configure do # we need this to perform database-related validation, i.e. uniqueness option :record # custom error messages config.messages_file = File.join(Rails.root, 'config', 'locales', 'validation_errors.en.yml') # sanitize input hash permitting only whitelisted parameters. # All parameters in this file will be whitelisted, # others will be filtered out config.input_processor = :sanitizer # universal uniqueness predicate def unique?(attr_name, value) !record.class.where.not(id: record.id).where(attr_name => value).exists? end # checking if value matches PHONE_REGEX def phone?(value) !PHONE_REGEX.match(value).nil? end end # wrap schema in :user, mimicking strong_parameters require method required(:user).schema do required(:full_name).filled required(:phone).filled(:phone?, unique?: :phone) optional(:password).filled(min_size?: 8) optional(:password_confirmation).filled optional(:email).filled(unique?: :email) # custom rules for password confirmation rule(password_confirmed?: [:password, :password_confirmation]) do |password, password_confirmation| password.filled?.then(password_confirmation.eql?(password)) end rule(password_confirmation_filled?: [:password, :password_confirmation]) do |password, password_confirmation| password.filled?.then(password_confirmation.filled?) end end end end
Tệp có lỗi tùy chỉnh sẽ trông giống như sau:
en: errors: unique?: 'Is not unique' phone: 'Phone should start with plus sign and contain only digits'
Mã này phục vụ như là một thay thế cho cả hai strong parameters và ActiveModel :: Validations. Hãy kiểm tra xem nó chính xác là gì
# controller user = User.new result = UserValidator::UserSchema.with(record: user) .call(user: { full_name: ', email: 'admin@admin.test', phone: '89', password: '12345678', password_confirmation: '1234567' }) result.success? # => false result.errors => {:user=> {:full_name=>["must be filled"], :phone=>["Phone should start with plus sign and contain only digits"], :email=>["Is not unique"], :password_confirmation=>["must be equal to 12345678"]}} # let's provide valid parameters: result = UserValidator::UserSchema.with(record: user) .call(user: { full_name: 'John', email: 'john@admin.test', phone: '+89', password: '12345678', password_confirmation: '12345678', active: true }) result.success? # => true result.output # Notice that excessive key :active is not present in the output hash, # because it was filtered out with sanitizer. # {:user=>{:full_name=>"John", :phone=>"+89", :password=>"12345678", # :password_confirmation=>"12345678", :email=>"john@admin.test"}} # And what if do not provide user: {} hash? UserValidator::UserSchema.with(record: user).call(something: {}).errors # => {:user=>["is missing"]}
Và kết quả là mã điều khiển sẽ giống như sau:
class UsersController < ApplicationController def create @user = User.new validation = UserValidator::UserSchema.with(record: @user).call(params) if validation.success? @user.attributes = validation.output[:user] @user.save redirect_to profile_path(@user) else @errors = validation.errors render :new end end # no strong parameters neeeded end # and the second one: class Mobile::UsersController < MobilesController def create @user = User.new validation = MobileUserValidator::UserSchema.with(record: @user).call(params) if validation.success? @user.attributes = validation.output[:user] @user.save redirect_to profile_path(@user) else @errors = validation.errors render :new end end
Bỏ qua mã cho MobileUserValidator tưởng tượng, bởi vì nó chủ yếu lặp lại UserValidator, ngoại trừ email, mật khẩu và xác nhận password_confirmation. Kết luận, dry-validation cung cấp một cách rất thuận tiện để thay thế cả hai xác nhận ActiveRecord và ActiveModel, và cũng là một sự thay thế đẹp hơn cho strong_parameters.
nguồn: https://solnic.codes/2015/12/07/introducing-dry-validation/
http://gafur.me/2017/11/13/refactoring-rails-application-with-dry-validation.html