IMHO, the current Database channel for saving notifications in Laravel is really bad design:
You can't use foreign key cascades on items for cleaning up notifications of a deleted item for example
Searching custom attributes in the data column (casted to Array) is not optimal
How would you go about extending the DatabaseNotification Model in vendor package?
I would like to add columns event_id, question_id, user_id (the user that created the notification) etc... to the default laravel notifications table
How do you override the send function to include more columns?
In:
vendor/laravel/framework/src/Illuminate/Notifications/Channels/DatabaseChannel.php
The code:
class DatabaseChannel
{
/**
* 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),
\\I want to add these
'user_id' => \Auth::user()->id,
'event_id' => $notification->type =='event' ? $notification->id : null,
'question_id' => $notification->type =='question' ? $notification->id : null,
\\End adding new columns
'data' => $this->getData($notifiable, $notification),
'read_at' => null,
]);
}
}
To create a custom Notification Channel:
First, create a Class in App\Notifications for example:
<?php
namespace App\Notifications;
use Illuminate\Notifications\Notification;
class CustomDbChannel
{
public function send($notifiable, Notification $notification)
{
$data = $notification->toDatabase($notifiable);
return $notifiable->routeNotificationFor('database')->create([
'id' => $notification->id,
//customize here
'answer_id' => $data['answer_id'], //<-- comes from toDatabase() Method below
'user_id'=> \Auth::user()->id,
'type' => get_class($notification),
'data' => $data,
'read_at' => null,
]);
}
}
Second, use this channel in the via method in the Notification class:
<?php
namespace App\Notifications;
use Illuminate\Notifications\Notification;
use App\Notifications\CustomDbChannel;
class NewAnswerPosted extends Notification
{
private $answer;
public function __construct($answer)
{
$this->answer = $answer;
}
public function via($notifiable)
{
return [CustomDbChannel::class]; //<-- important custom Channel defined here
}
public function toDatabase($notifiable)
{
return [
'type' => 'some data',
'title' => 'other data',
'url' => 'other data',
'answer_id' => $this->answer->id //<-- send the id here
];
}
}
Create and use your own Notification model and Notifiable trait and then use your own Notifiable trait in your (User) models.
App\Notifiable.php:
namespace App;
use Illuminate\Notifications\Notifiable as BaseNotifiable;
trait Notifiable
{
use BaseNotifiable;
/**
* Get the entity's notifications.
*/
public function notifications()
{
return $this->morphMany(Notification::class, 'notifiable')
->orderBy('created_at', 'desc');
}
}
App\Notification.php:
namespace App;
use Illuminate\Notifications\DatabaseNotification;
class Notification extends DatabaseNotification
{
// ...
}
App\User.php:
namespace App;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable
{
use Notifiable;
// ...
}
An example for #cweiske response.
If you really need extends the Illuminate\Notifications\Channels\DatabaseChannel not creating a new Channel you can:
Extends the channel:
<?php
namespace App\Notifications;
use Illuminate\Notifications\Channels\DatabaseChannel as BaseDatabaseChannel;
use Illuminate\Notifications\Notification;
class MyDatabaseChannel extends BaseDatabaseChannel
{
/**
* Send the given notification.
*
* #param mixed $notifiable
* #param \Illuminate\Notifications\Notification $notification
* #return \Illuminate\Database\Eloquent\Model
*/
public function send($notifiable, Notification $notification)
{
$adminNotificationId = null;
if (method_exists($notification, 'getAdminNotificationId')) {
$adminNotificationId = $notification->getAdminNotificationId();
}
return $notifiable->routeNotificationFor('database')->create([
'id' => $notification->id,
'type' => get_class($notification),
'data' => $this->getData($notifiable, $notification),
// ** New custom field **
'admin_notification_id' => $adminNotificationId,
'read_at' => null,
]);
}
}
And register the Illuminate\Notifications\Channels\DatabaseChannel on application container again:
app\Providers\AppServiceProvider.php
class AppServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* #return void
*/
public function boot()
{
//
}
/**
* Register any application services.
*
* #return void
*/
public function register()
{
$this->app->bind(
Illuminate\Notifications\Channels\DatabaseChannel::class,
App\Notifications\MyDatabaseChannel::class
);
}
}
Now when the Illuminate\Notifications\ChannelManager try createDatabaseDriver will return your registered database driver.
More one option to solve this problem!
Unlike "Bassem El Hachem", I wanted to keep the database keyword in the via() methods.
So in addition to a custom DatabaseChannel, I also wrote my own ChannelManager that returns my own DatabaseChannel in the createDatabaseDriver() method.
In my apps' ServiceProvider::register() method, I overwrote the singleton for the original ChannelManager class to return my custom manager.
I solved a similar problem by customizing notification class:
create the class for this action:
artisan make:notification NewQuestion
inside it:
public function __construct($user,$question)
{
$this->user=$user;
$this->question=$question;
}
...
public function toDatabase($notifiable){
$data=[
'question'=>$this->(array)$this->question->getAttributes(),
'user'=>$this->(array)$this->user->getAttributes()
];
return $data;
}
then you can access proper data in view or controller like this:
#if($notification->type=='App\Notifications\UserRegistered')
New question from {{$notification->data['user']['name']}}
#endif
Related
I have some notifications (that get sent out to SendGrid) and I am using table queues.
I build my notification as below
$data = [
'email_message' => $message,
'subject' => $request->subject,
'email' => $request->email
];
$user->notify(new EmailGeneralNotification($data));
My Notification looks like this;
<?php
namespace App\Notifications;
use Illuminate\Notifications\Notification;
use NotificationChannels\SendGrid\SendGridChannel;
use NotificationChannels\SendGrid\SendGridMessage;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
// use Illuminate\Queue\SerializesModels;
use App\Models\NotificationTemplate;
class EmailGeneralNotification extends Notification implements ShouldQueue
{
use Queueable;
// use SerializesModels;
protected $data;
/**
* Create a new notification instance.
*
* #return void
*/
public function __construct(array $data) {
$this->data = $data;
}
public function via($notifiable)
{
return [
SendGridChannel::class,
];
}
public function toSendGrid($notifiable)
{
$reference = 'internal_general_email';
$template = NotificationTemplate::where('reference',$reference)->latest('updated_at')->first();
if(!$template)
{
dd("No template was found for " . $reference);
}
return (new SendGridMessage($template->template_id))
/**
* optionally set the from address.
* by default this comes from config/mail.from
* ->from('no-reply#test.com', 'App name')
*/
/**
* optionally set the recipient.
* by default it's $notifiable->email:
* ->to('hello#example.com', 'Mr. Smith')
*/
->payload([
"subject" => (array_key_exists('subject', $this->data)) ? $this->data['subject'] : '',
"email_message" => (array_key_exists('email_message', $this->data)) ? $this->data['email_message'] : '',
]);
}
}
This gets sent via a SendGrid notification type.
The funny this is that when I remove the implements ShouldQueue and use Queueable and run my function everything works fine.
When I enable the queue I get the below;
[2022-10-30 08:34:08] local.ERROR: Declaration of Ramsey\Uuid\Uuid::unserialize(string $data): void must be compatible with Serializable::unserialize($serialized) {"exception":"[object] (Symfony\\Component\\ErrorHandler\\Error\\FatalError(code: 0): Declaration of Ramsey\\Uuid\\Uuid::unserialize(string $data): void must be compatible with Serializable::unserialize($serialized) at /var/www/html/vendor/ramsey/uuid/src/Uuid.php:307)
[stacktrace]
#0 {main}
"}
I have updated php to v8 and got Laravel to 8.83.25 but still no joy
I'm having issue fixing the queue part for Laravel.
I have a contact form which works perfectly. I submit the form and a mail is being sent to me with the input details from the form.
But the issue is that if you do not use the Laravel Queues then the UX is lacking because of the waiting time before the mail is being sent and after getting the success message.
So now I want to implement the queuing part, but im not doing it right. Im still a noob at it...
Here is my controller
KontaktformController.php
<?php
namespace App\Http\Controllers;
use App\Jobs\SendEmailJob;
use Carbon\Carbon;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Session;
use App\Kontaktform;
use Illuminate\Http\Request;
use App\Mail\SendEmailMailable;
class KontaktformController extends Controller
{
public function create()
{
return view('kontakt');
}
public function store(Request $request) {
$this->validate($request, [
'name' => 'required|string',
'mobile' => 'required',
'email' => 'required|email',
'subject' => 'required',
'message' => 'required',
]);
/*
Add mail functionality here.*/
$kontakt = new Kontaktform([
'navn' => $request['name'],
'mobilnr' => $request['mobile'],
'fastnetnr' => $request['landline'],
'mail' => $request['email'],
'emne' => $request['subject'],
'beskrivelse' => $request['message']
]);
$kontakt->save();
//$user['email'] = 'test#test.com';
$job = (new SendEmailJob($request['email']))
->delay(Carbon::now()->addSeconds(3));
dispatch($job);
}
}
SendEmailJob.php
<?php
namespace App\Jobs;
use App\Mail\SendEmailMailable;
use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Support\Facades\Mail;
class SendEmailJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $user;
/**
* Create a new job instance.
*
* #return void
*/
public function __construct($user)
{
$this->user = $user;
}
/**
* Execute the job.
*
* #return void
*/
public function handle()
{
$email = new SendEmailMailable('test#test.dk');
Mail::to('admin#admin.dk')->send($email);
}
}
SendEmailMailable.php
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Contracts\Queue\ShouldQueue;
class SendEmailMailable extends Mailable
{
use Queueable, SerializesModels;
public $request;
/**
* Create a new message instance.
*
* #return void
*/
public function __construct($request)
{
$this->request = $request;
}
/**
* Build the message.
*
* #return $this
*/
public function build()
{
return $this->from('test#test.dk')->view('mail.kontaktform');
}
}
kontaktform.blad.php
{{$request['email']}}
In your kontaktform.blad.php (which should be named kontaktform.blade.php btw), you expect the data to be an array because you try to fetch the index ['email'] though in your SendEmailJob.php in the method handle() you pass your email as a string.
So what you should change it to the following:
$email = new SendEmailMailable(['email' => 'test#test.dk']);
or do the following in the controller & in the job:
Controller:
$job = (new SendEmailJob($request))
->delay(Carbon::now()->addSeconds(3));
Job:
protected $request;
/**
* Create a new job instance.
*
* #return void
*/
public function __construct($request)
{
$this->request = $request;
}
/**
* Execute the job.
*
* #return void
*/
public function handle()
{
$mailable = new SendEmailMailable($this->request);
Mail::to('admin#admin.dk')->send($mailable);
}
Now all data from the request is should be available in your blade template.
I found the issue. I didnt pass data from mailable to my blade
public function build()
{
return $this->view( 'mail.kontaktform' )->with( [
'request' => $this->request,
] );
}
I'm trying to return a relationship using Laravels API resource classes. Each order has many order totals.
My orders model
namespace App;
use Illuminate\Database\Eloquent\Model;
class Orders extends Model
{
protected $table = 'zen_orders';
public function ordersTotals() {
return $this->hasMany(OrdersTotalsModel::class, 'orders_id');
}
}
My orders resource
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\Resource;
class OrdersResource extends Resource
{
/**
* Transform the resource into an array.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
public function toArray($request)
{
return [
'id' => $this->orders_id,
'customername' => $this->customers_name,
'paymentmethod' => $this->payment_method,
'datePurchased' => $this->date_purchased,
'status' => $this->orders_status,
'currency' => $this->currency,
'orderTotal' => $this->order_total,
'ordersTotals' => new OrdersTotalsResource($this->id),
];
}
}
My orders total model
namespace App;
use Illuminate\Database\Eloquent\Model;
class OrdersTotalsModel extends Model
{
protected $table = 'zen_orders_total';
protected $primaryKey = 'orders_total_id';
public function ordersTotals() {
return $this->belongsTo(Orders::class, 'orders_id');
}
}
My orders total resource
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\ResourceCollection;
class OrdersTotalsResource extends ResourceCollection
{
/**
* Transform the resource into an array.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
public function toArray($request)
{
return [
'id' => $this->id,
'type' => $this->title,
'amount' => $this->value,
];
}
}
I'm currently getting this error with the code provided:
Symfony\Component\Debug\Exception\FatalThrowableError: Call to a member function toBase() on null in file
Previous to this error I would just get this:
{
"id": 389331,
"customername": "John Smith",
"paymentmethod": "Credit/Debit Card",
"datePurchased": "2017-01-01 00:11:28",
"status": 3,
"currency": "GBP",
"orderTotal": "36.99",
"ordersTotals": []
},
I've followed the documentation as best I can and tried all the possible methods demonstrated there, I believe the orders total model isn't being called as any changes I make to the model and resource don't make a difference.
My approach was wrong, for future visitors I was trying to retrieve records from a relationship using a resource.
On the Orders Resource I added:
'ordersTotals' => OrdersTotalsResource::collection(
OrdersTotalsModel::where('orders_id', '=' , $this->orders_id)
->orderBy('sort_order')
->get()
),
I was querying the model wrong, and had my primary keys mixed up.
I used this debug method to help diagnose where I was going wrong.
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Log;
class AppServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* #return void
*/
public function boot()
{
//
}
/**
* Register any application services.
*
* #return void
*/
public function register()
{
if (env('APP_DEBUG') == true) {
Event::listen('Illuminate\Database\Events\QueryExecuted', function ($query) {
Log::debug($query->sql . ' - ' . serialize($query->bindings));
});
}
}
}
This would then log the query into the /storage/logs/laravel.log file
I am using hashid to hash the id parameters in url. I have it set up in my model to automatically hash the id. This is working fine. My problem is decoding the hash in a middleware returns null. I'm not sure if this is a problem with my middleware or because of the hashing.
Model:
public function getIdAttribute($value)
{
$hashids = new \Hashids\Hashids(env('APP_KEY'),10);
return $hashids->encode($value);
}
Middleware:
<?php
namespace App\Http\Middleware;
use Closure;
class HashIdsDecode
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle($request, Closure $next)
{
dd($request->id); //Returns null on show method - example localhost:8000/invoices/afewRfshTl
if($request->has('id'))
{
$hashids = new \Hashids\Hashids(env('APP_KEY'),10);
dd($hashids->decode($request->input('id')));
}
return $next($request);
}
}
Route:
Route::resource('invoices','InvoiceController');
Controller:
public function show($id)
{
$invoice = Invoice::find($id);
return view('invoices.show', [
'invoice' => $invoice,
'page_title' => ' Invoices',
'page_description' => 'View Invoice',
]);
}
NOTE: if I bypass the middleware and do it directly in my controller like this it works but it requires me to repeat myself over and over and probably not the best way to do it.
public function show($id)
{
$hashids = new \Hashids\Hashids(env('APP_KEY'),10);
$invoiceId = $hashids->decode($id)[0];
$invoice = Invoice::find($invoiceId);
return view('invoices.show', [
'invoice' => $invoice,
'page_title' => ' Invoices',
'page_description' => 'View Invoice',
]);
}
Personally, I would be more inclined to write a model trait. You can then use the trait on only the models required, rather than assuming every ID argument in a request is a Hash ID.
E.g.
namespace App\Models\Traits;
use Hashids\Hashids;
use Illuminate\Database\Eloquent\Builder;
trait HashedId
{
public function scopeHashId(Builder $query, $id)
{
$hashIds = new Hashids(env('APP_KEY'), 10);
$id = $hashIds->decode($id)[0];
return $query->where('id', $id);
}
}
Then to use it, you'd use the trait on your Invoice model (edit):
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Invoice extends Model
{
use \App\Models\Traits\HashedId;
// ...
}
And execute the following query in your controller:
public function show($id)
{
$invoice = Invoice::hashId($id)->firstOrFail();
return view('invoices.show', [
'invoice' => $invoice,
'page_title' => ' Invoices',
'page_description' => 'View Invoice',
]);
}
In my Laravel app users can disable (not delete) their account to disappear from the website. However, if they try to login again their account should be activated automatically and they should log in successfully.
This is done with "active" column in the users table and a global scope in User model:
protected static function boot() {
parent::boot();
static::addGlobalScope('active', function(Builder $builder) {
$builder->where('active', 1);
});
}
The problem now is that those inactive accounts can't log in again, since AuthController does not find them (out of scope).
What I need to achieve:
Make AuthController ignore global scope "active".
If username and password are correct then change the "active" column value to "1".
The idea I have now is to locate the user using withoutGlobalScope, validate the password manually, change column "active" to 1, and then proceed the regular login.
In my AuthController in postLogin method:
$user = User::withoutGlobalScope('active')
->where('username', $request->username)
->first();
if($user != null) {
if (Hash::check($request->username, $user->password))
{
// Set active column to 1
}
}
return $this->login($request);
So the question is how to make AuthController ignore global scope without altering Laravel main code, so it will remain with update?
Thanks.
Create a class GlobalUserProvider that extends EloquentUserProvider like below
class GlobalUserProvider extends EloquentUserProvider {
public function createModel() {
$model = parent::createModel();
return $model->withoutGlobalScope('active');
}
}
Register your new user provider in AuthServiceProvider:
Auth::provider('globalUserProvider', function ($app, array $config) {
return new GlobalUserProvider($this->app->make('hash'), $config['model']);
});
Finally you should change your user provider driver to globalUserProvider in auth.php config file.
'providers' => [
'users' => [
'driver' => 'globalUserProvider',
'model' => App\Models\User::class
]
]
protected static function boot()
{
parent::boot();
if (\Auth::check()) {
static::addGlobalScope('active', function(Builder $builder) {
$builder->where('active', 1);
});
}
}
Please try this for login issue, You can activate after login using withoutGlobalScopes().
#Sasan's answer is working great in Laravel 5.3, but not working in 5.4 - createModel() is expecting a Model but gets a Builder object, so when EloquentUserProvider calls $model->getAuthIdentifierName() an exception is thrown:
BadMethodCallException: Call to undefined method Illuminate\Database\Query\Builder::getAuthIdentifierName() in /var/www/vendor/laravel/framework/src/Illuminate/Database/Query/Builder.php:2445
Instead, follow the same approach but override more functions so that the right object is returned from createModel().
getQuery() returns the builder without the global scope, which is used by the other two functions.
class GlobalUserProvider extends EloquentUserProvider
{
/**
* Get query builder for the model
*
* #return \Illuminate\Database\Eloquent\Builder
*/
private function getQuery()
{
$model = $this->createModel();
return $model->withoutGlobalScope('active');
}
/**
* Retrieve a user by their unique identifier.
*
* #param mixed $identifier
* #return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function retrieveById($identifier)
{
$model = $this->createModel();
return $this->getQuery()
->where($model->getAuthIdentifierName(), $identifier)
->first();
}
/**
* Retrieve a user by their unique identifier and "remember me" token.
*
* #param mixed $identifier
* #param string $token
* #return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function retrieveByToken($identifier, $token)
{
$model = $this->createModel();
return $this->getQuery()
->where($model->getAuthIdentifierName(), $identifier)
->where($model->getRememberTokenName(), $token)
->first();
}
}
Sasan Farrokh has a right answer. The only thing not to rewrite createModel but newModelQuery and this will work
protected function newModelQuery($model = null)
{
$modelQuery = parent::newModelQuery();
return $modelQuery->withoutGlobalScope('active');
}
Extend the AuthController with the code you used in your OP. That should work.
public function postLogin(Request $request)
{
$user = User::withoutGlobalScope('active')
->where('username', $request->username)
->first();
if($user != null){
if (Hash::check($request->password, $user->password)){
$user->active = 1;
$user->save();
}
}
return $this->login($request);
}
I resolved it by creating the new package.
mpyw/scoped-auth: Apply specific scope for user authentication.
Run composer require mpyw/scoped-auth and modify your User model like this:
<?php
namespace App;
use Illuminate\Auth\Authenticatable;
use Illuminate\Contracts\Auth\Authenticatable as UserContract;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Mpyw\ScopedAuth\AuthScopable;
class User extends Model implements UserContract, AuthScopable
{
use Authenticatable;
public function scopeForAuthentication(Builder $query): Builder
{
return $query->withoutGlobalScope('active');
}
}
You can also easily pick Illuminate\Auth\Events\Login to activate User on your Listener.
<?php
namespace App\Providers;
use Illuminate\Support\Facades\Event;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
class EventServiceProvider extends ServiceProvider
{
/**
* The event listener mappings for the application.
*
* #var array
*/
protected $listen = [
\Illuminate\Auth\Events\Login::class => [
\App\Listeners\ActivateUser::class,
],
];
/**
* Register any events for your application.
*
* #return void
*/
public function boot()
{
parent::boot();
//
}
}
<?php
namespace App\Listeners;
use Illuminate\Auth\Events\Login;
class ActivateUser
{
/**
* Handle the event.
*
* #param Illuminate\Auth\Events\Login $event
* #return void
*/
public function handle(Login $event)
{
$event->user->fill('active', 1)->save();
}
}
I had to use
->withoutGlobalScopes() instead
in order for it to work