Setting up Laravel Pulse for multiple Authenticatable models (user types)

Published January 18th, 2024

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 too

  • find: returns an array of details for a single user, with name, extra and avatar properties

  • load: 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 ResolvesUsers
10{
11 protected array $resolvedUsers;
12 
13 public function load(Collection $keys): self
14 {
15 // ...
16 }
17 
18 public function find(int|string|null $key): object
19 {
20 // ...
21 }
22 
23 public function key(Authenticatable $user): int|string|null
24 {
25 // ...
26 }
27}
Copied!

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);
Copied!

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|null
4{
5 return get_class($user).':'.$user->getAuthIdentifier();
6}
Copied!

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 user
10 $user = $this->resolvedUsers[$class]->first(fn ($user) => $user->id == $id);
11 
12 // return the profile
13 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}
Copied!

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}
Copied!

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 users
12 $this->resolvedUsers = [
13 Student::class => Student::findMany($keys
14 ->filter(fn ($key) => $key[0] === Student::class)->map(fn ($key) => $key[1])->values()),
15 Teacher::class => Teacher::findMany($keys
16 ->filter(fn ($key) => $key[0] === Teacher::class)->map(fn ($key) => $key[1])->values()),
17 User::class => User::findMany($keys
18 ->filter(fn ($key) => $key[0] === User::class)->map(fn ($key) => $key[1])->values()),
19 ];
20 
21 return $this;
22}
Copied!

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 ResolvesUsers
13{
14 protected array $resolvedUsers;
15 
16 public function load(Collection $keys): self
17 {
18 // index 0 is the class, index 1 is the id
19 $keys = $keys->map(fn ($key) => explode(':', $key));
20 
21 // resolve students, teachers and users
22 $this->resolvedUsers = [
23 Student::class => Student::findMany($keys
24 ->filter(fn ($key) => $key[0] === Student::class)->map(fn ($key) => $key[1])->values()),
25 Teacher::class => Teacher::findMany($keys
26 ->filter(fn ($key) => $key[0] === Teacher::class)->map(fn ($key) => $key[1])->values()),
27 User::class => User::findMany($keys
28 ->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): object
35 {
36 [$class, $id] = explode(':', $key);
37 
38 // get the user
39 $user = $this->resolvedUsers[$class]->first(fn ($user) => $user->id == $id);
40 
41 // return the profile
42 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): object
50 {
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): object
59 {
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): object
68 {
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|null
77 {
78 return get_class($user).':'.$user->getAuthIdentifier();
79 }
80}
Copied!

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.

 

You may be interested in...