Build a realtime table using Laravel

Introduction

Often, you have sets of data you want to present to your application users in a table, organised by score, or weight. It could be something as simple as top 10 posts on your website, or a fun leaderboard for a game you created.

It's a poor experience for users if they have to reload the page every single time to see changes in the leaderboard or their ranking. A much better experience is for them to see changes in realtime. Thanks to event broadcasting and Pusher, you can implement that in a few minutes!

From the Real-Time Laravel Pusher Git Book:

Pusher is a hosted service that makes it super-easy to add real-time data and functionality to web and mobile applications.

In this tutorial, I'll be doing a walk-through on creating a simple realtime table for a Laravel/Vue.js app using Pusher.

First, we will build a simple game. Then, we will register and call events for interactions with the game, and finally, we will display our realtime table with Vue.js.

Setting up a Pusher account and app

If you do not already have a Pusher account, head over to Pusher and create a free account.

Then register a new app on the dashboard. The only compulsory options are the app name and cluster. A cluster simply represents the physical location of the Pusher server that would be handling your app's requests. You can read more about them here.

realtime-table-laravel-dashboard

Tip: You can copy out your App ID, Key and Secret from the "App Keys" section, as we will be needing them later on.

Configuration and setup

Let's start off by creating and setting up a new Laravel app. Via the Laravel installer:

laravel new card-game-app

Then, let's pull in the Pusher PHP SDK. We'll be using this to interact with Pusher from our server end:

composer require pusher/pusher-php-server

Installing the Laravel front-end dependencies:

npm install 

We will be using Laravel Echo and the PusherJS JavaScript package to listen for event broadcasts, so let's grab those too, and save them as part of our app's dependencies:

npm install --save laravel-echo pusher-js

To let Laravel know that we will be using Pusher to manage our broadcasts, we need to do some more minor config.

The broadcast config file is located at config/broadcasting.php, but we don't need to edit it directly as Laravel supports Pusher out of the box and has made provision for us to simply edit the .env with our Pusher credentials... so let's do that. Use the credentials you copied earlier:

1// .env
2
3BROADCAST_DRIVER=pusher
4
5PUSHER_APP_ID=your_pusher_add_id
6PUSHER_APP_KEY=your_pusher_app_key
7PUSHER_APP_SECRET=your_pusher_app_secret

Next, to listen for messages on our front-end, we'll be making use of Echo, so let's configure that by uncommenting and editing the values at the bottom of resources/assets/js/bootstrap.js:

1import Echo from "laravel-echo"
2
3 window.Echo = new Echo({
4     broadcaster: 'pusher',
5     key: 'your_pusher_key'
6});

Tip: Laravel Echo makes it easy to subscribe to channels and listen to event broadcasts. You can read more about it and see more config options here

Creating our game

We will be creating a simple game that will increase a user's score by a random number based on which coloured card the user clicks on. Not a very fun game, but it will serve its purpose for this tutorial.

To manage authentication, we will use the default auth scaffolding provided by Laravel:

php artisan make:auth

This will create our basic auth routes, pages and logic.

Migrations and DB structure

We need a cards table to store the different cards and corresponding card values we will be using for our game. Let's create the model for this:

php artisan make:model Card -m -c

Tip: The -m and -c flags create corresponding migration and controller files for the model.

Let's edit the cards migration file to reflect the structure we need:

1<?php
2
3use Illuminate\Support\Facades\Schema;
4use Illuminate\Database\Schema\Blueprint;
5use Illuminate\Database\Migrations\Migration;
6
7class CreateCardsTable extends Migration
8{
9    /**
10     * Run the cards table migrations.
11     * Creates the `cards` table and structure
12     * @return void
13     */
14    public function up()
15    {
16        Schema::create('cards', function (Blueprint $table) {
17            $table->increments('id');
18            $table->integer('value')->default(0);
19            $table->string('color');
20            $table->timestamps();
21        });
22    }
23
24    /**
25     * Reverse the migrations.
26     *
27     * @return void
28     */
29    public function down()
30    {
31        Schema::dropIfExists('cards');
32    }
33}

Tip: Migration files are located in the database/migrations directory

Next, we should also update the default users table migration file created by Laravel to include the user's score. Our final users migration file will look like this:

1<?php
2
3use Illuminate\Support\Facades\Schema;
4use Illuminate\Database\Schema\Blueprint;
5use Illuminate\Database\Migrations\Migration;
6
7class CreateUsersTable extends Migration
8{
9    /**
10     * Run the migrations.
11     *
12     * @return void
13     */
14    public function up()
15    {
16        Schema::create('users', function (Blueprint $table) {
17            $table->increments('id');
18            $table->string('name');
19            $table->string('email')->unique();
20            $table->integer('score')->default(0); //score column
21            $table->string('password');
22            $table->rememberToken();
23            $table->timestamps();
24        });
25    }
26
27    /**
28     * Reverse the migrations.
29     *
30     * @return void
31     */
32    public function down()
33    {
34        Schema::dropIfExists('users');
35    }
36}

Seeding our cards table

Let's create some seed data for our cards. Creating a seeder file:

php artisan make:seeder CardsTableSeeder

Next, we will define a model factory for our cards table, and edit its seeder file.

1# database/factories/ModelFactory.php
2
3/** 
4 * Defines the model factory for our cards table.
5 */
6$factory->define(\App\Card::class, function(Faker\Generator $faker) {
7    return [
8        'value' => $faker->numberBetween(0, 30),
9        'color' => $faker->hexColor
10    ];
11});

Our final seeder file located at database/seeds will look like this:

1<?php
2
3use Illuminate\Database\Seeder;
4
5class CardsTableSeeder extends Seeder
6{
7    /**
8     * Run the database seeds.
9     *
10     * @return void
11     */
12    public function run()
13    {
14        factory(\App\Card::class, 10)->create();
15    }
16}

This will create 10 random cards, with random values and colors thanks to Faker.

Tip: Model Factories are great for database testing. Remember, we want to keep our code DRY.

To run the seeder, we have to call it in the run method of the DatabaseSeeder class:

1/**
2  * Run the database seeds.
3  *
4  * @return void
5  */
6
7public function run(){
8    $this->call(CardsTableSeeder::class);
9}

To migrate our tables and seed data:

php artisan migrate --seed

Tip: Make sure you edit your database details in the .env before running the migrations and seeder.

Game page

The auth scaffolding provided by Laravel also creates a dashboard page on the /home route handled by the index method in the HomeController class.

Let's edit the index method to take 3 random cards from the database and display to users who are signed in and want to play!

1<?php
2
3namespace App\Http\Controllers;
4
5use Illuminate\Http\Request;
6use App\Card;
7
8class HomeController extends Controller
9{
10    public function __construct()
11    {
12        $this->middleware('auth');
13    }
14
15    public function index()
16    {
17        $cards = Card::inRandomOrder()->take(3)->get();
18        return view('home', compact('cards'));
19    }
20}

Next, we will create a route and controller action for when users click on a card.

Route::middleware('auth')->name('card')->get('/cards/{card}', 'CardController@show');

Tip: Our web routes are located at routes/web.php in Laravel 5.4 and app/routes.php in older versions.

Our card controller class will look like this:

1<?php
2
3namespace App\Http\Controllers;
4
5use App\Card;
6use Illuminate\Http\Request;
7
8class CardController extends Controller
9{
10    public function show(Card $card) {
11        $user = auth()->user();
12        $user->score = $user->score + $card->value;
13    $user->save();
14
15        return redirect()->back()->withValue($card->value);
16    }
17}

The show method above gets a user, adds the value of the selected card to their score, then redirects the user back with a flash message containing the value of the card.

Our final step in creating the game is to edit the home.blade.php template to show the user their 3 random cards:

1@extends('layouts.app')
2
3@section('content')
4<div class="container">
5    <div class="row">
6        <div class="col-md-8 col-md-offset-2">
7            <div class="panel panel-default text-center">
8                <div class="panel-heading">Welcome to the most fun game in the world!</div>
9
10                <div class="panel-body">
11                    @if($value = session('value'))
12                        <div class="alert alert-success">
13                            <h2>Congrats {{ auth()->user()->name }}! Your score just increased by {{ $value }}.</h2>
14                            <h4>Your current score is {{ auth()->user()->score }}</h4>
15                        </div>
16                    @endif
17                    <h4 class="text-center">Click a card and choose your fate.</h4>
18                    <div class="row">
19                        @foreach($cards as $card)
20                            <div class="col-sm-4">
21                                <a href="{{ route('card', $card->id) }}">
22                                    <div class="card" style="background-color: {{$card->color}}; height: 100px;"></div>
23                                </a>
24                            </div>
25                        @endforeach
26                    </div>
27                </div>
28
29                <div class="panel-heading">You can always check the <a href="/">leaderboard</a> for your ranking.</div>
30
31            </div>
32        </div>
33    </div>
34</div>
35@endsection

