前言、理由和应该如何做
我讨厌人们在 before_validation 钩子中更改模型。然后,当有一天由于某种原因模型需要使用 save(validate: false) 持久化时,一些原本应该始终在指定字段上运行的过滤器不会运行。当然,拥有无效数据通常是您想要避免的事情,但如果不使用,则不需要此类选项。另一个问题是,每次您从模型中询问是否有效时,这些修改也会发生。简单地询问模型是否有效可能导致模型被修改的事实是出乎意料的,甚至可能是不需要的。如果我必须选择一个钩子,我会选择before_save 钩子。但是,这对我来说不起作用,因为我们为模型提供了预览视图,并且会破坏预览视图中的 URI,因为永远不会调用钩子。为此,我决定最好将概念分离到一个模块或关注点中,并提供一种应用“猴子补丁”的好方法,确保更改字段值始终通过添加默认协议的过滤器(如果是)不见了。
模块
#app/models/helpers/uri_field.rb
module Helpers::URIField
def ensure_valid_protocol_in_uri(field, default_protocol = "http", protocols_matcher="https?")
alias_method "original_#{field}=", "#{field}="
define_method "#{field}=" do |new_uri|
if "#{field}_changed?"
if new_uri.present? and not new_uri =~ /^#{protocols_matcher}:\/\//
new_uri = "#{default_protocol}://#{new_uri}"
end
self.send("original_#{field}=", new_uri)
end
end
end
end
在你的模型中
extend Helpers::URIField
ensure_valid_protocol_in_uri :url
#Should you wish to default to https or support other protocols e.g. ftp, it is
#easy to extend this solution to cover those cases as well
#e.g. with something like this
#ensure_valid_protocol_in_uri :url, "https", "https?|ftp"
担心
如果出于某种原因,您宁愿使用 Rails Concern 模式,很容易将上述模块转换为关注模块(它的使用方式完全相同,除了您使用 include Concerns::URIField:
#app/models/concerns/uri_field.rb
module Concerns::URIField
extend ActiveSupport::Concern
included do
def self.ensure_valid_protocol_in_uri(field, default_protocol = "http", protocols_matcher="https?")
alias_method "original_#{field}=", "#{field}="
define_method "#{field}=" do |new_uri|
if "#{field}_changed?"
if new_uri.present? and not new_uri =~ /^#{protocols_matcher}:\/\//
new_uri = "#{default_protocol}://#{new_uri}"
end
self.send("original_#{field}=", new_uri)
end
end
end
end
end
附:上述方法已使用 Rails 3 和 Mongoid 2 进行了测试。
PPS 如果您发现此方法重新定义和别名太神奇,您可以选择不覆盖该方法,而是使用虚拟字段模式,就像密码(虚拟,可批量分配)和 encrypted_password(持久化,不可批量分配)并使用sanitize_url(虚拟,可批量分配)和 url(持久化,不可批量分配)。