In this tutorial, learn how to build a realtime table using Laravel and Pusher.
This blog post was written under the Pusher Guest Writer program.
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. You will need a working knowledge of PHP and Javascript to follow along. Bonus points if you’ve ever worked with Laravel and Vue.js.
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.
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.
Tip: You can copy out your App ID, Key and Secret from the “App Keys” section, as we will be needing them later on.
Let’s start off by creating and setting up a new Laravel app. Via the Laravel installer:
1laravel 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:
1composer require pusher/pusher-php-server
Installing the Laravel front-end dependencies:
1npm 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:
1npm 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
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:
1php artisan make:auth
This will create our basic auth routes, pages and logic.
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:
1php 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}
Let’s create some seed data for our cards. Creating a seeder file:
1php 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:
1php artisan migrate --seed
Tip: Make sure you edit your database details in the
.env
before running the migrations and seeder.
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.
1Route::middleware('auth')->name('card')->get('/cards/{card}', 'CardController@show');
Tip: Our web routes are located at
routes/web.php
in Laravel 5.4 andapp/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.
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.
When a user’s score is updated, we want to broadcast an event with details about the update. To create an event:
1php 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.
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 }
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
Laravel ships with Mix which removes the hassle out of configuring Webpack build steps. We can simply compile our assets with:
1npm 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:
On user registration:
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:
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.