You can start your local server and navigate to the /home route in your browser to register/login and see our brand new game.

realtime-table-laravel-card-game

Tip: Valet makes development of Laravel applications on your local machine much easier. You should check it out.

You can also create seeders for users, so you have some test data to work with for the table.

Broadcasting events

When a user's score is updated, we want to broadcast an event with details about the update. To create an event:

php artisan make:event ScoreUpdated

Tip: Events are created in the app/events folder.

We can now customise the event to suit our needs:

1<?php
2
3namespace App\Events;
4
5use Illuminate\Broadcasting\Channel;
6use Illuminate\Queue\SerializesModels;
7use Illuminate\Broadcasting\PrivateChannel;
8use Illuminate\Broadcasting\PresenceChannel;
9use Illuminate\Foundation\Events\Dispatchable;
10use Illuminate\Broadcasting\InteractsWithSockets;
11use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
12
13class ScoreUpdated implements ShouldBroadcast
14{
15    use Dispatchable, InteractsWithSockets, SerializesModels;
16
17    public $user;
18
19    public function __construct($user)
20    {
21        $this->user = $user;
22    }
23
24    public function broadcastOn()
25    {
26        return new Channel('leaderboard');
27    }
28
29    public function broadcastWith()
30    {
31        return [
32            'id' => $this->user->id,
33            'name' => $this->user->name,
34            'score' => $this->user->score
35        ];
36    }
37}

To inform Laravel that an event should be broadcast, we need to implement the Illuminate\Contracts\Broadcasting\ShouldBroadcast interface on the event class.

The broadcastOn method returns the channel that we want to broadcast on, and the broadcastWith method returns an array of the data we want to broadcast as the event payload. We have specified leaderboard as the name of the channel our app should broadcast on, so on the front-end we also need to listen on that channel to detect broadcasts. The Channel class is used for broadcasting on public channels, while PrivateChannel and PresenceChannel are for private channels (those would require authentication for access).

By default, Laravel serialises and broadcasts all of an event's public properties as its payload... broadcastWith helps us override that behaviour and have more control over what is sent.

To call the event when a score is updated, we will use the event helper function provided by Laravel:

1# app/Http/Controllers/CardController.php
2
3use App\Events\ScoreUpdated; // import event class at the top of the file
4
5public function show(Card $card)
6    {
7        $user = auth()->user();
8        $user->score = $user->score + $card->value;
9        $user->save();
10
11        event(new ScoreUpdated($user)); // broadcast `ScoreUpdated` event
12
13        return redirect()->back()->withValue($card->value);
14    }

Tip: The broadcast helper function could also be used. Check how that works here

At this point, playing the game and having scores updated would trigger this event, and broadcast the payload we specified to Pusher. We can log in to the Pusher dashboard, and check the debug console to see this.

realtime-table-laravel-pusher-console

We also want new users to be automatically added to our leaderboard, so we will trigger the ScoreUpdated event when a user registers by defining a register method in app/Http/Controllers/Auth/RegisterController.php to override the default register method created by the Auth scaffolding.

1# app/Http/Controllers/Auth/RegisterController.php  
2
3use Illuminate\Http\Request;
4use App\Events\ScoreUpdated;
5
6/**
7 * Handle a registration request.
8 *
9 */
10public function register(Request $request) {
11        $this->validator($request->all())->validate();
12
13        $user = $this->create($request->all());
14        event(new ScoreUpdated($user)); // `ScoreUpdated` broadcast event
15
16        $this->guard()->login($user);
17        return $this->registered($request, $user)
18            ?: redirect($this->redirectPath());
19    }

Leaderboard and event listeners

Finally, we can create our leaderboard and start listening for broadcasts.

