If you go to StackOverflow, you'll find hundreds of implementations for friendships. Creating a Twitter subscription model with a clear distinction between following and followers is easy. Mutual friendship, on the other hand, requires more code.
In this case, friends are added only after the user's approval:
Foo
sends a friend request to Bar
.Bar
accepts the friend request from Foo
.Foo
and Bar
are now friends.We'll have two join models: FriendRequest
and Friendship
. Each will have its controller with RESTful routes.
First, we'll create FriendRequest
resource.
$ rails generate resource FriendRequest user:references friend:references
$ rake db:migrate
Then, we'll establish the has_many :through
association between users. You can read more about associations here.
# app/models/user.rb:
class User < ActiveRecord::Base
has_many :friend_requests, dependent: :destroy
has_many :pending_friends, through: :friend_requests, source: :friend
end
We're using source: :friend
because we've changed the association name (it should've been friends
, but it is reserved for "original" friends).
Because the FriendRequest model has a self-referential association, we have to specify the class name:
class FriendRequest < ActiveRecord::Base
belongs_to :user
belongs_to :friend, class_name: 'User'
end
The corresponding controller will have four actions:
index
: view incoming and outgoing friend requestscreate
: send a friend request to another userupdate
: to accept friend requestsdestroy
: to decline friend requestsTo send a request we'll create a new FriendRequest
record with our current user as a friend:
# app/controllers/friend_requests_controller.rb
class FriendRequestsController < ApplicationController
before_action :set_friend_request, except: [:index, :create]
def create
friend = User.find(params[:friend_id])
@friend_request = current_user.friend_requests.new(friend: friend)
if @friend_request.save
render :show, status: :created, location: @friend_request
else
render json: @friend_request.errors, status: :unprocessable_entity
end
end
...
private
def set_friend_request
@friend_request = FriendRequest.find(params[:id])
end
end
Here, we're simply grabbing all incoming and outgoing requests.
def index
@incoming = FriendRequest.where(friend: current_user)
@outgoing = current_user.friend_requests
end
...
Cancelling a request is equivalent to destroying a friend request record.
def destroy
@friend_request.destroy
head :no_content
end
Now let's move on to another join model: Friendship
. We'll be using this association to get and destroy friends.
$ rails g model Friendship user:references friend:references
$ rails g controller Friends index destroy
$ rake db:migrate
First, we must set up an association:
# app/models/friendship.rb
class User < ActiveRecord::Base
...
has_many :friendships, dependent: :destroy
has_many :friends, through: :friendships
end
Friendship model looks like this:
class Friendship < ActiveRecord::Base
after_create :create_inverse_relationship
after_destroy :destroy_inverse_relationship
belongs_to :user
belongs_to :friend, class_name: 'User'
private
def create_inverse_relationship
friend.friendships.create(friend: user)
end
def destroy_inverse_relationship
friendship = friend.friendships.find_by(friend: user)
friendship.destroy if friendship
end
end
After that, we'll be able to query user records for friends:
User.first.friends # => [Users]
Next, we'll set up some ancillary methods in FriendRequest
model:
# app/models/friend_request.rb
class FriendRequest < ActiveRecord::Base
...
# This method will build the actual association and destroy the request
def accept
user.friends << friend
destroy
end
end
Thanks to has_many :through
association, we're now able to add friends using append (<<)
method.
Now it takes just one line of controller code to accept a friend request:
# app/controllers/friend_requests
def update
@friend_request.accept
head :no_content
end
The code for Friends
controller is straightforward:
# app/controllers/friends_controller.rb
class FriendsController < ApplicationController
before_action :set_friend, only: :destroy
def index
@friends = current_user.friends
end
...
private
def set_friend
@friend = current_user.friends.find(params[:id])
end
end
Unfriending users is a bit complicated since we must remove friendships from both sides of the association. We'll write a different method that will take this responsibility.
# app/models/user.rb
class User < ActiveRecord::Base
...
def remove_friend(friend)
current_user.friends.destroy(friend)
end
end
Now we're able to use this method in our controller:
# app/controllers/friends_controller.rb
def destroy
current_user.remove_friend(@friend)
head :no_content
end
You might want to add some validations to the models (uniqueness and presence):
validates :user, presence: true
validates :friend, presence: true, uniqueness: { scope: :user }
It's also a good idea to restrict self associations, so the user won't be able to befriend himself. This validation should go into both classes (Friendship
and FriendRequest
).
validate :not_self
private
def not_self
errors.add(:friend, "can't be equal to user") if user == friend
end
And users shouldn't be able to send friend requests if it already exists or if they're already friends:
# app/models/friend_request
class FriendRequest < ActiveRecord::Base
...
validate :not_friends
validate :not_pending
...
private
def not_friends
errors.add(:friend, 'is already added') if user.friends.include?(friend)
end
def not_pending
errors.add(:friend, 'already requested friendship') if friend.pending_friends.include?(user)
end
end