Rebuild Twitter with Laravel — Followers

Sky Chin
13 min readMar 1, 2017

--

This is the Part 2 of the Rebuild Twitter with Laravel. In Part 1, you set up a foundation of a social network platform. Now, you build your user a profile and allow the user to follow others.

If you’ve missed the first part, you can read it at here. Otherwise, you can clone the project from Github. I’ll guide you how to install the project before proceeding to the Part 2.

If you’ve done the Part 1, jump right to the planning.

Setup the project from Github

Clone the project

Project link: https://github.com/co0lsky/rebuild-twitter-with-laravel
Clone the project from Github.

// Terminal
git clone -b '#1_User_and_Authentication' https://github.com/co0lsky/rebuild-twitter-with-laravel.git laratweet
cd laratweet
composer install

Configure application environment file

Duplicate the .env.example to be .env.

Configure database access. I recommend you to create a new database for this application. The table name has no unique prefix or suffix, it might clash with your existing table which is having the same name, like users.

// .env
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laratweet
DB_USERNAME=homestead
DB_PASSWORD=secret

Generate Application Key

Next, you should generate an application key. The application key helps to secure your application’s user sessions and other encrypted data.

// Terminal
php artisan key:generate
Application key [base64:uJwG9Kge1xwH7O0/sckwN96pENJJy8cr5i+WbwQ7dYw=] set successfully.

Migration

Next, migrate your database.

// Terminal
php artisan migrate
Migration table created successfully.
Migrated: 2014_10_12_000000_create_users_table
Migrated: 2014_10_12_100000_create_password_resets_table

Test

Launch your application.

Welcome page

Register as a user.

Register page
Home page

Alright, the application is ready.

Planning

Twitter allows you to follow or unfollow a user at his profile page.

Twitter profile page

In this case, you are going to build a profile page which has follow or unfollow function.

These are the pages you’ll be developing,

  • Profile page
  • Following page (list of users you are following)
  • Followers page (list of users who are following you)

Alright, let’s summarise your planning

  • build a profile page
  • add a follow/unfollow button
  • build a following and a followers pages

Before you start, your application needs a new name. An application name boosts your ownership to the application. Let’s name is as Laratweet (you can name it as you like).

Laratweet

To rename the application, these are the files involve.

// config\app.php
'name' => 'Laratweet',
// resources\views\welcome.blade.php
<title>{{ config(‘app.name’, ‘Laravel’) }}</title>

<div class=”title m-b-md”>
Laratweet
</div>
<div class=”links”>
<a href=”https://medium.com/@just4sky/rebuild-twitter-with-laravel-user-and-authentication-9b0adb392dc6">User and Authentication</a>
</div>
Welcome page

Do you like the new welcome page? Don’t like it? Feel free to modify it.

Let’s build a profile page.

Profile

The first step, you create a new controller which finds the User model with username and pass the model to the View.

// Terminal
php artisan make:controller ProfileController
Controller created successfully.

Next, introduce a new route for the profile page, which is /{username}.

// routes/web.php
Route::get('/{username}', 'ProfileController@show');

Before you write any logic into the ProfileController, you have the View prepare, so that you’ll know what information does the View needs.

Create a new view file by duplicating the resources/views/home.blade.php and rename it as profile.blade.php.

Before you pass the information to the view, let’s make a small test to make sure the Route is communicating well with ProfileController. You output the route parameter (username) from the URL.

// app/Http/Controllers/ProfileController.php
public function show($username)
{
return view('profile', ['username' => $username]);
}
// resources/views/profile.blade.php
<div class="panel-heading">Profile</div>
<div class="panel-body">
{{ $username }}
</div>

Okay, try to browse the profile page. In my case, my URL is http://laratweet.app/sky.

Profile page

When you see the username is displaying on the page, you’re on the right track.

I prepare an upgraded version profile page for you. What’s the upgrade?

  • A layout file which will be sharing among profile, following, and followers page
  • A view file which will show the list of tweets (the list is empty for now)

Create a layout file named profile.blade.php in the resources/views/layouts. Copy the content from here.

Edit the resources/views/profile.blade.php by copying the content from here.

Then, you retrieve the necessary information to fulfil the requirement of the profile page.

