Now that we have the logic in place to record user activity in the database, in this tutorial we will work on fetching that user activity from the database and rendering it to the browser. To begin, we’ll just verify that new activities are in fact being recorded by manually checking in the database. Once that is complete, we can alter our controller logic to fetch user activity from the database. Next up, we’ll begin crafting the view files to display this information in the form of an activity feed. Finally, we’ll work a bit on grouping of different activities, and also refactoring to components in the view.
Testing User Activity Feature From The Browser
All of the supporting tests seem to indicate that the recording of user activity for the activity feed is working just great. We haven’t actually tried entering some new threads and replies via the browser to see if those activities are recorded. If they are recorded, then we should see those activity records in the activities table of the database. Let’s try that now.
First off, on the live server – we’ll need to manually run the migrations. Why? Because remember, our tests are doing everything in memory via an SQLite database. When the tests run, migrations are run, a whole new database schema is set up, then everything is rolled back upon completion. Anyhow, let’s run the migration.
vagrant@homestead:~/Code/forumio$ php artisan migrate Migrating: 2018_01_30_164752_create_activities_table Migrated: 2018_01_30_164752_create_activities_table
Great – the activities table is now in place. We can add a thread and see if it is recorded in the database.
Checking in the database shows that the new Thread Activity was created. Cool!
Let’s try one more thing. We need to make sure reply activity works as well.
Ah ha! It looks like we now have two activities in the database! It seems to be working.
Fetching Activity Data For The User Profile
The goal here is that when we view a user profile page, we will now see all the activity associated with that user. The show() method of the ProfilesController is where this occurs, so let’s go ahead and start fleshing out the api for this. We want to make use of an activity() method like so:
<?php
namespace AppHttpControllers;
use AppUser;
use IlluminateHttpRequest;
class ProfilesController extends Controller
{
public function show(User $user)
{
return view('profiles.show', [
'profileUser' => $user,
'threads' => $user->activity()->paginate(10)
]);
}
}
This means we need to define the relationship on the User Model. This is a hasMany relationship that is pretty easy to set up.
<?php
namespace App;
use IlluminateNotificationsNotifiable;
use IlluminateFoundationAuthUser as Authenticatable;
class User extends Authenticatable
{
use Notifiable;
protected $fillable = [
'name', 'email', 'password',
];
protected $hidden = [
'password', 'remember_token',
];
public function getRouteKeyName()
{
return 'name';
}
public function threads()
{
return $this->hasMany(Thread::class)->latest();
}
public function activity()
{
return $this->hasMany(Activity::class);
}
}
Viewing the relationship raw data
Very quickly, we can just add return $user->activity;
to the beginning of the show() method to see what gets sent to the browser. If we visit the user profile of http://forum.io/profiles/NikolaTesla we see that his activity is in fact being displayed. So far he has one Thread activity and two Reply activities.
Eager Loading The Subject Activity
It’s a good idea to make use of eager loading when you can, so we can add that to the method chain like so: return $user->activity()->with('subject')->get();
Now we can see that each Activity also includes with it the Subject of that activity. Very neat!
With these steps so far, here is where the show() method stands in the ProfilesController.
<?php
namespace AppHttpControllers;
use AppUser;
use IlluminateHttpRequest;
class ProfilesController extends Controller
{
public function show(User $user)
{
$activities = $user->activity()->with('subject')->get();
return view('profiles.show', [
'profileUser' => $user,
'activities' => $activities
]);
}
}
Preparing The profiles/show.blade.php View File
Getting the activity data from the database is now complete. The code above shows us fetching that activity data, and then sharing the data to the view file. We’ll need to update that show.blade.php view file to process the data now.
Basic View Side Polymorphism
The nature of activities is that there can be multiple types. There may be a Thread activity, or a Reply activity, or perhaps a Favorite activity. In a case like this you could set up multiple if else branches in the view to determine which to show, or you could set up some partial view files and use polymorphism to set this up. First we can set up some partial view files like so.
created_thread.blade.php
created_reply.blade.php
Now, in the main profiles/show.blade.php view, we can include different partial files like so:
@extends('layouts.app')
@section('content')
<div class="container">
<div class="row">
<div class="col-md-8 col-md-offset-2">
<div class="page-header">
<h1>
{{ $profileUser->name }}
<small>registered {{ $profileUser->created_at->diffForHumans() }}</small>
</h1>
</div>
@foreach($activities as $activity)
@include("profiles.activities.{$activity->type}")
@endforeach
</div>
</div>
</div>
@endsection
The variable of $activity->type is going to be different depending on if a thread or a reply is being passed through. This way, the correct partial panel gets loaded with no need for extensive if else blocks. Pretty neat!
Adding markup to the partial views
We did a good job making show.blade.php a neat and tidy file, now we need to add some markup in the actual partial files to display each activity type. Here is a start for the two partial view files.
created_thread.blade.php
<div class="panel panel-default">
<div class="panel-heading">
<div class="level">
<span class="flex">
{{ $profileUser->name }} published a thread
</span>
</div>
</div>
<div class="panel-body">
{{ $activity->subject->body }}
</div>
</div>
created_reply.blade.php
<div class="panel panel-default">
<div class="panel-heading">
<div class="level">
<span class="flex">
{{ $profileUser->name }} submitted a reply
</span>
</div>
</div>
<div class="panel-body">
{{ $activity->subject->body }}
</div>
</div>
Making sure the most recent activities are at the top of the profile page.
We also want to put the most current or latest activities at the top of the profile page when a user visits. That is pretty easy to do by making sure we include the latest() method in the controller.
<?php
namespace AppHttpControllers;
use AppUser;
use IlluminateHttpRequest;
class ProfilesController extends Controller
{
public function show(User $user)
{
$activities = $user->activity()->latest()->with('subject')->get();
return view('profiles.show', [
'profileUser' => $user,
'activities' => $activities
]);
}
}
Now, the beginnings of our activity feed is starting to take shape!
Linking To Individual Activities
On the activity feed, we should be able to click a link to take us to the specific activity that is being displayed. We can do this for both replies and threads.
On the Reply model, we are going to need a new belongsTo relationship. A Reply belongs to a Thread, represented by this code in the Reply model.
<?php
namespace App;
use IlluminateDatabaseEloquentModel;
class Reply extends Model
{
use Favoriteable, RecordsActivity;
protected $guarded = [];
protected $with = ['owner', 'favorites'];
public function owner()
{
return $this->belongsTo(User::class, 'user_id');
}
public function thread()
{
return $this->belongsTo(Thread::class);
}
}
Now we can update both partial files once again to include links to each individual activity.
created_thread.blade.php
<div class="panel panel-default">
<div class="panel-heading">
<div class="level">
<span class="flex">
{{ $profileUser->name }} published <a href="{{ $activity->subject->path() }}">{{ $activity->subject->title }}</a>
</span>
</div>
</div>
<div class="panel-body">
{{ $activity->subject->body }}
</div>
</div>
created_reply.blade.php
<div class="panel panel-default">
<div class="panel-heading">
<div class="level">
<span class="flex">
{{ $profileUser->name }} submitted a reply to
<a href="{{ $activity->subject->thread->path() }}">{{ $activity->subject->thread->title }}</a>
</span>
</div>
</div>
<div class="panel-body">
{{ $activity->subject->body }}
</div>
</div>
Ok! The activity feed is looking pretty good and now also has links going back to each thread or reply.
Grouping Activity By Day
Now we want to set up the activity feed so that all activities for any given day are grouped together. For this we will need to update the show() method in the ProfilesController to make use of a groupBy() clause on the collection. That looks like so:
<?php
namespace AppHttpControllers;
use AppUser;
use IlluminateHttpRequest;
class ProfilesController extends Controller
{
public function show(User $user)
{
$activities = $user->activity()->latest()->with('subject')->get()->groupBy(function ($activity){
return $activity->created_at->format('Y-m-d');
});
return view('profiles.show', [
'profileUser' => $user,
'activities' => $activities
]);
}
}
In addition, we need to now update the show.blade.php view as follows.
@extends('layouts.app')
@section('content')
<div class="container">
<div class="row">
<div class="col-md-8 col-md-offset-2">
<div class="page-header">
<h1>
{{ $profileUser->name }}
<small>registered {{ $profileUser->created_at->diffForHumans() }}</small>
</h1>
</div>
@foreach($activities as $date => $activity)
<h3 class="page-header">{{ $date }}</h3>
@foreach($activity as $record)
@include("profiles.activities.{$record->type}", ['activity' => $record])
@endforeach
@endforeach
</div>
</div>
</div>
@endsection
Now the activity feed is looking pretty good! Each day has a grouping of activities which is really slick.
Refactoring To A Component
We got the feed to work, and we’re pretty happy with that. Now however, we need to do some refactoring to make sure our view files are not a mess. To complete this, first we will create an activity component like so.
The markup will appear like so in the component:
<div class="panel panel-default">
<div class="panel-heading">
<div class="level">
<span class="flex">
{{ $heading }}
</span>
</div>
</div>
<div class="panel-body">
{{ $body }}
</div>
</div>
The other two partial view files now make use of the @component directive like so. Also note the use of the slot feature similar to how Vue uses slots.
created_thread.blade.php
@component('profiles.activities.activity')
@slot('heading')
{{ $profileUser->name }} published
<a href="{{ $activity->subject->path() }}">{{ $activity->subject->title }}</a>
@endslot
@slot('body')
{{ $activity->subject->body }}
@endslot
@endcomponent
created_reply.blade.php
@component('profiles.activities.activity')
@slot('heading')
{{ $profileUser->name }} submitted a reply to
<a href="{{ $activity->subject->thread->path() }}">{{ $activity->subject->thread->title }}</a>
@endslot
@slot('body')
{{ $activity->subject->body }}
@endslot
@endcomponent
The page loads the same, but now we have a much better system to put together our views. I know, I know, you’re thinking… But Why?
Well, think about if you end up having say another three or four activity types. You will now need three or four more partial view files for each type. Now let’s say you want to alter the markup slightly or change the styling on the activity feed. Guess what. Yep that’s right, without the activity.blade.php component, you would need to update five or six different view files. Yuck! Instead, with the single activity.blade.php file, you can make changes to markup or styling there once without the need to open up and edit all of those other partial files. Cool. Super cool in fact!
Display Activity Feed In The Browser Summary
Great job in setting up this activity feed for the application. It was a bit involved, but nothing we couldn’t handle. Additionally, we also learned about the groupBy() on collections and using components in the view files. Very nice!