Rebuild Twitter with Laravel — Timeline

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

If you’ve done the previous parts, jump right to the planning.

If you’ve missed the previous parts, you can go back to Part 1 or Part 2. Otherwise, you can clone the project from Github. I’ll guide you how to install the project and continue to this part.

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 ‘#2_Followers’ 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
Migrated: 2017_02_11_083844_create_followers_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 create a tweet in maximum 140 characters. You can read the stream of tweets from the users you are following.

In this case, you are going to build a Timeline page which has a list of tweets from the user you’re following.

Why build a timeline before creating a tweet?

Posting is the essential feature for a social media. Timeline (or news feed on Facebook) is the reason your user stay. They want to know what is happening around them.

It is easy to create a tweet. Basically, it is an insert query.

Displays the timeline is a complicated task. The timeline is the latest tweets from your following users. It forms on top of the relationship of the tweets and users. In some cases, you denormalize the table to improve the read performance which will change your insert query. This can reduce some refactoring works.

However, this is not the case. You don’t need to denormalize the table.

These are the pages you’ll be developing,

  • Home page (Timeline)
  • Profile page (Show tweets)

Alright, let’s summarise your planning

  • Create a relationship among tweets and users
  • Form the timeline stream
  • Display user’s tweets in profile page

First thing first, you generate a new migration.

Migration

Create a new table for storing all the tweets.

// Terminal
php artisan make:model Tweet — migration
Model created successfully.
Created Migration: 2017_03_27_083148_create_tweets_table

In the tweets table, you add a column for tweet’s body and a column for user’s id.

// database/migrations/2017_03_27_083148_create_tweets_table.php
class CreateTweetsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create(‘tweets’, function (Blueprint $table) {
$table->increments(‘id’);
$table->integer(‘user_id’)->unsigned()->index();
$table->foreign(‘user_id’)->references(‘id’)->on(‘users’)->onDelete(‘cascade’);
$table->string(‘body’, 140);
$table->timestamps();
});
}

}

Next, declare the two columns as fillable to allow mass insertion.

// app/Tweet.php
class Tweet extends Model
{
protected $fillable = [
'user_id', 'body',
];
}

Okay, the next step is to import test data.

Seeder

Laravel includes a simple method of seeding your database with test data using seed classes.

Generate a Seeder class.

// Terminal
php artisan make:seeder TweetsTableSeeder
Seeder created successfully.

Next, you define a model factory which generates dummy data for tweet’s body.

// database/factories/ModelFactory.php
$factory->define(App\Tweet::class, function (Faker\Generator $faker) {
return [
'body' => $faker->realText(140),
];
});

Then, generate ten tweets on behalf of the user whose ID is 2.

// database/seeds/TweetsTableSeeder.php
use App\Tweet;
use Illuminate\Database\Seeder;
class TweetsTableSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
factory(Tweet::class, 10)->create([
'user_id' => 2
]);
}
}

// Terminal
php artisan db:seed — class=TweetsTableSeeder

You’ll see ten records in the tweets table.

Insert hundred of tweets

Ten tweets probably not enough. Increase it to be a hundred.

// database/seeds/TweetsTableSeeder.php
public function run()
{
factory(Tweet::class, 100)->create([
'user_id' => 2
]);
}

In a more convenient way, enable the calling to TweetsTableSeeder in DatabaseSeeder run method.

// database/seeds/DatabaseSeeder.php
public function run()
{
$this->call(TweetsTableSeeder::class);
}

// Terminal
php artisan db:seed
Seeded: TweetsTableSeeder

Bang! A hundred records are in the house! Would you want to try a thousand? Maybe next time, seeding is not our main focus.

Let’s get the tweets list on the user’s profile page.

Tweets

Create a relationship among user and tweet.

// app/User.php
/**
* Get the tweets for the user.
*/
public function tweets()
{
return $this->hasMany('App\Tweet', 'user_id', 'id');
}

Then, list the tweets on the profile page.

// resources/views/profile.blade.php
<div class="panel-body">
<div class="list-group">
@forelse ($user->tweets()->get() as $tweet)
<a href="#" class="list-group-item">
<h4 class="list-group-item-heading">{{ $tweet->body }}</h4>
<p class="list-group-item-text">{{ $tweet->created_at }}</p>
</a>
@empty
<p>No tweet</p>
@endforelse
</div>

</div>
Home page with timeline

Before you start to build the timeline, there’s a small bug hide in the application.

Bug fix

You may have noticed the undefined variable error on the profile page. This error occurs when you open any profile page without login.

Undefined variable error
// app/Http/Controllers/ProfileController.php
public function show($username)
{
$user = User::where('username', $username)->firstOrFail();
    $followers_count =  $user->followers()->count();
    // Fix undefined variable error when user is not login
$following_count = 0;
    $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
]);
}

