Separation of Concerns with Laravel’s Eloquent Part 2: Putting It Into Practice

Part 1 — Part 2 — Part 3


Part 1 introduced the concept of using Eloquent’s ActiveRecord as a way of implementing DataMapper — that the two are not mutually exclusive concepts, and that it’s up to the developer to use Eloquent responsibly. This part will focus on a simple implementation that achieves the objectives in the previous part. We’ll start off with a Member model that you might see in a forum.

The Member Table

+ — — + — — — — — — + — — — — — — — + — — — — — — +
| id | login_name | display_name | posts |
+ — — + — — — — — — + — — — — — — — + — — — — — — +
| 1 | i_heart_ar | CodeMunky | 5 |
+ — — + — — — — — — + — — — — — — — + — — — — — — +

The Member Model Interface

The Eloquent Member Model Implementation

I just want to take a minute to call out the naming of Member and MemberInterface. In some respects, it might be better if the interface were called Member, and the model were called EloquentMember. I almost always choose the former because you have to do a bit of extra configuration with Eloquent to do the latter. It’s certainly possible, but my preference is to explicitly name the interface, and imply the implementation.

The Member Repository Interface

The Eloquent Member Repository Implementation

As an aside about finder methods in repositories, this is just a simple example. You may choose to implement criteria instead, though the principle will be the same: Eloquent remains encapsulated in Eloquent-specific criteria, but you can use the model to apply the criteria however you need. Criteria design itself is a topic well beyond the scope of this series, and one I am not very qualified to write about, at that!

The Factory

I’m going to leave factories out for brevity. The concept is basically identical to the above, all a factory should do is new up a Member by whatever means (passing in array of data, or what have you — the implementation is not important), and the DocBlock’s return type should be MemberInterface, not the concrete Eloquent Member.

Basic Usage

The above is a very contrived example, for which I apologize. I just wanted to illustrate how the repository and model APIs work together to form a DataMapper-esque relationship. It should also be assumed we’ve wired up the concrete EloquentMemberRepository to the MemberRepositoryInterface so Laravel knows what to inject there.

Now, what would happen if we didn’t take the time to set up our interfaces and repositories, and instead did something like this?

Leaky Eloquent usage

You can see how this is less expressive, and simple logical business rules (such as don’t decrement posts below a count of zero) aren’t encapsulated properly. Further, the various references to $member->posts will cause problems later on.

But what if you took a half-measure, and just encapsulated some of that post increment/decrement functionality with the save() method internally?

Leaky hybrid example

There are two fundamental problems. First, how will this work when/if you replace Eloquent implementations of your MemberInterface, with POPO implementations? How exactly are you going to be able to persist your new post counts without the same kind of internal coupling to the data store?

The second, is at some point you may need to make multiple independent updates to the model. For example an increaseReputation($points) method that occurs when the user creates a post in a certain forum. This too will need a save() method call, which means now you’re doing a needless extra database call when you call both the increaseReputation($points) and incrementPostCount() methods back-to-back.

By using repositories to persist state, and starting off with that decision, you create flexibility for yourself early on in the development process, which will make changes easier down the road — you’ve saved yourself some long-term technical debt with a very small design investment up front. You also save yourself from needing to call save() within each and every mutator your models require.

Part 3: Handling Collections, Relations, and Schema Changes