I am currently trying to send mail using a queue in Laravel 5.4 in order to speed up a few requests. But for some reason I just won't resolve.
My job looks like the following:
<?php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Support\Facades\Mail;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
class NotificationEmail implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $doer, $does, $user;
/**
* Create a new job instance.
*
* #param Podcast $podcast
* #return void
*/
public function __construct($doer, $does, $user)
{
$this->doer = $doer;
$this->does = $does;
$this->user = $user;
}
/**
* Execute the job.
*
* #param AudioProcessor $processor
* #return void
*/
public function handle()
{
$actions = [
'accepted.invite' => 'accepted your invited.',
'accepted.requesting' => 'accepted your request.',
'denied.invite' => 'denied your invite.',
'denied.requesting' => 'denied your request'
];
Mail::send('emails.notification', [
'doer' => $this->does,
'action' => $actions[$this->action]
], function ($m) {
$m->from('noreply#bigriss.com', 'Bigriss');
$m->to("myemail#gmail.com", 'Shawn')->subject('New Notification');
echo "SENT";
});
}
}
With it being dispatched in another class by:
NotificationEmail::dispatch($doer, $does, $user);
Upon listening to the queue, php artisan queue:listen, as soon as I dispatch the job, the listener just runs on endlessly trying to resolve the handle function. I am getting the message "SENT" but the email is never sent (as I can see on my email provider) and the queue is never actually remove instead, the attempts count just goes up indefinitely. Am I missing something here? Is this not what queues are good for?
You are passing string into your to function, and you're missing a variable in your closure.
When you have an anonymous function, you need to pass in any extra variables using use. I don't see a $user variable anywhere in your handle method. It will need to be passed in as a separate variable because you cannot use $this->user to pass it into the closure.
Right now you have
$m->to("$user->email", 'Shawn')->subject('New Notification');
Which is literally interpreting that as a string that says $user->email because you haven't passed anything in. (Side note: there's really no reason to use that here, save that for inline variables with file paths, etc. You don't need an inline variable with this string).
You would need to change it to
$user = $this->user;
Mail::send('emails.notification', [
'doer' => $this->does,
'action' => $actions[$this->action]
], function ($m) use ($user) {
$m->from('noreply#bigriss.com', 'Bigriss');
$m->to($user->email, 'Shawn')->subject('New Notification');
echo "SENT";
});
You may want to consider using something like Laravel Dusk to debug your queue and logging to better control this than trying to just view "SENT" in your browser.
Also, consider sanitizing your website address since you're posting source code from it.
Related
I always have lots of problems with Mail::queue and this time the subject is not being applied properly.
This is my class:
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
class PlanExpiringOrExpired extends Mailable
{
use Queueable, SerializesModels;
private $payment = null;
public function __construct($payment)
{
$this->payment = $payment;
$this->subject($this->payment->subject);
\Log::debug("Subject: {$this->payment->subject}");
}
public function build()
{
$this->to($this->payment->email, $this->payment->name)
->view('mails/payment')
->with('payment', $this->payment);
return $this;
}
}
And I call it this way:
$payment = \App\Models\Payments::findOrFail($id);
$payment->subject = 'Your account has been canceled';
\Mail::queue(new \App\Mail\PlanExpiringOrExpired($payment));
The log saved correctly the following content:
[2023-02-12 11:00:04] local.DEBUG: Subject: Your account has been canceled
Yet the user received as subject: Plan Expiring or Expired (which is basically the class name).
Since I've done this change recently, do you think this might be a cache-related problem? If so, I'm using Supervisor to run queues, how do I clear the cache (through PHP) without messing up the production server?
I have used in the past something like this.
\Artisan::call('cache:clear');
But I'm not sure if this is correct, or if it has any implications for my production server.
Have you tried it this way to setup the proper subject?
private $payment = null;
public function __construct($payment)
{
$this->payment = $payment;
}
public function build()
{
$this->to($this->payment->email, $this->payment->name)
->subject($this->payment->subject)
->view('mails/payment')
->with('payment', $this->payment);
\Log::debug("Subject: {$this->payment->subject}");
return $this;
}
Move the subject set into build
iam doing like this in queue class, EmailContactForm is a mailable class.
public function handle()
{
$email = new EmailContactForm([
'locale' => $this->data['locale'],
'from_email' => $this->data['from_email'],
'name' => $this->data['name'],
'topic' => $this->data['topic'],
'subject' => $this->data['subject'],
'msg' => $this->data['msg']
]);
Mail::to($this->data['to_email'])
->bcc(config('app.mail_from_address'))
->send($email);
}
Solved.
It was indeed a cache problem, it is also necessary to restart the queue. My solution was to create a private endpoint like /superadmin/clear-cache and use it whenever I need.
Route::get('/superadmin/clear-cache', function()
{
\Artisan::call('cache:clear');
\Artisan::call('queue:restart');
});
I have created an Event called UserWalletNewTransaction.php and added this to it:
public $transaction;
public function __construct($transaction) {
$this->$transaction = $transaction;
}
Now in order to fire this event at the Controller, I coded this:
$newTransaction = UserWalletTransaction::create(['user_id' => $user_id, 'wallet_id' => $wallet_id, 'creator_id' => $creator_id, 'amount' => $amount_add_value, 'description' => $trans_desc]);
event(new UserWalletNewTransaction($newTransaction));
Then at the Listener, UserWalletNotification.php, I tried:
public function handle(UserWalletNewTransaction $event) {
$uid = $event->transaction->user_id;
$user = User::find($uid);
// now sends alert message to the user
}
So the scenario is, when Admins create a new Transaction for a custom user, a new alert message must be sent for him/her to let him/her know that new transaction was added for him/her.
But I don't really know how to do that.. So if you know, please let me know, I would really appreciate that...
Thanks in advance.
If by alert you mean showing a message on the web interface, use flash data.
https://laravel.com/docs/5.8/session#flash-data
$newTransaction = UserWalletTransaction::create(...);
event(new UserWalletNewTransaction($newTransaction));
$request->session()->flash('status', 'Transaction done.');
return view(...)
<span>{{ session('status') }}</span>
If you mean sending an email, just use the Mail facade in your listener to send a mailable.
https://laravel.com/docs/5.8/mail#sending-mail
public function handle(UserWalletNewTransaction $event) {
$uid = $event->transaction->user_id;
$user = User::find($uid);
Mail::to($user)->send(new TransactionDoneMail($event->transaction)); // TransactionDoneMail being your mailable class, made with "php artisan make:email TransactionDoneMail"
}
There are nice examples on how to build a mailable class in the documentation.
https://laravel.com/docs/5.8/mail#writing-mailables
There are many different things you can do in terms of "alerting" the customer.
One route would be to send an email or text message in your event listener. See https://laravel.com/docs/5.8/mail for help doing it via email.
Another way would be using browser push notifications. You could use OneSignal for this. You would setup the front end to display an alert to a customer user asking if they would like to subscribe to push notifications. When they subscribe, you will get back an ID for that specific user. Make an API call to your Laravel app, and store that ID in the users table (you will need a migration). Then from within your event listener, you can make a call to OneSignal's API and send the user a notification, which will popup on their computer.
Here is an example of using OneSignal to send an event to a user via the API:
Your OneSignal service:
<?php
namespace App\Services;
use App\User;
use GuzzleHttp\Client;
class OneSignalService
{
public function sendNotificationToUser(User $user, string $title, string $message, string $url, string $subtitle = null)
{
if (!$user->one_signal_id) {
return;
}
$fields = [
'app_id' => config('services.onesignal.app_id'),
'include_player_ids' => [$user->one_signal_id],
'headings' => ['en' => $title],
'contents' => ['en' => $message],
'url' => $url,
];
if ($subtitle) {
$fields['subtitle'] = ['en' => $subtitle];
}
$client = new Client([
'base_uri' => 'https://onesignal.com/api/v1/',
'headers' => [
'Content-Type' => 'application/json; charset=utf-8',
'Authorization' => 'Basic <<API_KEY>>',
]
]);
$client->request('POST', 'notifications', [
'json' => $fields
])
}
}
UserWalletNotification:
public function handle(UserWalletNewTransaction $event) {
$uid = $event->transaction->user_id;
$user = User::find($uid);
// now sends alert message to the user
$oneSignal = new OneSignalService();
$oneSignal->sendNotificationToUser($user, 'New Transaction', 'You have a new transaction', 'yourwebsite.com');
}
The way I would go about this would be via broadcasting, which would use websockets to instantly send the customer user an alert to their browser, in which you could then display a popup of some sort. You could install Laravel Echo Server, but to keep things simple you can use Pusher. Follow the guide to install on the front end of your website.
Then, create a private channel specific to a customer user "transaction.created.{{USER ID}}" and listen for it on your front end.
Within Laravel you will install the PHP Pusher SDK via composer.
Then within your .env file set:
BROADCAST_DRIVER=pusher
Next, open up channels.php within your routes directory in Laravel and add:
Broadcast::channel('transaction.created.{id}', function ($user, $id) {
return (int) $user->id === (int) $id;
});
This will verify authentication for your user to the private channel.
Create an Laravel Event:
<?php
namespace App\Events;
use App\User;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class TransactionCreated implements ShouldBroadcastNow
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $user = null;
public $transaction = null;
/**
* Create a new event instance.
*
* #return void
*/
public function __construct(User $user, UserWalletTransaction $transaction)
{
$this->user = $user;
$this->transaction = $transaction;
}
public function broadcastWith(): array
{
return $this->transaction->toArray(); //Or whatever information you want to send to the front end
}
public function broadcastAs(): string
{
return 'TransactionCreated';
}
/**
* Get the channels the event should broadcast on.
*
* #return Channel|array
*/
public function broadcastOn()
{
return new PrivateChannel('transaction.created.' . $this->user->id);
}
}
Fire the event from UserWalletNotification:
public function handle(UserWalletNewTransaction $event) {
$uid = $event->transaction->user_id;
$user = User::find($uid);
// now sends alert message to the user
event(new TransactionCreated($user, $event->transaction));
}
Lastly, create some sort of popup and display it on the front end when your callback function for the private channel is hit.
If you need anymore help, feel free to comment.
What you want to do I believe, is asynchronous notifications.
Well, if you really mean flash messages - those who are stored in session - it will not be so easy.
Normal steps are create flash message for the user currently logged in on a website, stored in session that is unique for the current user. It can be shown only for this user.
What you want is to create flash message as the admin (from admin perspective) , then only to admin it can be shown.
I would do this, create new table, when these notification messages will be stored. Some table with columns like id, user_id, message, type, created_date, shown_date. Admins will put alert/notification messages for each user. Then create class (can be in controller for example) that will check this table for each user and if there is new not already shown message, show it normally in flash message for that current user. Dont forget to mark that message as shown. That is it.
So much for custom solution. I belive there must be some for example jQuery/other Jvascript plugins or Laravel plugins for asynchronous notifications, please check those.
Okay, so I basically need to generate a json file using Laravel's scheduler and a custom artisan command (said list is actually a list of popular cities in our application). So I went ahead and did just that. Here's the definition of my Artisan command:
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Services\PlaceService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Log;
class CitySearch extends Command
{
/**
* The name and signature of the console command.
*
* #var string
*/
protected $signature = 'city:search {--locale=}';
/**
* The console command description.
*
* #var string
*/
protected $description = 'Generates the list of the most popular cities to be used across the application when we need it.';
private $placesService;
/**
* Create a new command instance.
*
* #return void
*/
public function __construct(PlaceService $placesService)
{
$this->placesService = $placesService;
parent::__construct();
}
/**
* Execute the console command.
*
* #return mixed
*/
public function handle()
{
App::setLocale( $this->option('locale') );
$request = Request::create(route('api.search-places'), 'GET', ['maxResults' => 3000, 'isArtisan' => true]);
$request->headers->set('Accept', 'application/json');
$request->headers->set('Api-Key', 'aaaaaaaaaaaa');
// $request->headers->set('Api-Key', '43JSOSH333KSOH555WHO99');
$request->headers->set('App-client', 'web');
$response = app()->handle($request);
$content = json_decode($response->getContent());
$results = array_map(function($element){
if($element->type == "City")
$context = ['an', 'se', 'me'];
else
$context = ['se'];
return ['displayName' => $element->displayName, 'context' => $context];
}, $content->data);
print(json_encode($results));
}
}
Then I went into the scheduler and added the following to have the command run once a week:
namespace App\Console;
use App\Console\Commands\Admin\RedFalcon\PendingTransactionNotificator;
use App\Console\Commands\Admin\RedFalcon\FraudTransactionNotificator;
use App\Console\Commands\CardGenerate;
use App\Console\Commands\Communauto\Stats;
use App\Console\Commands\CPS\Archiver;
use App\Console\Commands\CPS\AutoCasher;
use App\Console\Commands\CPS\Autofixer;
use App\Console\Commands\CPS\Beta\Testers;
use App\Console\Commands\CPS\BNC\EmailFailedBankTransaction;
use App\Console\Commands\CPS\BNC\FeedComposer;
use App\Console\Commands\CPS\BNC\Feeder;
use App\Console\Commands\CPS\BNC\FeedMediator;
use App\Console\Commands\CPS\BNC\FeedReporter;
use App\Console\Commands\CPS\BNC\Parametrizer;
use App\Console\Commands\CPS\Captor;
use App\Console\Commands\CPS\ConfirmationFix;
use App\Console\Commands\CPS\ConfirmationCodeRemoval;
use App\Console\Commands\CPS\DB\RideArchiver;
use App\Console\Commands\CPS\DB\RideFixer;
use App\Console\Commands\CPS\Rider;
use App\Console\Commands\CPS\Test\Resetter;
use App\Console\Commands\CPS\Transactor;
use App\Console\Commands\CPS\Troubleshooting\Experiment1;
use App\Console\Commands\Notifications\ApnFeedbackService;
use App\Console\Commands\RelavelTester;
use App\Console\Commands\UpdateCityPopularity;
use App\Console\Commands\Util\FixBankTransactionTable;
use App\Console\Commands\Util\FixPreauthorizationTable;
use App\Console\Commands\Util\ResetPassword;
use App\Console\Commands\CitySearch;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
use Illuminate\Support\Facades\Log;
class Kernel extends ConsoleKernel
{
protected $list;
/**
* The Artisan commands provided by your application.
*
* #var array
*/
protected $commands = [
CardGenerate::class,
Rider::class,
Autofixer::class,
Captor::class,
Transactor::class,
Archiver::class,
FeedComposer::class,
FeedMediator::class,
Feeder::class,
Parametrizer::class,
RideFixer::class,
RideArchiver::class,
RelavelTester::class,
FixPreauthorizationTable::class,
PendingTransactionNotificator::class,
FraudTransactionNotificator::class,
FixBankTransactionTable::class,
Resetter::class,
Testers::class,
Stats::class,
Experiment1::class,
FeedReporter::class,
ResetPassword::class,
AutoCasher::class,
ConfirmationFix::class,
ConfirmationCodeRemoval::class,
CitySearch::class,
UpdateCityPopularity::class,
EmailFailedBankTransaction::class,
ApnFeedbackService::class
];
/**
* Define the application's command schedule.
*
* #param \Illuminate\Console\Scheduling\Schedule $schedule
* #return void
*/
protected function schedule(Schedule $schedule)
{
$schedule->command('city:search --locale=fr')
->mondays()
->at('14:40')
->sendOutputTo(storage_path() . "/app/city-fr.json");
$schedule->command('city:search --locale=en')
->mondays()
->at('14:40')
->sendOutputTo(storage_path() . "/app/city-en.json");
}
/**
* Register the Closure based commands for the application.
*
* #return void
*/
protected function commands()
{
require base_path('routes/console.php');
}
}
Now, this works relatively well... except sometimes it crashes. When that happens, the two json files become filled with error messages instead of the actual data. What I'd like to do is basically save the original list before the command executes and in case something fails, I'd like to output that list into the file and log the error. Right now, everything goes into the file and of course, I get a truckload of errors in my application because the city list is invalid.
Since I'm in Laravel 5.5 I tried using the "before" and "after" hooks (onFailure and onSuccess not available in my version of the framework) and came up with this:
$schedule->command('city:search --locale=fr')
->everyMinute()
->before( function(){
$this->list = json_decode(file_get_contents(storage_path() . "/app/city-fr.json"));
})
->after(function(){
$test = json_decode(file_get_contents(storage_path() . "/app/city-fr.json"));
Log::debug($test);
})
->sendOutputTo(storage_path() . "/app/city-fr.json");
So, while I can successfully get the original list from the file before the process begins, in the "after" hook, the file is still empty so there's no way for me to know whether the process failed or not.
Does anyone know how I should go about this? It feels like the solution is right in front of my face, but I'm just missing it.
Okay, this is a faceplant moment for me. Turns out in the 'after' hook, I did have access to the file, but the reason my output was empty was that the json_decode method returned false because the content of the file wasn't valid json (which was what I was trying to test from the start). Anyway, once I finished picking up the pieces of my scattered brain, the following turned out to work perfectly:
$schedule->command('city:search --locale=fr')
->everyMinute()
->before( function(){
$this->list = file_get_contents(storage_path() . "/app/city-fr.json");
})
->after(function(){
if(!json_decode(file_get_contents(storage_path() . "/app/city-fr.json")))
{
$fp = fopen(storage_path() . "/app/city-fr.json", 'w');
fwrite($fp, $this->list);
fclose($fp);
}
})
->sendOutputTo(storage_path() . "/app/city-fr.json");
Using Laravel framework and it's REPL named Tinker in my project, I want to set the request object to the same state it would be if I made some real HTTP request through my browser.
When I dump the request using
dd($request);
I receive a lot of data in $request like headers, form input data, and so on.
I want to receive the same data in $request in Tinker REPL.
How can I emulate HTTP request in Tinker from the command line?
You should be able to instantiate a request object and then use replace to get some input data into it. Something like this should work in tinker...
>> $r = new Illuminate\Foundation\Http\FormRequest()
>> $r->replace(['yada' => 'bla bla bla'])
>> $r->yada
That should output bla bla bla.
Request class has some set of methods to initiate it with names begins from create... And the create method allow to initiate it with manually gived params like url, method and additional optional parameters:
Illuminate\Foundation\Http\FormRequest::create('http://your-url', 'METHOD', ...)
so you can use it from REPL to play with your controllers and initiate them like you come from some route
I know this question is pretty old but I recently wanted to test a controller POST and found it difficult to find an answer, so here is my solution:
I created a formRequest class which I wanted to test in Tinker. The class looks something like this:
<?php
namespace App\Http\Requests;
use App\Models\Task;
use Carbon\Carbon;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Validator;
class TaskRequest extends FormRequest
{
private $_validator;
private $_task;
/**
* Determine if the user is authorized to make this request.
*
* #return bool
*/
// public function authorize()
// {
// return false;
// }
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules()
{
return [
'task_name' => ['required', 'max:100'],
'task_description' => ['nullable', 'max:1024'],
'due_date' => ['nullable', 'max:50','after:today'],
// ... and more
];
}
public function validator()
{
return $this->_validator ?? $this->_validator = Validator::make($this->sanitize(), $this->rules());
}
/**
* Convert any of your incoming variables, such as dates to required format
**/
public function sanitize()
{
// get request vars
$vars = $this->all();
if ( !empty($vars['due_date']) )
{
$date = new Carbon($vars['due_date'] );
$vars['due_date'] = $date->toDateTimeString();
}
return $vars;
}
public function save()
{
if ( $this->validator()->fails() )
return false;
return $this->_task = Task::create(
$this->validator()->validated()
);
}
In tinker:
$form = new App\Http\Requests\TaskRequest();
$form->merge(['task_name'=>'test task','due_date'=>'2022-04-22Z15:45:13UTC']);
$form->validator()->validated();
=> [
"task_name" => "test task",
"due_date" => "2022-04-22 15:45:13"
]
I am working on a module to send chats messages to user email (aka email transcript) using laravel 5.6.
I need to save all the chat messages to a txt file and send that file as attachment to user's email address.
I do not want to save the txt file to my server as many people would be using that application and it will increase the storage usage of the server i.e I need to generate the txt file in memory.
I am able to populate the chats in plain email without attachment but this is not the solution if the chat messages increase, email would be too lengthy and seems not professional to me.
This I have tried so far:
EmailTranscriptController.php
<?php
namespace App\Http\Controllers\Home;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Models\Trade;
use App\Models\ChatMessage;
use Auth;
use Illuminate\Support\Facades\Mail;
use App\Mail\EmailTradeChatMessages;
use Validator;
class EmailTranscriptController extends Controller
{
public function emailTradeTranscript($tradeId)
{
$userId = Auth::id();
$userEmail = Auth::user()->email;
$trade = Trade::findClosedTradeByIdByUserId($tradeId, $userId);
if (is_null($trade)) {
return response()->api(false, 'Trade not available', null);
}
$tradeStartTime = $trade->created_at;
$tradeCloseTime = $trade->updated_at;
$tradeChats = ChatMessage::getAllChatByTradeId($tradeId);
Mail::to($userEmail)->queue(new EmailTradeChatMessages(
$tradeChats,
$tradeStartTime,
$tradeCloseTime
));
return response()->api(true, 'Email Sent Successfully', null);
}
}
EmailTradeChatMessages.php
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Contracts\Queue\ShouldQueue;
class EmailTradeChatMessages extends Mailable
{
use Queueable, SerializesModels;
protected $chats;
protected $tradeStartTime;
protected $tradeCloseTime;
/**
* Create a new message instance.
*
* #return void
*/
public function __construct($chats, $tradeStartTime, $tradeCloseTime)
{
$this->chats = $chats;
$this->tradeStartTime = $tradeStartTime;
$this->tradeCloseTime = $tradeCloseTime;
}
/**
* Build the message.
*
* #return $this
*/
public function build()
{
return $this->markdown('emails.trade_chat_transcript')->with([
'chats' => $this->chats,
'tradeStartTime' => $this->tradeStartTime,
'tradeCloseTime' => $this->tradeCloseTime,
]);
}
}
trade_chat_transcript.blade.php (dummy)
#component('mail::message')
#Trade Started at: {{$tradeStartTime}}
#php
$count=0;
#endphp
#foreach($chats as $chat)
{{++$count}}
#endforeach
#Trade Closed at: {{$tradeCloseTime}}
Thanks,<br>
{{ config('app.name') }}
#endcomponent
Kindly help me getting the solution, I would also like to get other approaches to solution,if any.
Update
I found the solution for not storing the file on server itself and attach it using attachData() method, as follows:
public function build()
{
$email= $this->markdown('emails.trade_chat_transcript')->with([
'tradeId' => $this->tradeId,
'filename' => $this->filename,
'tradeStartTime' => $this->tradeStartTime,
'tradeCloseTime' => $this->tradeCloseTime,
])
->attachData($this->message,$this->filename,[
'mime'=>'text/plain'
]);
return $email;
}
Now I need to set metadata of the file to be attached in email eg. Author etc.
You need to create the file on the server. That being said you can delete it directly after. There is a method for that:
return response()->download($pathToFile)->deleteFileAfterSend(true);