We often need to register both a user and an account at the same time. A popular approach to this is to load up on active_model callbacks but that is a route to future problems. It also encourages far to much coupling to the framework itself.
This approach uses a hexagonal / ports & adapters approach separating out domain logic from Rails. We are using a fewadvanced techniques here.
- app/controllers/registrations_controller.rb
- lib/form_utilities/generate_haiku_name.rb
- app/forms/registration.rb
- app/models/registrar.rb
- lib/abilities/roles_admin.rb
- app/models/role.rb
- app/models/grant_event.rb
-
The
RegistrationsController
handles :new and :create actions only -
we use the
gem 'decent_exposure'
to expose methods to views instead of instance variables andexpose
aRegistration
gem dependency -
a
Registration
is a form object inapp/forms/registration.rb
-
a form object, such as registration, is an
ActiveModel::Model
-
Registration
is responsible for validating our form data and passing it to the correct models - in this caseUser
andAccount
which both inherit fromActiveRecord
and are themselves responsible for persistence -
the
RegistrationsController
sets a haiku name for the account shortname, on the registration form object exposed to the view, which delegates it to the setter of the account model internal to the form object -
note the
account
anduser
objects which theregistration
object creates come fromaccount_source
anduser_source
dependencies which are injected into theregistration
form object.
This allows them to be replaced as needed, for example with test doubles. See Avdi Grimm's Objects On Rails for more on this approach. -
when the form is submitted a new
Registrar
object is created with the responsibility of managing the process of registering the user and account from the information contained within theregistration
form. -
the
register
method is the only public method in the API of theregistrar
object. Either the registrar successfully registers the user and account and callsback to its listener (in the example here that is the RegistrationsController with acreate_registration_succeeded
message, or it does not and instead sendscreate_registration_failed
. -
the registration process involves saving the user, saving the account and then, because of course any sensible system of users and accounts has some kind of ACL layer managing permissions, we need to grant the new user the
role
ofowner
on the new account. -
the
build_account
andbuild_user
methods are hooks for any requirements pre-saving on either user or account, then thepersist!
method asks the registration object to save itself and if that works it invokes granting of the owner role. -
the
registration
object performs the validations, using theActiveModel::Validations::Callbacks
available to us, and then asks the account and user objects to save themselves. -
in order to grant the ownership role the registrar's
roles_admin
method getsadmin_user_source
(again dependency injection) to pass it an administrator object which has permissions to manage roles -
the
CustomerSupportUser
we instantiate here is little more than a null-object-pattern user, which we then extend with the necessary responsibilities to be able and allowed to grant the role we need -
to grant the role the
roles_admin
role player receives the command messagegrant_role
with the role_name ofowner
and the account to which the role is being applied and the user who is to receive the ownership role grant -
role granting uses a simple evented state machine approach so a to grant the role a
role
object is instantiated with the account and user set, then it is sent the messagegrant
and passed whatever object is playing the polymorphic roles_admin (in this case ourCustomerSupportUser
instance) as the originator of the grant_event -
role instantiates a new
GrantEvent
on itself, granted by theroles_admin
object -
this means that we have created the following:
- user
- account
- a role which joins the user and account
- i.e. user has role("name") on account
- a grant_event on that role of "owner"
Our registration process is now complete. The registrar.register
is
a success and so registrations_controller
receives its callback
create_registration_succeeded
with the saved registration form
object, which it can ask for the saved user, to set as current_user
before redirecting to the newly registered user.
I think this approach shows promise. It all works nicely in production. There are obviously several ways it can be improved, but it does a good job of avoiding too many responsibilities in objects, as Rails can often encourage. It also uses dependency injection to make it both more flexible for the future, but also lightning fast to test. This is very testable code, allowing for fast tests.