Build a realtime activity feed with Laravel
A basic understanding of Laravel and Vue.js are needed to follow this tutorial.
Activity feeds display our behaviour into an event-based timeline, so we can follow along with our actions as we experience a product. Having a realtime feed improves the user experience and gives instant data synchronisation relating to the actions taken by the user or their collaborators.
Today, we will create a realtime Activity Feed using Laravel and Pusher Channels. With the release of Echo, Laravel has provided an out of the box solution for implementing a realtime data synchronisation using event broadcasting. It is simple and we can get started in a matter of few minutes.
Setup a Channels app on Pusher
We need to sign up to Pusher and create a new Channels app.
Install Laravel, Pusher SDK and Echo
First, we will grab a fresh copy of Laravel:
laravel new activity-feed-pusher
This will install the latest version of the Laravel framework and download the necessary dependencies. Next, we will install the Pusher PHP SDK using Composer:
composer require pusher/pusher-php-server
Next, we will install the JavaScript dependencies:
npm install
Now, we need to install two Javascript libraries necessary for realtime event broadcasting: Laravel Echo and Pusher JS
npm install --save laravel-echo pusher-js
We require some form of user authentication mechanism to demonstrate the functionality. Let us use the default authentication scaffolding provided by Laravel:
php artisan make:auth
Configuration
First, we need to set the APP_ID
, APP_KEY
, APP_SECRET
and APP_CLUSTER
in the environment file. We can get these details in our Pusher app dashboard:
# .env
BROADCAST_DRIVER=pusher
PUSHER_APP_ID=your-pusher-app-id
PUSHER_APP_KEY=your-pusher-app-key
PUSHER_APP_SECRET=your-pusher-app-secret
PUSHER_APP_CLUSTER=your-pusher-app-cluster
Next, we need to create a fresh Echo instance in our applications’s JavaScript. We can do so at the bottom of our resources/assets/js/bootstrap.js
file:
import Echo from "laravel-echo"
window.Echo = new Echo({
broadcaster: 'pusher',
key: 'your-pusher-app-key',
cluster: 'ap2',
encrypted: true
});
Our application
We will create a basic to-do application for the purpose of this article. We will not cover anything relating to writing CRUD functionality using Laravel. We will concentrate on the code necessary for implementing a realtime Activity Feed. The code is available on a Github repository for cloning and understanding purposes.
Migrations
Next, we need a activities
table, where we can record all the actions taken by a user. Let us create a model and migration:
php artisan make:model Activity -m
The activities
table would require the following fields:
- Name of each recorded activity - Eg: created, updated or deleted
- A field to link the activity to the user that created it
- A polymorphic relation to store the details of the model for which the activity is being recorded
Below is our migration file for the activities
table:
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateActivitiesTable extends Migration
{
public function up()
{
Schema::create('activities', function(Blueprint $table) {
$table->increments('id');
$table->integer('subject_id')->index();
$table->string('subject_type')->index();
$table->string('name');
$table->integer('user_id')->index();
$table->timestamps();
});
}
public function down()
{
Schema::drop('activities');
}
}
Recording activity
To record activity for a specific model, we need to track any model updates whenever it is created
, updated
or deleted
. We will create a trait which will hijack these events and store the necessary data into the activities
table.
The trait will have following methods:
bootRecordsActivity
- Eloquent will automatically trigger this method. This will be the starting point for the process of recording the activity.recordActivity
- This method will store the activity details in theactivities
tablegetActivityName
- This will return the name of activity viz. created_task or updated_taskgetModelEvents
- This method will return an array of model events that are to be recorded. We can overwrite this in our Model if necessary.
The RecordsActivity
trait will look like this:
# RecordsActivity.php
namespace App;
use App\Events\ActivityLogged;
use ReflectionClass;
trait RecordsActivity
{
/**
* Register the necessary event listeners.
*
* @return void
*/
protected static function bootRecordsActivity()
{
foreach (static::getModelEvents() as $event) {
static::$event(function ($model) use ($event) {
$model->recordActivity($event);
});
}
}
/**
* Record activity for the model.
*
* @param string $event
* @return void
*/
public function recordActivity($event)
{
$activity = Activity::create([
'subject_id' => $this->id,
'subject_type' => get_class($this),
'name' => $this->getActivityName($this, $event),
'user_id' => $this->user_id
]);
event(new ActivityLogged($activity));
}
/**
* Prepare the appropriate activity name.
*
* @param mixed $model
* @param string $action
* @return string
*/
protected function getActivityName($model, $action)
{
$name = strtolower((new ReflectionClass($model))->getShortName());
return "{$action}_{$name}";
}
/**
* Get the model events to record activity for.
*
* @return array
*/
protected static function getModelEvents()
{
if (isset(static::$recordEvents)) {
return static::$recordEvents;
}
return [
'created', 'deleted', 'updated'
];
}
}
Next, we will include this trait in the Task
and Comment
models:
# app/Task.php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Task extends Model
{
use RecordsActivity;
...
...
}
# app/Comment.php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Task extends Model
{
use RecordsActivity;
...
...
}
Now, any changes in the above models will be recorded in the activities
table. Let us create a task:
This action is recorded in the activities
table:
>>> Activity::all();
=> Illuminate\Database\Eloquent\Collection {#671
all: [
App\Activity {#672
id: 1,
subject_id: 1,
subject_type: "App\Task",
name: "created_task",
user_id: 1,
created_at: "2017-02-16 10:54:00",
updated_at: "2017-02-16 10:54:00",
},
],
}
Let us now comment on Task 1:
This action is also recorded in the activities
table:
>>> Activity::latest()->first();
=> App\Activity {#690
id: 2,
subject_id: 1,
subject_type: "App\Comment",
name: "created_comment",
user_id: 1,
created_at: "2017-02-16 11:03:05",
updated_at: "2017-02-16 11:03:05",
}
Broadcasting feed
Whenever an activity is logged, we need to fire an event which will be broadcasted over Pusher. For broadcasting an event, it should implement the ShouldBroadcast
interface. Let us first create the ActivityLogged
event:
php artisan make:event ActivityLogged
broadcastOn method
The event should implement a broadcastOn
method. This method should return the channels to which the event should be broadcast.
broadcastWith method
By default, Laravel will broadcast all the public properties in JSON format as the event payload. We can define our logic to broadcast only the necessary data in the broadcastWith
method.
We do not want any user of the app to able to listen to all the broadcast activity. To avoid this, we will use the PrivateChannel
to broadcast our event. For broadcasting on a public channel, we can simply use the Channel
class.
Below is our ActivityLogged
event:
# app/Events/ActivityLogged.php
namespace App\Events;
use App\Activity;
use App\Transformers\ActivityTransformer;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
class ActivityLogged implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $activity;
public function __construct(Activity $activity)
{
$this->activity = $activity;
}
public function broadcastOn()
{
return new PrivateChannel('activity.' . $this->activity->user->id);
}
public function broadcastWith()
{
return fractal($this->activity, new ActivityTransformer())->toArray();
}
}
In the RecordsActivity
trait, we broadcast this event whenever a new activity is recorded. The snippet from recordActivity
method:
public function recordActivity($event)
{
$activity = Activity::create([
'subject_id' => $this->id,
'subject_type' => get_class($this),
'name' => $this->getActivityName($this, $event),
'user_id' => $this->user_id
]);
broadcast(new ActivityLogged($activity));
}
Next, we need to start our queue to actually listen for jobs and broadcast any events that are recorded. We can use the database queue listener on our local environment:
php artisan queue:listen
Listening to feed
Installation and configuration of Laravel Echo is a must before we can start listening to feeds. We have covered the process in detail in the above section of this article. Please go through it if you might have skipped it.
We can listen to a public channel using Echo.channel(channel)
. For listening to a private channel, we need to use Echo.private(channel)
. As we have broadcasted the ActivityLogged
event on a private channel, we will use Echo.private()
:
Echo.private('activity.' + this.user.id)
.listen('ActivityLogged', (e) => {
//push to feed variable
});
Authorization
As we are listening on a private channel, we need to authenticate that the current logged in user should be able to listen on this private channel. Laravel Echo will automatically call the necessary authorization routes if we are listening to a private channel. But, we need to write the authentication logic which will actually authorize the user.
Authorization logic is written in the routes/channels.php
. The authorization logic for our activity
channel:
Broadcast::channel('activity.{id}', function ($user, $id) {
return (int) $user->id === (int) $id;
});
That’s it! Now, whenever a new activity is recorded, it will be broadcast and we can listen using this private channel.
We can even listen to multiple events on the same channel:
Echo.private('activity.' + this.user.id)
.listen(...)
.listen(...)
.listen(...);
Below is our Activity component written using Vue.js
<template>
<div class="container">
<div class="panel panel-info">
<!-- Default panel contents -->
<div class="panel-heading">Activity Dashboard</div>
<ul class="list-group">
<li class="list-group-item" v-for="item in feed">
{{ item.description }}
<span class="pull-right">{{ item.lapse }}</span>
</li>
</ul>
</div>
</div>
</template>
<script>
export default {
props: ['user'],
data() {
return {
feed: {}
}
},
mounted() {
console.log('Component mounted.')
},
created() {
this.getFeed();
this.listenForActivity();
},
methods: {
getFeed() {
var self = this;
return axios.get('/api/activities?api_token=' + this.user.api_token, {})
.then(function(response) {
self.feed = response.data.data;
});
},
listenForActivity() {
Echo.private('activity.' + this.user.id)
.listen('ActivityLogged', (e) => {
this.feed.unshift(e.data);
});
}
}
}
</script>
Here is the screenshot of what our Activity Feed looks like:
Conclusion
In this article, we have covered how to create a realtime Activity Feed for our application. We have covered the configuration options necessary to get started, and the examples above should help you fill in the gaps and give an overview of some of the other configuration options available to you.
The code is hosted on public Github repository. You can download it for educational purposes. How do you use Laravel and Pusher Channels for activity feed? Can you think of any advanced use cases for this library? What are they? Let us know in the comments!
6 March 2017
by Viraj Khatavkar