PHP - - By Christopher Vundi

Building a Social Network with Laravel and Stream? Easy!

Real-time Laravel

In the previous post, we saw how to add the follow functionality to a Laravel app. We also looked at how to configure our app to use Stream. This part will focus on:

  • configuring our models in order to make it possible to track activities.
  • the different types of feeds that Stream provides.
  • getting feeds from Stream.
  • rendering the different types of feeds in a view.

Laravel and Stream Logo Merger

Activity Fields

When using Stream, models are stored in feeds as activities. An activity is composed of at least the following data fields: actor, verb, object, time. You can also add more custom data if needed.

  • object is a reference to the model instance itself
  • actor is a reference to the user attribute of the instance
  • verb is a string representation of the class name

Let’s define the activity verb inside our Post model:

[...]
class Post extends Model
{
    [...]
    /**
    * Stream: Change activity verb to 'created':
    */
    public function activityVerb()
    {
        return 'created';
    }
}

Feed Manager

We’ll leverage the FeedManager to make our app lively. Stream Laravel comes with a FeedManager class that helps with all common feed operations. We can get an instance of the manager with FeedManager which we set as the facade alias earlier inside the config/app.php file.

Pre-Bundled Feeds

To get us started, the manager has feeds pre-configured. We could also add more feeds if our app needed them. The three feeds are divided into three categories: User Feed, News Feed and Notification Feed. The User feed, for example, stores all activities for a user. Let’s think of it as our personal Facebook page. We can easily get this feed from the manager.

For this application, however, we are more interested in getting notifications for posts created by people we follow and also notifications for new follows, thus we’ll just stick to the News Feed and the Notification Feed. For more information on the other types of feeds and how to use them visit this link.

Follow / Unfollow Functionality – Using FeedManager

We need to update the follow and unfollow methods inside the FollowController, to take note of the FeedManager:

app/Http/Controllers/FollowController.php

[...]
public function follow(User $user)
{
    if (!Auth::user()->isFollowing($user->id)) {
        // Create a new follow instance for the authenticated user
        Auth::user()->follows()->create([
            'target_id' => $user->id,
        ]);
        \FeedManager::followUser(Auth::id(), $user->id);

        return back()->with('success', 'You are now friends with '. $user->name);
    } else {
        return back()->with('error', 'You are already following this person');
    }

}

public function unfollow(User $user)
{
    if (Auth::user()->isFollowing($user->id)) {
        $follow = Auth::user()->follows()->where('target_id', $user->id)->first();
        \FeedManager::unfollowUser(Auth::id(), $follow->target_id);
        $follow->delete();

        return back()->with('success', 'You are no longer friends with '. $user->name);
    } else {
        return back()->with('error', 'You are not following this person');
    }
}
[...]

This code inside the follow method lets the current user’s timeline and timeline_aggregated feeds follow another user’s personal feed. Inside the unfollow method, we unsubscribe from another user’s personal feed.

Displaying the Different Types of Feed

To display the different types of feed, let’s start by creating a FeedsController:

php artisan make:controller FeedsController

When reading data from feeds, activities come back in a format that is not suitable for use in our views. For example, a post creation activity will look like this:

{'actor': 'User:1', 'verb': 'created', 'object': 'Post:1'}

This is far from ready for usage in our templates. We call the process of loading the references from the database enrichment. We’ll enrich our activities before displaying them in the views.

NewsFeed

Let’s create a newsFeed method inside this controller for getting this type of feed. We should also create a private method within this controller responsible for instantiating the Enricher class:

[...]
use GetStream\StreamLaravel\Enrich;
[...]
class FeedsController extends Controller
{
    public function newsFeed(Request $request)
    {
        // Timeline feed:
        $feed = \FeedManager::getNewsFeeds($request->user()->id)['timeline'];

        //  get 25 most recent activities from the timeline feed:
        $activities = $feed->getActivities(0,25)['results'];
        $activities = $this->enrich()->enrichActivities($activities);

        return view('feed.newsfeed', [
            'activities' => $activities,
        ]);
    }

