Skip to content

Instantly share code, notes, and snippets.

@RichardJordan
Last active August 29, 2015 13:56
Show Gist options
  • Save RichardJordan/9059633 to your computer and use it in GitHub Desktop.
Save RichardJordan/9059633 to your computer and use it in GitHub Desktop.
Registration of user and account: a series of gists which show a way to handle registration where multiple models need to be created and saved.

Registration of user and account

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.

See also gists:

How this all works:

  • The RegistrationsController handles :new and :create actions only

  • we use the gem 'decent_exposure' to expose methods to views instead of instance variables and expose a Registration gem dependency

  • a Registration is a form object in app/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 case User and Account which both inherit from ActiveRecord 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 and user objects which the registration object creates come from account_source and user_source dependencies which are injected into the registration 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 the registration form.

  • the register method is the only public method in the API of the registrar object. Either the registrar successfully registers the user and account and callsback to its listener (in the example here that is the RegistrationsController with a create_registration_succeeded message, or it does not and instead sends create_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 of owner on the new account.

  • the build_account and build_user methods are hooks for any requirements pre-saving on either user or account, then the persist! 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 the ActiveModel::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 gets admin_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 message grant_role with the role_name of owner 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 message grant and passed whatever object is playing the polymorphic roles_admin (in this case our CustomerSupportUser instance) as the originator of the grant_event

  • role instantiates a new GrantEvent on itself, granted by the roles_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.

Thoughts on this refactoring

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment