This guide covers the process of implementing mentionable things in Laravel (usually users) from start to finish.
We'll start with the backend detection and storage of mentions for any model, and then look at how to implement a frontend search when typing in a textarea.
Prefer to watch? We have a full course covering Laravel Mentions available!
While we'll be using a basic frontend example with Alpine.js, you'll easily be able to transfer this to your framework of choice.
You may not be working with username mentions (like @alex), but either way... we need a column that we can reference when someone or something is mentioned.
Let's add a username
column.
php artisan make:migration add_username_to_users_table
Add a unique username column within the up
method of the migration.
Schema::table('users', function (Blueprint $table) {
$table->string('username')->unique();
});
And finally migrate your changes.
php artisan migrate
I'll leave it up to you to add the UI for username updates. Or, you can watch the Laravel Mentions course for a full guide to this.
It's likely you'll be implementing mentionable functionality as part of model like when a new Comment
is created, so let's roll with that example.
Create a model and migration for this:
php artisan make:model Comment -m
And add the following schema to the up
migration method.
Schema::create('comments', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained();
$table->text('body');
$table->timestamps();
});
And then migrate your changes.
php artisan migrate
Now open up the Comment
model and adjust it to unguard it and add a relationship back to the User
who posted the comment.
namespace App\Models;
class Comment extends Model
{
protected $guarded = false;
public function user()
{
return $this->belongsTo(User::class);
}
}
When a new comment is created, we want to scan the body
to check if it contains a mention (or several). The easiest way to do this globally is by using an observer.
An observer will allow us to react to a mention regardless of where a comment was created, making our application a lot more consistent.
Start by creating the observer.
php artisan make:observer CommentObserver
Now attach the observer to the Comment
model.
namespace App\Models;
use App\Observers\CommentObserver;
#[ObservedBy(CommentObserver::class)]
class Comment extends Model
{
//...
}
Inside the CommentObserver
, add the following code to detect a mention when creating or updating (editing) a comment.
namespace App\Observers;
use App\Models\Comment;
use App\Models\User;
class CommentObserver
{
public function created(Comment $comment)
{
$this->findAndSyncMentions(
$comment
);
}
public function updated(Comment $comment)
{
$this->findAndSyncMentions(
$comment
);
}
protected function findAndSyncMentions(Comment $comment)
{
preg_match_all(
'(\@(?P<username>[a-zA-Z0-9\-\_]+))',
$comment->body,
$mentions,
PREG_SET_ORDER
);
if (count($mentions) === 0) {
return;
}
// TODO: Sync mentions
}
}
Ok, let's take a step back and figure out what's going on inside this findAndSyncMentions
method (which isn't actually syncing to the database yet, we'll get to that later).
preg_match_all
uses a regular expression to find multiple matches based on a pattern. In our case, the pattern looks like this:
(\@(?P<username>[a-zA-Z0-9\-\_]+))
This pattern will match for usernames like this:
You may want to adjust the regular expression to match more or fewer characters, depending on what you're building. Use a 'regex tester' tool (a quick Google search will bring up many choices) to test your new patterns.
preg_match_all
adds any pattern detections to the $mentions
variable, which we're then checking a count
on. There's no point doing anything if there are no mentions, so we perform an early return.
Once you're comfortable with how mentions are detected here, let's move on and look at syncing them to the database for some permanant storage!
Now we've detected mentions, it would be a good idea to store who's been mentioned so we can reference it later.
Syncing this to the database means you're able to implement features like "comments I've been mentioned in" lists. It's totally optional, but a good idea to store this data now rather than later.
First, we'll need a pivot table.
php artisan make:migration create_comments_mentions_table
Add the schema to the up
method of the migration.
Schema::create('comments_mentions', function (Blueprint $table) {
$table->id();
$table->foreignId('comment_id')->constrained();
$table->foreignId('user_id')->constrained();
$table->timestamps();
});
And then migrate your changes.
php artisan migrate
Open up your Comment
model and add in the mentions relationship, making sure to specify the comments_mentions
pivot table since we're not following Laravel's conventions here.
#[ObservedBy(CommentObserver::class)]
class Comment extends Model
{
protected $guarded = false;
public function user()
{
return $this->belongsTo(User::class);
}
public function mentions()
{
return $this->belongsToMany(User::class, 'comments_mentions')
->withTimestamps();
}
}
Now we have this relationship, open up the CommentObserver
and add the following to the findAndSyncMentions
method.
protected function findAndSyncMentions(Comment $comment)
{
preg_match_all(
'(\@(?P<username>[a-zA-Z0-9\-\_]+))',
$comment->body,
$mentions,
PREG_SET_ORDER
);
if (count($mentions) === 0) {
return;
}
$comment->mentions()->sync(
User::whereIn('username', collect($mentions)->pluck('username'))
->pluck('id')
->toArray()
);
}
This looks up all users who have been mentioned by their username and syncs them to the pivot table we created.
Importantly, this also happens when a comment is updated, so this pivot will be kept up-to-date with only the users who have been mentioned, whether they're added or removed!
Now we've got mentions being detected, let's implement mentionable textarea functionality.
We'll perform a really basic database search here, but if you'd like to implement full-text search with a Laravel Scout integration, the Laravel Mentions course covers that.
First, we'll create a controller whose only job is to return a list of users based on a search term.
Before we do this, you might want to generate a bunch of users to test this with. Open up your UserFactory
and add a username under the definition
method.
class UserFactory extends Factory
{
//...
public function definition(): array
{
return [
'username' => fake()->unique()->userName(),
'name' => fake()->name(),
'email' => fake()->unique()->safeEmail(),
'email_verified_at' => now(),
'password' => static::$password ??= Hash::make('password'),
'remember_token' => Str::random(10),
];
}
}
Now open up a Tinker session.
php artisan tinker
And then generate as many users as you need. Here, we'll generate 1000.
User::factory(1000)->create();
Great, now we've got some users to play with, create a route and controller for the search.
php artisan make:controller UserSearchController
Here's what the controller should look like.
namespace App\Http\Controllers;
use App\Models\User;
use Illuminate\Http\Request;
class UserSearchController extends Controller
{
public function __invoke(Request $request)
{
return User::query()
->where('username', 'LIKE', $request->get('q', '') . '%')
->get()
->map(fn (User $user) => [
'key' => $user->name,
'value' => $user->username,
]);
}
}
This takes in a q
query string value and returns users whose username roughly matches. Not the best solution, but it'll do for now.
Notice we're also using map
to structure the returned data. We'll need this structure for our mentionable textarea later.
Next, create the route for this.
Route::get('/users/search', UserSearchController::class);
Give it a whirl in the browser by heading to /users/search?q=mabel
, replacing q
to find the user.
Once you have that working, we're able to implement the textarea plugin to hit this endpoint and return a list of results.
We'll use Tribute, a well-established textarea mentions package, to turn any textarea in our application into a mentionable search. Then, we'll create an Alpine.js plugin so we can easily attach this to any textarea.
Start off by installing Tribute.
npm install tributejs
Then, somewhere in your Laravel JavaScipt bundle (probably resources/js/app.js
), add the following to activate Tribute and pull in the CSS required for it to display.
import Tribute from 'tributejs'
import 'tributejs/dist/tribute.css'
Next, create a mentionable.js
file and create an Alpine.js plugin for Tribute.
export default (Alpine) => {
const userSearch = (text, cb) => {
axios.get("/users/search?q=" + text).then((response) => {
cb(response.data)
})
}
Alpine.directive('mentionable', (el) => {
let tribute = new Tribute({
trigger: "@",
values: (text, cb) => userSearch(text, users => cb(users)),
lookup: "value",
fillAttr: "value",
menuItemTemplate: (item) => {
return "@" + item.original.value + " (" + item.original.key + ")"
}
})
tribute.attach(el)
})
}
Here, we're using Tribute to attach to the el
(HTML element) we're going to be using this plugin on (more on that in a second).
We've configured Tribute to hit the search endpoint we built earlier and return a list of results. Remember when we mapped through our results in the controller earlier?
->map(fn (User $user) => [
'key' => $user->name,
'value' => $user->username,
]);
That's where lookup
and fillAttr
come in. It tells us what property we're using to search on and display. There's a bunch more you can configure with Tribute, but we're trying to keep it as simple as possible to start with. Feel free to fiddle around once we're done.
Once you've understood what's happening, let's register this plugin in app.js
.
import Tribute from 'tributejs'
import 'tributejs/dist/tribute.css'
import mentionable from './plugins/mentionable.js'
import { Livewire, Alpine } from '../../vendor/livewire/livewire/dist/livewire.esm.js'
window.Tribute = Tribute
Alpine.plugin(mentionable)
Livewire.start()
This may vary depending on which starter kit or stack you're using.
Now it's time to use the plugin! Apply x-mentionable
(the plugin name we gave) to any textarea — and you should now have mentionable functionality that searches your users.
<div>
<textarea
class="border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm"
x-mentionable
></textarea>
</div>
We've covered the detection, storing and basic search functionality for mentionable things in Laravel.
If you'd like to see the full working comment system implementation covered from start to finish, the Laravel Mentions course covers this. Plus, we go deeper into search and integrate Laravel Scout for faster, full-text search.
Happy mentioning!