Extra columns with Laravel database notifications

As you may already know, Laravel (since 5.3) ships with a great out-the-box notifications support, it has native support for many "channels", also, extra channels like Telegram can be obtained from http://laravel-notification-channels.com, it's worth a look.

But in this post we're most concerned with the database driver..

The database driver will essentially dump useful info about your notification to a table for future reference, this info is of course customizable. Well, at least to a certain degree.

Laravel will save the following:

  • type: this is your FQN (Fully Qualified Name) class name of the notification you just sent.
  • notifiable id and type: this is your target model and it's id,
  • data: by default Laravel will only let you customize the data field content, on the notification class you'll use the toDatabase() or toArray() methods to return an array representation of the notification, which will then be json encoded and saved in the data column.

Note: the toArray() method is used for the database and the broadcast channels, the broadcast is the channel used to send the notification to your JavaScript client, If you don't want the same data structure to be sent to both channels, implementing a toDatabase() method will use that specifically for the database channel.

In Audiogram, however, we have a feature-set called "Mersal" (translates to Messenger or Postman). In Mersal, we can design how the notification will look like, will it open a modal or not, will it navigate to somewhere in the app or not and where, and more importantly, who are the target audience, great stuff. But the other day we wanted to build some analytical tools to help us know how our notification campaigns are doing, we had an idea of a notification batch that will hold all of the notification data along with how many people opened it and what's the conversion rate for this batch.

Since we're using the native Laravel Notification services, we needed to link every notification record from the notifications table to the notification batch it was sent in. The easiest way that popped into mind is to just add a "batch_id" column to the notifications table and be done with it, right? well, kind of...

Turns out (as far as I know), that Laravel doesn't support this out of the box, the array you return from the toDatabase() method will be json encoded and saved into the data column. One workaround is creating a new channel class, extend Laravel's class and override the send method, then inject the new class in the container instead of Laravel's native one, now let's see how to do that.

First, let's create the new class:

<?php
 
namespace App\Channels;
 
use Illuminate\Notifications\Notification;
use Illuminate\Notifications\Channels\DatabaseChannel as IlluminateDatabaseChannel;
 
class DatabaseChannel extends IlluminateDatabaseChannel
{
/**
* Send the given notification.
*
* @param mixed $notifiable
* @param \Illuminate\Notifications\Notification $notification
* @return \Illuminate\Database\Eloquent\Model
*/
public function send($notifiable, Notification $notification)
{
return $notifiable->routeNotificationFor('database')->create([
'id' => $notification->id,
'type' => get_class($notification),
'batch_id'=> $notification->batch_id ?? null,
'data' => $this->getData($notifiable, $notification),
'read_at' => null,
]);
}
}

notice the added "batch_id" to the columns to be saved, setting the batch_id is as easy as adding a property to the specific notification class you are gonna send.
now we need to inject this class instead of the native one, we can do so in the boot method of any of our service providers, I just did so in the App\Providers\AppServiceProvider, you could however create a service provider just for that, but I think it's fine for now.

use App\Channels\DatabaseChannel;
use Illuminate\Notifications\Channels\DatabaseChannel as IlluminateDatabaseChannel;
 
.
.
.
 
/**
* Bootstrap any application services.
*/
public function boot()
{
$this->app->instance(IlluminateDatabaseChannel::class, new DatabaseChannel);
 
}

One last thing is left, add the actual column to the database 🙄

$ php artisan make:migration add_batch_id_column_to_notifications_table --table=notifications

Now let's edit the migration file (notification_batches table is assumed to exist because it's details is irrelevant here)

/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('notifications', function (Blueprint $table) {
$table->integer('batch_id')->unsigned()->after('type')->nullable();
 
$table->foreign('batch_id')
->references('id')->on('notification_batches');
});
}
 
/**
* Reverse the migrations, if you're into this 🤷🏼‍♂️
*
* @return void
*/
public function down()
{
Schema::table('notifications', function (Blueprint $table) {
$table->dropForeign(['batch_id']);
$table->dropDolumn('batch_id');
});
}

and now migrate.

$ php artisan migrate

That's all there is to it, all you need to do is to set the batch_id on the notification object and it will be saved to the table, of course this is a demo of the technique, our use-case is probably different from yours, forge it your needs ;)

Hope this was of help to you, and as always feel free to hit me at <[email protected]> if you wanna say Hi or you're facing issues.