How to Create a Group Video Chat App with Symfony, PHP, Twilio, and React.js

October 12, 2020
Written by
Oluyemi Olususi
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by
Diane Phan
Twilion

How to Create a Group Video Chat App with Symfony, PHP, Twilio, and React

If the news and current statistics about remote work are any indicator of the future, it’s obvious that working from home is gradually becoming the new normal for employees all over the world, including numerous tech companies. While effective communication may be defined differently for various companies, one of the most promising ways to improve productivity and keep your team together involves using video conferencing or chat.

This tutorial will guide you on how to create a video conferencing chat application using Symfony, React.js, and Twilio Programmable Video. The video chat application will allow users to connect and participate in a predefined chat room.

Prerequisites

To participate in this tutorial, you will need the following:

Create a New Symfony Application

Considered as one of the leading PHP frameworks used for building web applications and RESTful APIs, Symfony is built with an elegant structure and is robust enough for building any kind of applications irrespective of the size.

To get started with creating a new Symfony application, you will use Composer. Run the following command from the terminal:

$ composer create-project symfony/website-skeleton symfony-react-video-chat

This will scaffold a new Symfony application within the  symfony-react-video-chat folder in your development folder. Alternatively, you can use Symfony Installer to set up your project using the following instructions.

Install Twilio PHP SDK for the Project

Move into the project’s folder and issue the following command to install other required dependencies:

$ cd symfony-react-video-chat
$ composer require twilio/sdk

Here, we installed:

  • twilio/sdk: A Twilio PHP SDK that will facilitate interaction between the Twilio API and our application

Obtain Credentials and Update the Environment File

As mentioned earlier, a Twilio account is required for this tutorial and by now, you should have created one. Navigate to your Twilio console to access your Twilio credentials. Copy the TWILIO_ACCOUNT_SID from your dashboard and keep it safe:

Account SID

Proceed to the API keys list and see the following page:

Create New API Key

Give your API key a friendly name such as "sample-key" and click on the Create API Key button to complete the process. This will generate a random unique API Key SID and API Key Secret for you.

Navigate to the .env file for your project and include the values of the TWILIO_ACCOUNT_SID, API_KEY_SID, and API_KEY_SECRET as obtained from the Twilio console. The .env file should have the content displayed below:

TWILIO_ACCOUNT_SID=YOUR_ACCOUNT_SID
TWILIO_API_KEY_SID=YOUR_API_KEY
TWILIO_API_KEY_SECRET=YOUR_API_SECRET

These credentials are crucial to generating an access token and room permissions for a user. A participant in a chat room which will be generated in the next section.

Generate Access Tokens

In a programmable video application, the access token controls the identity and room permissions of a participant in a chat through the Twilio SDK. It usually contains information about the details of your Twilio Account SID and API key.

Next you will create an endpoint that will use these credentials to generate an access token for users.

Similar to what is obtainable in other MVC structured web applications, endpoints are often mapped to a particular Controller action for proper handling of HTTP requests, business logic, and returning the appropriate responses. For this purpose, we will use the maker bundle that comes installed with Symfony to generate a controller and its corresponding view. Run the following command to create a new controller:

$ php bin/console make:controller TokenController

This will create two new files for you:

  • A controller located in src/Controller/TokenController.php
  • A view page in templates/token/index.html.twig

Open the newly created controller and replace its content with the following:

<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Twilio\Jwt\AccessToken;
use Twilio\Jwt\Grants\VideoGrant;

class TokenController extends AbstractController
{
    /**
     * @Route("/", name="token")
     */
    public function index()
    {
        return $this->render('token/index.html.twig', [
            'controller_name' => 'TokenController',
        ]);
    }


    /**
     * @param Request $request
     * @return \Symfony\Component\HttpFoundation\JsonResponse
     * @Route("access_token", name="access_token", methods={"POST"})
     */
    public function generate_token(Request $request)
    {
        $accountSid = getenv('TWILIO_ACCOUNT_SID');
        $apiKeySid = getenv('TWILIO_API_KEY_SID');
        $apiKeySecret = getenv('TWILIO_API_KEY_SECRET');
        $identity = uniqid();

        $roomName = json_decode($request->getContent());

        // Create an Access Token
        $token = new AccessToken(
            $accountSid,
            $apiKeySid,
            $apiKeySecret,
            3600,
            $identity
        );

        // Grant access to Video
        $grant = new VideoGrant();
        $grant->setRoom($roomName->roomName);
        $token->addGrant($grant);
        return $this->json(['token' => $token->toJWT()], 200);
    }
}

Here, we modified the default code generated by the maker bundle by configuring the index() method to render the content of the token/index.html.twig on the homepage route. The video chat will be visible on this page at the completion of  this tutorial.

