Setting up Laravel Pulse for multiple Authenticatable models (user types)
Laravel Pulse was revealed at Laracon AU 2023, and offers real-time application performance monitoring – and a glimpse at average application behaviour. It’s not a replacement for full error logging, but is a great starting point to review your application’s performance.
This also means that the user who is performing actions get logged too, and out of the box is up and running with Laravel’s User model, which is great for many use cases.
There are times when your app needs multiple user types – in other words, different models that each are Authenticatable and have their own guard configuration.
But here’s the issue: out of the box, Pulse will log the user’s ID with the entry – let’s say ID 10. When you have multiple models, who is ID 10? Is it the User? The Teacher? The Student?
This is the exact scenario I have. Three different portals, three different models – a User (for admin), a Teacher (manages a class) and a Student (a member of the class). How can I get Pulse to work with multiple user types?
With Pulse being so new, Google wasn’t being too helpful, but thought I’d have a dig around in Pulse’s Issues to see what I could find: and another dev has this exact scenario. Jess Archer, one of the authors of Pulse, gave a great response that helped me get Pulse up and running for the different users in my app. This article’s aim is to expand upon Jess’s notes to look at how to do this.
Before we get going, this assumes your app and auth is set up and working with multiple user types. Just looking at Pulse’s User Resolver here.
Under the hood
Because the Laravel team are so awesome, Pulse has been designed with extensibility in mind. Not just for building your own Cards, but also for working with multiple Users.
Pulse comes with a UserResolver that shows how all of this works with a single User model. Its Contract has three methods that handles the logging and lookups of User details for Pulse – and we can write our own User Resolver that implements that Contract to work for our own app.
These methods are:
key
: returns a string that uniquely identifies the user – an ID for a single model type, but with multiple models, we need to add more information to identify the model type toofind
: returns an array of details for a single user, withname
,extra
andavatar
propertiesload
: eager loads an internal store of users
These give us a way to uniquely identify a user (key
), get a specific user profile (find
) and eagerly load all needed users for Pulse (load
). Simple, right? Let’s get going then!
Create your own UserResolver
Create your own User Resolver within your app – where you put this is up to you and your app’s structure: for me, placed it in app\Pulse\UserResolver.php
– tells me what it is, and what part of the app its for.
Our UserResolver
class needs to implement the ResolvesUsers
contract, which means we need to implement the key
, find
and load
methods.
1<?php 2 3namespace App\Pulse; 4 5use Illuminate\Contracts\Auth\Authenticatable; 6use Illuminate\Support\Collection; 7use Laravel\Pulse\Contracts\ResolvesUsers; 8 9class UserResolver implements ResolvesUsers10{11 protected array $resolvedUsers;12 13 public function load(Collection $keys): self14 {15 // ...16 }17 18 public function find(int|string|null $key): object19 {20 // ...21 }22 23 public function key(Authenticatable $user): int|string|null24 {25 // ...26 }27}
In your AppServiceProvider
, we can bind our UserResolver to the container for Pulse to use:
1$this->app->bind(\Laravel\Pulse\Users::class, \App\Pulse\UserResolver::class);
That’s the basic wiring up done: let’s look at each method of the resolver.
key
The key
method accepts an Authenticatable
model – a user. The base User
model that ships with Laravel is an Authenticatable
model: but if your app has multiple user types – like Users, Teachers and Students – each of your models would already be set up to be Authenticatable
too. This user parameter could be any of these user types.
You can then determine how you want to structure your unique identifier for this user and type of user. This could be an int, string or null.
A simple approach is the full class name with the user ID:
1use Illuminate\Contracts\Auth\Authenticatable;2 3public function key(Authenticatable $user): int|string|null4{5 return get_class($user).':'.$user->getAuthIdentifier();6}
For Teacher 10, the key would be App\Models\Teacher:10
. What I really like about this is how descriptive it is – there’s no misinterpretation: clearly the Teacher model, ID of 10.
find
The find
method accepts a key
, an int, string or null, and returns a simple object
with name
, extra
and avatar
properties.
This is where we need to find the user for the given key, and build that user’s profile.
Firstly, we need to know what to do with our key
– if we use the pattern from the key
section above, we can explode the string and end up with $class
and $id
variables.
Using the ID and the class, we can then find the user – you’ll notice we’re using this resolvedUsers
property – just smile and nod for the moment, as we’ll cover that more in the load
section.
Based on the $class
, we can return a profile object.
1use App\Models\Student; 2use App\Models\Teacher; 3use App\Models\User; 4 5public function find(int|string|null $key): object 6{ 7 [$class, $id] = explode(':', $key); 8 9 // get the user10 $user = $this->resolvedUsers[$class]->first(fn ($user) => $user->id == $id);11 12 // return the profile13 return match (get_class($user)) {14 Student::class => $this->getStudentProfile($user),15 Teacher::class => $this->getTeacherProfile($user),16 User::class => $this->getUserProfile($user)17 };18}
For each class type, we then can have a different profile function. This is the getUserProfile
function for my User
model:
1use App\Models\User; 2 3protected function getUserProfile(User $user): object 4{ 5 return (object) [ 6 'name' => $user->name, 7 'extra' => $user->email, 8 'avatar' => $user->profilePhotoUrl, 9 ];10}
You could just do a lookup for each user type as it is needed – but doing this means multiple database calls. 10 users, 10 queries. This is where the load
method can be used to eagerly load our required users, and sets the resolvedUsers
property we used above. 3 user types, totaling 20 users, 3 queries.
load
The load
method accepts a Collection
of keys
, and returns itself.
This is where we can eagerly load the users and store in the resolver for the find
method to use.
Because we know the key structure – class:id – we can explode this to a more complex collection, and use this to findMany
for each of our user types, and store in an internal resolvedUsers
collection.
For each user type, we can findMany
from a subset of our exploded keys – only where the class matches the user type. That’s the purpose of the keys collection filter, map and values within each lookup.
1use App\Models\Student; 2use App\Models\Teacher; 3use App\Models\User; 4use Illuminate\Support\Collection; 5 6public function load(Collection $keys): self 7{ 8 // index 0 is the class, index 1 is the id 9 $keys = $keys->map(fn ($key) => explode(':', $key));10 11 // resolve students, teachers and users12 $this->resolvedUsers = [13 Student::class => Student::findMany($keys14 ->filter(fn ($key) => $key[0] === Student::class)->map(fn ($key) => $key[1])->values()),15 Teacher::class => Teacher::findMany($keys16 ->filter(fn ($key) => $key[0] === Teacher::class)->map(fn ($key) => $key[1])->values()),17 User::class => User::findMany($keys18 ->filter(fn ($key) => $key[0] === User::class)->map(fn ($key) => $key[1])->values()),19 ];20 21 return $this;22}
By creating the array with the class as the key, we can then use the $class
in the load
method above to know which collection of users to find a given user from.
Putting it all together
This works great for my app – but your app is different. So while this is functioning code for me, you’ll need to make changes for it to work with your app. But here’s what it looks like when it is all put together:
1<?php 2 3namespace App\Pulse; 4 5use App\Models\Student; 6use App\Models\Teacher; 7use App\Models\User; 8use Illuminate\Contracts\Auth\Authenticatable; 9use Illuminate\Support\Collection;10use Laravel\Pulse\Contracts\ResolvesUsers;11 12class UserResolver implements ResolvesUsers13{14 protected array $resolvedUsers;15 16 public function load(Collection $keys): self17 {18 // index 0 is the class, index 1 is the id19 $keys = $keys->map(fn ($key) => explode(':', $key));20 21 // resolve students, teachers and users22 $this->resolvedUsers = [23 Student::class => Student::findMany($keys24 ->filter(fn ($key) => $key[0] === Student::class)->map(fn ($key) => $key[1])->values()),25 Teacher::class => Teacher::findMany($keys26 ->filter(fn ($key) => $key[0] === Teacher::class)->map(fn ($key) => $key[1])->values()),27 User::class => User::findMany($keys28 ->filter(fn ($key) => $key[0] === User::class)->map(fn ($key) => $key[1])->values()),29 ];30 31 return $this;32 }33 34 public function find(int|string|null $key): object35 {36 [$class, $id] = explode(':', $key);37 38 // get the user39 $user = $this->resolvedUsers[$class]->first(fn ($user) => $user->id == $id);40 41 // return the profile42 return match (get_class($user)) {43 Student::class => $this->getStudentProfile($user),44 Teacher::class => $this->getTeacherProfile($user),45 User::class => $this->getUserProfile($user)46 };47 }48 49 protected function getStudentProfile(Student $user): object50 {51 return (object) [52 'name' => $user->name_first,53 'extra' => $user->access_key ? $user->formatAccessKey() : 'Previewing Program',54 'avatar' => 'https://gravatar.com/avatar?d=mp',55 ];56 }57 58 protected function getTeacherProfile(Teacher $user): object59 {60 return (object) [61 'name' => $user->name.' (Teacher)',62 'extra' => $user->email ?? '',63 'avatar' => sprintf('https://gravatar.com/avatar/%s?d=mp', hash('sha256', trim(strtolower($user->email)))),64 ];65 }66 67 protected function getUserProfile(User $user): object68 {69 return (object) [70 'name' => $user->name,71 'extra' => $user->email,72 'avatar' => $user->profilePhotoUrl,73 ];74 }75 76 public function key(Authenticatable $user): int|string|null77 {78 return get_class($user).':'.$user->getAuthIdentifier();79 }80}
What I absolutely love about this is that in 81 lines of code you’re able to tell Pulse how to key, find and load the users of a multi-guarded app.
The Pulse and Laravel team have made it so easy to expand what and how Pulse does its thing: flexible and extensible. This forethought and developer experience is what makes me so happy to be in this ecosystem.