Skip to content

Instantly share code, notes, and snippets.

@laracasts
Last active January 27, 2016 13:08
Show Gist options
  • Save laracasts/f797e671a0aa7143006a to your computer and use it in GitHub Desktop.
Save laracasts/f797e671a0aa7143006a to your computer and use it in GitHub Desktop.
So you want to allow one user to "follow" another user (like Twitter-style). Using Laravel and Eloquent, what's your preference?
<?php
// Option 1: the follow method immediately references the relationship and saves it.
class User extends Eloquent {
public function follows()
{
return $this->belongsToMany(self::class, 'follows', 'follower_id', 'followed_id');
}
public function follow($userIdToFollow)
{
return $this->follows()->attach($userIdToFollow);
}
}
// Option 2: The follow method just stores an array of follow ids, which you can save later.
class User extends Eloquent {
public $follows = []; // public just for example
public function follows()
{
return $this->belongsToMany(self::class, 'follows', 'follower_id', 'followed_id');
}
public function follow($userIdToFollow)
{
$this->follows[] = $userIdToFollow;
return $this;
}
}
// And then, maybe in your repository:
class EloquentUserRepository {
public function save(User $user)
{
if ( ! empty($user->follows))
{
$user->follows()->attach($user->follows);
}
return $user->save();
}
}
@fuelingtheweb
Copy link

Option 1

@rupertjeff
Copy link

I prefer the former, as I find it to be more direct and to the point. With the 2nd option, it seems to me that the functionality is separated into two locations, so I would have to do some digging to figure out exactly how it works.

@tcql
Copy link

tcql commented Jul 29, 2014

If you're embracing ORM, option 1 is simpler. If you're shooting for separating responsibility, and keeping your business logic separate from infrastructure, option 2 is better.

@codeATbusiness
Copy link

I would select the second option because could be more adaptable and could be procesed asynchronously later using the Array. Would be possible to apply within this functionality the Composite Pattern to maintain the logic of the Following functionality within an Interface IFollow?

@ax3lst
Copy link

ax3lst commented Jul 29, 2014

I like the 2nd Option!

@Cosmicist
Copy link

2nd one, I like to have that business logic in the repo. I try to have as less business logic in the model as possible.

@dalabarge
Copy link

Option 1 with a change in that I would keep the "actor" noun like User is by calling the relationship followers() and the "action" verb follow() as being the attach method. You could also make it more fluent as in $user->follows($id) by calling it follows(). While I like the idea of using repositories even with Eloquent ORM, I would put this sort of relationship logic on the model itself as it's more pertaining to the database logic over business logic.

@jtgrimes
Copy link

If I phrase the choice as "persist this now" vs "hope to remember to persist this later," will it make clear what I think and why?

@laracasts
Copy link
Author

@jtgrimes - Not really. A follow method doesn't imply persistence.

@mdwheele
Copy link

As with many things in our field, "it depends" and in some cases, a specific solution to two seemingly similar problems can vary even within the same project.

To me, there are a few thoughts / trade-offs to consider.

Eloquent is an implementation of Active Record. Active Record is defined as "an object that wraps a row in a database table or view, encapsulates the database access, and adds domain logic on that data.". With that said, you could be considering what what boils down to logical orchestration of a crud operation. This use of AR may imply "funneling of information through a few business rules straight into the database and back out again".

Now, forget Eloquent was mentioned at all.

Consider now that you're writing a model of some business concept (a user following another user is fair enough). The business doesn't care about anything about how a "follow" is stored; an argument could then be made that storage / persistence isn't the primary responsibility of your abstraction in the first place (I think most will agree here). The primary responsibility of a domain model in most systems is to model behaviour.

That said, if you accept that the single responsibility of a given model (or Entity) is to model behaviour, then the object itself (and every method on it) represent a consistency boundary. You're expressing explicitly that this model is consistent the day it is instantiated and every method call has preconditions and postconditions that always leave the model in a consistent state. Any deviance from this is "exceptional".

If you reason about models in this way, you might ask: Does a particular operation or behaviour have a invariant (or post-condition) for [immediate or eventual] consistency with the rest of the model? Your answer influences persistence of the model as part of an external use-case implementation. In this particular use-case (a user following a single user) there probably isn't a requirement for immediate consistency.

This gives us some options:

  1. We could choose to immediately persist the model / use-case because it's simple. (your option one)
  2. We could choose to defer persistence to a collaborating service (your option two / Repository)

However, IF we have additional requirements that (for whatever arbitrary reason) state that when the use-case of "following multiple users" is began, you either follow ALL of the users or NONE and we have other upstream / downstream systems that are interesting in those events, then the answer changes. At this point, we should REALLY be thinking Unit of Work (for transactional consistency) and might even consider taking a Domain Event approach to publishing the fact that this happened. Taking the DE approach sounds good (and is in a few Laracasts lessons) AND you could even have the Repository et al. (or another listener that has access to it) listen for the event of "UserFollowedUsers" and do whatever (use a public Eloquent method that isn't to be used by services/controllers, it's there to be used by repository only, OR stores by making query builder calls, OR whatever)... At this point, I'm getting cloudy on explanation and code sample would better serve discussion, but I don't have time at the moment 😦 This whole paragraph is poorly worded.

All in all, there comes a point (in my opinion) where models that are backed by Active Record start taking on too much responsibility as far as the implementation of a saga or other use-case/long-running process. In this particular example and based on assumptions above, it seems reasonable to just associate the followed user and be done with it. In another use-case where there were collaborating models that had to be made immediately consistent with changes in a single model, you're talking Unit of Work and you start considering something similar to option two. It all comes down to case-by-case analysis of what's going on and picking a technical implementation as late as possible. This is one of my drivers for avoiding Active Record in highly behavioural portions of applications I work on. I strategically choose where to use it vs. blind adoption.

Note on above: Please do not take anything I've said as an argument against Active Record or Eloquent. Both are tools to solve a problem and as with everything, there are trade-offs. I take advantage of them where I need, and avoid them when I forsee issues in the long-term. I try to subscribe to Robert Martin's viewpoint of "A good architecture allows you to defer critical decisions, it doesn’t force you to defer them." Active Record forces you to make decisions on persistence very early. If you have enough information to make those decisions, then Active Record can do work for you and save time.

All of this said, "preference" can only be applied / considered up to a certain point in implementation. At a certain point, it's important to leave emotion out and chunk up to why to gain insight in the decision-making process.

@jtgrimes
Copy link

@laracasts When I see "extends Eloquent," I expect relationships to be persisted and I think I expect that to be the default. That's just how Eloquent usually rolls.

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