Rebuild Twitter with Laravel — Upgrade to 5.4, Post Tweet, Link Preview, URL Shortener

This is Part 4 of Rebuild Twitter with Laravel. Previously, you created the foundation, built profile and timeline. In this part, you’re going to introduce the tweeting feature to your user.

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 or Part 3. Otherwise, you can clone the project from Github. I’ll guide you how to install the project and continue to this part.

Offline version

If you are interested to study without internet, you can get the FREE tutorials of this series in PDFs. Subscribe to my email list and I’ll deliver the PDF to your inbox.

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 ‘#3_Timeline’ 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
Migrated: 2017_03_27_083148_create_tweets_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 include a link in your tweet, then, a link preview will be generated and present below your tweet. And, Twitter translate your link to a shorter link.

Twitter’s Home page

In this case, you’ll build a link preview and an URL shortener modules that run behind the scene.

In the previous part, you built the timeline. You’ve crafted the Tweet table right. It is ready for the heavy job.

You allow your user to create a tweet. Like I mentioned before, creating a tweet is easy, it is an insert query. Behind this simple feature, you’ll add two features to make the tweet becomes more interactive and meaningful.

Alright, let’s summarise your planning,

  1. Create a post box on Home page to tweet
  2. Generate link preview for the URL user tweeted
  3. Shorten the URL tweeted by user

Rebuild Twitter with Laravel was started before the Laravel 5.4 was released. This project is based on Laravel 5.3. It is been awhile and this project is going to left behind very soon (5.5 is coming soon).

So, let’s update to 5.4.

3 steps to upgrade to 5.4 from 5.3

It is easy to upgrade the project, it needs only 3 steps.

Step 1 — composer update

Update your project’s dependency of laravel/framework from 5.3.* to 5.4.* and phpunit/phpunit to ~5.7 in composer.json.

// composer.json
"require": {
"php": ">=5.6.4",
"laravel/framework": "5.4.*"
},
"require-dev": {
"fzaninotto/faker": "~1.4",
"mockery/mockery": "0.9.*",
"phpunit/phpunit": "~5.7",
"symfony/css-selector": "3.1.*",
"symfony/dom-crawler": "3.1.*"
},

Perform composer update.

// Terminal
composer update

Step 2 — Reinstall Laravel Tinker

Somehow, in the Laravel upgrade guide, it recommends reinstalling Laravel Tinker.

// Terminal
composer require laraval/tinker

Add TinkerServiceProvider in config/app.php.

// config/app.php
'providers' => [
    ...
Laravel\Tinker\TinkerServiceProvider::class,
],

Step 3 — Clear cache

Once you’ve upgraded the project, the application may still cache the compiled services from the previous version.

Remove compiled services file bootstrap/cache/compiled.php

After that, flush the view cache.

// Terminal
php artisan view:clear
Compiled views cleared!

Alright, these are the basic instructions to update from 5.3 to 5.4. If you want more in detail, check it out here.

Migration

Create a new table for storing the links from the tweets. Whenever user tweet with a link in the body, extract the link and insert into the Link table.

// Terminal
php artisan make:model Link -m
Model created successfully.
Created Migration: 2017_05_23_031547_create_links_table

