Add Chat to a Laravel PHP App Using Twilio Chat

December 14, 2019
Written by
Chimezie Enyinnaya
Contributor
Opinions expressed by Twilio contributors are their own

Add Chat to a Laravel App Using Twilio Chat

In this tutorial, I’ll be showing you how to add chat functionality to a Laravel application using Twilio Programmable Chat.

Prerequisites

In order to follow this tutorial, you will need the following:

What We’ll Be Building

For the purpose of this tutorial, we’ll be building a one-to-one chat functionality. Users will be able to select another user to begin chat.

Getting Twilio Credentials

Login to your Twilio dashboard and copy both your ACCOUNT SID and AUTH TOKEN.

Twilio Account Credentials

Before you can start using the Twilio Programmable Chat API, you need to first create a chat service:

Twilio Programmable Chat Dashboard

Take note of your SERVICE SID.

Base Configuration

Lastly, you need to create an API key:

New API Key screen

Also, take note of both your API SECRET and API SID.

Creating a New Laravel Application

Let’s get started by creating a new Laravel application. We’ll be using the Laravel installer mentioned in the prerequisites. Run the following command in your console:

$ laravel new laravel-twilio-chat
$ cd laravel-twilio-chat
$ composer require laravel/ui --dev
$ php artisan ui vue --auth

Version 2.3 of the Laravel installer was recently released. It allows us to specify that we want authentication scaffolding at the point of creating a new Laravel application.

