Build a realtime activity feed with Laravel

Introduction

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.

activity-feed-laravel-create-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:

1# .env
2
3BROADCAST_DRIVER=pusher
4
5PUSHER_APP_ID=your-pusher-app-id
6PUSHER_APP_KEY=your-pusher-app-key
7PUSHER_APP_SECRET=your-pusher-app-secret
8PUSHER_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:

1import Echo from "laravel-echo"
2
3window.Echo = new Echo({
4    broadcaster: 'pusher',
5    key: 'your-pusher-app-key',
6    cluster: 'ap2',
7    encrypted: true
8});

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:

1use Illuminate\Database\Schema\Blueprint;
2use Illuminate\Database\Migrations\Migration;
3
4class CreateActivitiesTable extends Migration 
5{
6
7    public function up()
8    {
9        Schema::create('activities', function(Blueprint $table) {
10                $table->increments('id');
11                $table->integer('subject_id')->index();
12                $table->string('subject_type')->index();
13                $table->string('name');
14                $table->integer('user_id')->index();
15                $table->timestamps();
16            });
17    }
18
19    public function down()
20    {
21        Schema::drop('activities');
22    }
23
24}

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 the activities table
  • getActivityName - This will return the name of activity viz. created_task or updated_task
  • getModelEvents - 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:

1# RecordsActivity.php
2
3namespace App;
4
5use App\Events\ActivityLogged;
6use ReflectionClass;
7
8trait RecordsActivity
9{
10    /**
11     * Register the necessary event listeners.
12     *
13     * @return void
14     */
15    protected static function bootRecordsActivity()
16    {
17        foreach (static::getModelEvents() as $event) {
18            static::$event(function ($model) use ($event) {
19                $model->recordActivity($event);
20            });
21        }
22    }
23
24    /**
25     * Record activity for the model.
26     *
27     * @param  string $event
28     * @return void
29     */
30    public function recordActivity($event)
31    {
32        $activity = Activity::create([
33            'subject_id' => $this->id,
34            'subject_type' => get_class($this),
35            'name' => $this->getActivityName($this, $event),
36            'user_id' => $this->user_id
37        ]);
38
39        event(new ActivityLogged($activity));
40    }
41
42    /**
43     * Prepare the appropriate activity name.
44     *
45     * @param  mixed  $model
46     * @param  string $action
47     * @return string
48     */
49    protected function getActivityName($model, $action)
50    {
51        $name = strtolower((new ReflectionClass($model))->getShortName());
52
53        return "{$action}_{$name}";
54    }
55
56    /**
57     * Get the model events to record activity for.
58     *
59     * @return array
60     */
61    protected static function getModelEvents()
62    {
63        if (isset(static::$recordEvents)) {
64            return static::$recordEvents;
65        }
66
67        return [
68            'created', 'deleted', 'updated'
69        ];
70    }
71}

Next, we will include this trait in the Task and Comment models:

1# app/Task.php
2
3namespace App;
4
5use Illuminate\Database\Eloquent\Model;
6
7class Task extends Model
8{
9    use RecordsActivity;
10
11    ...
12    ...
13}      
14
15# app/Comment.php
16
17namespace App;
18
19use Illuminate\Database\Eloquent\Model;
20
21class Task extends Model
22{
23    use RecordsActivity;
24
25    ...
26    ...
27}

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:

1>>> Activity::all();
2=> Illuminate\Database\Eloquent\Collection {#671
3     all: [
4       App\Activity {#672
5         id: 1,
6         subject_id: 1,
7         subject_type: "App\Task",
8         name: "created_task",
9         user_id: 1,
10         created_at: "2017-02-16 10:54:00",
11         updated_at: "2017-02-16 10:54:00",
12       },
13     ],
14   }

Let us now comment on Task 1:

activity-feed-laravel-add-comment

This action is also recorded in the activities table:

1>>> Activity::latest()->first();
2=> App\Activity {#690
3     id: 2,
4     subject_id: 1,
5     subject_type: "App\Comment",
6     name: "created_comment",
7     user_id: 1,
8     created_at: "2017-02-16 11:03:05",
9     updated_at: "2017-02-16 11:03:05",
10   }

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:

1# app/Events/ActivityLogged.php
2
3namespace App\Events;
4
5use App\Activity;
6use App\Transformers\ActivityTransformer;
7use Illuminate\Broadcasting\Channel;
8use Illuminate\Broadcasting\InteractsWithSockets;
9use Illuminate\Broadcasting\PresenceChannel;
10use Illuminate\Broadcasting\PrivateChannel;
11use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
12use Illuminate\Foundation\Events\Dispatchable;
13use Illuminate\Queue\SerializesModels;
14use Illuminate\Support\Facades\Log;
15
16class ActivityLogged implements ShouldBroadcast
17{
18    use Dispatchable, InteractsWithSockets, SerializesModels;
19
20    public $activity;
21
22    public function __construct(Activity $activity)
23    {
24        $this->activity = $activity;
25    }
26
27    public function broadcastOn()
28    {
29        return new PrivateChannel('activity.' . $this->activity->user->id);
30    }
31
32    public function broadcastWith()
33    {
34        return fractal($this->activity, new ActivityTransformer())->toArray();
35    }
36}

In the RecordsActivity trait, we broadcast this event whenever a new activity is recorded. The snippet from recordActivity method:

1public function recordActivity($event)
2{
3    $activity = Activity::create([
4        'subject_id' => $this->id,
5        'subject_type' => get_class($this),
6        'name' => $this->getActivityName($this, $event),
7        'user_id' => $this->user_id
8    ]);
9
10    broadcast(new ActivityLogged($activity));
11}

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():

1Echo.private('activity.' + this.user.id)
2    .listen('ActivityLogged', (e) => {
3        //push to feed variable
4    });

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:

1Broadcast::channel('activity.{id}', function ($user, $id) {
2    return (int) $user->id === (int) $id;
3});

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:

1Echo.private('activity.' + this.user.id)
2    .listen(...)
3    .listen(...)
4    .listen(...);

Below is our Activity component written using Vue.js

1<template>
2    <div class="container">
3        <div class="panel panel-info">
4      <!-- Default panel contents -->
5      <div class="panel-heading">Activity Dashboard</div>
6
7        <ul class="list-group">
8            <li class="list-group-item" v-for="item in feed">
9                {{ item.description }}  
10                <span class="pull-right">{{ item.lapse }}</span>
11            </li>
12        </ul>
13    </div>
14    </div>
15</template>
16
17<script>
18    export default {
19        props: ['user'],
20        data() {
21            return {
22                feed: {}
23            }
24        },
25        mounted() {
26            console.log('Component mounted.')
27
28        },
29        created() {
30            this.getFeed();
31            this.listenForActivity();
32        },
33        methods: {
34            getFeed() {
35                var self = this;
36                return axios.get('/api/activities?api_token=' + this.user.api_token, {})
37                .then(function(response) {
38                    self.feed = response.data.data;
39                });
40            },
41            listenForActivity() {
42                Echo.private('activity.' + this.user.id)
43                    .listen('ActivityLogged', (e) => {
44                        this.feed.unshift(e.data);
45                    });
46            }
47        }
48    }
49</script>

Here is the screenshot of what our Activity Feed looks like:

activity-feed-laravel-example

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!