A method named generate_token() is created. In this method, we accessed the environment variables defined in the .env file earlier and passed them as a part of the required parameters for a new instance of the AccessToken() method from theTwilio SDK.

The preferred room name, which will be posted from the frontend, was retrieved via the HTTP Request method and set as the room in which the holder of the generated access token can only connect to.

Navigate to the templates/token/index.html.twig and replace the content with the following HTML:

{% extends 'base.html.twig' %}

{% block title %} Symfony React Video Chat !{% endblock %}

{% block body %}
    <div id="root"></div>
{% endblock %}

This is a basic twig template file for a Symfony application which will remain empty until the React.js frontend is appended to the <div> with an id of root.

Start the Application

Start the application by issuing the following command in the terminal:

$ symfony server:start

Enter localhost:8000 in your preferred web browser. You’ll notice an empty page. Leave the server running for now. We will revisit it later.

Build the Frontend Logic

Now that the backend functionality has been implemented and running, set up the React application to connect it to the created backend API, making our video chat application functional. Open another terminal within the root directory of the project and use Composer to install Symfony Webpack Encore by running the command below:

$ composer require symfony/webpack-encore-bundle

In addition to:

$ yarn install

Webpack Encore is a JavaScript library that uses Webpack under the hood. It is popularly known for working with CSS and JavaScript files within a Symfony application. The preceding command will create a webpack.config.js file, an assets folder, and add the node_modules folder to the .gitignore file.

After the installation is complete, use Yarn to install ReactAxios, and Twilio Programmable Video JavaScript SDK by running the following commands:

$ yarn add react react-dom @babel/preset-react --dev
$ yarn add axios twilio-video --save
$ yarn install

Configure Webpack Encore and Enable React

Here, we will enable React and set up an entry point within the webpack.config.js file at the root of our project as shown here:

var Encore = require('@symfony/webpack-encore');

// Manually configure the runtime environment if not already configured yet by the "encore" command.
// It's useful when you use tools that rely on webpack.config.js file.
if (!Encore.isRuntimeEnvironmentConfigured()) {
    Encore.configureRuntimeEnvironment(process.env.NODE_ENV || 'dev');
}

Encore
    // directory where compiled assets will be stored
    .setOutputPath('public/build/')
    // public path used by the web server to access the output path
    .setPublicPath('/build')
    // only needed for CDN's or sub-directory deploy
    //.setManifestKeyPrefix('build/')

    /*
     * ENTRY CONFIG
     *
     * Add 1 entry for each "page" of your app
     * (including one that's included on every page - e.g. "app")
     *
     * Each entry will result in one JavaScript file (e.g. app.js)
     * and one CSS file (e.g. app.css) if your JavaScript imports CSS.
     */
    .addEntry('app', './assets/app.js')

    // When enabled, Webpack "splits" your files into smaller pieces for greater optimization.
    .splitEntryChunks()

    // will require an extra script tag for runtime.js
    // but, you probably want this, unless you're building a single-page app
    .enableSingleRuntimeChunk()

    /*
     * FEATURE CONFIG
     *
     * Enable & configure other features below. For a full
     * list of features, see:
     * https://symfony.com/doc/current/frontend.html#adding-more-features
     */
    .cleanupOutputBeforeBuild()
    .enableBuildNotifications()
    .enableSourceMaps(!Encore.isProduction())
    // enables hashed filenames (e.g. app.abc123.css)
    .enableVersioning(Encore.isProduction())

    // enables @babel/preset-env polyfills
    .configureBabelPresetEnv((config) => {
        config.useBuiltIns = 'usage';
        config.corejs = 3;
    })

    // enables Sass/SCSS support
    //.enableSassLoader()

    // uncomment if you use TypeScript
    //.enableTypeScriptLoader()

    // uncomment to get integrity="..." attributes on your script & link tags
    // requires WebpackEncoreBundle 1.4 or higher
    //.enableIntegrityHashes(Encore.isProduction())

    // uncomment if you're having problems with a jQuery plugin
    //.autoProvidejQuery()

    // uncomment if you use API Platform Admin (composer req api-admin)
    .enableReactPreset()
    //.addEntry('admin', './assets/js/admin.js')
;

module.exports = Encore.getWebpackConfig();

From the code above, we instructed Encore to load ./assets/js/app.js as the entry point of the application and uncommented the .enableReactPreset() method.

Create a Chat Component

Encore has already created an existing structure for us to build on. Create a new component named Chat.js file within the assets/js folder and paste the following code in it:

import React, { useState } from "react";
import axios from "axios";
import Video from "twilio-video";