With the application created, we need to install the NPM dependencies (because https://getbootstrap.com/Bootstrap, Vue.js, and some other dependencies, which we’ll be using comes pre-packed as an NPM dependency):

$ npm install

Next, you will run the default migrations that come with Laravel after a quick set up of the database. We’ll be making use of SQLite for our database. Update the .env file as below:

DB_CONNECTION=sqlite
DB_DATABASE=/absolute/path/to/database.sqlite

Create the database.sqlite file:

$ touch database/database.sqlite

Now, run the migration:

$ php artisan migrate

Next, add your Twilio credentials to the .env file:

TWILIO_AUTH_SID=YOUR_TWILIO_AUTH_SID
TWILIO_AUTH_TOKEN=YOUR_TWILIO_AUTH_TOKEN
TWILIO_SERVICE_SID=YOUR_TWILIO_CHAT_SERVICE_SID
TWILIO_API_SECRET=YOUR_TWILIO_API_SECRET
TWILIO_API_SID=YOUR_TWILIO_API_SID

Displaying a List of Users to Chat With

We want users to be able to select the user they want to chat with. To accomplish this, the navigation links inside app.blade.php will need to be updated to include a link to the currently authenticated user’s messages. Add the code below before the user drop-down menu:

// resources/views/layouts/app.blade.php

<li class="nav-item">
  <a class="nav-link" href="{{ route('messages.index') }}">Messages</a>
</li>

Next, the named route used inside the link will need to be created in web.php:

// routes/web.php

Route::get('/messages', 'MessageController@index')->name('messages.index');

Next, let’s create the MessageController and the corresponding method:

$ php artisan make:controller MessageController

Then replace its content with the following code:

// app/Http/Controllers/MessageController.php

<?php
namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\User;

class MessageController extends Controller
{
        public function __construct()
        {
                $this->middleware('auth');
        }

        public function index(Request $request)
        {
                $users = User::where('id', '<>', $request->user()->id)->get();

                return view('messages.index', compact('users'));
        } 
}

First, we are applying the auth middleware to all the methods in the class through the constructor. The index() method then fetches all the users in the database aside from the currently authenticated user.

Now, we can create the view to display the list of users. Create a messages directory inside the views directory. Then create an index.blade.php file inside of it and paste the code below:

// resources/views/messages/index.blade.php

@extends('layouts.app')

@section('content')
  <div class="container">
    <div class="row">
      <div class="col-md-3">
        @include('messages.shared.users')
      </div>
      <div class="col-md-9">
        <div class="card">
          <div class="card-body text-center">
            <p class="font-weight-bold">You don’t have a chat selected</p>
            <p>Choose a user to continue an existing chat or start a new one.</p>
          </div>
        </div>
      </div>
    </div>
  </div>
@endsection

We’ll be needing the code to render the list of users in another view, hence the need to extract it to a partial that can be reused in multiple views. Create a shared directory inside the messages directory, and within it create a users.blade.php file with the following code:

// resources/views/messages/shared/users.blade.php

<div class="card">
  <div class="card-header">Users</div>
    <div class="card-body">
      @if ($users->isEmpty())
        <p>No users</p>
      @else
        <ul class="list-group list-group-flush">
          @foreach ($users as $user)
            <a href="#" class="list-group-item list-group-item-action">{{ $user->name }}</a>
          @endforeach
        </ul>
      @endif
  </div>
</div>

If there are no users, we display an appropriate message. Otherwise, we display the list of users.

Laravel app displaying active users

Creating Unique Channels for Users

Channels are the heart of Twilio Programmable Chat, as all interactions revolve around it. We can look at it in terms of a container, as it will contain all chat messages, users, etc. Since we are building a one-to-one chat application, we need to create a private channel and make it unique for each chat conversation. One way to achieve that is to create the channel name of the users' IDs, that way it will be unique to both users in the conversation.

We want this to work such that once a user selects a user from the list of users, the channel will be created automatically if it doesn’t exist yet. Update the anchor tag in the users' list as shown below:

// resources/views/messages/shared/users.blade.php

<a href="{{ route('messages.chat', [ 'ids' => auth()->user()->id  . '-' . $user->id ]) }}" class="list-group-item list-group-item-action">{{ $user->name }}</a>

We are joining the authenticated user’s ID to that of the other users with a hyphen.

Next, we need to create the route:

// routes/web.php

Route::get('/messages/{ids}', 'MessageController@chat')->name('messages.chat');

The corresponding method needs to be added in the MessageController:

// app/Http/Controllers/MessageController.php

// add this at the top
use Twilio\Rest\Client;

public function chat(Request $request, $ids)
{
        $authUser = $request->user();
        $otherUser = User::find(explode('-', $ids)[1]);
        $users = User::where('id', '<>', $authUser->id)->get();

        $twilio = new Client(env('TWILIO_AUTH_SID'), env('TWILIO_AUTH_TOKEN'));

        // Fetch channel or create a new one if it doesn't exist
        try {
                $channel = $twilio->chat->v2->services(env('TWILIO_SERVICE_SID'))
                        ->channels($ids)
                        ->fetch();
        } catch (\Twilio\Exceptions\RestException $e) {
                $channel = $twilio->chat->v2->services(env('TWILIO_SERVICE_SID'))
                        ->channels
                        ->create([
                                'uniqueName' => $ids,
                                'type' => 'private',
                        ]);
        }

        // Add first user to the channel
        try {
                $twilio->chat->v2->services(env('TWILIO_SERVICE_SID'))
                        ->channels($ids)
                        ->members($authUser->email)
                        ->fetch();

        } catch (\Twilio\Exceptions\RestException $e) {
                $member = $twilio->chat->v2->services(env('TWILIO_SERVICE_SID'))
                        ->channels($ids)
                        ->members
                        ->create($authUser->email);
        }

        // Add second user to the channel
        try {
                $twilio->chat->v2->services(env('TWILIO_SERVICE_SID'))
                        ->channels($ids)
                        ->members($otherUser->email)
                        ->fetch();

        } catch (\Twilio\Exceptions\RestException $e) {
                $twilio->chat->v2->services(env('TWILIO_SERVICE_SID'))
                        ->channels($ids)
                        ->members
                        ->create($otherUser->email);
        }

        return view('messages.chat', compact('users', 'otherUser'));
}

First, we get the ID of the other user by exploding $ids and getting the second item in the array (which is that of the other user). Then we fetch the user from the database using the given ID. A new instance of the Twilio PHP SDK is created and stored in $twilio. Using the $twilio instance, an attempt is made to fetch a channel with the unique name as the users IDs. If the channel exists, we fetch it, otherwise, we create a new one using the same IDs.

Once the channel is identified, both users need to be added as members using their email addresses as their identity. Also, we check to make sure they are not already added to the channel. Finally, the view is rendered.

Let’s not forget to install the Twilio PHP SDK:

$ composer require twilio/sdk

Next, let’s create the chat.blade.php view with the following code:

// resources/views/messages/chat.blade.php

@extends('layouts.app')

@section('content')
  <div class="container">
    <div class="row">
      <div class="col-md-3">
        @include('messages.shared.users')
      </div>
      <div class="col-md-9">
        <!-- chat component will be rendered here -->
      </div>
    </div>
  </div>
@endsection

We’ll be updating this file to display the chat later on.

Generating the Access Token

Before moving to the Vue.js part of our chat application, let’s add the implementation for generating access tokens for authenticated users. This access token will be required on the client-side. Add the code below inside api.php:

// routes/api.php
Route::post('/token', 'TokenController@generate');

Create the TokenController:

$ php artisan make:controller TokenController

Then replace its content with the following:

// app/Http/Controllers/TokenController.php

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Twilio\Jwt\AccessToken;
use Twilio\Jwt\Grants\ChatGrant;

class TokenController extends Controller
{
        public function generate(Request $request)
        {
                $token = new AccessToken(
                        env('TWILIO_AUTH_SID'),
                        env('TWILIO_API_SID'),
                        env('TWILIO_API_SECRET'),
                        3600,
                        $request->email
                );

                $chatGrant = new ChatGrant();
                $chatGrant->setServiceSid(env('TWILIO_SERVICE_SID'));
                $token->addGrant($chatGrant);

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

Since we used the user’s email address to identify the user as a member of a channel, we need to also make use of the email address as the identity to generate an access token. Once the token has been generated, we need to grant the token access to our chat service. Finally, we return the token as a JWT.

Creating the Chat Component

Now, let’s create a ChatComponent that will render the chat messages and an input to send messages. By default, Laravel comes with an ExampleComponent.vue file. Rename it to ChatComponent.vue. We also need to update the reference to the file inside app.js as well:

// resources/js/app.js

Vue.component('chat-component', require('./components/ChatComponent.vue').default)

Next, update the script section of the ChatComponent as below:

// resources/js/components/ChatComponent.vue

<script>
  export default {
    name: "ChatComponent",
    props: {
      authUser: {
        type: Object,
        required: true
      },
      otherUser: {
        type: Object,
        required: true
      }
    },
    data() {
      return {
        messages: [],
        newMessage: "",
        channel: ""
      };
    }
  };
</script>

This component accept two props: authUser and otherUser for the authenticated user and the other user respectively. Also, it has three data properties: a messages array, a new message to be sent and chat channel.

Initializing the Chat Client

To have access to the Twilio Programmable Chat functionality on the client-side, we need to install the JavaScript SDK. We can choose to either install it through NPM or use it directly from a CDN. For the purpose of this tutorial, we’ll be making use of a CDN. Add the code below inside app.blade.php, just before the close of the body tag:

// resources/views/layouts/app.blade.php

<script src="https://media.twiliocdn.com/sdk/js/chat/v3.3/twilio-chat.min.js"></script>

Now we can make use of the SDK! Add the following code inside the script section of the ChatComponent below the data():

// resources/js/components/ChatComponent.vue

async created() {
  const token = await this.fetchToken();
  await this.initializeClient(token);
},
methods: {
  async fetchToken() {
    const { data } = await axios.post("/api/token", {
      email: this.authUser.email
    });


    return data.token;
  },
  async initializeClient(token) {
    const client = await Twilio.Chat.Client.create(token);


    client.on("tokenAboutToExpire", async () => {
      const token = await this.fetchToken();


      client.updateToken(token);
    });
  },
}

Once the component has been created we call two methods. The fetchToken() makes an HTTP request to our api/token endpoint to fetch an access token for the authenticated use. Once the token is retrieved, we pass it to the initializeClient() to create and initialize the chat client.

Since access tokens have a short lifespan of 24hours, we make sure we are not making use of an expired access token (which will result in an error). To prevent that, we need a way to update the access tokens after they expire. We can easily do that with the JavaScript SDK. Twilio Programmable Chat will fire two events pertaining to access token expiration:  tokenAboutToExpire and tokenExpired. All we have to do is listen for any of these events (we are using tokenAboutToExpire in this tutorial) and re-fetch the access token and update the client with it.

Fetching Chat Messages

Though we have not sent any messages, let’s add the functionality for fetching all messages on the channel. Update the template section of the ChatComponent with:

// resources/js/components/ChatComponent.vue

<template>
  <div class="card">
      <div class="card-header">{{ otherUser.name }}</div>
      <div class="card-body">
          <div v-for="message in messages" v-bind:key="message.id">
              <div
                  :class="{ 'text-right': message.author === authUser.email }"
              >
                  {{ message.body }}
              </div>
          </div>
      </div>
  </div>
</template>

We are displaying the name of the user the authenticated user is chatting with. Then we display the actual messages. If the message is sent by the authenticated user, we align the message to the right to clarify the sender.

To complete this section we need to first get the channel. Add the code below inside the initializeClient():

// resources/js/components/ChatComponent.vue

this.channel = await client.getChannelByUniqueName(
  `${this.authUser.id}-${this.otherUser.id}`
);

We are getting the channel by its unique name just as we created it on the backend.

Now, we can get all the messages on the channel. Create a new fetchMessages() method with the following code:

// resources/js/components/ChatComponent.vue

async fetchMessages() {
  this.messages = (await this.channel.getMessages()).items;
},

Finally, let’s call the method inside the created hook as shown below:

// resources/js/components/ChatComponent.vue

async created() {
  await this.fetchMessages();
},

Sending a Message

Now, let’s create a way to send new messages. Add the following code after the .card-body div:

// resources/js/components/ChatComponent.vue

<div class="card-footer">
  <input
    type="text"
    v-model="newMessage"
    class="form-control"
    placeholder="Type your message..."
    @keyup.enter="sendMessage"
  />
</div>

We now have an input field for sending  new messages which calls sendMessage() once the enter key is pressed.

Let’s create the sendMessage() method:

// resources/js/components/ChatComponent.vue

sendMessage() {
  this.channel.sendMessage(this.newMessage);
  this.newMessage = "";
}

In the code above, we call sendMessage() method on the channel and pass the message a user types into the input field. Then we clear out the input field.

Once a new message is sent, messageAdded event is fired. To display the newly sent message to both users in real time, we need to listen for this event. Add the following code at the end of the initializeClient() method:

// resources/js/components/ChatComponent.vue

this.channel.on("messageAdded", message => {
  this.messages.push(message);
});

Before we head over to testing our application, let’s update chat.blade.php so it renders the ChatComponent. Replace the comment with the code below:

// resources/views/messages/chat.blade.php

<chat-component :auth-user="{{ auth()->user() }}" :other-user="{{ $otherUser }}"></chat-component>

Testing Our Application

Let’s test what we’ve been building so far. First, let’s make sure our app is running:

$ php artisan serve

Then compile the JavaScript:

$ npm run dev

Now, open two browser windows, login as different users on both and try chatting with each other.

Sample chat window

Conclusion

In this tutorial, we learned how to add chat to a Laravel application using Twilio Programmable Chat. We’ve only scratched the surface of the functionality provided by Twilio Programmable Chat. To learn more about Twilio Programmable Chat and what you can do with it, check out the docs.

You can find the complete source for this tutorial on GitHub.

Software developer and Instructor

Website: https://adonismastery.com

Twitter: https://twitter.com/ammezie

Email: meziemichael@gmail.com

GitHub: https://github.com/ammezie