// app/Http/Controllers/ProfileController.php
use App\User;
public function show($username)
{
$user = User::where('username', $username)->firstOrFail();
return view('profile', ['user' => $user]);
}
New profile page

The profile page is looking good! Wait a minute, what will happen if you browse a non-existing user profile?

404 page

It doesn’t look good. You have to handle the exception by redirecting the user to the right place.

404 page

Speaking of error handling, Laravel is a silent hero who did all the necessary jobs without you knowing. The proof is the page you are looking right now, which is the custom error page instead of the web server error page.

What do you need to do is create a 404.blade.php in the resources/views/errors. Here is the file I’ve prepared for you.

New 404 page

Now, the profile page is ready. Let’s move to the next feature, followers.

Followers

In this feature, the key part is the database table design.

A user can follow many users.
Many users can follow a user.

It is a many-to-many relationship.

Many-to-many relationship

Database normalization, or simply normalization, is the process of organizing the columns (attributes) and tables (relations) of a relational database to reduce data redundancy and improve data integrity. — Wikipedia

Database normalisation reduces data redundancy, however, denormalisation improves read performance.

In the real world development, denormalisation is a common technique to reduce the number of joined tables in your select query.

You can read the simple explanation from Elena at here.
In more detail, you can read from this link.

So, the question is normalise or not?

In this stage, my suggestion, your main goal is getting things done. You don’t spend much time on debating normalisation without any testing.

Migration

Table followers serves as a pivot table. It has a column named user_id which stores the user.id and another column named follower_user_id which stores follower user.id.

follower_user_id follows user_id.

Let’s create a migration.

// Terminal
php artisan make:migration create_followers_table --create=followers
Created Migration: 2017_02_11_083844_create_followers_table
// database/migrations/2017_02_11_083844_create_followers_table.php
Schema::create('followers', function (Blueprint $table) {
$table->integer('user_id')->unsigned()->index();
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
$table->integer('follower_user_id')->unsigned()->index();
$table->foreign('follower_user_id')->references('id')->on('users')->onDelete('cascade');
$table->timestamps();
});

onDelete(‘cascade’) means the child table’s record will be removed when the foreign ID is deleted from the parent table.

A foreign key with cascade delete means that if a record in the parent table is deleted, then the corresponding records in the child table will automatically be deleted. This is called a cascade delete in SQL Server. — TechOnTheNet

Execute the migration.

// Terminal
php artisan migrate
Migrated: 2017_02_11_083844_create_followers_table

User Model

Next, declare the relationship between the followers and users table.

// app/User.php
/**
* The following that belong to the user.
*/
public function following()
{
return $this->belongsToMany('App\User', 'followers', 'follower_user_id');
}

Now the followers table is empty. You need to enable the following function to generate the data. How? Build a follow button on the profile page.

Follow button

When the user clicks the follow button, send a request to the controller to perform the following action. You need 2 methods which are follows and unfollows in the controller.

Although you have a ProfileController, but, you are not adding the methods into it. Why? ProfileController is serving the purpose for finding User model and providing the necessary information to the view.

You create a new controller which is serving for user’s actions.

// Terminal
php artisan make:controller UserController
Controller created successfully.

Create a route for the follow action.

// routes/web.php
Route::group(['middleware' => 'auth'], function () {
Route::get('/follows/{username}', 'UserController@follows');
Route::get('/unfollows/{username}', 'UserController@unfollows');
});

The route is protected by the auth (Middleware). Guest users cannot perform the follows action without login.

Then, add a follows method in the UserController.

// app/Http/Controllers/UserController.php
public function follows($username)
{
// Find the User. Redirect if the User doesn't exist
$user = User::where('username', $username)->firstOrFail();
// Find logged in User
$id = Auth::id();
$me = User::find($id);
$me->following()->attach($user->id); return redirect('/' . $username);
}
public function unfollows($username)
{
// Find the User. Redirect if the User doesn't exist
$user = User::where('username', $username)->firstOrFail();
// Find logged in User
$id = Auth::id();
$me = User::find($id);
$me->following()->detach($user->id); return redirect('/' . $username);
}

Next step is upgrading your profile page. You build a button which will switch between the functions of edit profile, follow and unfollow. Edit profile is not the focus of this post, so the button will not do anything.