// database/migrations/2017_05_23_031547_create_links_table.php
class CreateLinksTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
// Create Links table
Schema::create('links', function (Blueprint $table) {
$table->increments('id');
$table->string('url')->unique();
$table->string('cover')->nullable();
$table->string('title')->nullable();
$table->text('description')->nullable();
$table->string('short_url')->nullable();
$table->timestamps();
});
}
    /**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('links');
}
}

// Terminal
php artisan migrate
Migrated: 2017_05_23_031547_create_links_table

// app/Link.php
class Link extends Model
{
protected $fillable = [
'url', 'cover', 'title', 'description',
];
    public function tweets()
{
return $this->hasMany('App\Tweet', 'link_id');
}
}

Alright, let’s craft the view.

Craft the view

Sample first is a very good framework on knowing what kind of information you need to pass to the view.

You build a sample link preview layout on the Home page.

// resources/home.blade.php
<div class="panel-body" id="list-1" v-infinite-scroll="loadMore" infinite-scroll-disabled="busy" infinite-scroll-distance="10">
<template v-for="tweet in items">
<div class="list-group-item">
<h4 class="list-group-item-heading">@{{ tweet.user_id }}</h4>
<p>@{{ tweet.body }}</p>
<div class="list-group-item-text panel panel-default">
<div class="media">
<div class="media-middle">
<img class="media-object center-block" src="
https://cdn.boogiecall.com/media/images/872398e3d9598c494a2bed72268bf018_1440575488_7314_s.jpg">
</div>
<div class="media-body panel-body">
<h3 class="media-heading">
Events, parties & live concerts in Melbourne
</h3>
<div>
List of events in Melbourne. Nightlife, best parties and concerts in Melbourne, event listings and reviews.
</div>
</div>
</div>
</div>
<p class="list-group-item-text">@{{ tweet.created_at }}</p>
</div>
</template>

</div>
Template of link preview

How does it look? The author’s user ID doesn’t fit there. Let’s display the author name instead.

Present author name

In the /timeline, author information doesn’t return in the response. You need to fetch the author information along with the timeline.

In Tweet, you create a new relationship between it and User. Tweet has an author.

// app/Tweet.php
public function author()
{
return $this->belongsTo('App\User', 'user_id');
}

To eager load nested relationships, you may use “dot” syntax. You eager load the timeline and tweet’s author information in one Eloquent statement

// app/User.php
public function timeline()
{
$following = $this->following()->with(['tweets.author' => function ($query) {
...
    }])->get();
    ...
}

Display author’s name on Timeline.

// resources/home.blade.php
<template v-for="tweet in items">
<div class="list-group-item">
<h4 class="list-group-item-heading">@{{ tweet.author.name }}</h4>
...
</div>
</template>
Author name is presented

Alright, real work starts now.

Tweeting

Users share their stories to their followers via a simple text post, it is called tweeting. Besides the story, user tweet about interesting stories and inspiring articles from blogs or websites.

Let’s start with adding a post box on the Home page.

Add a post box

You add a post box like Twitter on top of the timeline.

// resources/home.blade.php
<div class="panel-body">
<form method="POST" action="/tweet">
{{ csrf_field() }}
<div class="form-group input-group">
<input type="text" class="form-control" id="tweet_body" name="tweet_body" placeholder="What's happening?">
<span class="input-group-btn">
<button class="btn btn-default" type="submit">Tweet</button>
</span>
</div>
</form>
...
</div>
Post box on Home page

Create request

Then, create a custom FormRequest to hold the validation logic. In this case, tweet_body is expecting to post to the controller.

// Terminal
php artisan make:request PostTweetRequest
Request created successfully.

In the PostTweetRequest, there are authorize() and rules().

In authorize(), you can check whether the authenticated user has the authority to perform the request. However, you have the authentication logic in the middleware Auth to handle the part, so, simply return true.

In rules(), you declare the tweet_body as a mandatory field, only string, and accepts 140 characters or less only.

// app/Http/Requests/PostTweetRequest.php
class PostTweetRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
    /**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'tweet_body' => 'required|string|max:140',
];
}
}

Controller

I believe you still remember the Single Action Controller (SAC) you’ve learned in Part 3.

You create a SAC which accepts PostTweetRequest and creates a new tweet in the database.

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

// app/Http/Controllers/PostTweet.php
use Auth;
use App\Tweet;
use App\Http\Requests\PostTweetRequest;
class PostTweet extends Controller
{
public function __invoke(PostTweetRequest $request)
{
$tweet = new Tweet(['body' => $request->tweet_body]);
Auth::user()->tweets()->save($tweet);
        return redirect('home');
}

}

Routing

Don’t forget to add the new post URL into the route.

// routes/web.php
Route::group(['middleware' => 'auth'], function () {
Route::get('/timeline', 'ShowTimeline');
Route::post('/tweet', 'PostTweet');
Route::get('/following', 'ProfileController@following')->name('following');
Route::post('/follows', 'UserController@follows');
Route::post('/unfollows', 'UserController@unfollows');
});
Post box on Home page

If a user submits a tweet which is not fulfilling the requirements in the PostTweetRequest, Laravel will return error message before reaching to the PostTweet controller.

You are a good coder, you handle errors. You display the error message below the post box.

// resources/views/home.blade.php
<form method="POST" action="/tweet">
{{ csrf_field() }}
@if ($errors->has('tweet_body'))
<div class="form-group input-group has-error">
@else
<div class="form-group input-group">
@endif

<input type="text" class="form-control" id="tweet_body" name="tweet_body" placeholder="What's happening?">
<span class="input-group-btn">
<button class="btn btn-default" type="submit">Tweet</button>
</span>
</div>
@if ($errors->has('tweet_body'))
<div class="has-error">
<span class="help-block">{{ $errors->first('tweet_body') }}</span>
</div>
@endif

</form>
Post box with error message

Next step, generate a link preview from the tweet that has a link within.

Link Preview

When user clips a link within the tweet, you don’t know what’s it about. A link preview gives the reader a quick glance at what’s behind the link.

In the planning, links are stored in a table and reuse for all the tweets. Although user will include one or more links in the tweet, there is only one preview only. So you bind the link to the tweet.

// Terminal
php artisan make:migration add_link_to_tweet
Created Migration: 2017_05_24_023654_add_link_to_tweet

Add link_id as a foreign key in tweets table.

// database/migrations/2017_05_24_023654_add_link_to_tweet.php
class AddLinkToTweet extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('tweets', function (Blueprint $table) {
$table->integer('link_id')->after('body')->unsigned()->index()->nullable();
$table->foreign('link_id')->references('id')->on('links')->onDelete('cascade');
});
}
    /**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('tweets', function (Blueprint $table) {
$table->dropColumn('link_id');
});
}
}

Perform the migration.

// Terminal
php artisan migrate
Migration table created successfully.
Migrated: 2017_05_24_023654_add_link_to_tweet

When a new tweet is created, call the fresh() to refresh the model with its newly generated ID.

Detect the link within the tweet body with a regular expression and create an entry in the links table. With the Link::firstOrCreate(), the model will create if the link doesn’t exist, otherwise, the existing model will return.

// app/Http/Controllers/PostTweet.php
use App\Link;
class PostTweet extends Controller
{
public function __invoke(PostTweetRequest $request)
{
$tweet = new Tweet(['body' => $request->tweet_body]);
Auth::user()->tweets()->save($tweet);
        $this->detectLink($tweet->fresh());
        return redirect('home');
}
    private function detectLink(Tweet $tweet)
{
$reg_exLink = "/(http|https)\:\/\/[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(\/\S*)?/";
// Check if there is a url in the text
if(preg_match($reg_exLink, $tweet->body, $url)) {
$link = Link::firstOrCreate(['url' => $url[0]]);
        $tweet->link_id = $link->id;
$tweet->save();
}
}

}

Now, generate the link preview.

Link preview generation

Link preview generation is an event happening after a new link created. You create a listener to listen to the link created event and perform necessary tasks.

Install the link preview package.

// Terminal
composer require dusterio/link-preview
Register the event and listener.

// app/Providers/EventServiceProvider.php
class EventServiceProvider extends ServiceProvider
{
/**
* The event listener mappings for the application.
*
* @var array
*/
protected $listen = [
'App\Events\LinkCreated' => [
'App\Listeners\NotifyLinkPreviewGenerator',
],

];
    ...
}

