Custom Validators in Ruby on Rails

1. Validations Overview

Mình nói qua về Validator thông qua 1 ví dụ nho nhỏ như sau:

class User < ApplicationRecord
  validates :Tel, presence: true
end
 
User.create(tel: "0969696969").valid? # => true
User.create(tel: nil).valid? # => false
Có thể hiểu đơn giản nhất Validates là các thao tác kiểm tra dữ liệu trước khi lưu 1 object vào DB

2. Validations Helper

Trong Ruby on Rails thì ActiveRecord có sẵn rất nhiều các helper để trợ giúp validation. Chúng ta có thể kể ra 1 vài helper mà RoR hỗ trợ như: presence, absence, uniqueness, confirmation ... Để tìm hiểu thêm về các helper này chúng ta có thể tham khảo trên trang chủ Ruby on Rails hoặc 1 vài bài viết về Validations trên viblo: http://guides.rubyonrails.org/active_record_validations.html

3. Custom Validations

Có thể thấy việc Validations Trong Ruby on Rails được hỗ trợ rất mạnh, tuy nhiên trong thực tế, dựa theo yêu cầu nghiệp vụ của các dự án mà chúng ta gặp phải 1 số trường cần Validates theo cách đặc biệt. Khi đó thì việc Custom Validations là cần thiết

4. Ví dụ thực tế về Custom Validators

Ở đây mình sẽ đưa ra 1 bài toán nhỏ nhỏ như sau: Trường Tel trong bảng User khi nhập cần thỏa mã các yêu cầu sau: không được bỏ trống, là số, từ 10-11 số, cho phép nhập hyphens ("-") và space (khoảng trắng), không được có 2 hyphens hoặc 2 spaces liên tiếp và không được để hyphens, spaces cạnh nhau liên tiếp. Phân tích bài toán 1 chút mình thấy: về đầu bài toán "không được bỏ trống, là số, từ 10-11 số" thì khá đơn giản, nhưng nửa sau tương đối phức tạp. Cách đơn giản nhất mà mình nghĩ ra đầu tiên là viết 1 module để validation thẳng Tel rồi include vào model

app/models/concerns/verify_tel.rb
module VerifyTel
   extend ActiveSupport::Concern
  VALID_PHONE_REGEX = /\A[0-9\s-]+\z/

  def verify_tel object, tel
    phone = object.send(tel).gsub(/[\s-]/, "")
    if object.send(tel).present?
      if object.send(tel).last == "-" || object.send(tel).match(VALID_PHONE_REGEX).nil? ||
        object.send(tel).include?("  ") || object.send(tel).include?("--") || (phone.size != 10 && phone.size != 11)
        errors.add tel.to_sym, I18n.t("errors.messages.invalid")
      end
    end
  end
end

\\Giải thích 1 chút là đầu tiên mình gsub để loại bỏ hyphesn và space trong input, sau đó mình if để kiểm tra sao cho thỏa mãn yêu cầu bài toàn rồi add errors 
app/models/user.rb
include VerifyTel
validate :verify_phone_number

def verify_chophone_number		
    verify_tel self, "tel"		
 end

Về cơ bản cách này cũng tương đối ổn, nhưng nghĩ 1 chút thì thấy khi cần tái sử dụng việc validation này trong các model khác hoặc cho các trường khác là không thể. Chúng ta sẽ lại phải include, rồi khai báo các hàm 1 lần nữa, thật mệt mỏi --> Mình sẽ thử sử dụng Custom Validation mà ROR hỗ trợ nhé

app/validators/phone_number_validator.rb
class PhoneNumberValidator < ActiveModel::EachValidator
 VALID_PHONE_REGEX = /\A[0-9\s-]+\z/

 def validate_each record, attribute, value
   phone = value.gsub /[\s-]/, ""
   if value.last == "-" || value.match(VALID_PHONE_REGEX).nil? ||
     value.include?("- ") || value.include?(" -") || value.include?("  ") ||
     value.include?("--") || (phone.size != 10 && phone.size != 11)
     record.errors[attribute] << (options[:messages] || I18n.t("errors.messages.invalid"))
   end
 end
end

\\ PhoneNumberValidator được kế thừa từ ActiveModel::EachValidator
app/models/user.rb
validates :tel, presence: true, phone_number: true

Có thể thấy việc được kế thừa từ ActiveModel::EachValidator thì PhoneNumberValidator được hiểu như 1 method được ROR hỗ trợ mặc định (giống như presence, absence, uniqueness ... mà mình kể ở trên) với cách này thì chúng ta có thể dễ dàng tái sự dụng cho các trường khác nhau hay ở các model khác nhau

Tài liệu tham khảo

http://guides.rubyonrails.org/active_record_validations.html http://www.rails-dev.com/custom-validators-in-ruby-on-rails-4/