Laravel: Is possible send notification with delay, but changing smtp settings dynamically? - laravel

I'm developing a Multi Tenant (multiple database) with Laravel v5.7 and I'm successful in sending queue emails.
In some specific situations, I'd like to send on-demand notifications with 'delay', similar to the guide On-Demand Notifications, but informing the SMTP settings that should be used before sending.
I've developed a class that changes the values of config().
app/Tenant/SmtpConfig.php
class SmtpConfig
{
public static function setConnection(SmtpConta $conta = null)
{
// get connection default settings
$config = config()->get("mail");
// populate connection default settings
foreach ($config as $key => $value) {
if ( $key == 'host' ) { $config[$key] = $conta->mail_host ?? $config[$key]; }
if ( $key == 'from' ) { $config[$key] = [
'address' => ( $conta->mail_host === 'smtp.mailtrap.io' ) ? $config[$key]['address'] : $conta->mail_username,
'name' => $conta->conta ?? $config[$key]['name']
]; }
if ( $key == 'username' ) { $config[$key] = $conta->mail_username ?? $config[$key]; }
if ( $key == 'password' ) { $config[$key] = !empty($conta->mail_password) ? $conta->mail_password : $config[$key]; }
}
$config['encryption'] = ( $conta->mail_host === 'smtp.mailtrap.io' ) ? null : 'ssl';
// set connection default settings
config()->set("mail", $config);
}
}
... and I call this SmtpConfig class in notification:
/**
* Create a new notification instance.
*
* #param $conta
* #param $subject
* #return void
*/
public function __construct(SmtpConta $conta = null, $subject = null)
{
$this->conta = $conta;
$this->subject = $subject;
$when = \Carbon\Carbon::now()->addSecond(100);
$this->delay($when);
app(\App\Tenant\SmtpConfig::class)::setConnection($this->conta);
}
I can send the 'delayed' notification successfully, but apparently it always uses the default values of the .env file.
Now I'm not sure if where I'm calling the class makes any sense or even how can I tell the notification what SMTP configuration it should use.

I'm currently facing a similar challenge, on a Laravel 5.2 codebase using the Notification backport library.
This is an example of my solution, similar to Kit Loong's suggestion. We just extend the Illuminate\Notifications\Channels\MailChannel class and override the send() method.
You'll need to be able to determine the SMTP config from the recipient(s), or notification objects, so you'll need to edit my example as necessary.
Also this assumes your app is using the default Swift_Mailer so YMMV...
<?php
declare (strict_types = 1);
namespace App\Notifications\Channels;
use Illuminate\Notifications\Channels\MailChannel;
use Illuminate\Notifications\Notification;
class DynamicSmtpMailChannel extends MailChannel
{
/**
* Send the given notification.
*
* #param mixed $notifiable
* #param \Illuminate\Notifications\Notification $notification
* #return void
*/
public function send($notifiable, Notification $notification)
{
//define this method on your model (note $notifiable could be an array or collection of notifiables!)
$customSmtp = $notifiable->getSmtpConfig();
if ($customSmtp) {
$previousSwiftMailer = $this->mailer->getSwiftMailer();
$swiftTransport = new \Swift_SmtpTransport(
$customSmtp->smtp_server,
$customSmtp->smtp_port,
$customSmtp->smtp_encryption
);
$swiftTransport->setUsername($customSmtp->smtp_user);
$swiftTransport->setPassword($customSmtp->smtp_password);
$this->mailer->setSwiftMailer(new \Swift_Mailer($swiftTransport));
}
$result = parent::send($notifiable, $notification);
if (isset($previousSwiftMailer)) {
//restore the previous mailer
$this->mailer->setSwiftMailer($previousSwiftMailer);
}
return $result;
}
}
It may also be beneficial to keep an ephemeral store of custom swift mailers so you can re-use them in the same invokation/request (think about long-running workers) - like a collection class where a hash of the smtp config is used as the item key.
Best of luck with it.
Edit:
I should probably mention you may need to bind this in the service container. Something like this should suffice:
// in a service provider
public function register()
{
$this->app->bind(
\Illuminate\Notifications\Channels\MailChannel::class
\App\Notifications\Channels\DynamicSmtpMailChannel::class
);
}
Or alternatively, register it as a seperate notification channel.

