Separation of Concerns with Laravel’s Eloquent Part 3: Collections, Relations, Eager Loading, and Schema Changes

Part 1  Part 2 — Part 3


Part 1 introduced the idea of using ActiveRecord as a way to implement DataMapper. Part 2 showed a simple example of this in practice, and this third and final part will show further examples that involve relations, collections, and schema changes.

Before I dive into more examples, I just want to summarize the gist of the goals from part one: we want to be able to swap out Eloquent-dependent implementations of things without breaking our application. Part of this involves making sure what we return from our repositories, factories, and relations has a consistent signature that can be easily replicated by whatever we replace the Eloquent-y implementations with.

One such challenge is Collections.

Collections

Collections returned by Laravel relations or query builders are Illuminate\Database\Eloquent\Collection, meaning by default, if you are using Eloquent models as table gateways to fetch collections, and returning those collections through your repository, you have a semi-leaky abstraction, but one that is relatively easy to overcome.

The abstraction and tight coupling to Eloquent only happens if you start explicitly type hinting to Illuminate\Database\Eloquent\Collection, or you start depending on its public behaviors like load() (which actually hits the database) or add() (which is seemingly a superfluous alias of push() from its parent). For example:

class FooController
{
public function bar(PostRepositoryInterface $repository)
{
$posts = $repository->findActivePosts();
$posts->load('author');
}
}

The above is a leaky abstraction that has silently butchered your attempts to keep your application layer unaware of Eloquent.

To overcome this problem, you have a few directions you can go.

  1. You can just type hint the return type from your repository aggregate methods as Illuminate\Support\Collection instead of Illuminate\Database\Eloquent\Collection and then make it a point to avoid using the underlying Eloquent collection methods outside of repositories.
  2. You can re-wrap the Eloquent collection in a plain old support collection so that you eliminate the possibility of other developers accidentally using Eloquent collection methods they shouldn’t be using.
  3. You can re-wrap in your own very specific collection class or library with or without an accompanying interface at your discretion.
  4. You can achieve #3 by defining those custom collections in your Eloquent models to make it easier to always obtain one of those collection instances if you have several finder methods in your repositories. If you do this, however, and you later move away from Eloquent, you will have to remember to create and use those custom collections in whatever your replacement repositories are.

There is no explicit advice to give on this subject other than to be mindful of leaking the Eloquent-specific collection extension methods around your app, and to be mindful of LSP at all times. Whether or not you choose to live with Laravel’s support collections, or implement your own, is entirely dependent on your needs.

Eager Loading

Eager loading APIs (such as with() and load() from either the aforementioned Eloquent collection or on the model directly), can be a leaky abstraction if you use them outside of your repositories.

I’ve often seen Laravel repository libraries that directly expose a with() method or provide an optional argument for an array of eager loaded relationships. This too is a leaky abstraction that should simply not exist, because in order to use that eager-loading-aware API, it means now the consumers of that API (controllers et. al.) also need to know how to manage eager vs lazy loading — something they should not know or care about.

class FooController
{
public function bar(PostRepositoryInterface $repository)
{
$posts = $repository->findActivePosts(['author']);
}
}

While the above variation of the previous example looks rather innocent, it still exposes the same problem. For starters, it’s not good API design, because you’re polluting the findActivePosts() method with an optional argument that is not directly related to its function.

Further, it’s very likely you might want to add another argument that limits the number of posts, or performs some other such meaningful filter on them. Now your arguments have changed, and so too much every place they are called. Additionally, you’ll likely want additional finder methods like findActivePostsInDateRange($start, $end, $eagerLoading = null) which means now you’re propagating that eager loading throughout your method calls.

And lastly, the problem is still that your application layer (or other layers/classes) now need to know that ‘author’ should be an eager-loaded relation that exists on posts, and as such has too much knowledge about that implementation detail. While a replacement repository could effectively just ignore the eager loading parameter (or could implement it as needed), that still leaves you with orphaned arguments scattered about your code.

Eager loading vs lazy loading is very much an optimization detail that should have no bearing on the overall API of your domain and service layer. While you absolutely want to avoid the N+1 query problem, you don’t want your application layer knowing how to solve it. Instead, such an optimization should be neatly encapsulated and tucked away behind your repository. While this might mean you’ll always be eager loading some relations where eager loading is not necessary, this is often preferable to having N+1 queries.

If at some point you do in fact need to eager load relations for some contexts, but not for others (which should be rare, really), you have a few options.

  1. You can cheat a little, and just expose a specialized eager loading method on the same interface that decorates the non-eager loading method, but still keep the *specifics* of what is being eager loaded, encapsulated away.
  2. Or you can implement a full on repository decorator class assuming you’re returning an Eloquent Collection from the inner repository, and have access to load(). The decorator would have exactly the same method signature, it would just be sure to also eager load the relations. Then you can choose to use the inner repository without eager loading, or the decorating repository with eager loading, depending on your needs.
  3. Or you can just extend and override the base EloquentMemberRepository’s offending method, while supplying a specialized interface you can bind it to.
  4. And to supplement #3, you can create the specialized extension of the EloquentMemberRepository and in the context you know you need the eager loading, you can leverage the when()->needs()->give() contextual binding capability of Laravel’s service container.

