Modelling business requirements in Eloquent

15 Dec 2015

Like many developers, I can have quite a strong opinion about what I should or shouldn’t be doing regarding coding patterns and toolsets. A couple of years ago that opinion was a little too strong (some might say arrogant) in regards to certain ways. One of those thought patterns was that I simply couldn’t model what I wanted on Eloquent. Nothing could be further from the truth. However, Eloquent (or more accurately, active record) does pose a few problems that we often have to work around.

The use case

For this article we’re going to model a multi-tenant application using accounts and users and look at how you can use Eloquent to manage these business requirements. So to start, let’s define a few requirements:

  1. A user can register with an account
  2. A user cannot exist without being associated with an account
  3. A user’s membership with an account can be deactivated
  4. A user’s membership must be confirmed per account

Before we move on to Eloquent, it’s important to point out interesting words or terms in these requirements that we’ve been handed by the business. The following terms in particular are interesting: register, account, user, membership, confirmed, deactivated. These are business language terms that we want to ensure are modeled in our domain and most importantly, reflected in our code.

Modeling requirements

From the requirements above we can make the following assessment:

  1. An account can have many users
  2. A user associated with an account can be deactivated

So before we go onto modeling the business requirements, let’s setup our Account and User models.

 1 class Account
 2 {
 3     public function users()
 4     {
 5         return $this->belongsToMany(User::class, 'memberships')->withPivot('confirmed', 'deactivated');
 6     }
 7 }
 8 
 9 class User
10 {
11     public function accounts()
12     {
13         return $this->belongsToMany(Account::class, 'memberships')->withPivot('confirmed', 'deactivated');
14     }
15 }

So there we have it - a pretty simple setup for our Account and User models. However, something is bugging me. Before we stated that an account could deactivate a user and this makes sense, but there’s one term in the business language, and associated functionality that makes something a bit hazy.

In our models above, we’ve setup a many:many relationship between Account and User. In addition we have a requirement that users must be confirmed and it’s possible that their association could be deactivated. This would mean that we’ll at the very least need to work with associated fields on the pivot table (confirmed and deactivated). When you start to see this sort of pattern, it hints at a missing model. In this case, the business talked about something called memberships. So let’s model that and see what it looks like.

A better domain model

 1 class Account
 2 {
 3     public function users()
 4     {
 5         return $this->belongsToMany(User::class, 'memberships');    
 6     }
 7 }
 8 
 9 class Membership
10 {
11     public function account()
12     {
13         return $this->belongsTo(Account::class);
14     }
15     
16     public function user()
17     {
18         return $this->belongsTo(User::class);
19     }
20 }
21 
22 class User
23 {
24     public function accounts()
25     {
26         return $this->belongsToMany(Account::class, 'memberships');
27     }
28 }

Here you can see we’ve removed the pivot table setup and instead elevated those requirements in the domain, by representing them by its own Membership model. This makes working with the requirements such as confirmation and deactivation much simpler, and means we can now model those requirements accurately. The question is, where exactly do we model them?

Our first requirement is that a user can register with an account. To me it makes sense that we model this requirement on the User itself, as that’s how the business talks about it.

Modeling registrations

In order for this requirement to be met, we’ll need to create a user record, and associate it with an account in an unconfirmed state, so we’ll need to make sure that memberships are manageable from User, so let’s add that relationship.

1 // class User
2 public function memberships()
3 {
4     return $this->hasMany(Membership::class);
5 }

Easy. Now we can create the requirement. We’re not going to bother with user fields like email.etc. in this example as it will only convolute our modeling, but in a real-world example you’d simply add these fields as part of the method below.

 1 // class User
 2 public static function register(Account $account)
 3 {
 4     if (!$account->exists) {
 5         throw new InvalidAccount;
 6     }
 7     
 8     $user = new User;
 9     $user->save();
10     $user->memberships()->create(['account_id' => $account->id]);
11     
12     return $user;
13 }

So now, once we have the account that the user is registering for (probably fetched via the account’s domain or something), we can easily register a new user:

1 $user = User::register($account);

This registration method solves two requirements: registration, and the fact that only a valid account can be assigned to a user. Ie. $account = new Account wouldn’t work!

Great, so let’s model our last two requirements.

User confirmation and deactivation

One of the requirements is that users can have their accounts deactivated, on a per-account basis. Again, this means that we should be working with the Membership model to manage this. We could model this in any number of ways - we could do it directly on Membership, we could do it from Account, or User. Where does it make the most sense?

Whenever a particular requirement seems a little grey around the edges, you have one of two options:

  1. Extrapolate the requirement based on the language (if possible) or
  2. Have a discussion with the business owners and gather more information

In this case however, I think we have enough information to go on. The business stated that: “a USER’S membership can be deactivated”. This to me says that this should be modeled from the user’s perspective, and not on Membership or Account.

1 public function deactivate(Account $account)
2 {
3     $membership = $this->memberships()->whereAccountId($account->id)->first();
4     $membership->deactivated = true;
5     $membership->save();
6 }

Now we simply call it on the user:

1 $user->deactivate($account);

We can do the same thing for user confirmation, again referring to the business language to determine where to place this logic.

1 public function confirm(Account $account)
2 {
3     $membership = $this->memberships()->whereAccountId($account->id)->first();
4     $membership->confirmed = true;
5     $membership->save();
6 }

After we implement this confirmation step, we get a new requirement from the business - apparently because of spammers.

“We’re finding that users can still confirm their accounts even after they’ve been deactivated - please stop this from happening!”

Because we’ve modelled confirmation on the User model and this is how confirmation happens, we can now easily implement this business rule.

 1 public function confirm(Account $account)
 2 {
 3     $membership = $this->memberships()->whereAccountId($account->id)->first();
 4     
 5     if ($membership->deactivated) {
 6         throw new MembershipDeactivated;
 7     }
 8     
 9     $membership->confirmed = true;
10     $membership->save();
11 }

So now if the membership has already been deactivated, a user cannot confirm their account. For all intents and purposes - they’re dead to us :P

Issues with active record

Earlier on in the article you’ll note that I hinted at some problems with the active record pattern, and there are. The main one (and probably the biggest) has got nothing to do with mixed responsbilities (such as persistence knowledge), but rather that your domain isn’t very well protected. Even though we’ve modeled these requirements as best as we can, any developer can easily work around them by simply not using the methods we’ve highlighted. This is problematic, but easily solved by ensuring the team knows how to do things properly. A good simple rule I find is to not allow ->save() calls outside of the models themselves, as this prevents leaky abstractions which will eventually result in broken business rules.

I hope this article helps you in modeling your business domains accurately and effectively using the Active Record pattern. Despite its flaws, the pattern is such a great way to work as it allows for very fast development - which our clients love ;)

comments powered by Disqus