Build a realtime table using Laravel
A basic understanding of Laravel and Vue.js are needed to follow this tutorial.
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.
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:
// .env
BROADCAST_DRIVER=pusher
PUSHER_APP_ID=your_pusher_add_id
PUSHER_APP_KEY=your_pusher_app_key
PUSHER_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
:
import Echo from "laravel-echo"
window.Echo = new Echo({
broadcaster: 'pusher',
key: 'your_pusher_key'
});
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:
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateCardsTable extends Migration
{
/**
* Run the cards table migrations.
* Creates the `cards` table and structure
* @return void
*/
public function up()
{
Schema::create('cards', function (Blueprint $table) {
$table->increments('id');
$table->integer('value')->default(0);
$table->string('color');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('cards');
}
}
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:
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateUsersTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('users', function (Blueprint $table) {
$table->increments('id');
$table->string('name');
$table->string('email')->unique();
$table->integer('score')->default(0); //score column
$table->string('password');
$table->rememberToken();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('users');
}
}
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.
# database/factories/ModelFactory.php
/**
* Defines the model factory for our cards table.
*/
$factory->define(\App\Card::class, function(Faker\Generator $faker) {
return [
'value' => $faker->numberBetween(0, 30),
'color' => $faker->hexColor
];
});
Our final seeder file located at database/seeds
will look like this:
<?php
use Illuminate\Database\Seeder;
class CardsTableSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
factory(\App\Card::class, 10)->create();
}
}
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:
/**
* Run the database seeds.
*
* @return void
*/
public function run(){
$this->call(CardsTableSeeder::class);
}
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!
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Card;
class HomeController extends Controller
{
public function __construct()
{
$this->middleware('auth');
}
public function index()
{
$cards = Card::inRandomOrder()->take(3)->get();
return view('home', compact('cards'));
}
}
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 andapp/routes.php
in older versions.
Our card controller class will look like this:
<?php
namespace App\Http\Controllers;
use App\Card;
use Illuminate\Http\Request;
class CardController extends Controller
{
public function show(Card $card) {
$user = auth()->user();
$user->score = $user->score + $card->value;
$user->save();
return redirect()->back()->withValue($card->value);
}
}
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:
@extends('layouts.app')
@section('content')
<div class="container">
<div class="row">
<div class="col-md-8 col-md-offset-2">
<div class="panel panel-default text-center">
<div class="panel-heading">Welcome to the most fun game in the world!</div>
<div class="panel-body">
@if($value = session('value'))
<div class="alert alert-success">
<h2>Congrats {{ auth()->user()->name }}! Your score just increased by {{ $value }}.</h2>
<h4>Your current score is {{ auth()->user()->score }}</h4>
</div>
@endif
<h4 class="text-center">Click a card and choose your fate.</h4>
<div class="row">
@foreach($cards as $card)
<div class="col-sm-4">
<a href="{{ route('card', $card->id) }}">
<div class="card" style="background-color: {{$card->color}}; height: 100px;"></div>
</a>
</div>
@endforeach
</div>
</div>
<div class="panel-heading">You can always check the <a href="/">leaderboard</a> for your ranking.</div>
</div>
</div>
</div>
</div>
@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.
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:
<?php
namespace App\Events;
use Illuminate\Broadcasting\Channel;
use Illuminate\Queue\SerializesModels;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
class ScoreUpdated implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $user;
public function __construct($user)
{
$this->user = $user;
}
public function broadcastOn()
{
return new Channel('leaderboard');
}
public function broadcastWith()
{
return [
'id' => $this->user->id,
'name' => $this->user->name,
'score' => $this->user->score
];
}
}
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:
# app/Http/Controllers/CardController.php
use App\Events\ScoreUpdated; // import event class at the top of the file
public function show(Card $card)
{
$user = auth()->user();
$user->score = $user->score + $card->value;
$user->save();
event(new ScoreUpdated($user)); // broadcast `ScoreUpdated` event
return redirect()->back()->withValue($card->value);
}
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.
# app/Http/Controllers/Auth/RegisterController.php
use Illuminate\Http\Request;
use App\Events\ScoreUpdated;
/**
* Handle a registration request.
*
*/
public function register(Request $request) {
$this->validator($request->all())->validate();
$user = $this->create($request->all());
event(new ScoreUpdated($user)); // `ScoreUpdated` broadcast event
$this->guard()->login($user);
return $this->registered($request, $user)
?: redirect($this->redirectPath());
}
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:
# routes/web.php
Route::get('/leaderboard', 'CardController@leaderboard');
And the corresponding controller method:
# app/Http/Controllers/CardController.php
use App\User; // import `User` class at the top of the file
public function leaderboard () {
return User::all(['id', 'name', 'score']);
}
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:
<!-- /resources/assets/js/components/Leaderboard.vue -->
<template>
<table class="table table-striped">
<thead>
<tr>
<th>Rank</th>
<th>Name</th>
<th>Score</th>
</tr>
</thead>
<tbody>
<tr :class="{success: user.id == current}" v-for="(user, key) in sortedUsers">
<td>{{ ++key }}</td>
<td>{{ user.name }}</td>
<td>{{ user.score }}</td>
</tr>
</tbody>
</table>
</template>
<script>
export default {
props: ['current'],
data() {
return {
users: []
}
},
created() {
this.fetchLeaderboard();
this.listenForChanges();
},
methods: {
fetchLeaderboard() {
axios.get('/leaderboard').then((response) => {
this.users = response.data;
})
},
listenForChanges() {
Echo.channel('leaderboard')
.listen('ScoreUpdated', (e) => {
var user = this.users.find((user) => user.id === e.id);
// check if user exists on leaderboard
if(user){
var index = this.users.indexOf(user);
this.users[index].score = e.score;
}
// if not, add 'em
else {
this.users.push(e)
}
})
}
},
computed: {
sortedUsers() {
return this.users.sort((a,b) => b.score - a.score)
}
}
}
</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
:
// /resources/assets/js/app.js
Vue.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:
{{-- resources/views/welcome.blade.php --}}
@extends('layouts.app')
@section('content')
<div class="container">
<div class="row">
<div class="col-md-8 col-md-offset-2">
<leaderboard :current="{{ auth()->user() ? auth()->user()->id : 0 }}"></leaderboard>
</div>
</div>
</div>
@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:
On user registration:
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.
24 March 2017
by Olayinka Omole