Now, you are have the biggest challenge, display the timeline.

Timeline

In order to retrieve the list of the tweets,

  1. find the following users,
  2. find their tweets,
  3. sort by creation date in descending order

The concept is simple, the real challenge is how to achieve it.

In this case, Eager Loading is the answer.

// app/User.php
/**
* Get timeline.
*/
public function timeline()
{
$following = $this->following()->with(['tweets' => function ($query) {
$query->orderBy('created_at', 'desc');
}])->get();
    // By default, the tweets will group by user.
// [User1 => [Tweet1, Tweet2], User2 => [Tweet1]]
//
// The timeline needs the tweets without grouping.
// Flatten the collection.
$timeline = $following->flatMap(function ($values) {
return $values->tweets;
});
    // Sort descending by the creation date
$sorted = $timeline->sortByDesc(function ($tweet) {
return $tweet->created_at;
});
    return $sorted->values()->all();
}

Eager Loading returns a set of collection which is grouping by the user. The collection looks like this.

[
User1 => [
Tweet1,
Tweet2
],
User2 => [
Tweet1
]
]

This is definitely not the output that you want. You flatten the collection in order to remove the grouping. After that, sort it again by creation date.

Next, update the Home page.

// resources/views/home.blade.php
<div class="panel-body">
@forelse ($user->timeline() as $tweet)
<a href="#" class="list-group-item">
<h4 class="list-group-item-heading">{{ $tweet->body }}</h4>
<p class="list-group-item-text">{{ $tweet->created_at }}</p>
</a>
@empty
<p>No tweet</p>
@endforelse
</div>

Pass the user model to the view.

// app/Http/Controllers/HomeController.php
use Auth;
...
class HomeController extends Controller
{
...
    /**
* Show the application dashboard.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
return view('home', [
'user' => Auth::user(),
]
);
}
}
Timeline

Oh wait, there is a “has-many-through” relationship, isn’t it? It defines the User has Tweet through Follower.

Well. This is not the case.

Let me show you why.

// app/Follower.php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Follower extends Model
{
/**
* The tweets that belong to the user.
*/
public function tweets()
{
return $this->belongsToMany('App\Tweet', 'user_id', 'user_id');
}
}

// app/User.php
public function timeline()
{
return $this->hasManyThrough(
'App\Tweet', 'App\Follower',
'user_id', 'user_id', 'follower_user_id'
)->get();
}

You probably will get this result when you browse the home page.

Unknown column error

This is the valid “has-many-through” relationship.

“has-many-through” relationship

Probably not this.

Complicated relationship

A country has many states and a state has many cities. A country doesn’t have direct relationship with the city.

In your case, a user has many following users and a user has many tweets. A tweet’s immediate parent is the user, not the following user.

As you can see from the error message, Laravel couldn’t generate the valid query.

select `tweets`.*, `followers`.`user_id` from `tweets` inner join `followers` on `followers`.`id` = `tweets`.`user_id` where `followers`.`user_id` is null)

This is the expected query.

SELECT `tweets`.*
FROM followers
INNER JOIN tweets ON `followers`.`user_id` = `tweets`.`user_id`
WHERE `followers`.`follower_user_id` = ‘1’
ORDER BY `tweets`.`created_at` DESC;
Expected query output

Alright, how does the eager loading achieve it? It is using two queries instead of one. Check out the documentation.

select `users`.*, `followers`.`follower_user_id` as `pivot_follower_user_id`, `followers`.`user_id` as `pivot_user_id`, `followers`.`created_at` as `pivot_created_at`, `followers`.`updated_at` as `pivot_updated_at` from `users` inner join `followers` on `users`.`id` = `followers`.`user_id` where `followers`.`follower_user_id` = ?
select * from `tweets` where `tweets`.`user_id` in (?) order by `created_at` desc

Eager loading is making more sense than “has-many-through” relationship here.

Next step is improving the performance.

The timeline is worked, but your job hasn’t done here. What if you have more than a thousand users in your following list? Your homepage probably very slow.

So, your next step is improving the performance.

Improve the Performance

Your Home page is slowing down as you follow more users.

What pops up in your mind? MySQL? Probably the database is the first thing you blame.

It is not the MySQL’s fault. MySQL is capable of returning thousands row of data in milliseconds. Then, what?

Your greatest enemy here is your user’s browser. The browser takes ten to twenty seconds to render the Home page. Before the rendering done, your user already leaves and tell his friends that your website is slow.

In the old time, the Internet Explorer cannot even open the homepage.

To improve the performance, you implement infinite scrolling feature. The content will be fetched in a chunk, ten tweets per time. When you are scrolling down, the new contents will load via AJAX.