// resource/views/layouts/profile.blade.php
<div class="col-md-2">
@if (Auth::check())
@if ($is_edit_profile)
<a href="#" class="navbar-btn navbar-right">
<button type="button" class="btn btn-default">Edit Profile</button>
</a>
@elseif ($is_follow_button)
<a href="{{ url('/follows/' . $user->username) }}" class="navbar-btn navbar-right">
<button type="button" class="btn btn-default">Follow</button>
</a>
@else
<a href="{{ url('/unfollows/' . $user->username) }}" class="navbar-btn navbar-right">
<button type="button" class="btn btn-default">Unfollow</button>
</a>
@endif
@endif
</div>

You will check with User model whether the logged in user is following this user (viewing profile). You assign the values to $is_edit_profile and $is_follow_button in the ProfileController. $is_edit_profile and $is_follow_button helps view to determining which button function to appear on the page.

// app/User.php
/**
* The following that belong to the user.
*/
public function following()
{
return $this->belongsToMany('App\User', 'followers', 'follower_user_id', 'user_id')->withTimestamps();
}
public function isFollowing(User $user)
{
return !is_null($this->following()->where('user_id', $user->id)->first());
}
// app/Http/Controllers/ProfileController.php
public function show($username)
{
$user = User::where('username', $username)->firstOrFail();
$me = Auth::user();
$is_edit_profile = (Auth::id() == $user->id);
$is_follow_button = !$is_edit_profile && !$me->isFollowing($user);
return view('profile', ['user' => $user, 'is_edit_profile' => $is_edit_profile, 'is_follow_button' => $is_follow_button]);
}

Try it out.

Follows and Unfollows

You are not finished here.

Currently, user’s action is performing by sending GET request, which is not recommended. By using GET request, the parameter is visible and less secure. Switch it to POST request instead to hide the parameter and improve the security.

And, it’s not cool to refresh the page every time an action performed, let’s change it to Ajax calling.

Switch GET to POST

Let’s start with the View.

// resource/views/layouts/profile.blade.php
<meta id="token" name="csrf-token" content="{{ csrf_token() }}”>
@if (Auth::check())
@if ($is_edit_profile)
<a href="#" class="navbar-btn navbar-right">
<button type="button" class="btn btn-default">Edit Profile</button>
</a>
@else
<button type="button" v-on:click="follows" class="navbar-btn navbar-right btn btn-default">@{{ followBtnText }}</button>
@endif
@endif
<!-- <script src="/js/app.js"></script> -->
<script src="https://unpkg.com/vue@2.1.10/dist/vue.js"></script>
<script src="https://unpkg.com/vue-resource@1.2.0/dist/vue-resource.min.js"></script>
<script>
new Vue({
el: '#app',
data: {
username: '{{ $user->username }}',
isFollowing: {{ $is_following ? 1 : 0 }},
followBtnTextArr: ['Follow', 'Unfollow'],
followBtnText: ''
},
methods: {
follows: function (event) {
var csrfToken = Laravel.csrfToken;
var url = this.isFollowing ? '/unfollows' : '/follows';
this.$http.post(url, {
'username': this.username
}, {
headers: {
'X-CSRF-TOKEN': csrfToken
}
})
.then(response => {
var data = response.body;
if (!data.status) {
alert(data.message);
return;
}
this.toggleFollowBtnText();
});
}, toggleFollowBtnText: function() {
this.isFollowing = (this.isFollowing + 1) % this.followBtnTextArr.length;
this.setFollowBtnText();
},
setFollowBtnText: function() {
this.followBtnText = this.followBtnTextArr[this.isFollowing];
}
},
mounted: function() {
this.setFollowBtnText();
}
});
</script>

Then, you do a small change on ProfileController@show to inform View that current user is following this user.

// app/Http/Controllers/ProfileController.php
public function show($username)
{
$user = User::where('username', $username)->firstOrFail();
$is_edit_profile = false;
$is_following = false;
if (Auth::check()) {
$is_edit_profile = (Auth::id() == $user->id);
$me = Auth::user();
$is_following = !$is_edit_profile && $me->isFollowing($user);
}
return view('profile', ['user' => $user, 'is_edit_profile' => $is_edit_profile, 'is_following' => $is_following]);
}