Pagination

I’m not going to cover pagination much in this article, mostly because I don’t have enough experience with using it in the context of this proposed architecture (have mostly used it only in a JSON API context), but also because the principles are going to be largely the same — don’t leak abstractions that are going to tightly couple you to Eloquent or active record behavior.

When you call paginate(50) on either a query builder or eloquent model, you are returning a Paginator instance, which is devoid of coupling to Eloquent and the query builder, but is coupled to Illuminate\Support\Collection. If you plan on taking advantage of Laravel and are fine with the paginator it ships with, then it should be safe to return paginator instances from any repository or relation implementations, else, you’ll have to implement your own. It’s up to you how badly you want to avoid Laravel’s utilities, while still using Laravel...

As far as how to handle pagination results in your repository API, that very much depends on how abstract you want to go. In theory, pagination is an application-specific problem that your domain model (which your repositories help implement) should know nothing about. But if we’re honest with ourselves, there’s no such thing as a fully decoupled domain layer. Laravel is for building websites. Websites have forms and paginated lists. To pretend these don’t exist for the sake of keeping your domain model “pure” is a bit extreme (though you may very well have a valid reason for this).

Either way, the end goal is the same. If you were to replace Eloquent repositories with some other type of repository, you want to make sure you’re not violating the Liskov Substitution Principle — the return types of all implementations of the repository should be identical, as should the method signatures. If you expose pagination arguments in your repository interface, then you’ll need to make sure you have a way of satisfying that behavior in any repository that implements that interface.

Relations

Relations are thankfully very easy to implement in a loosely coupled way in Eloquent. The answer? Let’s modify our interface and get back to some concrete code examples:

interface MemberInterface
{
public function getID();
public function getLoginName();
public function getDisplayName();
public function getPostCount();
public function incrementPostCount();
public function decrementPostCount();
public function getPosts();
public function getFavoritePosts();
}

And the new methods on our model:

class Member extends Model implements MemberInterface
{
   ...

   public function posts()
{
return $this->hasMany(Post::class);
}

   public function getPosts()
{
return $this->posts;
}

   public function getFavoritePosts()
{
// I think this will work!
       return $this->posts()
->newQuery()
->whereHas('favorites', function($q) {
$q->where(Favorite::ATTR_MEMBER_ID, $this->getID());
})->get();
}

   ...
}

Quick side note on relation method visibility. Unfortunately, it’s not possible to set the relation methods to protected or private to avoid them being used outside of the model itself. Any time you make references with whereHas(), or eager loading within a repository or another Eloquent model, it tries to call those methods and will fail if they’re not public.

Note here how the Eloquent Member model has some heavy eloquent relation stuff going on in its getFavoritePosts() method? That’s perfectly fine, as it’s encapsulated away. If this happened to be a POPO instead, then it would likely be the responsibility of the alternate repository to supply the collection of favorite posts upon retrieval (or lamba function for getting them lazily), but what’s important is that the rest of the application does not care how that collection is generated, it only cares that it gets returned when the getFavoritePosts() method is called.

But also note how we now have a property collision with the $this->posts property? In one context, it refers to the posts database column from part 2, but here it’s how Eloquent does its magic relation retrieval for the collection of the user’s posts. It seems that our initial name for that column was poorly chosen to begin with, and should have been called post_count from the beginning.

Schema Changes

It’s a good thing we took the time to define a constant for that field at the beginning, because now we only need to change one thing in our our entire application after changing the column in our database.

const ATTR_POST_COUNT = ‘post_count’;

Without that constant, we would have had to change that field reference in at least four different places as per the code in part 2 (three in the model, and one in the repository).

Imagine if we didn’t have a constant, and didn’t have a getter explicitly defined by an interface, and $member->posts had been referenced in dozens of views, all over our controllers, and services like in the later usage examples in part 2? Our only hope would have rested in our code editor’s search and replace functionality, which isn’t always going to be reliable.

Wrap Up

I was going to “prove” that we accomplished all four of our goals in the first part of this series, but that proof would require lots of contrived code examples and the caveat that you understand why following LSP is vital to achieving those goals.

The architecture explained in this article series requires very minimal abstraction (repositories, interfaces, and maybe simple factories), and some slightly verbose encapsulation and API definition on models to ensure they have a “clean” API, but in the end you will have applications that are vastly more maintainable, easier to test, and allow for flexibility in your data storage should your needs ever change in the future.

The key to avoiding Eloquent’s lack of separation of concerns, is all in how you choose to use it. Further, because Eloquent makes persistence and relations so damn easy, it actually provides an extremely straight-forward way of implementing a DataMapper-like architecture that would otherwise require making a full (and daunting) transition to Doctrine, or writing *a lot* of your own boilerplate code. If you want the benefits of DataMapper with the simplicity of ActiveRecord, Eloquent is your friend.

Eloquent’s ActiveRecord gives you DataMapper for cheap.