In our application, for some actions we send out notifications and emails to a rather large number of users (several hundred to thousand). For some purposes we have to send these emails/notifications separately which I did, using a foreach loop.
For about 200-300 users, that's working fine, but as soon as there are more users to be notified, we get a timeout after a while.
What I was now thinking to do, is redirect to a new page, after e.g. a document is created, and handle the email/notification send out there by sending out let's say 20 emails, then display an update like "20 out of xxx emails/notifications have been sent out", then continue sending the next 20, displaying an update and so on.
Is there any body who already did something like this or has ideas on that?
Here is my code so far:
{
document gets created...
$em->persist($document);
$em->flush();
$this->addFlash(
'success',
'Your document has been created!'
);
return $this->redirectToRoute('documentBundle_document_send_notifications', array('id' => strtok($document->getId(), '_')));
}
/**
* #Route("/document/sendNotifications/{id}", name="documentBundle_document_send_notifications", requirements={"id" = "\d+"})
*/
public function sendDocumentNotifications($id){
$em = $this->getDoctrine()->getManager();
$document = $em->getRepository('DocumentBundle:Document')->findOneById($id);
$statusRepository = $em->getRepository('DocumentBundle:Status');
/*
* NOTIFICATION MANAGEMENT
*/
//Users
$user = $em->getRepository('UserBundle:User');
$user = $user->findByDocumentAgency($user, $document);
$users = array_unique($user->getResult());
if(count($users)>0){
/*
* SEND NOTIFICATION
*/
foreach ($users as $user){
$manager = $this->get('mgilet.notification');
$notif = $manager->generateNotification('A Document has been created!');
$manager->addNotification($user, $notif);
}
/*
* SEND EMAIL
*/
foreach ($users as $user){
$recipient = $user->getEmail();
$this->get('MailerHelper')->sendMessage($recipient,...);
}
}
}
return $this->redirectToRoute('documentBundle_document_list');
}
EDIT
added a console command to run as a background process
/**
* #Route("/document/sendNotifications/{id}", name="documentBundle_document_send_notifications", requirements={"id" = "\d+"})
*/
public function sendDocumentNotifications($id){
$em = $this->getDoctrine()->getManager();
$document = $em->getRepository('DocumentBundle:Document')->findOneById($id);
$process = new Process('php app/console app:send-notifications', $id );
$process->run();
dump($process);
return $this->redirectToRoute('documentBundle_document_bulkDeactivate');
}
when dumping the process, I get the correct commandLine and the input is the correct document Id, but it says:
Process {#1539 ▼
-callback: null
-commandline: "php app/console app:send-notifications"
-cwd: "/srv/http/sp/web"
-env: null
-input: "320"
-starttime: 1511378616.9664
-lastOutputTime: 1511378616.9852
-timeout: 60.0
-idleTimeout: null
-options: array:2 [▼
"suppress_errors" => true
"binary_pipes" => true
]
-exitcode: 1
-fallbackStatus: []
-processInformation: array:8 [▼
"command" => "php app/console app:send-notifications"
"pid" => 29887
"running" => false
"signaled" => false
"stopped" => false
"exitcode" => 1
"termsig" => 0
"stopsig" => 0
I then tried it with
$process->mustRun() and gut the following error:
The command "php app/console app:send-notifications" failed.
Exit Code: 1(General error)
Working directory: /srv/http/sp/web
Output:
================ Could not open input file: app/console
Error Output:
500 Internal Server Error - ProcessFailedException
TL;DR: to prevent time-outs, offload your process to a background process.
First step: use a spool
A quick win would be to Spool Emails:
When you are using the SwiftmailerBundle to send an email from a
Symfony application, it will default to sending the email immediately.
You may, however, want to avoid the performance hit of the
communication between Swift Mailer and the email transport, which
could cause the user to wait for the next page to load while the email
is sending.
The actual mails will be sent by a command line script:
php bin/console swiftmailer:spool:send --env=prod.
Next step: create a command that handles notifications
Learn how to create a console command. Something like this (not tested, just as an example):
class NotificationCommand extends ContainerAwareCommand
{
protected function configure()
{
$this
// the name of the command (the part after "bin/console")
->setName('app:send-notifications')
// the short description shown while running "php bin/console list"
->setDescription('Send notifications to information users about a new document.')
->addArgument('document', InputArgument::REQUIRED, 'The ID of the new document.')
;
}
public function execute(InputInterface $input, OutputInterface $output)
{
$em = $this->getContainer()->get('doctrine')->getManager();
$document = $em->getRepository('DocumentBundle:Document')->findOneById($input->getArgument('document'));
$statusRepository = $em->getRepository('DocumentBundle:Status');
/*
* NOTIFICATION MANAGEMENT
*/
//Users
$user = $em->getRepository('UserBundle:User');
$user = $user->findByDocumentAgency($user, $document);
$users = array_unique($user->getResult());
if(count($users)>0){
/*
* SEND NOTIFICATION
*/
foreach ($users as $user){
$manager = $this->get('mgilet.notification');
$notif = $manager->generateNotification('A Document has been created!');
$manager->addNotification($user, $notif);
}
/*
* SEND EMAIL
*/
foreach ($users as $user){
$recipient = $user->getEmail();
$this->get('MailerHelper')->sendMessage($recipient,...);
}
}
}
}
}
To execute this, run bin/console app:send-notifications 1 on your command line.
Schedule your command
I guess you don't want to log into the server manually to send the notifications, so instead you can create a general 'send notifications if needed' command that can scheduled by a cron job.
As a next step you can take a look at a way to queue your commands. Take JMSJobQueueBundle for example.
Bonus tip: start by making your controllers thinner
Try to make your controllers thinner. Read this old, but still interesting blog. If you create services and call them from the controller (instead of having a fat controller), the 'refactoring' job of removing code from controller to background job (and maybe visa versa) is much easier.
Related
It's my first time trying to implement Task Scheduling, I'm trying to send automatic E-mails at a certain time:
Before implementing my cron I first tested my email sending code manually in a normal class to see if there is no error, and there was no error, the email was sent successfully.
After that, I started implementing the Task Scheduling
Democron.php
protected $signature = 'demo:cron';
protected $description = 'Command description';
public function __construct()
{
parent::__construct();
}
public function handle()
{
$tasks = Task::all();
$date = Carbon::now()->toDateTimeString();
foreach ($tasks as $task) {
if($task->completed_at != null){
$validad = $task->completed_at;
$receiver_id = User::findOrFail($task->user_id);
if($date > $validad){
$details = [
'task_id' =>$task->id,
'receiver_id' => $receiver_id
];
$subject = 'TeamWork - Você tem tarefas em atraso!';
$view = 'emails.project.delaydtask';
Mail::to($receiver_id->email)->send(new SendMail($details, $subject, $view));
Log::info('Email enviado com sucesso para '.$receiver_id->email);
}
}
}
}
Kernel.php
protected $commands = [
DemoCron::class,
];
protected function schedule(Schedule $schedule)
{
$schedule->command('demo:cron')
->twiceDaily(12, 15)
->timezone('Africa/Maputo');
}
protected function commands()
{
$this->load(__DIR__.'/Commands');
require base_path('routes/console.php');
}
I added to CRON JOBS on CPANEL
and set twiceDaily at 12 and 15
/usr/local/bin/php /.......myProjectPath/artisan schedule:run >> /dev/null 2>&1
I printed a LOG in my DemoCron.php to see if it really works
Result 1: when I select schedule once per minute it prints my LOG respecting all the conditions that are in my Democron.php , but it doesn't send the email.
Result 2: When I select a certain time (Twice per day or once a day) my LOG does not print anything and it does not send the email.
What am I doing wrong? Help me please!
UPDATE
my SendMail class that i use to send emails manually works perfectly,
but the scheduled emails are not going
class SendMail extends Mailable
{
use Queueable, SerializesModels;
public $details, $subject, $view;
public function __construct($details, $subject, $view)
{
$this->details = $details;
$this->subject = $subject;
$this->view = $view;
}
public function build()
{
return $this->subject($this->subject)
->view($this->view, ['details' => $this->details]);
}
}
After trying several times I found a workaround.
1- create a new controller
I created a new controller called MailController instead of using the Kernel.php and Democron.php classes that I generated through Laravel Scheduling
class MailController extends Controller
{
public function delayedtask(){
try {
$tasks = Task::all();
$date = Carbon::now()->toDateTimeString();
foreach ($tasks as $task) {
if($task->completed_at != null){
$validad = $task->completed_at;
$receiver_id = User::findOrFail($task->user_id);
if($date > $validad){
$details = [
'task_id' =>$task->id,
'receiver_id' => $receiver_id
];
$subject = 'TeamWork - Você tem tarefas em atraso!';
$view = 'emails.project.delaydtask';
Mail::to($receiver_id->email)->send(new SendMailQueue($details, $subject, $view));
Log::info('Email enviado com sucesso para '.$receiver_id->email);
}
}
}
return "Done!";
} catch (Exception $e) {
return "Something went wrong!";
}
}
}
2-add a new route
added a new route without Auth
Route::get('/delayedtask',[MailController::class, 'delayedtask']);
3-Added a cronjob on Cpanel
curl -s "https://myWebsiteURL/delayedtask">/dev/null 2>&1
First of all lets check all things:
Verify your mail configurations in your .env;
Verify in your email class if have implements ShouldQueue;
If you are implementing ShouldQueue, you must have to verify too your queue´s configuration in .env;
If is not implementing ShouldQueue, don´t miss time verifying queue´s config;
All right all things validated and still not sending email:
Add the Send mail in try catch and log the catch if something went wrong;
If don´t log nothing in try catch, try to create an command that just send a simple email;
If dosen´t work try to send an email by your mail in Cpanel, because this should be the problem;
Finally
In my cases using cPanel, I always create the croon task to all seconds like * * * * * and in the kernel of my laravel project I verify if some command must be executed with the laravel commands like ->twiceDaily(12, 15).
Try all things and if the error still, please update this thread!
I had the same problem,
i tried a new smtp email server
MAIL_HOST=pro.eu.turbo-smtp.com
MAIL_ENCRYPTION=ssl
instead of
MAIL_HOST=smtpauth.online.net
MAIL_ENCRYPTION=tls
I don't know if it's about the encryption or host features,
but it worked for me
I have a custom class in Laravel that tracks the analytics of my app through Segment (using this package for php: https://github.com/AltThree/Segment).
Here is a snippet of my class and a function I am calling through my listener to track a login:
class Tracking {
private function segmentTrack(User $user, string $event, array $properties = null) {
$segment = Segment::track([
"userId" => $user->id,
"event" => $event,
"properties" => $properties
]);
dd($segment);
}
/**
* Handle Login tracking
*
* #param User $user
* #return void
*/
public function login (User $user) {
$this->segmentTrack($user, "Login");
}
}
Notice the dd in the segmentTrack function. When I run the Laravel queue and I then trigger the Tracking->login() event through my app, the listener goes off fine and with the dd function, it will send that data to Segment and I can see it in their live debugger, all is well.
However, when I remove that dd, and the listener goes off and shows as successful - the data is never seen in Segment.
Can someone tell me what i'm missing? This is my first time using the Laravel queue system so a little confused why it might not be working.
For queued jobs, use:
Segment::track($payload);
Segment::flush();
I am using davibennun/laravel-push-notification package for sending notification to device. But I want to send notification to multiple users for that i want to use laravel queue. But i am new to laravel that's why don't know how to use queue with notification.
I have created the migrate the queue table and have created the job by
php artisan make:job SendPushNotification command.
After running following command
php artisan make:job SendPushNotification
From your controller
$user = User::all();
$other_data = array('message' => 'This is message');
SendPushNotification::dispatch($user, $other_data);
In app\Jobs\SendPushNotification.php
protected $contacts;
protected $sms_data;
public function __construct($users, $other_data)
{
//
$this->users = $users;
$this->other_data = $other_data;
}
public function handle()
{
$users = $this->users;
$other_data = $this->other_data;
foreach($users as $key => $value){
// You code
}
Run following command
php artisan queue:work
I'm facing two situations that I would like to address / understand.
1st - How do I Unit Test Laravel's Mail Queue Class?
The code that I want to test is this:
// Create new customer record
$account = $this->create(['account_id' => $account->id]);
// Get email address to send welcome email.
$email = $data['email'];
// Email Subject
$subject = $this->word('emails.welcome.subject');
$this->mailQueue->queue('emails.welcome',
['some_data' => 'data'],
function ($message) use ($email, $subject) {
$message->to($email)->subject($subject);
}, true);
return $account;
I would like to know where is the shouldReceive method that will work for me when using Illuminate\Contracts\Mail\MailQueue class.
Right now I have this unit test for this:
/**
* #tests
*/
public function it_should_sign_up_a_new_user() {
// MailQueue::shouldReceive() does not exist.
list($account, $email) = $this->getAccountData();
$request = array_merge($account, $email);
$account['password'] = $this->hash($account['password']);
$this->post('/signup', $request, $this->header)
->assertResponseOk()
->seeInDatabase('account', $account)
->seeInDatabase('email', $email);
}
2nd - Why Unit Test does not require php artisan queue:listen or queue:work?
Every time I run the Unit Test, the email gets dispatched even though I have no queue:listen running. I would like to understand how this awesome magic happens.
Per the docs it looks like they recommend using Mail::queue . i.e.
Mail::queue('emails.welcome', $data, function($message)
{
$message->to('foo#example.com', 'John Smith')->subject('Welcome!');
});
The Mail Facade has shouldReceive built into it so you should be able to do:
Mail::shouldReceive('queue')->once()
https://laravel.com/docs/5.0/mail#queueing-mail
I'm having a nightmare trying to set up a cron job in my Symfony2 project.
I understand the principle of setting it up and where to put the code but I just cannot get it to do what I need.
Basically, I need the cron job to run every day and check a database of clients in order to find out if an invoice needs sending. The actual client referencing is yet to be done but I have written a test which I want to generate and email and invoice based on hardcoded values I pass to the function.
// AppBundle/Command/CronRunCommand.php
protected function execute(InputInterface $input, OutputInterface $output)
{
$request = new Request();
$request->attributes->set('client','14');
$request->attributes->set('invoice_id','3');
$request->attributes->set('dl','0');
$output->writeln('<comment>Running Invoice Cron Task...</comment>');
return $this->getContainer()->get('invoices')->generateInvoiceAction($request);
}
I have set invoices up as a service in my config.yml:
services:
invoices:
class: AppBundle\Controller\InvoiceController
And in InvoiceController there is a function that will generate an invoice by using Invoice Painter Bundle and then send it to the specified email address (currently hard coded for development purposes).
When I run the cron command on my console, it throws the following error:
[Symfony\Component\Debug\Exception\FatalErrorException]
Error: Call to a member function has() on null
I have searched for this and I believe it is to do with the fact that it's referencing a controller method and my command file does not extend controller, but I'm so confused about how I can do this - surely there is a way of running a method in a controller as a cron job?
Any help appreciated.
Michael
I fear you may still not be understanding the big picture. Console apps don't have a request object and thus the whole request_stack is not going to work. I know you tried creating a request object but that is not going to impact the request stack.
Your console app should look something like:
protected function execute(InputInterface $input, OutputInterface $output)
{
$data = [
'client' => 14,
'invoice' => 3,
'dl' => 0,
];
$invoiceManager = $this->getContainer()->get('invoices');
$results = $invoiceManager->generateInvoice($data);
}
Your controller action would be something like:
public function generateInvoiceAction(Request $request)
{
$data = [
'client' => $request->attribute->get('client'),
'invoice' => $request->attribute->get('invoice'),
'dl' => $request->attribute->get('dl'),
];
$invoiceManager = $this->getContainer()->get('invoices');
$results = $invoiceManager->generateInvoice($data);
The invoice manager might look like:
class InvoiceManager {
public function __construct($em) {
$em = $this->em;
}
public function generateInvoice($data) {
$client = $this->em->find('Client',$data['client']);