Change the route method to post.

// routes/web.php
Route::group(['middleware' => 'auth'], function () {
Route::post('/follows', 'UserController@follows');
Route::post('/unfollows', 'UserController@unfollows');
});

Next, you accept a Request object passing into the follows and unfollows methods and response a JSON object.

// app/Http/Controllers/UserController.php
use Illuminate\Database\Eloquent\ModelNotFoundException;
...public function follows(Request $request)
{
$username = $request->input('username');
try {
$user = User::where('username', $username)->firstOrFail();
} catch (ModelNotFoundException $exp) {
return $this->responseFail('User doesn\'t exists');
}
// Find logged in User
$id = Auth::id();
$me = User::find($id);
$me->following()->attach($user->id); return $this->responseSuccess();
}
public function unfollows(Request $request)
{
$username = $request->input('username');
try {
$user = User::where('username', $username)->firstOrFail();
} catch (ModelNotFoundException $exp) {
return $this->responseFail('User doesn\'t exists');
}
// Find logged in User
$id = Auth::id();
$me = User::find($id);
$me->following()->detach($user->id); return $this->responseSuccess();
}
private function responseSuccess($message = '')
{
return $this->response(true, $message);
}
private function responseFail($message = '')
{
return $this->response(false, $message);
}
private function response($status = false, $message = '')
{
return response()->json([
'status' => $status,
'message' => $message,
]);
}

What’s next? The following and followers pages.

Following & Followers page

Following and followers pages are having a list of username. They are very simple. The main concern here is the routes.

// routes/web.php
Route::group(['middleware' => 'auth'], function () {
Route::get('/following', 'ProfileController@following');
Route::post('/follows', 'UserController@follows');
Route::post('/unfollows', 'UserController@unfollows');
});
Route::get('/{username}', 'ProfileController@show');
Route::get('/{username}/followers', 'ProfileController@followers');

The sequence of the route declaration is important. You may realise the route of user’s profile, /{username} is placing at the bottom. This sequence is due to the /following and /{username} are having the similar pattern of the route. When the user hits the address bar with /following, Laravel will match the route from top to bottom.

If your routes are declared as below,

Route::get('/{username}', 'ProfileController@show');
Route::get('/{username}/followers', 'ProfileController@followers');
Route::group(['middleware' => 'auth'], function () {
Route::get('/following', 'ProfileController@following');
Route::post('/follows', 'UserController@follows');
Route::post('/unfollows', 'UserController@unfollows');
});

When the user hits /following in the address bar, /{username} will be matched instead of /following. You will not enter the /following.

When the routes are ready, you start to work with the Views. There are couple of things to do with the profile page,

  • Show the following tab only if this is your own profile
  • Set 2 new routes
  • Show following and followers count
// resources/views/layouts/profile.blade.php
<div class="col-md-8">
<ul class="nav navbar-nav">
<li class="active">
<a href="#" class="text-center">
<div class="text-uppercase">Tweets</div>
<div>0</div>
</a>
</li>
@if ($is_edit_profile)
<li>
<a href="{{ url('/following') }}" class="text-center">
<div class="text-uppercase">Following</div>
<div>{{ $following_count }}</div>
</a>
</li>
@endif
<li>
<a href="{{ url('/' . $user->username . '/followers') }}" class="text-center">
<div class="text-uppercase">Followers</div>
<div>{{ $followers_count }}</div>
</a>
</li>
</ul>
</div>

Next, you create 2 new view files in resources/views.

// resources/views/following.blade.php
// The content refers to the https://github.com/co0lsky/rebuild-twitter-with-laravel/blob/%232_Followers/resources/views/following.blade.php

// resources/views/followers.blade.php
// The content refers to the https://github.com/co0lsky/rebuild-twitter-with-laravel/blob/%232_Followers/resources/views/followers.blade.php

Then, you introduce a new relationship in the User model.

// app/User.php
/**
* The followers that belong to the user.
*/
public function followers()
{
return $this->belongsToMany('App\User', 'followers', 'user_id', 'follower_user_id')->withTimestamps();
}

Lastly, you count the followings and followers in the ProfileController@show. And, add ProfileController@following and ProfileController@followers.