// Terminal
php artisan event:generate
Events and listeners generated successfully!

Map the event with the link created event.

// app/Link.php
use App\Events\LinkCreated;
class Link extends Model
{
...
    /**
* The event map for the model.
*
* @var array
*/
protected $events = [
'created' => LinkCreated::class,
];
    ...
}

In LinkCreated, you inject the Link from the created event.

// app/Events/LinkCreated.php
use App\Link;
class LinkCreated
{
...
    public $link;
    /**
* Create a new event instance.
*
* @return void
*/
public function __construct(Link $link)
{
$this->link = $link;
}
    ...
}

In NotifyLinkPreviewGenerator.php, you generate the link preview with the installed package.

// app/Listeners/NotifyLinkPreviewGenerator.php
use Dusterio\LinkPreview\Client;
class NotifyLinkPreviewGenerator
{
...
    /**
* Handle the event.
*
* @param LinkCreated $event
* @return void
*/
public function handle(LinkCreated $event)
{
$link = $event->link;
        $previewClient = new Client($link->url);
        // Get a preview from specific parser
$preview = $previewClient->getPreview('general');
        // Convert output to array
$preview = $preview->toArray();
        // Update Link
$link->cover = $preview['cover'];
$link->title = $preview['title'];
$link->description = $preview['description'];
$link->save();

}
}

Boom! The cover, title, and description are automatically filled up in the table after the link is inserted.

Post a tweet with link
The link preview is updated in the database

You display it on the Home page.

// app/Tweet.php
class Tweet extends Model
{
...
    public function link()
{
return $this->belongsTo('App\Link', 'link_id');
}
}

// app/User.php
public function timeline()
{
$following = $this->following()->with(['tweets.author' => 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);
    }, 'tweets.link'])->get();
    ...
}

// resources/views/home.blade.php
<div id="list-1" v-infinite-scroll="loadMore" infinite-scroll-disabled="busy" infinite-scroll-distance="10">
<template v-for="tweet in items">
<div class="list-group-item">
<h4 class="list-group-item-heading">@{{ tweet.author.name }}</h4>
<p>@{{ tweet.body }}</p>
<div class="list-group-item-text panel panel-default" v-if="tweet.link">
<a v-bind:href="tweet.link.url" target="_blank" style="text-decoration: none;">
<div class="media">
<div class="media-middle">
<img class="media-object center-block" style="max-width: 100%;" v-bind:src="tweet.link.cover">
</div>
<div class="media-body panel-body">
<h3 class="media-heading">
@{{ tweet.link.title }}
</h3>
<div>
@{{ tweet.link.description }}
</div>
</div>
</div>
</a>
</div>
<p class="list-group-item-text">@{{ tweet.created_at }}</p>
</div>
</template>
</div>
Link Preview on Home page