    private function enrich()
    {
        return new Enrich;
    }
}

In the code block above, we are calling the getNewsFeeds method from the FeedManager (It’s important to specify the format we want to get back. In our case, we want the feed in a timeline format). After that, we get the 25 most recent activities from the feed and then enrich them. These activities should be displayed in the newsfeed view which we’ll be creating shortly.

Next, let’s create a route mapping to the newsFeed method, which when visited will take us to a view with the newsfeed. This route also falls inside the route group with the auth middleware since users have to be authenticated for the feed to load:

[...]
Route::group(['middleware' => ['auth']], function () {
    [...]
    Route::get('/feed', 'FeedsController@newsFeed');
});
[...]

Templating

We can now render the enriched activities in a view:

resources/views/feed/newsfeed.blade.php

@extends('layouts.app')

@section('content')
    <div class="container">
        @if ($activities)
            <div class="panel panel-default">
                <div class="panel-heading">
                    News Feed
                </div>

                <div class="panel-body">
                    @foreach ($activities as $activity)
                        @include('stream-laravel::render_activity', array('activity'=>$activity))
                    @endforeach
                </div>
            </div>
        @else
        <p>You are not following anyone.Follow other users <a href="/users">here</a> in order to see their activities</p>
        @endif
    </div>
@endsection

The stream-laravel::render_activity view tag will render the view activity.$activity["verb"] view with the activity as context. To make things work, we need to create an activity folder inside of views. After doing that, we can create a partial for the post creation activity. The name of the partial should correspond to the activity verb:

resources/views/activity/created.blade.php

<div class="well well-sm">
    <p><small class="text-muted">{{ date('F j, Y, g:i a', strtotime($activity['time'])) }}</small></p>

    <p><strong>{{ $activity['actor']['name'] }}</strong> created a new post titled <strong>{{ $activity['object']['title'] }}</strong></p>
</div>

By visiting the URL \feed, we should be able to see our newsfeed. Initially, we’ll see the text “You are not following anyone” as we had not yet integrated Stream into our app at the time when we were testing the different button variations based on the follow status. Also note that had we not removed the very first activity from the stream dashboard, the page would error complaining about a missing [.app.\user] view.

Let’s go ahead create a new post from our account. If we follow ourselves from a different account, this post creation activity should show up in the feed of this other account like so:

Post Creation

From the screenshot above, if the user called chris creates another post, the new post creation activity from chris will show up on morris' feed:

Another Post

Alternatively, instead of creating new accounts and posts to test out this behavior, we can truncate the data in our app from Stream’s dashboard then pre-populate our database again with seed data. The gif below illustrates how to truncate data:

Truncating Data

When we seed our database this time around, the post creation activity will be noted on Stream:

php artisan migrate:refresh --seed

We can then follow some of the users who were created upon seeding the database. If we now visit the URL \feed, we’ll see their activities:

seeded users post creation

Right now, we can only get notifications for new posts created by people we follow. We also want to make it so that we get notifications for new follows.

Notification Feed

For this, let’s start by updating our Follow model to take note of follow activities

app/Follow.php

class Follow extends Model
{
    use \GetStream\StreamLaravel\Eloquent\ActivityTrait;

    [...]
    public function target()
    {
        return $this->belongsTo(User::class);
    }

    public function activityNotify()
    {
        $targetFeed = \FeedManager::getNotificationFeed($this->target_id);
        return array($targetFeed);
    }

    public function activityVerb()
    {
        return 'follow';
    }

