Backend Following - shibotsu/obs-clone GitHub Wiki

👥 Following System

This document explains how the following system works in the backend, including database structure, API routes, and relationships.

📋 Overview

The following system allows users to follow and unfollow other users. It is implemented using a Follows table that tracks relationships between users.

🗃️ Database Structure

The Follows table stores information about user follow relationships.

Columns:

  • id (Primary Key): Unique identifier for the follow record.
  • follower_id (Foreign Key): ID of the user who is following.
  • following_id (Foreign Key): ID of the user being followed.
  • created_at (Timestamp): When the follow record was created.
  • updated_at (Timestamp): When the follow record was last updated.

Notes:

  • The combination of follower_id and following_id must be unique to prevent duplicate follow records.

Relationships:

  • follower_id and following_id reference users(id) — Many-to-Many: users can follow multiple users.

Model Structure

To use the database correctly, you need to edit the model for users. First, you need to edit the fillable fields to look like this:

protected $fillable = [
    'username',
    'email',
    'password',
    'birthday',
    'profile_picture',
];

Then, it is important to create functions that will be used to establish a many-to-many relationship between users who are being followed and followers. To show this relationship, we use the belongsToMany function, specifying that it is used for the follows table because the default table is users.

public function following()
{
    return $this->belongsToMany(User::class, 'follows', 'follower_id', 'following_id');
}

public function followers()
{
    return $this->belongsToMany(User::class, 'follows', 'following_id', 'follower_id');
}

🔗 API Endpoints

All endpoints are guarded by auth:api, so only logged-in users can follow each other.

API Routes (api.php)

Route::middleware('auth:api')->group(function () {
    Route::post('/follow/{id}', [ProfileController::class, 'follow']);
    Route::delete('/unfollow/{id}', [ProfileController::class, 'unfollow']);
    Route::get('/{id}/followers', [ProfileController::class, 'followers']);
    Route::get('/following', [ProfileController::class, 'following']);
    Route::get('/isfollowing/{id}', [ProfileController::class, 'isFollowing']);
});

Route::get('/most_followed', [ProfileController::class, 'mostFollowed']);

POST /api/follow/{id}

Allows a user to follow another user. The authenticated user follows the user specified by the ID in the URL. The request requires a Bearer token for authentication.

public function follow($id)
{
    $user = User::find($id);
    if (!$user) {
        return response()->json(['message' => 'User not found.'], 404);
    }

    if (auth()->id() === $user->id) {
        return response()->json(['message' => 'You cannot follow yourself.'], 400);
    }

    if (auth()->user()->following()->where('following_id', $user->id)->exists()) {
        return response()->json(['message' => 'You are already following this user.'], 400);
    }

    auth()->user()->following()->attach($user->id);
    $user->number_of_followers++;
    $user->save();

    return response()->json(['message' => 'Followed successfully']);
}

First, the user to be followed is found via the ID from the URL. If there are no issues and they aren't already following each other, the attach() function creates a new row in the follows table with the ID from the URL and the ID of the authenticated user. At the end, the number of followers is updated, and a JSON response is sent.

DELETE /api/unfollow/{id}

Allows a user to unfollow another user. The authenticated user unfollows the user specified by the ID in the URL. The request requires a Bearer token for authentication.

public function unfollow($id)
{
    $user = User::find($id);
    if (!$user) {
        return response()->json(['message' => 'User not found.'], 404);
    }

    if (auth()->id() === $user->id) {
        return response()->json(['message' => 'You cannot unfollow yourself.'], 400);
    }

    auth()->user()->following()->detach($user->id);
    $user->number_of_followers--;
    $user->save();

    return response()->json(['message' => 'Unfollowed successfully']);
}

The same process as before. First, the user is found using their ID, and then the row is removed from the follows table.

GET /api/{id}/followers

Returns all followers of a specific user.

public function followers($id)
{
    $user = User::find($id);
    if (!$user) {
        return response()->json(['message' => 'User not found.'], 404);
    }

    $followers = $user->followers;
    return response()->json(['followers' => $followers]);
}

The specified user is found, and all their followers are retrieved.

GET /api/following

Returns all users the authenticated user is following. This means that only the authenticated user can see who they follow. The request requires a Bearer token.

public function following()
{
    $following = auth()->user()->following;
    foreach ($following as $follow) {
        $follow->profile_picture = $follow->profile_picture
            ? asset('storage/' . $follow->profile_picture)
            : null;
    }
    return response()->json(['following' => $following]);
}

All users the authenticated user follows are fetched, along with their profile pictures.

GET /api/isfollowing/{id}

Checks if the authenticated user follows a specific user and returns a boolean.

public function isFollowing($id)
{
    $userToCheck = User::find($id);

    if (!$userToCheck) {
        return response()->json(['message' => 'User not found.'], 404);
    }

    $isFollowing = auth()->user()
        ->following()
        ->where('following_id', $userToCheck->id)
        ->exists();

    return response()->json(['isFollowing' => $isFollowing]);
}

GET /api/most_followed

Returns the 20 most followed users. Authentication is not required.

public function mostFollowed()
{
    $users = User::orderByDesc('number_of_followers')->take(20)->get(['id', 'username', 'number_of_followers', 'profile_picture']);
    foreach ($users as $user) {
        $user->profile_picture = $user->profile_picture
            ? asset('storage/' . $user->profile_picture)
            : null;
    }

    return response()->json(['users' => $users]);
}