Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

oliverbusk's avatar

Storing notification settings per user

Hi everyone!

I am developing a fairly simple application, where I want my users to be able to subscribe to notifications. So the system should:

  • Send notifications when a certain event they are subscribed to happens.
  • Send notifications to the channels they have selected (email or slack)

Below is an example of different notifications they each user can subscribe to.

enter image description here

I am wondering how to do this using Laravel. My first idea was to:

  1. Create a notifications JSON column on the users table, and store it like (probably using the learnings from the Managing Mass User Settings lesson.)
{
  "todo": {
    "assigned": [
      {
        "email": true,
        "slack": true
      }
    ],
    "mentioned": [
      {
        "email": true,
        "slack": true
      }
    ]
  },
  "project": {
    "created": [
      {
        "email": true,
        "slack": true
      }
    ]
  }
}

However, I am unsure if this is good practice. Further, I am also a unsure on how to actually send out the notifications dynamically.

For sending it out, I want to use Laravels notification system:

Notification::send($user, new TodoCreated($todo));

I am not sure if this is the best way, or if it makes more sense to use an Event/Listener setup? A

Also, can I utilize the via() method on the Notification class to dynamically specify the channels according to the user settings?

Any input would be greatly appreciated.

0 likes
8 replies
martinbean's avatar

@oliverbusk You’ll need a pivot table between the available notification types, and the users in your system.

When a user opts in for a particular notification type, insert a row in the pivot table for that notification type ID and user ID. If a user opts out, remove the corresponding row.

When you have a notification to send, you’ll need to first check that the target user has opted in to receive that type of notification.

2 likes
oliverbusk's avatar

Thank you for replying @martinbean! So if I understand correctly, it will correspond to something like the below:

Pivot table (many-to-many) notification_user

user_id      | notification_type_id
1            | 1  
1            | 2

notification_types:

id | type            | channel
1  | todo_assigned   | slack
2  | todo_assigned   | email
3  | project_created | email 

In the above scenario, user.1 have the below notification enabled:

  • New to do has been assigned => notify via slack and e-mail
  • New project has been created => notify via e-mail

Is that correct? In the first case (both Slack & E-mail), how to check for this in the Notification class?

martinbean's avatar

@oliverbusk Yeah, that looks about right.

In the first case (both Slack & E-mail), how to check for this in the Notification class?

You’ll need to associate a notification class with a notification type in your database some how. Could just be a class constant or property. You can then pluck the channels (if there are any) for the notifiable (user) and notification type:

class TodoAssignedNotification extends Notification
{
	protected $notificationType = 'todo_assigned';

    public function via($notifiable)
    {
        return $notifiable
            ->notificationPreferences()
            ->whereHas('notificationType', function ($query) {
                $query->where('type', '=', $this->notificationType);
            })
            ->pluck('channel')
            ->toArray();
    }
}

This will get the notification preferences for the current user, for notification type 1 (todo_assigned), and pluck the channel values for any matches. If there are no matches (the user has not opted in for any todo_assigned notifications) then the array will be empty and the notification won’t be delivered.

If you’re going to create many of these notification classes, then it may be worth creating an abstract class with the via method so you’re not duplicating it in lots of classes:

abstract class OptInNotification extends Notification
{
    public function via($notifiable)
    {
        return $notifiable
            ->notificationPreferences()
            ->whereHas('notificationType', function ($query) {
                $query->where('type', '=', $this->notificationType());
            })
            ->pluck('channel')
            ->toArray();
    }

    abstract protected function notificationType();
}
class TodoAssignedNotification extends OptInNotification
{
    // No need for via method in this class

    protected function notificationType()
    {
        return 'todo_assigned';
    }
}
1 like
oliverbusk's avatar

I was thinking, does it have to be a "many-to-many" relationship? What about a one to many (a user can have many notification types):

notification_types:

id | user_id | type            | channel
1  | 1       | todo_assigned   | slack, email
3  | 1       | project_created | email 

The channel column is just a comma separated string that stores the different channels for the specific user and type?

Then simple in my user model:

//User.php
public function notificationTypes()
{
	return $this->hasMany(NotificationType::class);
}
Snapey's avatar

I have built a similar solution that uses events and a listener class that dispatches the appropriate notification type

The user has a relationship to an alerts table. This table holds the user_id, the event type, and a comma separated list of via

So, one record per user per event

Then the listener

<?php

namespace App\Listeners;

use App\Digest;
use App\Mail\AlertMail;
use App\Notifications\GeneralAlertNotification;
use App\User;
use Illuminate\Support\Str;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Mail;

class NotifyUsersListener
{

    /**
     * Handle the event.
     *
     * @param  object  $event
     * @return void
     */
    public function handle($event)
    {
        // send the event to all the users that are interested.
        // except for the user that created it
        $users = User::with(['alerts'=>function($query) use($event) {
            $query->where('event', class_basename($event));
        }])->whereHas('alerts', function($query) use ($event){
            $query->where('event', class_basename($event));
        })->where('id','!=',Auth::id())
        ->get();
        
        foreach($users as $user) {

            $vias = explode(',',$user->alerts->first()->via ?? '');

            foreach($vias as $via) {
                if(method_exists($this,$via)) {
                    $this->$via($event,$user);
                }
            }

        }
    }

    public function email($event,User $user)
    {
        Mail::to($user)->queue(new AlertMail($event));
    }

    public function digest($event,User $user)
    {
        Digest::create([
            'id' => Str::uuid(),
            'type' => class_basename($event),
            'notifiable_type' => User::class,
            'notifiable_id' => $user->id,
            'data' => [
                'described' => $event->described,
                'entityName' => $event->entityName,
                'entityId' => $event->entityId,
                'showRoute' => $event->showRoute,
            ],
        ]);
    }

    public function internal($event,User $user)
    {
        $user->notify(new GeneralAlertNotification($event));
    }
}

receives the event and gets all users that have an interest in that event by filtering the Alert records to those mentioning the base classname of the event.

Then for each user interested in that event, explode their vias and call the appropriate method.

Mail and database are as they say

Digest is a daily email (sent by cron) for the events that occurred that day.

The only thing I have not really worked out is registering the listeners. There must be a better way than this;

 protected $listen = [
        Registered::class => [
            SendEmailVerificationNotification::class,
        ],
        FoodbankAddedEvent::class => [
            NotifyUsersListener::class,
        ],
        FoodbankApprovedEvent::class => [
            NotifyUsersListener::class
        ],
        ShipmentCreatedEvent::class => [
            NotifyUsersListener::class
        ],
        ShipmentCancelledEvent::class => [
            NotifyUsersListener::class
        ],
        ShipmentReceivedEvent::class => [
            NotifyUsersListener::class
        ],
        AllocationCompleteEvent::class => [
            NotifyUsersListener::class
        ],

You see the same notification registered for every event (this is only half of the list)

3 likes
orest's avatar

@oliverbusk

I have a similar problem to solve and I also used JSON column instead of extra tables

So the notificationPreferences column would look like this

{
  "todo_assigned": [ "email", "slack" ],
  "todo_mentioned":  ["email", "slack" ],
}

So following the Jeffrey's video, in order to get the channels for the todo assigned event you would have to do something like

$user->notificationPreferences()->todo_assigned

which will return all the channels as an array

And in your notification class

class TodoAssigned extends Notification
{
      public function via($notifiable)
      {
             return $notifiable->notificationPreferences()->todo_assigned;
       }
}
2 likes
avrahamm's avatar

Hi @orest ! Which table did you add notificationPreferences JSON column? users or notifications? Thanks

Please or to participate in this conversation.