const Chat = () => {
    const [roomName, setRoomName] = useState('');
    const [hasJoinedRoom, setHasJoinedRoom] = useState(false);

    const joinChat = event => {
    event.preventDefault();
      if (roomName) {
            axios.post('/access_token', { roomName }, ).then((response) => {
                connectToRoom(response.data.token);
                setHasJoinedRoom(true);
                setRoomName('');

            }).catch((error) => {
                console.log(error);
            })
        } else {
            alert("You need to enter a room name")
        }
    };


    return(
        <div className="container">
            <div className={"col-md-12"}>
                <h1 className="text-title">Symfony React Video Chat</h1>
            </div>

            <div className="col-md-6">
                <div className={"mb-5 mt-5"}>
                    {!hasJoinedRoom && (
                        <form className="form-inline" onSubmit={joinChat}>
                            <input type="text" name={'roomName'} className={"form-control"} id="roomName"
                                   placeholder="Enter a room name" value={roomName} onChange={event => setRoomName(event.target.value)}/>

                                <button type="submit" className="btn btn-primary">Join Room</button>

                        </form>
                    )}

                </div>
                <div id="video-chat-window"></div>
            </div>
        </div>
    )
};

export default Chat;

This component will be responsible for connecting with the created API endpoint, retrieving the generated access token from the API, and rendering the chat window. To achieve these steps, we created a form with an input field so that the users can enter their preferred name for the chat room and post it to the API endpoint once the Join Room button is clicked.

Once the form is submitted, the joinChat() method will be called and a POST HTTP request will be sent to the access_token endpoint. Upon a successful response, the retrieved token will be used to connect to a live video chat session.

To join the video chat, add the following method named connectToRoom() immediately after the joinChat() function:

...
const Chat = () => {
    ...

    const connectToRoom = (token) => {
        const { connect, createLocalVideoTrack } = Video;

        let connectOption = { name: roomName };

        connect( token, connectOption).then(room => {

            console.log(`Successfully joined a Room: ${room}`);

            const videoChatWindow = document.getElementById('video-chat-window');

            createLocalVideoTrack().then(track => {
                videoChatWindow.appendChild(track.attach());
            });

            room.on('participantConnected', participant => {
                console.log(`Participant "${participant.identity}" connected`);

                participant.tracks.forEach(publication => {
                    if (publication.isSubscribed) {
                        const track = publication.track;
                        videoChatWindow.appendChild(track.attach());
                    }
                });

                participant.on('trackSubscribed', track => {
                    videoChatWindow.appendChild(track.attach());
                });
            });
        }, error => {
            console.error(`Unable to connect to Room: ${error.message}`);
        });
    };

    ...
};

export default Chat;

Here, we passed the generated token obtained from the backend API and used it to set up a connection to the chat room. We also created a local video track and appended it to a chat window where it will be displayed.

Once a new participant is connected to the chat room, the room.on() method displays their name within the chat window.

Update the AppComponent

Here, we will initialize React and bind it to an HTML element with an id of root. Open the assets/app.js file and replace its content with:

import React from 'react';
import ReactDOM from "react-dom";
import Chat from './js/Chat';
import './styles/app.css'


ReactDOM.render(<Chat/>, document.getElementById("root"));

Modify the Base Template

Thanks to the Webpack Encore, React code will be compiled down to JavaScript. To include the generated script in the project, navigate to the base template located in templates/base.html.twig and replace its content with the following:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>{% block title %}Welcome!{% endblock %}</title>

    </head>
    <body>
        {% block body %}{% endblock %}


        {% block stylesheets %}
            {{  encore_entry_link_tags('app') }}
        {% endblock %}


        {% block javascripts %}

            {{ encore_entry_script_tags('app') }}
        {% endblock %}
    </body>
</html>

Include Bootstrap CDN

To introduce some default styles for this project, we will include a Bootstrap CDN file. Bootstrap is a CSS framework for creating page layouts. It offers a lot of helper classes that can be included in the HTML element of web pages. To include the Bootstrap CDN within this project, open assets/styles/app.css file and add the following link:

@import url(https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css);

Test the Symfony App

Ensure that the Symfony application is still running from a separate terminal within your project directory. If it's not running, use the command below to resume the Symfony application:

$ php bin/console server:run

In a second terminal session, run the following command to compile the React application and watch the JavaScript files for any changes:

$ yarn encore dev --watch

Navigate to http://localhost:8000 to view the application. Open two different windows and join a room:

Test the application

Conclusion

The video chat application built here can use some improvement by extending its business logic to accommodate more features, such as screen sharing and displaying the number of participants in a chat room. Check the official documentation of Twilio’s Programmable Video to learn more about building virtual experiences with video.

I do hope that you found this helpful. The complete source code for this project can be found here on GitHub. Happy coding!

Olususi Oluyemi is a tech enthusiast, programming freak, and web development junkie who loves to embrace new technology.