I think you can also refer to this implementation.
https://stackoverflow.com/a/46135925/6011908
You could execute by passing custom smtp configs.
$transport = new Swift_SmtpTransport(
$customSmtp->host,
$customSmtp->port,
$customSmtp->encryption
);

Related

Why using additive paramerer in Resource collection raised error?

In laravel 9 app I want to add additive paramerer into Resource and looking at this
Laravel 5.6 - Pass additional parameters to API Resource?
branch I remade in app/Http/Resources/CurrencyResource.php :
<?php
namespace App\Http\Resources;
use App\Library\Services\DateFunctionalityServiceInterface;
use Illuminate\Http\Resources\Json\JsonResource;
use App;
use Illuminate\Support\Facades\File;
use Spatie\Image\Image;
use App\Http\Resources\MediaImageResource;
class CurrencyResource extends JsonResource
{
protected $show_default_image = false;
public function showDefaultImage($value){
$this->show_default_image = $value;
return $this;
}
/**
* Transform the resource into an array.
*
* #param \Illuminate\Http\Request $request
*
* #return array
*/
public function toArray($request)
{
$dateFunctionality = App::make(DateFunctionalityServiceInterface::class);
$currencyImage = [];
$currencyMedia = $this->getFirstMedia(config('app.media_app_name'));
if ( ! empty($currencyMedia) and File::exists($currencyMedia->getPath())) {
$currencyImage['url'] = $currencyMedia->getUrl();
$imageInstance = Image::load($currencyMedia->getUrl());
$currencyImage['width'] = $imageInstance->getWidth();
$currencyImage['height'] = $imageInstance->getHeight();
$currencyImage['size'] = $currencyMedia->size;
$currencyImage['file_title'] = $currencyMedia->file_name;
}
else {
\Log::info( varDump($this->show_default_image, ' -1 $this->show_default_image::') );
$currencyImage['url'] = $this->show_default_image ? '/images/default-currency.jpg' : '';
}
// $currencyMedia = $currency->getFirstMedia(config('app.media_app_name'));
return [
'id' => $this->id,
'name' => $this->name,
...
and with code in control :
'currency' => (new CurrencyResource($currency))->showDefaultImage(false),
its work ok, but I got an error :
Method Illuminate\Support\Collection::showDefaultImage does not exist.
when I applyed this method for collection:
return (CurrencyResource::collection($currencies))->showDefaultImage(true);
But in link above there is a similar way :
UserResource::collection($users)->foo('bar');
What is wrong in my code and how that can be fixed ?
Thanks!
I wonder if there is a reason you can't use this approach: https://stackoverflow.com/a/51689732/8485567
Then you can simply use the request parameters to modify your response.
If you really want to get that example working in that way, it seems you are not following the example correctly.
You need to override the static collection method inside your CurrencyResource class:
public static function collection($resource){
return new CurrencyResourceCollection($resource);
}
You also need to create the CurrencyResourceCollection class and define the showDefaultImage method and $show_default_image property on that class as in the example you referred to.
Then you should be able to do:
CurrencyResource::collection($currencies)->showDefaultImage(true);
The reason the way you are doing it doesn't work is because you haven't defined the static collection method on your resource hence it's defaulting to the normal behavior of returning a default collection object as you can see in your error message.

How to add auth user id or session to logging in Laravel 5.7?

My Goal
I'm willing to add the current authenticated user id to log lines (I'm using syslog driver) as a custom log formatting.
As part of my research, I've seen there are nice packages and tutorials to log user activity to database. But at least for now my intention is to move forward without extra packages and logging to syslog as I'm currently doing.
A typical log call in my app would look like:
logger()->info("Listing all items");
I could also do something like:
$uid = Auth::id();
logger()->info("Listing all items for {$uid}");
It would be similar to what a documentation example suggests:
I'm currently doing this for certain lines, but since I'd like to have it in all log calls it would become like repeating myself every time I log something.
Desired Output
Current:
Dec 10 23:54:05 trinsic Laravel[13606]: local.INFO: Hello world []
Desired:
Dec 10 23:54:05 trinsic Laravel[13606]: local.INFO [AUTH_USER_ID=2452]: Hello world []
My Approach
I have tried to tap the logger with success on changing the format, as suggested on docs.
But my current problem is that at the point the CustomizeFormatter class is excecuted, the Auth id seems not to be resolved just yet (not sure about this, but dd() returns null, so that's my guess):
<?php
namespace App\Logging;
use Illuminate\Support\Facades\Auth;
use Monolog\Formatter\LineFormatter;
class CustomizeFormatter
{
/**
* Customize the given logger instance.
*
* #param \Illuminate\Log\Logger $logger
* #return void
*/
public function __invoke($logger)
{
$authUser = Auth::id(); // null
foreach ($logger->getHandlers() as $handler) {
$formatter = new LineFormatter("%channel%.%level_name% [AUTH={$authUser}]: %message% %extra%");
$handler->setFormatter($formatter);
}
}
}
Setup
Laravel 5.7
Log Driver: syslog
// config/logging.php
'syslog' => [
'driver' => 'syslog',
'tap' => [ App\Logging\CustomizeFormatter::class ],
'level' => 'debug',
],
My Question(s)
Is there any way to resolve the auth user at this point, or any other approach to achieve this?
Try passing the request object as a constructor arg to your custom formatter class, then access the user from there:
<?php
namespace App\Logging;
use Illuminate\Support\Facades\Auth;
use Monolog\Formatter\LineFormatter;
class CustomizeFormatter
{
public $request;
public function __construct(Request $request)
{
$this->request = $request;
}
/**
* Customize the given logger instance.
*
* #param \Illuminate\Log\Logger $logger
* #return void
*/
public function __invoke($logger)
{
$authUser = $this->request->user();
foreach ($logger->getHandlers() as $handler) {
$formatter = new LineFormatter("%channel%.%level_name% [AUTH={$authUser->id}]: %message% %extra%");
$handler->setFormatter($formatter);
}
}
}
Based on the answer of DigitalDrifter and part of this post from Emir Karşıyakalı I managed to get a fair enough solution.
I could not grab the User ID since (as per my understanding at this point) it can't be resolved just yet. But I felt satisfied with getting a more or less accurate client id and session id, so I can trace a user interaction thread on logs:
<?php
namespace App\Loggers;
use Monolog\Formatter\LineFormatter;
class LocalLogger
{
private $request;
public function __construct(\Illuminate\Http\Request $request)
{
$this->request = $request;
}
public function __invoke($logger)
{
foreach ($logger->getHandlers() as $handler) {
$handler->setFormatter($this->getLogFormatter());
}
}
protected function getLogFormatter()
{
$uniqueClientId = $this->getUniqueClientId();
$format = str_replace(
'[%datetime%] ',
sprintf('[%%datetime%%] %s ', $uniqueClientId),
LineFormatter::SIMPLE_FORMAT
);
return new LineFormatter($format, null, true, true);
}
protected function getUniqueClientId()
{
$clientId = md5($this->request->server('HTTP_USER_AGENT').'/'.$this->request->ip());
$sessionId = \Session::getId();
return "[{$clientId}:{$sessionId}]";
}
}
Config:
// config/logger.php
'syslog' => [
'driver' => 'syslog',
'level' => 'debug',
'tap' => [App\Loggers\LocalLogger::class],
],
Result
Now I can get something like:
Dec 11 13:54:13 trinsic Fimedi[13390]: [2018-12-11 16:54:13] c6c6cb03fafd4b31493478e76b490650 local.INFO: Hello world
or
Dec 11 13:55:44 trinsic Fimedi[13390]: [2018-12-11 16:55:44] [c6c6cb03fafd4b31493478e76b490650:xUs2WxIb3TvKcpCpFNPFyvBChE88Nk0YbwZI3KrY] local.INFO: Hello world
Depending on how I calculate the user/session ids.

How to cancel queued job in Laravel or Redis

How can I browse all the pending jobs within my Redis queue so that I could cancel the Mailable that has a certain emailAddress-sendTime pair?
I'm using Laravel 5.5 and have a Mailable that I'm using successfully as follows:
$sendTime = Carbon::now()->addHours(3);
Mail::to($emailAddress)
->bcc([config('mail.supportTeam.address'), config('mail.main.address')])
->later($sendTime, new MyCustomMailable($subject, $dataForMailView));
When this code runs, a job gets added to my Redis queue.
I've already read the Laravel docs but remain confused.
How can I cancel a Mailable (prevent it from sending)?
I'd love to code a webpage within my Laravel app that makes this easy for me.
Or maybe there are tools that already make this easy (maybe FastoRedis?)? In that case, instructions about how to achieve this goal that way would also be really helpful. Thanks!
Update:
I've tried browsing the Redis queue using FastoRedis, but I can't figure out how to delete a Mailable, such as the red arrow points to here:
UPDATE:
Look at the comprehensive answer I provided below.
Make it easier.
Don't send an email with the later option. You must dispatch a Job with the later option, and this job will be responsible to send the email.
Inside this job, before send the email, check the emailAddress-sendTime pair. If is correct, send the email, if not, return true and the email won't send and the job will finish.
Comprehensive Answer:
I now use my own custom DispatchableWithControl trait instead of the Dispatchable trait.
I call it like this:
$executeAt = Carbon::now()->addDays(7)->addHours(2)->addMinutes(17);
SomeJobThatWillSendAnEmailOrDoWhatever::dispatch($contactId, $executeAt);
namespace App\Jobs;
use App\Models\Tag;
use Carbon\Carbon;
use Exception;
use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Log;
class SomeJobThatWillSendAnEmailOrDoWhatever implements ShouldQueue {
use DispatchableWithControl,
InteractsWithQueue,
Queueable,
SerializesModels;
protected $contactId;
protected $executeAt;
/**
*
* #param string $contactId
* #param Carbon $executeAt
* #return void
*/
public function __construct($contactId, $executeAt) {
$this->contactId = $contactId;
$this->executeAt = $executeAt;
}
/**
* Execute the job.
*
* #return void
*/
public function handle() {
if ($this->checkWhetherShouldExecute($this->contactId, $this->executeAt)) {
//do stuff here
}
}
/**
* The job failed to process.
*
* #param Exception $exception
* #return void
*/
public function failed(Exception $exception) {
// Send user notification of failure, etc...
Log::error(static::class . ' failed: ' . $exception);
}
}
namespace App\Jobs;
use App\Models\Automation;
use Carbon\Carbon;
use Illuminate\Foundation\Bus\PendingDispatch;
use Log;
trait DispatchableWithControl {
use \Illuminate\Foundation\Bus\Dispatchable {//https://stackoverflow.com/questions/40299080/is-there-a-way-to-extend-trait-in-php
\Illuminate\Foundation\Bus\Dispatchable::dispatch as parentDispatch;
}
/**
* Dispatch the job with the given arguments.
*
* #return \Illuminate\Foundation\Bus\PendingDispatch
*/
public static function dispatch() {
$args = func_get_args();
if (count($args) < 2) {
$args[] = Carbon::now(TT::UTC); //if $executeAt wasn't provided, use 'now' (no delay)
}
list($contactId, $executeAt) = $args;
$newAutomationArray = [
'contact_id' => $contactId,
'job_class_name' => static::class,
'execute_at' => $executeAt->format(TT::MYSQL_DATETIME_FORMAT)
];
Log::debug(json_encode($newAutomationArray));
Automation::create($newAutomationArray);
$pendingDispatch = new PendingDispatch(new static(...$args));
return $pendingDispatch->delay($executeAt);
}
/**
* #param int $contactId
* #param Carbon $executeAt
* #return boolean
*/
public function checkWhetherShouldExecute($contactId, $executeAt) {
$conditionsToMatch = [
'contact_id' => $contactId,
'job_class_name' => static::class,
'execute_at' => $executeAt->format(TT::MYSQL_DATETIME_FORMAT)
];
Log::debug('checkWhetherShouldExecute ' . json_encode($conditionsToMatch));
$automation = Automation::where($conditionsToMatch)->first();
if ($automation) {
$automation->delete();
Log::debug('checkWhetherShouldExecute = true, so soft-deleted record.');
return true;
} else {
return false;
}
}
}
So, now I can look in my 'automations' table to see pending jobs, and I can delete (or soft-delete) any of those records if I want to prevent the job from executing.
Delete job by id.
$job = (new \App\Jobs\SendSms('test'))->delay(5);
$id = app(Dispatcher::class)->dispatch($job);
$res = \Illuminate\Support\Facades\Redis::connection()->zscan('queues:test_queue:delayed', 0, ['match' => '*' . $id . '*']);
$key = array_keys($res[1])[0];
\Illuminate\Support\Facades\Redis::connection()->zrem('queues:test_queue:delayed', $key);
Maybe instead of canceling it you can actually remove it from the Redis, from what Ive read from official docs about forget command on Redis and from Laravel official doc interacting with redis you can basically call any Redis command from the interface, if you could call the forget command and actually pass node_id which in this case I think it's that number you have in your image DEL 1517797158 I think you could achieve the "cancel".
hope this helps
$connection = null;
$default = 'default';
//For the delayed jobs
var_dump( \Queue::getRedis()->connection($connection)->zrange('queues:'.$default.':delayed' ,0, -1) );
//For the reserved jobs
var_dump( \Queue::getRedis()->connection($connection)->zrange('queues:'.$default.':reserved' ,0, -1) );
$connection is the Redis connection name which is null by default, and The $queue is the name of the queue / tube which is 'default' by default!
source : https://stackoverflow.com/a/42182586/6109499
One approach may be to have your job check to see if you've set a specific address/time to be canceled (deleted from queue). Setup a database table or cache a value forever with the address/time in an array. Then in your job's handle method check if anything has been marked for removal and compare it to the mailable's address/time it is processing:
public function handle()
{
if (Cache::has('items_to_remove')) {
$items = Cache::get('items_to_remove');
$removed = null;
foreach ($items as $item) {
if ($this->mail->to === $item['to'] && $this->mail->sendTime === $item['sendTime']) {
$removed = $item;
$this->delete();
break;
}
}
if (!is_null($removed)) {
$diff = array_diff($items, $removed);
Cache::set(['items_to_remove' => $diff]);
}
}
}
I highly recommend checking out the https://laravel.com/docs/master/redis (I run dev/master) but it shows you where they are headed. Most of it works flawlessly now.
Under laravel 8.65 you can just set various status's depending.
protected function listenForEvents()
{
$this->laravel['events']->listen(JobProcessing::class, function ($event) {
$this->writeOutput($event->job, 'starting');
});
$this->laravel['events']->listen(JobProcessed::class, function ($event) {
$this->writeOutput($event->job, 'success');
});
$this->laravel['events']->listen(JobFailed::class, function ($event) {
$this->writeOutput($event->job, 'failed');
$this->logFailedJob($event);
});
}
You can even do $this->canceled;
I highly recommend Muhammads Queues in action PDF. Trust me well worth the money if your using. queues for very important things.... especially with redis . At first TBH I was turned off a bit cause hes a Laravel employee and I thought he should just post things that are helpful but he goes into specific use cases that they do with forge and other items he does plus dives deep into the guts of how queue workers work whether its horizon or whatever. Total eyeopener for me.
Removing all queued jobs:
Redis::command('flushdb');
Using redis-cli I ran this command:
KEYS *queue*
on the Redis instance holding queued jobs,
then deleted whatever keys showed up in the response
DEL queues:default queues:default:reserved
Delete the job from the queue.
$this->delete();

how to get facebook email id from Nodge/yii2-eauth

Am trying to integrate Yii2 EAuth for facebook login integration.
I made configaration * in model am using below code
public static function findIdentity($id) {
if (Yii::$app->getSession()->has('user-'.$id)) {
return new self(Yii::$app->getSession()->get('user-'.$id));
}
else {
return isset(self::$users[$id]) ? new self(self::$users[$id]) : null;
}
}
/**
* #param \nodge\eauth\ServiceBase $service
* #return User
* #throws ErrorException
*/
public function findByEAuth($service) {
if (!$service->getIsAuthenticated()) {
throw new ErrorException('EAuth user should be authenticated before creating identity.');
}
$id = $service->getServiceName().'-'.$service->getId();
// echo $id;exit;
print_r($service->getAttribute('email'));
echo '<pre>';
print_r($service->getAttributes());
exit;
$attributes = array(
'id' => $id,
'username' => $service->getAttribute('name'),
'authKey' => md5(#$id),
'profile' => $service->getAttributes(),
);
$attributes['profile']['service'] = $service->getServiceName();
Yii::$app->getSession()->set('user-'.$id, $attributes);
return new self($attributes);
}
i want email , pls can any one help me to get facebook email id...thanks in advance......
I managed to get the email of the user from facebook after changing the few setting in vendor\nodge\yii2-eauth\src\services\FacebookOAuth2Service.php.
Edit FacebookOAuth2Service.php
Override protected $scopes = array(self::SCOPE_EMAIL);
And modify the fetchAttributes() functions. It should look like this:
protected function fetchAttributes()
{
$info = $this->makeSignedRequest('me');
$this->attributes['id'] = $info['id'];
$this->attributes['name'] = $info['name'];
$this->attributes['url'] = $info['link'];
$this->attributes['email'] = $info['email'];
return true;
}
Try and see it it works for you.

Laravel 4 mail class, how to know if the email was sent?

I'm using the new mail class in Laravel 4, does anybody know how to check if the email was sent? At least that the mail was successfully handed over to the MTA...
If you do
if ( ! Mail::send(array('text' => 'view'), $data, $callback) )
{
return View::make('errors.sendMail');
}
You will know when it was sent or not, but it could be better, because SwiftMailer knows to wich recipients it failed, but Laravel is not exposing the related parameter to help us get that information:
/**
* Send the given Message like it would be sent in a mail client.
*
* All recipients (with the exception of Bcc) will be able to see the other
* recipients this message was sent to.
*
* Recipient/sender data will be retrieved from the Message object.
*
* The return value is the number of recipients who were accepted for
* delivery.
*
* #param Swift_Mime_Message $message
* #param array $failedRecipients An array of failures by-reference
*
* #return integer
*/
public function send(Swift_Mime_Message $message, &$failedRecipients = null)
{
$failedRecipients = (array) $failedRecipients;
if (!$this->_transport->isStarted()) {
$this->_transport->start();
}
$sent = 0;
try {
$sent = $this->_transport->send($message, $failedRecipients);
} catch (Swift_RfcComplianceException $e) {
foreach ($message->getTo() as $address => $name) {
$failedRecipients[] = $address;
}
}
return $sent;
}
But you can extend Laravel's Mailer and add that functionality ($failedRecipients) to the method send of your new class.
EDIT
In 4.1 you can now have access to failed recipients using
Mail::failures();
Antonio has a good point about not knowing which failed.
The real questions is success though. You do not care which failed as much as if ANY failed.
Here is a example for checking if any failed.
$count=0;
$success_count = \Mail::send(array('email.html', 'email.text'), $data, function(\Illuminate\Mail\Message $message) use ($user,&$count)
{
$message->from($user->primary_email, $user->attributes->first.' '.$user->attributes->last );
// send a copy to me
$message->to('me#example.com', 'Example')->subject('Example Email');
$count++
// send a copy to sender
$message->cc($user->primary_email);
$count++
}
if($success_count < $count){
throw new Exception('Failed to send one or more emails.');
}
if(count(Mail::failures()) > 0){
//$errors = 'Failed to send password reset email, please try again.';
$message = "Email not send";
}
return $message;

Resources