We will need an endpoint to call to get a list of users and their scores when the page loads, so let's add that:

1# routes/web.php
2Route::get('/leaderboard', 'CardController@leaderboard');

And the corresponding controller method:

1# app/Http/Controllers/CardController.php
2use App\User; // import `User` class at the top of the file
3
4public function leaderboard () {
5    return User::all(['id', 'name', 'score']);
6}

Tip: Laravel 5.4 already ships with Vue.js as a front-end dependency, hence after npm install, we can get to writing Vue.js code right away, and Laravel Mix (via Webpack) will handle the rest.

Next, we will create and register our Leaderboard component with all the logic we will need to display leaderboards to the users:

1<!-- /resources/assets/js/components/Leaderboard.vue -->
2<template>
3    <table class="table table-striped">
4        <thead>
5        <tr>
6            <th>Rank</th>
7            <th>Name</th>
8            <th>Score</th>
9        </tr>
10        </thead>
11        <tbody>
12        <tr :class="{success: user.id == current}" v-for="(user, key) in sortedUsers">
13            <td>{{ ++key }}</td>
14            <td>{{ user.name }}</td>
15            <td>{{ user.score }}</td>
16        </tr>
17        </tbody>
18    </table>
19</template>
20
21<script>
22    export default {
23        props: ['current'],
24        data() {
25            return {
26                users: []
27            }
28        },
29        created() {
30            this.fetchLeaderboard();
31            this.listenForChanges();
32        },
33        methods: {
34            fetchLeaderboard() {
35                axios.get('/leaderboard').then((response) => {
36                    this.users = response.data;
37                })
38            },
39            listenForChanges() {
40                Echo.channel('leaderboard')
41                .listen('ScoreUpdated', (e) => {
42                    var user = this.users.find((user) => user.id === e.id);
43                        // check if user exists on leaderboard
44                        if(user){
45                            var index = this.users.indexOf(user);
46                            this.users[index].score = e.score;
47                        }
48                        // if not, add 'em
49                        else {
50                            this.users.push(e)
51                        }
52                    })
53            }
54        },
55        computed: {
56            sortedUsers() {
57                return this.users.sort((a,b) => b.score - a.score)
58            }
59        }
60    }
61</script>

In the listenForChanges function above, we instruct Echo to listen for ScoreUpdated broadcasts on the leaderboard channel. Remember, we specified this channel when we were configuring our broadcast event.

Now, whenever Pusher receives a broadcast and simultaneously sends it to our app, Echo will be listening, and will use the callback function we specified as the second argument for the listen function. Our callback basically checks for a user on our leaderboard, then updates their score, or adds them to the table, depending on whether they already exist or not.

We also make use of the computed property sortedUsers for sorting our users according to their score in descending order.

Don't forget to register the component in app.js:

1// /resources/assets/js/app.js
2Vue.component('Leaderboard', require('./components/Leaderboard.vue'));

For simplicity, we can just edit welcome.blade.php provided by Laravel on installation, and fill it with template content for our leaderboard. The final file will look like this:

1{{-- resources/views/welcome.blade.php --}}
2@extends('layouts.app')
3
4@section('content')
5    <div class="container">
6        <div class="row">
7            <div class="col-md-8 col-md-offset-2">
8                <leaderboard :current="{{ auth()->user() ? auth()->user()->id : 0 }}"></leaderboard>
9            </div>
10        </div>
11    </div>
12@endsection

Bringing it all together

Laravel ships with Mix which removes the hassle out of configuring Webpack build steps. We can simply compile our assets with:

npm run dev

And we're all set! You can navigate to the app homepage to see the leaderboard.

Tip: You can always run php artisan serve to use PHP's built in server for testing purposes.

Here is an image demonstrating how the system works:

realtime-table-laravel-live-table

On user registration:

realtime-table-laravel-user-register

Conclusion

Pusher and Laravel work really well together. There are a lot more improvements we could add to our little game as a result of the features Pusher offers, such as:

  • Notifying users of changes in their position on the leaderboard
  • Creating an activity feed for users/admins to see all the game developments
  • Show when a user joins or leaves the game

And a whole lot more. If you've thought of any other great ways to use Pusher and Laravel, let us know in the comments.

The entire code for this tutorial is hosted on Github. You can look through and ask questions if you need more information.