// app/Http/Controllers/ProfileController.php
public function show($username)
{
$user = User::where('username', $username)->firstOrFail();
$followers_count = $user->followers()->count(); $is_edit_profile = false;
$is_following = false;
if (Auth::check()) {
$is_edit_profile = (Auth::id() == $user->id);
$me = Auth::user();
$following_count = $is_edit_profile ? $me->following()->count() : 0;
$is_following = !$is_edit_profile && $me->isFollowing($user);
}
return view('profile', [
'user' => $user,
'followers_count' => $followers_count,
'is_edit_profile' => $is_edit_profile,
'following_count' => $following_count,
'is_following' => $is_following
]);
}
public function following()
{
$me = Auth::user();
$followers_count = $me->followers()->count();
$following_count = $me->following()->count();
$list = $me->following()->orderBy('username')->get(); $is_edit_profile = true;
$is_following = false;
return view('following', [
'user' => $me,
'followers_count' => $followers_count,
'is_edit_profile' => $is_edit_profile,
'following_count' => $following_count,
'is_following' => $is_following,
'list' => $list,
]);
}
public function followers($username)
{
$user = User::where('username', $username)->firstOrFail();
$followers_count = $user->followers()->count(); $list = $user->followers()->orderBy('username')->get(); $is_edit_profile = false;
$is_following = false;
if (Auth::check()) {
$is_edit_profile = (Auth::id() == $user->id);
$me = Auth::user();
$following_count = $is_edit_profile ? $me->following()->count() : 0;
$is_following = !$is_edit_profile && $me->isFollowing($user);
}
return view('followers', [
'user' => $user,
'followers_count' => $followers_count,
'is_edit_profile' => $is_edit_profile,
'following_count' => $following_count,
'is_following' => $is_following,
'list' => $list,
]);
}

Here you go!

Following page
Followers page

Tab bar active status

The last thing to make the profile page looks great which is highlight the navigation tab correctly.

I have 2 suggestions here,

  • You get current route and do a matching, whichever tab is matched, highlight it.
  • I recommend named route which makes the views more organise.
// routes/web.php
Route::group(['middleware' => 'auth'], function () {
Route::get('/following', 'ProfileController@following')->name('following');
Route::post('/follows', 'UserController@follows');
Route::post('/unfollows', 'UserController@unfollows');
});
Route::get('/{username}', 'ProfileController@show')->name('profile');
Route::get('/{username}/followers', 'ProfileController@followers')->name('followers');
// resources/views/layouts/profile.blade.php
<ul class="nav navbar-nav">
<li class="{{ !Route::currentRouteNamed('profile') ?: 'active' }}">
<a href="{{ url('/' . $user->username) }}" class="text-center">
<div class="text-uppercase">Tweets</div>
<div>0</div>
</a>
</li>
@if ($is_edit_profile)
<li class="{{ !Route::currentRouteNamed('following') ?: 'active' }}">
<a href="{{ url('/following') }}" class="text-center">
<div class="text-uppercase">Following</div>
<div>{{ $following_count }}</div>
</a>
</li>
@endif
<li class="{{ !Route::currentRouteNamed('followers') ?: 'active' }}">
<a href="{{ url('/' . $user->username . '/followers') }}" class="text-center">
<div class="text-uppercase">Followers</div>
<div>{{ $followers_count }}</div>
</a>
</li>
</ul>

Ta-da!

Profile page
Following page
Followers page

Before you leave

Why don’t I provide the complete source code instead of editing along the way?

In my opinion, providing the complete source code is not the correct learning path. In the real world development, you edit the same file for hundred of times. My purpose is showed you how do I improve the application line by line.

By reading my guides, you’ll learn the steps that never teach in most of the courses or tutorials. I believe that you can do it better now, no matter you are writing an application from scratch or improving an existing one.

Feel free to leave your response below or send me an email at sky@iteachyouhowtocode.com

What’s next?

There are two things you can do

  1. Subscribe to my email list, I will email you when the next post is ready.
  2. If you find this guide helpful, share this guide to your friend. I feel grateful if you do that, I’m sure your friend will thank you as well.

--

--

Sky Chin
Sky Chin

Written by Sky Chin

Engineering Manager @ Mindvalley

Responses (11)