    public function activityExtraData()
    {
        return array('followed' => $this->target, 'follower' => $this->user);
    }

The activityNotify method is used to build the notification feed. This type of feed is useful to notify certain users about an action. In our case, we are notifying a user that someone has followed them. The activityExtraData() method lets us store more data than just the basic fields. In our case, we want to store the target for a new follow and also the person who followed the target.

Let’s go ahead and create the controller action, the route, and the view to display the notification feed:

app/Http/Controllers/feedController

[...]
class FeedsController extends Controllers
{
    [...]
    public function notification(Request $request)
    {
        //Notification feed:
        $feed = \FeedManager::getNotificationFeed($request->user()->id);
        $activities = $feed->getActivities(0,25)['results'];
        $activities = $this->enrich()->enrichActivities($activities);

        return view('feed.notifications', [
            'activities' => $activities,
        ]);
    }
}

We get this feed the same way we got the newsfeed, the only difference is we are calling the getNotificationFeed method on the FeedManager instead of the getNewsFeeds method.

Let’s now create the route mapping to this controller action:

Route::group(['middleware' => 'auth'], function () {
    [...]
    Route::get('/notifications', 'FeedsController@notification');
});

Templating

When displaying the notification feed, we will follow the same procedure as when displaying the newsfeed, i.e create a partial with the information we want to display, then render the partial in a view. We’ll start by creating the view:

resources/views/feed/notifications.blade.php

@extends('layouts.app')

@section('content')
    <div class="container">
        @if ($activities)
            <div class="panel panel-default">
                <div class="panel-heading">
                    Notification feed
                </div>

                <div class="panel-body">
                    @foreach ($activities as $activity)
                        @foreach ($activity['activities'] as $activity)
                            @include('stream-laravel::render_activity', array('aggregated_activity'=>$activity, 'prefix'=>'notification'))
                        @endforeach
                    @endforeach
                </div>
            </div>
        @else
        <p>You don't have any follow activities</p>
        @endif
    </div>
@endsection

Notice we have to go two levels deep in our view to access the notification feed? This is because of the data format we get back after calling the getNotificationFeed method. Also, note the line of code to render the partial has a prefix key the value of which is set to notification. Let me explain what’s going on. For this app, we want two different templates for the same activity i.e. the follow activity. To achieve this with Stream, we send a third parameter to change the view selection. In this case we’ll create a partial called notification_follow.blade.php inside the activity folder:

resources/views/activity/notification_follow.blade.php

<div class="well well-sm">
    <p><small class="text-muted">{{ date('F j, Y, g:i a', strtotime($activity['time'])) }}</small></p>
    <p>You are now friends with <strong>{{ $activity['follower']['name'] }}</strong></p>
</div>

If we visit the /notifications URL, we should see feed for every follow we received and the name of the person who followed us:

New Follows

From the screenshot, both Morris and Kevin followed me.

We also want the feed for follow activities to show up in our NewsFeed page. However, when displaying this, we want to say who became friends with whom. This is the reason we had to come up with different partials for the follow activity. More information on templating is available on Streams GitHub Page:

resources/views/activity/follow.blade.php

<div class="well well-sm">
    <p><small class="text-muted">{{ date('F j, Y, g:i a', strtotime($activity['time'])) }}</small></p>
    <p><strong>{{ $activity['actor']['name'] }}</strong> is now friends with <strong>{{ $activity['followed']['name'] }}</strong></p>
</div>

Let’s now visit the /feed URL. Follow activities should also show up:

timeline feed

To make navigation easier, we can add links to access the newsfeed and the notification feeds in the navbar next to the New Post link:

resources/views/layouts/app.blade.php

<ul class="nav navbar-nav">
     
    <li><a href="{{ url('/posts/create') }}">New Post</a></li>
    <li><a href="{{ url('/users') }}">Users</a></li>
    <li><a href="{{ url('/feed') }}">News Feed</a></li>
    <li><a href="{{ url('/notifications') }}">Notification Feed</a></li>
</ul>

Conclusion

Stream makes it extremely easy to add feeds to any app compared to coding the logic from scratch.

We can track just about anything in an app, even liking or commenting on posts. I’ll leave that as a homework assignment so that you get to play around with the API. Stream also provides a low-level API access for PHP projects that don’t use Laravel. More information on how to use the low-level PHP client API directly is available here.

If you found this tutorial helpful, please hit the like button and don’t forget to share with your friends and colleagues!

Sponsors
Login or Create Account to Comment
Login Create Account