Laravel 5.3 - Single Notification for User Collection (followers) - php

When I have a single notifiable user, a single entry in the notifications table is inserted, along with a mail/sms sent which is perfectly working via channels.
The issue is when I have a user collection, a list of 1k users following me, and I post an update. Here is what happens when using the Notifiable trait as suggested for multi-user case:
1k mails/sms sent (issue is not here)
1k notification entries added to the DB's notifications table
It seems that adding 1k notifications to the DB's notifications table is not an optimal solution. Since the toArray data is the same, and everything else in the DB's notifications table is the same for 1k rows, with the only difference being the notifiable_id of the user notifiable_type.
An optimal solution out of the box would be:
Laravel would pick up the fact that it's an array notifiable_type
Save a single notification as notifiable_type user_array or user with notifiable_id 0 (zero would only be used to signify it's a multi notifiable user)
Create/Use another table notifications_read using the notification_id it just created as the foreign_key and insert 1k rows, of just these fields:
notification_id notifiable_id notifiable_type read_at
I am hoping there is already a way to do this as I am at this point in my application and would love to use the built in Notifications and channels for this situation, as I am firing off emails/sms notifications, which is fine to repeat 1k times I think, but it's the entry of the same data into the database that is the problem that needs to be optimized.
Any thoughts/ideas how to proceed in this situation?

Updated 2017-01-14: implemented more correct approach
Quick example:
use Illuminate\Support\Facades\Notification;
use App\Notifications\SomethingCoolHappen;
Route::get('/step1', function () {
// example - my followers
$followers = App\User::all();
// notify them
Notification::send($followers, new SomethingCoolHappen(['arg1' => 1, 'arg2' => 2]));
});
Route::get('/step2', function () {
// my follower
$user = App\User::find(10);
// check unread subnotifications
foreach ($user->unreadSubnotifications as $subnotification) {
var_dump($subnotification->notification->data);
$subnotification->markAsRead();
}
});
How to make it work?
Step 1 - migration - create table (subnotifications)
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateSubnotificationsTable extends Migration
{
/**
* Run the migrations.
*
* #return void
*/
public function up()
{
Schema::create('subnotifications', function (Blueprint $table) {
// primary key
$table->increments('id')->primary();
// notifications.id
$table->uuid('notification_id');
// notifiable_id and notifiable_type
$table->morphs('notifiable');
// follower - read_at
$table->timestamp('read_at')->nullable();
});
}
/**
* Reverse the migrations.
*
* #return void
*/
public function down()
{
Schema::dropIfExists('subnotifications');
}
}
Step 2 - let's create a model for new subnotifications table
<?php
// App\Notifications\Subnotification.php
namespace App\Notifications;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Notifications\DatabaseNotification;
use Illuminate\Notifications\DatabaseNotificationCollection;
class Subnotification extends Model
{
// we don't use created_at/updated_at
public $timestamps = false;
// nothing guarded - mass assigment allowed
protected $guarded = [];
// cast read_at as datetime
protected $casts = [
'read_at' => 'datetime',
];
// set up relation to the parent notification
public function notification()
{
return $this->belongsTo(DatabaseNotification::class);
}
/**
* Get the notifiable entity that the notification belongs to.
*/
public function notifiable()
{
return $this->morphTo();
}
/**
* Mark the subnotification as read.
*
* #return void
*/
public function markAsRead()
{
if (is_null($this->read_at)) {
$this->forceFill(['read_at' => $this->freshTimestamp()])->save();
}
}
}
Step 3 - create a custom database notification channel
Updated: using static variable $map to keep first notification id and insert next notifications (with the same data) without creating a record in notifications table
<?php
// App\Channels\SubnotificationsChannel.php
namespace App\Channels;
use Illuminate\Notifications\DatabaseNotification;
use Illuminate\Notifications\Notification;
class SubnotificationsChannel
{
/**
* Send the given notification.
*
* #param mixed $notifiable
* #param \Illuminate\Notifications\Notification $notification
*
* #return void
*/
public function send($notifiable, Notification $notification)
{
static $map = [];
$notificationId = $notification->id;
// get notification data
$data = $this->getData($notifiable, $notification);
// calculate hash
$hash = md5(json_encode($data));
// if hash is not in map - create parent notification record
if (!isset($map[$hash])) {
// create original notification record with empty notifiable_id
DatabaseNotification::create([
'id' => $notificationId,
'type' => get_class($notification),
'notifiable_id' => 0,
'notifiable_type' => get_class($notifiable),
'data' => $data,
'read_at' => null,
]);
$map[$hash] = $notificationId;
} else {
// otherwise use another/first notification id
$notificationId = $map[$hash];
}
// create subnotification
$notifiable->subnotifications()->create([
'notification_id' => $notificationId,
'read_at' => null
]);
}
/**
* Prepares data
*
* #param mixed $notifiable
* #param \Illuminate\Notifications\Notification $notification
*
* #return mixed
*/
public function getData($notifiable, Notification $notification)
{
return $notification->toArray($notifiable);
}
}
Step 4 - create a notification
Updated: now notification supports all channels, not only subnotifications
<?php
// App\Notifications\SomethingCoolHappen.php
namespace App\Notifications;
use App\Channels\SubnotificationsChannel;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
class SomethingCoolHappen extends Notification
{
use Queueable;
protected $data;
/**
* Create a new notification instance.
*
* #return void
*/
public function __construct($data)
{
$this->data = $data;
}
/**
* Get the notification's delivery channels.
*
* #param mixed $notifiable
* #return array
*/
public function via($notifiable)
{
/**
* THIS IS A GOOD PLACE FOR DETERMINING NECESSARY CHANNELS
*/
$via = [];
$via[] = SubnotificationsChannel::class;
//$via[] = 'mail';
return $via;
}
/**
* Get the mail representation of the notification.
*
* #param mixed $notifiable
* #return \Illuminate\Notifications\Messages\MailMessage
*/
public function toMail($notifiable)
{
return (new MailMessage)
->line('The introduction to the notification.')
->action('Notification Action', 'https://laravel.com')
->line('Thank you for using our application!');
}
/**
* Get the array representation of the notification.
*
* #param mixed $notifiable
* #return array
*/
public function toArray($notifiable)
{
return $this->data;
}
}
Step 5 - helper trait for "followers"
<?php
// App\Notifications\HasSubnotifications.php
namespace App\Notifications;
trait HasSubnotifications
{
/**
* Get the entity's notifications.
*/
public function Subnotifications()
{
return $this->morphMany(Subnotification::class, 'notifiable')
->orderBy('id', 'desc');
}
/**
* Get the entity's read notifications.
*/
public function readSubnotifications()
{
return $this->Subnotifications()
->whereNotNull('read_at');
}
/**
* Get the entity's unread notifications.
*/
public function unreadSubnotifications()
{
return $this->Subnotifications()
->whereNull('read_at');
}
}
Step 6 - update your Users model
Updated: no required followers method
namespace App;
use App\Notifications\HasSubnotifications;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable
{
use Notifiable;
/**
* Adding helpers to followers:
*
* $user->subnotifications - all subnotifications
* $user->unreadSubnotifications - all unread subnotifications
* $user->readSubnotifications - all read subnotifications
*/
use HasSubnotifications;
/**
* The attributes that are mass assignable.
*
* #var array
*/
protected $fillable = [
'name', 'email', 'password',
];
/**
* The attributes that should be hidden for arrays.
*
* #var array
*/
protected $hidden = [
'password', 'remember_token',
];
}

Yes you are right i guess with the default Notifiable trait, you could create a custom channel.
You can check the Illuminate\Notifications\Channels\DatabaseChannel class for default creation and adopt it to a pivot-table one.
Hope this helps to create a new channel with a pivot table. Also, implement a HasDatabasePivotNotifications trait (or similar name) to your own Notifiable trait.

Related

Laravel: Notifications table with custom column

I am using Laravel's default notifications system. However, I need an extra column in the notifications table to check if the user already has the same unread-notification or not.
For example: If user's profile is not complete then on every login he/she will be reminded until the profile is complete. But if the previously generated notification is still unread then the new notification will not be generated.
Table: notifications
default_fields
notify
...
...
profile_incomplete
...
...
password_change_overdue
...
Notifications class
class NotifyToCompleteProfileNotification extends Notification
{
public function __construct()
{
$this->notify = 'profile_incomplete';
}
/**
* Get the notification's delivery channels.
*
* #param mixed $notifiable
* #return array
*/
public function via($notifiable)
{
return ['database'];
}
/**
* Get the array representation of the notification.
*
* #param mixed $notifiable
* #return array
*/
public function toArray($notifiable)
{
return [
'data' => '...',
];
}
}
You can use custom notification channel to make this works. Let's assume you want to add a group field to notifications. First, add this field to table, then make file GroupedDbChannel.php:
namespace App\Notifications;
use Illuminate\Notifications\Notification;
class GroupedDbChannel
{
public function send($notifiable, Notification $notification)
{
$data = $notification->toDatabase($notifiable);
return $notifiable->routeNotificationFor('database')->create([
'id' => $notification->id,
'group' => $notification->group,
'type' => get_class($notification),
'data' => $data,
'read_at' => null,
]);
}
}
Next, you need to define custom group and channel for notification:
namespace App\Notifications;
use Illuminate\Notifications\Notification;
class TestNotification extends Notification {
/*
* Here you define additional value that will
* be used in custom notification channel.
*/
public string $group = 'incomplete-profile';
public function via($notifiable) {
return [GroupedDbChannel::class];
}
public function toArray($notifiable) {
return [
// notification data
];
}
/*
* It's important to define toDatabase method due
* it's used in notification channel. Of course,
* you can change it in GroupedDbChannel.
*/
public function toDatabase($notifiable): array
{
return $this->toArray($notifiable);
}
}
And it's done. Use notifications as standard.
$user->notify(new TestNotification());
Now, value incomplete-profile from $group field goes to notifications table to group column.

Laravel Slack Notification not sent when using implements ShouldQueue

I am struggling with dispatching a Slack notification when the Notification class implements ShouldQueue.
This is how I dispatch the notification
/**
* Handles the sendout of booking request confirmation to the customer
*
* #return void
*/
public function sendCustomerNotifications()
{
$this->booking->customer->notify((new CustomerBookingRequested($this->booking)));
}
This is how I my CustomerBookingRequested notification class looks like
class CustomerBookingRequested extends Notification implements ShouldQueue
{
use Queueable;
private $booking;
/**
* Create a new notification instance.
*
* #return void
*/
public function __construct(Booking $booking)
{
//
$this->booking = $booking;
}
/**
* Get the notification's delivery channels.
*
* #param mixed $notifiable
* #return array
*/
public function via($notifiable)
{
return ['mail','slack'];
}
...
//code for toMail
...
/**
* Get the Slack representation of the notification.
*
* #param mixed $notifiable
* #return \Illuminate\Notifications\Message\SlackMessage
*/
public function toSlack($notifiable)
{
return (new SlackMessage)
->success()
->content('New booking requested!');
}
My Customer Model uses Notifiable
class Customer extends Model implements HasLocalePreference
{
use HasFactory;
use Billable;
use SoftDeletes;
use Notifiable;
...
I also added to my Customer Model the routing method
/**
* Route notifications for the Slack channel.
*
* #param \Illuminate\Notifications\Notification $notification
* #return string
*/
public function routeNotificationForSlack($notification)
{
return env('SLACK_WEBHOOK');
}
When I remove implements ShouldQueue from my Notification class, both Slack and Mail Message is sent. When I keep implements ShouldQueue, the Mail message is sent, Slack message is not sent.
I basically want to send the customer a mail notification with a booking confirmation. At the same time I want to send a Slack message to the team's slack workspace. That's why I just added a static webhook URL in the customer model which is linked to the company's slack workspace.
I am a bit stuck here. Probably it's something obvious, but I can't find what I do wrong.
Thanks for your support!
Using Laravel 8.0 with "laravel/slack-notification-channel": "^2.3"
Move the SLACK_WEBHOOK env variable to a config.
File: config/sample.php
return [
'url' => env('SLACK_WEBHOOK')
];
Then reference the config from the Customer model.
File: app/Models/Customer.php
public function routeNotificationForSlack($notification) {
return config('sample.url');
}

No query results for model [App\Models\Match]

I'm building an API with Laravel and want to send push notification using the Laravel Notifications system. I've a model for matches (which is basically a post), another user can like this match. When the match is liked, the creator of the post will get a push notification. It's just like Instagram, Facebook, etc.
Often the push notification wasn't send to the user. I installed Laravel Horizon to see if there where errors. Sometimes the notification was send and sometimes it wasn't. With the exact same data:
The notification fails sometimes with the exact same data (same user, same match).
The error is as followed:
Illuminate\Database\Eloquent\ModelNotFoundException: No query results
for model [App\Models\Match] 118 in
/home/forge/owowgolf.com/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Builder.php:312
I'm sure the match and the user exists in the database, I've verified that before sending the notification. Does anybody know what's going wrong? Everything I could find online is that people didn't save their model before sending the notification into the queue. But the line where the code send's the notification into the queue wouldn't even be reached if the model didn't exists. Because of Implicit Binding in the route/controller.
Controller method:
/**
* Like a match.
*
* #param \App\Models\Match $match
* #return \Illuminate\Http\JsonResponse
*/
public function show(Match $match)
{
$match->like();
$players = $match->players()->where('user_id', '!=', currentUser()->id)->get();
foreach ($players as $user) {
$user->notify(new NewLikeOnPost($match, currentUser()));
}
return ok();
}
Notification:
<?php
namespace App\Notifications;
use App\Models\Match;
use App\Models\User;
use Illuminate\Bus\Queueable;
use NotificationChannels\Apn\ApnChannel;
use NotificationChannels\Apn\ApnMessage;
use Illuminate\Notifications\Notification;
use Illuminate\Contracts\Queue\ShouldQueue;
class NewLikeOnPost extends Notification implements ShouldQueue
{
use Queueable;
/**
* The match instance.
*
* #var \App\Models\Match
*/
private $match;
/**
* The user instance.
*
* #var \App\Models\User
*/
private $user;
/**
* Create a new notification instance.
*
* #param \App\Models\Match $match
* #param \App\Models\User $user
*/
public function __construct(Match $match, User $user)
{
$this->user = $user;
$this->match = $match;
$this->onQueue('high');
}
/**
* Get the notification's delivery channels.
*
* #param \App\Models\User $notifiable
* #return array
*/
public function via($notifiable)
{
if ($notifiable->wantsPushNotification($this)) {
return ['database', ApnChannel::class];
}
return ['database'];
}
/**
* Get the mail representation of the notification.
*
* #param \App\Models\User $notifiable
* #return \NotificationChannels\Apn\ApnMessage
*/
public function toApn($notifiable)
{
return ApnMessage::create()
->badge($notifiable->unreadNotifications()->count())
->sound('success')
->body($this->user->username . ' flagged your match.');
}
/**
* Get the array representation of the notification.
*
* #param mixed $notifiable
* #return array
*/
public function toArray($notifiable)
{
return [
'user_id' => $this->user->id,
'body' => "<flag>Flagged</flag> your match.",
'link' => route('matches.show', $this->match),
'match_id' => $this->match->id,
];
}
/**
* Get the match attribute.
*
* #return \App\Models\Match
*/
public function getMatch()
{
return $this->match;
}
}
This is not a complete solution, but it will lower your chances of running into this error in the future.
Instead of passing in the whole Match model into the job, only pass the id of the model. You can then fetch that model in the constructor.
/**
* Like a match.
*
* #param \App\Models\Match $match
* #return \Illuminate\Http\JsonResponse
*/
public function show(Match $match)
{
$match->like();
$players = $match->players()->where('user_id', '!=', currentUser()->id)->get();
foreach ($players as $user) {
$user->notify(new NewLikeOnPost($match->id, currentUser()->id));
}
return ok();
}
Notification:
class NewLikeOnPost extends Notification implements ShouldQueue
{
use Queueable;
private const QUEUE_NAME = 'high';
/**
* The match instance.
*
* #var \App\Models\Match
*/
private $match;
/**
* The user instance.
*
* #var \App\Models\User
*/
private $user;
/**
* Create a new notification instance.
*
* #param int $match
* #param int $user
*/
public function __construct(int $matchId, int $userId)
{
$this->user = User::query()->where('id', $userId)->firstOrFail();
$this->match = Match::query()->where('id', $matchId)->firstOrFail();
$this->onQueue(self::QUEUE_NAME);
}
// Rest of the class is still the same...
}
You can use the SerializesModels trait, but it doesn't work well when you add a delay to a queued job. This is because it will try to reload the model on __wakeup() and sometimes it cannot find the class.
Hopefully this helps :)
Its probably because $user is not an object of User model, its an object of Match model. You need to do a User::findorfail or User::firstOrFail then notify the user.
public function show(Match $match)
{
$match->like();
$players = $match->players()->where('user_id', '!=', currentUser()->id)->get();
foreach ($players as $user) {
$someUser = User::findOrFail($user->user_id);
$someUser->notify(new NewLikeOnPost($match, currentUser()));
}
return ok();
}
Unless the notify trait is used in Match model. Or you could use eager loading which will cost way less queries!
Check your .env to be sure that u really use REDIS
BROADCAST_DRIVER=redis
CACHE_DRIVER=redis
SESSION_DRIVER=redis
SESSION_LIFETIME=120
QUEUE_DRIVER=redis
then clear cache ( php artisan cache:clear , php artisan view:clear ), that should clear the issue
EDIT
I had similar problems but now I use Docker only and before I had to check for cached configfiles, wrong file/folderpermissions and so on (REDIS for broadcast only, others were standard). I started using redis only - that`s a lot easier, faster and more debugfriendly for me ! And together with Docker really helpful to not use messed up nginx/apache/php/redis/ ...

laravel 5.3 database notification customization

I am creating laravel 5.3 database notifications.I have created notifications as per video published on https://laracasts.com/series/whats-new-in-laravel-5-3/episodes/10 ,
Now i want to add custom fields to the notification table as per my requirements.
Please help me how to pass custom data to notification and access it.
When I needed to put custom fields to Notification, I'd just put on data field, as it is a Json field, works perfectly. Like this:
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
class TaskNotification extends Notification
{
use Queueable;
private $message;
/**
* #param String $message
*/
public function __construct($message=false)
{
if ($message)
$this->message = $message;
}
/**
* Get the notification's delivery channels.
*
* #param mixed $notifiable
* #return array
*/
public function via($notifiable)
{
return ['database'];
}
/**
* Get the array representation of the notification.
*
* #param mixed $notifiable
* #return array
*/
public function toArray($notifiable)
{
return [
'message' => $this->message,
'link' => route('mymodel.show'),
'task'=> 1, // This is one variable which I've created
'done'=> 0 // This is one variable which I've created
];
}
}

How to subscribe user with laravel notification channel for OneSignal

I am trying to use the laravel-notifications-channel/onesignal and I am having some problems with users in my laravel app set up to receive notifications. The documentation on the github page does not really cover how a user authenticates them self to receive a notification.
Even reading over the OneSignal docs for sending users to OneSignal is not working for me.
How do I set it up where when a user is using our web app they are notified to receive notifications and then I can send notifications to them using laravel notifications?
Here is my AssignedToTask Notification file:
<?php
namespace App\Notifications;
use App\Task;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use NotificationChannels\OneSignal\OneSignalChannel;
use NotificationChannels\OneSignal\OneSignalMessage;
use NotificationChannels\OneSignal\OneSignalWebButton;
class AssignedToTask extends Notification
{
use Queueable;
protected $task;
/**
* Create a new notification instance.
*
* #return void
*/
public function __construct(Task $task)
{
//
$this->task = $task;
}
/**
* Get the notification's delivery channels.
*
* #param mixed $notifiable
* #return array
*/
public function via($notifiable)
{
return ['mail', OneSignalChannel::class];
}
public function toOneSignal($notifiable)
{
return OneSignalMessage::create()
->subject("Your {$notifiable->service} account was approved!")
->body("Click here to see details.")
->url('http://onesignal.com')
->webButton(
OneSignalWebButton::create('link-1')
->text('Click here')
->icon('https://upload.wikimedia.org/wikipedia/commons/4/4f/Laravel_logo.png')
->url('http://laravel.com')
);
}
/**
* Get the mail representation of the notification.
*
* #param mixed $notifiable
* #return \Illuminate\Notifications\Messages\MailMessage
*/
public function toMail($notifiable)
{
return (new MailMessage)
->subject('You have been assigned a new task')
->line('You have a new task: ' . $this->task->title)
->action('View Task', url('tasks/' . $this->task->id));
}
/**
* Get the array representation of the notification.
*
* #param mixed $notifiable
* #return array
*/
public function toArray($notifiable)
{
return [
//
];
}
}
In my user model:
<?php
namespace App;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Zizaco\Entrust\Traits\EntrustUserTrait;
use HipsterJazzbo\Landlord\BelongsToTenants;
use Cmgmyr\Messenger\Traits\Messagable;
class User extends Authenticatable
{
use Notifiable;
use EntrustUserTrait;
use BelongsToTenants;
use Messagable;
/**
* The attributes that are mass assignable.
*
* #var array
*/
protected $fillable = [
'name', 'email', 'password', 'company_id'
];
/**
* The attributes that should be hidden for arrays.
*
* #var array
*/
protected $hidden = [
'password', 'remember_token', 'company_id'
];
....
public function routeNotificationForOneSignal()
{
return 'ONE_SIGNAL_PLAYER_ID';
}
public function routeNotificationForMail()
{
return $this->email_address;
}
}
How do I set and get the ONE_SIGNAL_PLAYER_ID in user model so a user accepts notifications and I can send them notifications?
EDIT - 2
Since you don't know what's happening is, Let me try to explain how you can work with OneSignal.
This is a push messaging system just like any other push notification system. (FCM (google), PubNub).
How it Works
First goto OneSignal.Com Create your account, and then create an app for you. Once you create an app it will give you SDK for Mobile, where your consumers are.
Now whenever your consumers install and start your app, they will notify your web server with their own unique id and user information.
The information you received about the user is unique player_id which you will store in your database against that user.
Now when you want to send a notification to any mobile app just call the API Post Notification method with player_id and OneSignal will send a push notification to that mobile app.
EDIT - 1
I think now i understand your confusion about Notifications with OneSignalChannel
Flow
You already have the players_ids stored in your app database against every user.
Now when you want to push a notification to a player, you just take that users player_id from db and push a notification to OneSignal.
Well you took this meaning literally. Which was causing issue
public function routeNotificationForOneSignal()
{
return 'ONE_SIGNAL_PLAYER_ID';
}
From the error message this function should have return a unique id (UUID).
Change the return value to actual player id at OneSignalChannel
That's all my friend.

Categories