Bug fix

You realise that the timeline is not sorting descending. The mutation of the created_at is the main cause, which is a bad call. Now you patch a hotfix to sort it right. In this case, you have updated_at which is not mutated yet.

// app/User.php
public function timeline()
{
...
    // Sort descending by the creation date
$sorted = $timeline->sortByDesc(function ($tweet) {
return $tweet->updated_at;
});
    ...
}
Newest post is on top

The newest post is back to the top.

Okay, let’s move on to the next task, URL shortener.

URL shortener

With third party service provider, you can know what’s the trending stories your user are sharing, and the shorten link optimize the sharing across devices. Today, Bitly is your service provider.

Install the URL shortener package.

// Terminal
composer require mremi/url-shortener

Register a new listener to listen to the LinkCreated event.

// app/Providers/EventServiceProvider.php
protected $listen = [
'App\Events\LinkCreated' => [
'App\Listeners\NotifyLinkPreviewGenerator',
'App\Listeners\NotifyUrlShortener',
],
];

// Terminal 
php artisan event:generate
Events and listeners generated successfully!

Before you can use Bitly’s service, you need an account.

Setup Bitly account

Register a Bitly account here if you have none. Then, generate a Generic Access Token at here.

Generate Generic Access Token

Setup environment

Add BITLY_GENERIC_ACCESS_TOKEN to your application environment file.

// .env
BITLY_GENERIC_ACCESS_TOKEN={your generic access token}

NotifyUrlShortener

When a link created, shorten the link with the BitlyProvider with your Generic Access Token and update the short link to the database.

// app/Events/NotifyUrlShortener.php
...
use Mremi\UrlShortener\Model\Link;
use Mremi\UrlShortener\Provider\Bitly\BitlyProvider;
use Mremi\UrlShortener\Provider\Bitly\OAuthClient;
use Mremi\UrlShortener\Provider\Bitly\GenericAccessTokenAuthenticator;
class NotifyUrlShortener
{
...
    /**
* Handle the event.
*
* @param LinkCreated $event
* @return void
*/
public function handle(LinkCreated $event)
{
$link = new Link;
$link->setLongUrl($event->link->url);
        $bitlyProvider = new BitlyProvider(
new GenericAccessTokenAuthenticator(env('BITLY_GENERIC_ACCESS_TOKEN')),
array('connect_timeout' => 10, 'timeout' => 10)
);
        $bitlyProvider->shorten($link);
        $event->link->short_url = $link->getShortUrl();
$event->link->save();
}
}

Upgrade view

In consideration of the same tweet layout will be used in another part of the application, like Profile page, you upgrade the div to become a Vue Component.

// resources/views/home.blade.php
<div id="list-1" v-infinite-scroll="loadMore" infinite-scroll-disabled="busy" infinite-scroll-distance="10”>
...
<template v-for="item in items">
<tweet-component :tweet="item"></tweet-component>
</template>

</div>
...
<script type="text/javascript">
var page = 1;
    Vue.component('tweet-component', {
props: ['tweet'],
template: '\
<div v-if="tweet.author" class="list-group-item">\
<h4 class="list-group-item-heading">@{{ tweet.author.name }}</h4>\
<p v-if="tweet.link && tweet.link.short_url">@{{ tweet.link.url }}</p>\
<p v-else>@{{ tweet.body }}</p>\
<div class="list-group-item-text panel panel-default" v-if="tweet.link">\
<a v-bind:href="tweet.link.short_url || tweet.link.url" target="_blank" style="text-decoration: none;">\
<div class="media">\
<div class="media-middle">\
<img class="media-object center-block" style="max-width: 100%;" v-bind:src="tweet.link.cover">\
</div>\
<div class="media-body panel-body">\
<h3 class="media-heading">\
@{{ tweet.link.title }}\
</h3>\
<div>\
@{{ tweet.link.description }}\
</div>\
</div>\
</div>\
</a>\
</div>\
<p class="list-group-item-text">@{{ tweet.created_at }}</p>\
</div>\
'
});
    new Vue({
...
});
</script>
Post a tweet with link
Short URL show when you hover the link preview

Bang! The timeline look very nice now!

Conclusion

In real world development, your application will not work as expected all the time. There are many unexpected situations occur.

What if the link preview generation fails?

What if the short URL connection timeout?

My advice, you can add a service constantly looking for the Link that is failed to generate link preview or missing short URL, and then broadcast an event. The convenience of the listener is it can listener to multiple events. The listeners you’ve created can pick up the job to patch the information.

Next Step

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.