Community based websites often times allow you to mention another user by screen name. For example, if you post a message to Twitter and include a user’s name including the @ symbol, then the user will get a notification that you mentioned them. This tutorial will recreate that type of feature in the forum based application we have been building. In the replies area of threads, if a user types in a reply and includes @Tom as an example, then when Tom logs into his account he should see a notification in the upper right navigation bar area to alert him that he has a thread to check out.
Mentioned Users Test
We can start by creating a feature test and we’ll name it MentionUsersTest.
What do we want this test to do? Well it should test for notifications to mentioned users. Here is the pseudo code.
- Given we have a user Tom that is signed in
- Given we also have a user named Sarah
- If we have a thread
- Then Tom replies to that thread and mentions @Sarah
- Then @Sarah should receive a notification
With these goals in mind, we can create the test_mentioned_users_in_a_reply_are_notified() method in the MentionUsersTest class.
<?php
namespace TestsFeature;
use IlluminateFoundationTestingDatabaseMigrations;
use TestsTestCase;
use IlluminateFoundationTestingWithFaker;
use IlluminateFoundationTestingRefreshDatabase;
class MentionUsersTest extends TestCase
{
use DatabaseMigrations;
function test_mentioned_users_in_a_reply_are_notified()
{
// Given we have a user Tom that is signed in.
$tom = create('AppUser', ['name' => 'Tom']);
$this->signIn($tom);
// Given we also have a user named Sarah.
$sarah = create('AppUser', ['name' => 'Sarah']);
// If we have a thread
$thread = create('AppThread');
// Then Tom replies to that thread and mentions @Sarah.
$reply = make('AppReply', [
'body' => 'Hey @Sarah check this out.'
]);
$this->json('post', $thread->path() . '/replies', $reply->toArray());
// Then @Sarah should receive a notification.
$this->assertCount(1, $sarah->notifications);
}
}
So we have the test, but no supporting code yet. We know if will fail, but we can let the errors help us in building the code to make it work. Currently we are getting an error of “Failed asserting that actual size 0 matches expected size 1.”
vagrant@homestead:~/Code/forumio$ phpunit --filter test_mentioned_users_in_a_reply_are_notified PHPUnit 6.5.5 by Sebastian Bergmann and contributors. F 1 / 1 (100%) Time: 1.61 seconds, Memory: 10.00MB There was 1 failure: 1) TestsFeatureMentionUsersTest::test_mentioned_users_in_a_reply_are_notified Failed asserting that actual size 0 matches expected size 1. /home/vagrant/Code/forumio/tests/Feature/MentionUsersTest.php:33 FAILURES! Tests: 1, Assertions: 1, Failures: 1.
How can we make this work? Consider when a user fills out the form to submit a reply. Maybe the user includes a name like @Tom in that text. As that body of text gets processed, the application needs to inspect the body of the reply text for user name mentions. If one or more is found, then those users should get a notification. So we need a function that will fetch all mentioned users within the reply’s body. This is a great job for regular expressions. We’ll add a mentionedUsers() method in the Reply model that leverages PHP’s preg_match_all() function.
<?php
namespace App;
use IlluminateDatabaseEloquentModel;
class Reply extends Model
{
use Favoriteable, RecordsActivity;
protected $guarded = [];
protected $with = ['owner', 'favorites'];
protected $appends = ['favoritesCount', 'isFavorited'];
public function owner()
{
return $this->belongsTo(User::class, 'user_id');
}
public function thread()
{
return $this->belongsTo(Thread::class);
}
public function mentionedUsers()
{
preg_match_all('/@([w-]+)/', $this->body, $matches);
return $matches[1];
}
public function path()
{
return $this->thread->path() . '#reply-' . $this->id;
}
}
Create A Notification Class
We recently set up notifications for users that are subscribed to specific threads. So we have an idea of how to set up notifications in Laravel. Recall that we can use artisan to scaffold out a Notification class for us.
vagrant@homestead:~/Code/forumio$ php artisan make:notification YouWereMentioned Notification created successfully.
The code we can use in this new notification class is here. It’s worth paying attention to the highlighted section of the toArray() method. The contents of that method become the actual message or notification which gets stored in the database. Again, we had customized this method during our subscriber notifications tutorial, but now we need to tweak things a bit for this user mentions tutorial. Also note that we are accepting the $reply variable as a protected property in this class. That is the reply for which a user was mentioned in. This class needs access to that reply in order to do it’s job.
<?php
namespace AppNotifications;
use IlluminateBusQueueable;
use IlluminateNotificationsNotification;
class YouWereMentioned extends Notification
{
use Queueable;
protected $reply;
public function __construct($reply)
{
$this->reply = $reply;
}
public function via($notifiable)
{
return ['database'];
}
public function toArray($notifiable)
{
return [
'message' => $this->reply->owner->name . ' mentioned you in ' . $this->reply->thread->title,
'link' => $this->reply->path()
];
}
}
Using An Event To Notify A User
We are already firing an event in the addReply() method of the Thread model.
public function addReply($reply)
{
$reply = $this->replies()->create($reply);
event(new ThreadReceivedNewReply($reply));
return $reply;
}
In addition to that, we have listener set up for that event which handles notifications for subscribed users. Wouldn’t it be cool if we could simply add another listener that would handle notifying mentioned users as well? That is exactly what we’ll do here. We’ll simply add a new listener to the array as highlighted here. Now, when the ThreadReceivedNewReply fires, both NotifySubscribers and NotifyMentionedUsers will handle that event.
<?php
namespace AppProviders;
use IlluminateFoundationSupportProvidersEventServiceProvider as ServiceProvider;
class EventServiceProvider extends ServiceProvider
{
protected $listen = [
'AppEventsThreadReceivedNewReply' => [
'AppListenersNotifySubscribers',
'AppListenersNotifyMentionedUsers'
],
];
public function boot()
{
parent::boot();
}
}
Once again we can run php artisan event:generate to scaffold out our new class file.
vagrant@homestead:~/Code/forumio$ php artisan event:generate Events and listeners generated successfully!
Now we see the new NotifyMentionedUsers class in the Listeners directory.
Now within the handle() method, we implement the logic to notify a user when they were mentioned.
<?php
namespace AppListeners;
use AppEventsThreadReceivedNewReply;
use AppNotificationsYouWereMentioned;
use AppUser;
class NotifyMentionedUsers
{
public function handle(ThreadReceivedNewReply $event)
{
User::whereIn('name', $event->reply->mentionedUsers())
->get()
->each(function ($user) use ($event) {
$user->notify(new YouWereMentioned($event->reply));
});
}
}
It looks like the test is now passing!
vagrant@homestead:~/Code/forumio$ phpunit --filter test_mentioned_users_in_a_reply_are_notified PHPUnit 6.5.5 by Sebastian Bergmann and contributors. . 1 / 1 (100%) Time: 1.53 seconds, Memory: 10.00MB OK (1 test, 1 assertion)
User Mention Notification Lifecycle
To summarize, here is how the lifecycle of a user mention notification works.
A user adds a new reply, and an event is fired. This happens in addReply() of Thread.php.
The announcement to the application that a thread has received a new reply is made by the ThreadReceivedNewReply event class.
Now, in EventServiceProvider, it is configured that when ThreadReceivedNewReply makes an announcement, then NotifyMentionedUsers will respond to that. In fact NotifySubscribers will also respond, but in this scenario we are more concerned with NotifyMentionedUsers responding. The mapping between an event firing and a listener responding happens here.
Now the listener takes over and handles that announcement that was made. Or in other words, it handles the event.
Within the code above, we see it leans on that mentionedUsers() method which exists on the Reply model. This syntax is kind of a fancy way to check for all mentioned users, then look them up in the database.
Then for each one you find, a notification is made via YouWereMentioned notification class.
Putting The Rubber To The Road
Ok it’s time to put the rubber to the road so to speak. We want to actually test this out in the browser and see what happens. First off, we’ll log in as the user Sarah and find a thread where we can respond to Tom. We’ll at mention him with the @Tom syntax and tell him his recipe is great.
Now we just go log in as user Tom and we should see a new notification. In fact we do! We see that Sarah mentioned us in the “Holy Guacamole!” thread. Very Cool!
Mentions And Notifications Summary
This tutorial built upon our knowledge of events and notifications in Laravel to set up a new notification system for mentioned users. Now you are armed with the knowledge of how to create your own user mention notification system like you see on many popular social websites.