Additionally, I recommend you to use Single Action Controller which is specifically designed for a single action.

Single Action Controller

Single action controller does one thing only.

For example, AddPostController inserts a new post, EditPostController updates a post, and ShowPostController returns a post. AddPostController doesn’t update post.

You can learn more in detail from Michael Dyrynda. This is his post, Single action controllers in Laravel.

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

// app/Http/Controllers/ShowTimeline.php
use Auth;
...
class ShowTimeline
{
public function __invoke()
{
$user = Auth::user();
        return response()->json($user->timeline());
}
}

Add a new route.

// routes/web.php
Route::group(['middleware' => 'auth'], function () {
Route::get('/timeline', 'ShowTimeline');
...
});

Hit the browser address bar with http://laratweet.app/timeline. You will see this.

/timeline

Next, deal with the pagination.

Pagination

Eloquent makes it extremely easy.

Laravel’s paginator is integrated with the query builder and Eloquent ORM and provides convenient, easy-to-use pagination of database results out of the box. — Laravel Pagination
// app/User.php
/**
* Get timeline.
*/
public function timeline()
{
$following = $this->following()->with(['tweets' => function ($query) {
$query->orderBy('created_at', 'desc');
        // Sort id descending because the data generated by seeder is too close
$query->orderBy('id', 'desc');
        // Set 10 items per page
$query->paginate(10);

}])->get();
    // collection sorting
...
}

Now, hits the refresh button.

Page 1 of /timeline

Add the query string ?page=2 behind the URL. Example, http://laratweet.app/timeline?page=2

Page 2 of /timeline

Great work. The backend system is ready. Let’s do the front-end job.

Ajax

Define a new section of content in app layout for its child page to inject javascript.

// resources/views/layouts/app.blade.php
<!-- <script src="/js/app.js"></script> -->
<script src="https://unpkg.com/vue@2.1.10/dist/vue.js"></script>
@yield('script')

You install vue-infinite-scroll.

// resources/views/home.blade.php
@extends('layouts.app')
@section('content')
<div class="container">
<div class="row">
<div class="col-md-8 col-md-offset-2">
<div class="panel panel-default">
<div class="panel-heading">Timeline</div>
                <div class="panel-body" id="list-1" v-infinite-scroll="loadMore" infinite-scroll-disabled="busy" infinite-scroll-distance="10">
<a href="#" class="list-group-item" v-for="tweet in items">
<h4 class="list-group-item-heading">@{{ tweet.body }}</h4>
<p class="list-group-item-text">@{{ tweet.created_at }}</p>
</a>
</div>

</div>
</div>
</div>
</div>
@endsection
@section('script')
<script src="
https://unpkg.com/vue-resource@1.2.0/dist/vue-resource.min.js"></script>
<script src="
https://unpkg.com/vue-infinite-scroll@2.0.0"></script>
<script type="text/javascript">
var page = 1;
    new Vue({
el: '#list-1',
data: {
page: 1,
items: [],
busy: false
},
methods: {
loadMore: function() {
this.busy = true;
            var url = '/timeline' + (this.page > 1 ? '?page=' + this.page : '');
            this.$http.get(url)
.then(response => {
var data = response.body;
                // Push the response data into items
for (var i = 0, j = data.length; i < j; i++) {
this.items.push(data[i]);
}
                // If the response data is empty,
// disable the infinite-scroll
this.busy = (j < 1);
                // Increase the page number
this.page++;
});
}
}
});
</script>
@endsection
Ajax timeline

Oops! Probably you don’t like the date time. So do I. It’s not user-friendly. Let’s fix it.

By default, Eloquent will convert the created_at, updated_at, and deleted_at columns to instances of Carbon (refer here). And, Carbon has a very useful function to convert date format to human understandable format.

http://carbon.nesbot.com/docs/#api-humandiff

However, you don’t print the output on the View. I suggest defining an accessor for the created_at attribute.

// app/Tweet.php
use Carbon\Carbon;
...
class Tweet extends Model
{
...
    public function getCreatedAtAttribute($value) {
return Carbon::createFromFormat('Y-m-d h:i:s', $value)->diffForHumans();
}
}
Final timeline

Beautiful!

Conclusion

Planning your development is important. List down the to-dos before starting to write any code.

By spending ten to twenty minutes to make the to-dos list, you know exactly what you are doing and what you are going to do next. You be more focus on your task and increase productivity.

Planning in the early stage can help you discovering the new problem and clearing all the doubts. When you have a question regarding the development, you can find the answer before you start to dive into the code.

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

Next step

There are the things you can do

  1. Drop me a response below tell me what do you want to add into your Laratweet.
  2. Subscribe to my email list, I will email you when the next post, Tweet and Retweet is ready.
  3. 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.