April 15th, 2022 by Alex Garrett-Smith

Setting up Laravel Friendship Relations

Friendships between users in an app is a pretty easy concept to grasp (you add me and we're friends), but to implement it properly isn't as straightforward.

I've gone through several iterations of a friend system implementation – and here's the solution I've settled on.

We're assuming you've got a fresh app set up here, ready to go.

Prefer screencasts? Watch the Build a Friend System in Laravel course over on Codecourse!

The friends table

We need a connection between two users, so a pivot table works nicely here. We don't need a model to represent this.

Schema::create('friends', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained();
$table->foreignId('friend_id')->constrained('users');
$table->integer('accepted')->default(0);
$table->timestamps();
});

Here, we've got a user_id (the person initiating the friend request) and the friend_id (the person who is being added).

We also have an accepted boolean (or integer here) telling us whether this friend request has been accepted. Of course, by default it's false or 0.

The base friend relationships

We have two sides to this relationship.

  1. The people we have added as friends
  2. The people who have added us as friends

The two relationships required for this to work can be added to the User model.

class User extends Authenticatable
{
//...
 
public function friendsTo()
{
return $this->belongsToMany(User::class, 'friends', 'user_id', 'friend_id')
->withPivot('accepted')
->withTimestamps();
}
 
public function friendsFrom()
{
return $this->belongsToMany(User::class, 'friends', 'friend_id', 'user_id')
->withPivot('accepted')
->withTimestamps();
}
}

We're explicitly requesting the accepted column to be included, and also using withTimestamps so the created_at and updated_at columns get filled when we create/update a friend request.

Scoping the relationships

The relationships we've defined above give us all records back, regardless of whether they've been accepted or not.

To make life easier, we'll need 4 more methods to represent the pending and *accepted *requests for both sides of the relation.

These go in the User model too.

public function pendingFriendsTo()
{
return $this->friendsTo()->wherePivot('accepted', false);
}
 
public function pendingFriendsFrom()
{
return $this->friendsFrom()->wherePivot('accepted', false);
}
 
public function acceptedFriendsTo()
{
return $this->friendsTo()->wherePivot('accepted', true);
}
 
public function acceptedFriendsFrom()
{
return $this->friendsFrom()->wherePivot('accepted', true);
}

We're just referencing the relations we defined in the last step and scoping them by the pivot value.

Let's recap. We have have:

  1. People we've added that haven't accepted
  2. People we've added that have accepted
  3. People who've added us that we haven't accepted
  4. People who've added us that we have accepted

The problem with this two-sided relationship

If we want to see a list of our friends, we'd need to merge the people we've added, but also the people who've added us. This is because user_id in our pivot table is always the user who initiates the request.

Here's an example of the process.

  1. Alex (user_id) adds Mabel (friend_id) as a friend
  2. Mabel accepts the request (this sets accepted to true)
  3. Alex can call acceptedFriendsTo and see Mabel, since it originated from him
  4. Mabel can call acceptedFriendsFrom since she was added
  5. But, Alex and Mabel do not have a common relationship they can use to see all of their friends

This is where friendship systems start to get confusing. How do we merge these relationships together?

Merging friends the bad way

We could create a method or accessor to merge the two collections from acceptedFriendsTo and acceptedFriendsFrom.

public function friends()
{
return $this->acceptedFriendsFrom->merge($this->acceptedFriendsTo);
}

This would work. Either user can call this method and as long as they've formed a friendship, would see each other.

However, this method returns a Collection – meaning you're very limited in what you can do from here. Pagination would be harder. Getting distant models through your friends would be harder.

The proper way to merge friendships

Ideally, we want to merge relationships at the database level. To do this, we can create an SQL view.

Rather than do this manually, the laravel-merged-relations package handles this nicely for us.

Let's install it and use it. Be sure to check the docs on the GutHit repository – things may change.

composer require staudenmeir/laravel-merged-relations:"^1.0"

Now create a plain migration and do this in the up method.

use Staudenmeir\LaravelMergedRelations\Facades\Schema;
 
//...
 
public function up()
{
Schema::createMergeView(
'friends_view',
[(new User())->acceptedFriendsTo(), (new User())->acceptedFriendsFrom()]
);
}

Run the migration, and you'll now see a 'friends_view' SQL view in your database.

Over on the User model, use the HasMergedRelationships trait that comes with this package, and create a friends relationship out.

class User
{
use HasMergedRelationships;
 
//...
 
public function friends()
{
return $this->mergedRelationWithModel(User::class, 'friends_view');
}
}

This new friends relationship works like a normal Eloquent relationship. You can now access all of your friends like this.

auth()->user()->friends

This will return a collection of friends. But, you could also paginate the results.

auth()->user()->friends()->paginate(10);

Great. We now have a 'real' relationship on our User model for either people we've added, or who have added us.

There's more

We've dealt with the relationships for friends here, but to learn how to add, accept, delete, decline and display friends, you might want to check out the course over on Codecourse which guides you through building the entire friendship system.

That's all for now, friend 👋