Rails by default has a couple of “buckets” to put your code into. Some people claim that this is not sufficient when building larger apps, other people hold against, that you could upgrade your “model” directory and put all kinds of Ruby POROs (Plain Ruby Objects) into it. I am not totally convinced to mix database-backed models and all kinds of different objects, but rather like to identify common patterns and create subdirectories directly under app/
, like: app/queries
, app/api
, app/api_clients
or the bespoken Form Models under app/forms
.
Advantages of Form Models
Validations
The longer I work with Rails, and the longer or larger the project horizon is planned, the less I like to use complex validations on the model. Why? Because in my experience, the validations only make sense, when the model is created by a user through a UI. But not all objects are created by a UI. Think of:
- Ad-hoc creation of small objects in development, test
- Cronjobs that create or update a model and fail because one item has gone invalid because a validation / column has been introduced after the creation of the item itself - this happens quite often IME
- different validations depending on the state of the user (Form wizards, registration vs. updating profile, etc.)
Side Effects
The second big win of form models are side effects , like sending a email/notification, enqueuing jobs, creating log/audit records, update elasticsearch, etc.pp. Doing those in the controller is maybe feasible but it can go out of hand very fast. Doing those in callbacks is IMO a very bad practice: Thinking about backfilling some attributes, but accidentally sending a notification to all. You always have to know, which side effects are present, even when updating in the background. So, I think the save
method of a Form Object is a perfect place to kick off various actions.
Database-less actions
Also, you sometimes have actions that not necessarily have a database table attached, think of: CSV export (with config), Providing a test action for an integration (Webhook test, IMAP integration, SAML integration, …). Those are perfect candidates for Form Models!
Controller does not need to know the model’s attributes
Another advantage, which I later found out about, is that I can get rid of the permitted_params
/ params.require
stuff from the controller (which is there rightly so to prevent Mass Assignment Injections). Because our form model can only reveal the attributes which the user can update anyways, we can build a very simple wrapper, that automatically permits all attributes of the form model. I really like that, because now the controller does not have to know about the model’s fields – How often did you forgot to add a missing attribute to the permit(..)
method?
Our Form base class
Over the years, our base class changed. One thing I want of a Form Model, is Parameter Coercion (e.g. casting “1” to true for a boolean). In the past, we used the virtus
Gem to handle the definition of the attributes and coercion. But recently, after Rails released the Attributes API, we can just use ActiveModel::Attributes
.
# app/forms/application_form
class ApplicationForm
# ActiveModel: We get validations, model_name stuff, etc.
# now our object quaks almost like an ActiveRecord::Base model
include ActiveModel::Model
# Gives us the `attribute `` method to define attributes with data types:
# attribute :email, :string,
# attribute :active, :boolean, default: true
include ActiveModel::Attributes
# Helper Method to call from the controller:
#
# MyForm.new_with_permitted_params(params)
#
# It fetches the correct key, e.g. params.require(:my_form).permit(:a, :b, c: [], d: {})
def self.new_with_permitted_params(params)
permitted_params = params.
require(model_name.param_key).
permit(*permitted_params_arguments)
new(permitted_params)
end
# Maps the defined `attributes` to a argument list for params.permit()
# Array and Hash attribues must be written in hash form.
def self.permitted_params_arguments
structures, primitives = attribute_types.
map { |name, type|
if type == :array
{ name => [] }
elsif type == :hash
{ name => {} }
else
name
end
}.partition { |i| i.is_a?(Hash) } # rubocop:disable Style/MultilineBlockChain
params = [*primitives, structures.reduce(&:merge)].reject(&:blank?)
if params.length == 1
params.first
else
params
end
end
# placeholder to implement by the inherited form instances
def save
return false unless valid?
raise NotImplementedError
end
end
Example Usage
Imagine you are for once not using Devise and implementing Password Reset yourself.
# app/forms/password_reset_form.rb
class PasswordResetForm < ApplicationForm
attribute :email, :string
validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
def save
return unless valid?
account = User.where(email: email).first
unless account
sleep 1
return true
end
account.regenerate_reset_password_token_if_not_active
Mailer.password_reset(account).deliver_now
true
end
end
As the model is 100% compatible with a ActiveRecord Model, we can use it the same in the controller:
class PasswordResetController < ApplicationController
def new
@form = PasswordResetForm.new
end
def create
@form = PasswordResetForm.new_with_permitted_params(params)
if @form.save
redirect_to root_path, notice: "E-Mail instructions have been sent"
else
render :new
end
end
end
Now, if we get the requirement to log account activities (audit trail), the save method is a perfect place to continue. For this purpose, I usually define “normal” attribute accessors that the controllers fill. Those fields will not be available through the permitted params sieve and are safe for this purpose.
# ...
def create
@form = PasswordResetForm.new_with_permitted_params(params)
@form.request = request
...
end
###
class PasswordResetForm < ApplicationForm
# ...
attr_accessor :request
def save
#...
account.activities.create(ip: request.ip, user_agent: request.user_agent)
#.
end
Hope that helps you in your organisation of form models! For us, those are a frequently used pattern.
Top comments (0)