this is my scenario: I have a Laravel queued jod, here the handle method content:
$invoiceContract = app('AsteBolaffi\Repositories\Backend\Invoice\InvoiceContract');
$error = $invoiceContract->recordInvoice($this->invoiceId);
d($error);
if (!empty($error) && !empty($this->userEmail)) {
$userEmail = $this->userEmail;
$invoice = $invoiceContract->findOrThrowException($this->invoiceId);
$invoice->load('customer');
d("Going to send mail to " . $userEmail);
d($error);
d($invoice->customer->business_name);
$data["error"] = $error;
$data["business_name"] = $invoice->customer->business_name;
$data["document_number"] = $invoice->document_number;
d($data);
\Mail::queueOn('mail', "emails.record_invoice", $data, function ($message) use ($userEmail) {
$message->from('admin#astebolaffi.it', 'AsteBolaffi');
$message->to($userEmail);
$message->subject('Errori contabilizzazione fattura');
d("HERE I AM");
});
d("Completed");
}
return;
If the recordInvoice method returns a value (e.g. "Item not found") the if clause is satisfied and it has to add a mail queue. But the mail queue is not created on db and the current job is not deleted even if the console prints the last d method value ("Completed").
If recordInvoice does not returns any error the job is deleted.
Non-sense thing (at least for me)
If I comment the recordInvoice method and set a value to error, for example:
$error = "test";
It works properly, adds the mail queue and deletes the current one.
Any tip about it?
I don't know why, but moving the project from php 5.6 to 7.0 it works correctly.
Related
Starting from version 5.7 Laravel suggests to use the array driver for Mail during testing:
Unfortunately, the documentation tells nothing about this driver. According to the source code, the driver stores all the messages in memory without actually sending them. How to get the stored "sent" messages during unit testing (in order to check them)?
EDIT: With Laravel 9+, use:
$emails = app()->make('mailer')->getSymfonyTransport()->messages();
dd($emails);
Be sure your mail driver is set to array in your .env or phpunit.xml file.
With Laravel 7+ or if you get error Target class [swift.transport] does not exist use this to get the list of emails sent with the array driver:
$emails = app()->make('mailer')->getSwiftMailer()->getTransport()->messages();
$count = $emails->count();
$subject = $emails->first()->getSubject();
$to = $emails->first()->getTo();
$body = $emails->first()->getBody();
Call app()->make('swift.transport')->driver()->messages(). The return value is a collection of Swift_Mime_SimpleMessage objects.
An example of a full PHPUnit test:
public function testEmail()
{
Mail::to('user#example.com')->send(new MyMail);
$emails = app()->make('swift.transport')->driver()->messages();
$this->assertCount(1, $emails);
$this->assertEquals(['user#example.com'], array_keys($emails[0]->getTo()));
}
My custom assertion based on Finesse's answer.
protected function assertMailSentTo($user, $expected = 1)
{
$messages = app('swift.transport')->messages();
$filtered = $messages->filter(function ($message) use ($user) {
return array_key_exists($user->email, $message->getTo());
});
$actual = $filtered->count();
$this->assertTrue(
$expected === $actual,
"Sent {$actual} messages instead of {$expected}."
);
}
I can see in the SWF Management Console a Workflow has around 18 events, with the 16th event being my ActivityTaskCompleted event however whenever i poll for decisions i only get up to the 15th event so i never get to call RespondDecisionTaskCompleted with the decision type CompleteWorkflowExecution as such my workflows are always sitting in the Active state until they timeout.
The flow i'm using is from a PHP SWF git i found a while ago, i unfortunately do not have the link to it anymore though.
$response = $this->swf->PollForDecisionTask($opts);
$decision_list = self::_decide(new HistoryEventIterator($this->swf, $opts, $response), $this->swf);
if(count($decision_list) > 0)
{
//Some decisions
}
else
{
//No decisions
}
Where the HistoryEventIterator looks like so
public function __construct(Aws\Swf\SwfClient $swf_client, $original_request, $original_response) {
$this->swf = $swf_client;
$this->events = array();
$this->event_index = 0;
$this->next_page_token = null;
$this->original_poll_request = $original_request;
$this->_process_poll_response($original_response);
$this->workflow_id = $original_response->get('workflowExecution')['workflowId'];
$this->run_id = $original_response->get('workflowExecution')['runId'];
}
protected function _process_poll_response($response) {
if ($response->hasKey("nextPageToken")) {
$this->next_page_token = (string) $response->get("nextPageToken");
} else {
$this->next_page_token = null;
}
$next_events = $response->get("events");
$next_events_object = new ArrayObject($next_events);
$next_events_copy = $next_events_object->getArrayCopy();
$this->events = array_merge($this->events, $next_events);
}
I have omitted error checking and functions of HistoryEventIterator that would not be called in this scenario.
I have output the next_page_token of HistoryEventIterator and found it was always NULL.
Should the RespondDecisionTaskCompleted called from an Activity reach the decider? If so, what could be the cause for mine not? Surely it wouldn't be paging after 15 events, and simply not paging correctly.
I can verify that the Domain, Activity Task List, and Decider Task List are accurate as the Workflow shows up in the SWF Management Console, as does the decisions and the Activity (The Activity even has the status Completed) There is appropriate error checking and Try/Catch blocks and in no cases are there any exceptions.
I've been using Chat-API (https://github.com/WHAnonymous/Chat-API) since it was WhatsAPI, and yet I don't know for sure how to receive messages properly.
Right now, I have a cron file that runs once every minute with this basic structure:
$wa = new WhatsProt($WA_NUMBER, $WA_NICKNAME);
$wa->connect();
$wa->loginWithPassword($WA_PASSWORD);
$wa->pollMessage();
$data = $wa->getMessages();
foreach ($data as $item) {
$from_number = $item->getAttribute("from");
$from_nickname = $item->getAttribute("notify");
if ($item->getAttribute("type") == "text") {
$msg = $item->getChild("body")->getData();
} else {
$msg = $item->getChild("media")->getAttribute("url");
}
...
}
$wa->disconnect();
I've also tried running a PHP script constantly in the background like this:
while (true) {
$wa->pollMessage();
$data = $wa->getMessages();
...
}
The first option is more reliable than the second one, but neither is the right solution.
Is there any way making use of sockets to connect to Whatsapp servers as a phone would do? I mean, open a socket and keep it open, triggering a function every time a new message is received (using XMPP protocol).
i have my queues set up and working (the jobs get run), however the script doesn't seem to wait for my exec line to run before progressing with the next line of code.
This means i'm getting exceptions in the next few lines (because it's looking for a file that hasn't been produced yet)
My closure is:
Queue::push(function($job) use ($gid,$eid)
{
$phantomLoc = base_path()."/vendor/bin/phantomjs";
$scriptLoc = app_path()."/libraries/makeVideo.js";
$pageAddress = route('image_maker_video', array($gid,$eid));
$imageName = base_path().'/../data/team_images/'.$gid.'/video-sheets/'.$eid."/";
$execString = $phantomLoc.' '.$scriptLoc.' '.$pageAddress.' '.$imageName;
//empty the folder first
Helpers::emptyFolder($imageName);
exec($execString, $return_array, $return_value);
if ($return_value == 0) {
//now convert image sequence to video
$outputPath = base_path().'/../data/team_images/'.$gid.'/video-sheets/'.$eid;
$return_value = Helpers::PNGsToVideo($imageName, $outputPath);
if ($return_value == 0) {
//it worked!!
Helpers::emptyFolder($imageName);
//rmdir($imageName);
return "video in progress";
return Redirect::to('/team_images/'.$gid.'/video-sheets/'.$eid.".mkv");
} else {
Log::error($return_value." - ffmpeg return val");
abort(500, $return_value." - ffmpeg return val");
}
} else {
Log::error($return_value." - video phantom return val");
abort(500, $return_value." - video phantom return val");
}
$job->delete();
});
and it seems to skip straight through the exec line, although i do think it is still being run.
Note, if i change the driver back to sync then it all runs completely fine (but obviously not in a queue)
Any idea how to wait for exec?
It turns out that exec was erroring out, but the error (from phatomjs)gave a return code of 0.
Turns out that the error was because the line
$pageAddress = route('image_maker_video', array($gid,$eid));
Was providing a url that just had localhost rather than localhost:8888
So i've hacked it to do a string replace for localhost.
Not sure why putting it into a queue would cause laravel to provide incorrect urls. But at least it's working!
Question
I want to know if it is possible to asynchronously invoke a Yii controller method from one of its actions while the action renders a view, leaving the method to complete a long running operation. I would love to do something like the code below and I don't need to return a result from my_long_running_func.
public function actionCreate() {
$model = new Vacancies;
if (isset($_POST['Vacancies'])) {
$model->setAttributes($_POST['Vacancies']);
$model->save();
//I wish :)
call_user_func_async('my_long_running_func',$model);
}
$this->render('create', array( 'model' => $model));
}
Problem
I am trying to write a controller action in Yii that posts a vacancy and notifies interested subscribers of the post. The problem is that it takes a long time to execute the notification query.
Now I am searching for a way to asynchronously run the query so the poster sees his response in as little time as possible while the query runs in the background in a way similar to C# delegates or events.
The solutions I googled up performed asynchronous request(s) during the course of the controller action but all I want to do is to run a method of the controller asynchronously and the action had to wait till the request(s) were completed.
Attempted
I have tried the following methods but the query is still slow for my test data of about 1500 users.
Yii ActiveRecord
if ($vacancy->save()) {
if($vacancy->is_active == 1) {
$url = Yii::app()->createUrl('vacancies/view',array('id'=>$model->id));
$trainees = YumUser::getUsersByRole('Trainees');
if($trainees!=null) {
foreach($trainees as $trainee){
$message = new YumMessage;
$message->from_user_id = Yii::app()->user->id;
$message->title = 'Vacancy Notification: '.date('M j, Y');
$message->message = "A new vacancy has been posted at <a href='{$url}'>{$url}</a>.";
$message->to_user_id = $trainee->id;
$message->save();
}
}
}
}
Yii Data Access Objects
if ($vacancy->save()) {
if($vacancy->is_active == 1) {
$url = Yii::app()->createAbsoluteUrl('vacancies/view',array('id'=>$model->id));
$trainee_ids=Yii::app()->db->createCommand()->select('user_id')->from('trainee')->queryColumn();
$fid=Yii::app()->user->id;
$msg="A new vacancy has been posted at <a href='{$url}'>{$url}</a>.";
$ts = time();
$tt = 'Vacancy Notification: '.date('M j, Y');
if($trainee_ids!=null) {
foreach($trainee_ids as $trainee_id){
Yii::app()->db->createCommand()
->insert('message',array('timestamp'=>$ts,'from_user_id'=>$fid,'to_user_id'=>$tid,'title'=>$tt,'message'=>$msg));
}
}
}
}
Prepared Statements
if ($vacancy->save()) {
if($vacancy->is_active == 1) {
$url = Yii::app()->createUrl('vacancies/view',array('id'=>$model->id));
$trainee_ids=Yii::app()->db->createCommand()->select('user_id')->from('trainee')->queryColumn();
$fu=Yii::app()->user->id;
$msg="A new vacancy has been posted at <a href='{$url}'>{$url}</a>.";
$ts = time();
$tt = 'Vacancy Notification: '.date('M j, Y');
$sql="INSERT INTO message (timestamp,from_user_id,title,message,to_user_id) VALUES (:ts,:fu,:tt,:msg,:tu)";
if($trainee_ids!=null) {
foreach($trainee_ids as $trainee_id){
$command=Yii::app()->db->createCommand($sql);
$command->bindParam(":ts",$ts,PDO::PARAM_INT);
$command->bindParam(":fu",$fu,PDO::PARAM_INT);
$command->bindParam(":tt",$tt,PDO::PARAM_STR);
$command->bindParam(":msg",$msg,PDO::PARAM_STR);
$command->bindParam(":tu",$trainee_id,PDO::PARAM_INT);
$command->execute();
}
}
}
}
Research
I have also checked the following websites (I'm only allowed to post two links) but they either require the action to wait for the request to be completed or need curl (which I don't have access to on the deployment server) or need an external library. I was hoping for a native PHP implementation.
PHP Simulated Multi-Threading
Multithreading in php
Asynchronous PHP calls?
Asynchronous processing in PHP
Edit
I was able to decrease response time considerably by rewriting my query in this way (moving the user loop to the database layer):
public function actionCreate() {
$user=YumUser::model()->findByPk(Yii::app()->user->id);
$model = new Vacancies;
$model->corporate_id=$user->professional->institution->corporate->id;
$model->date_posted=date('Y-m-d');
$model->last_modified=date('Y-m-d H:i:s');
if (isset($_POST['Vacancies'])) {
$model->setAttributes($_POST['Vacancies']);
if ($model->save()) {
if($model->is_active == 1) {
$url = Yii::app()->createAbsoluteUrl('vacancies/view',array('id'=>$model->id));
$fu=Yii::app()->user->id;
$msg="A new vacancy has been posted at <a href='{$url}'>{$url}</a>.";
$ts = time();
$tt = 'New Vacancy: '.$model->title;
$sql='INSERT INTO message (timestamp,from_user_id,title,message,to_user_id) SELECT :ts,:fu,:tt,:msg,t.user_id FROM trainee t';
Yii::app()->db->createCommand($sql)->execute(array(':ts'=>$ts,':fu'=>$fu,':tt'=>$tt,':msg'=>$msg));
}
if (Yii::app()->getRequest()->getIsAjaxRequest())
Yii::app()->end();
else
$this->redirect(array('view', 'id' => $model->id));
}
}
$this->render('create', array( 'model' => $model));
}
Notwithstanding, it would be nice if someone could post a way to call functions asynchronously.
Typically, the solution for these kind of problems would be to integrate a message-bus in your system. You could consider a product like Beanstalkd. This requires installing software on your server.
I suppose this suggestion would be called "using an external library".
If you can access the deployment server and you can add cronjob (or maybe a sysadmin can) you could consider a cronjob that does a php-cli call to a script that reads jobs from a job queue in your database which is filled by the controller method.
If you cannot install software on the server you're running, you could consider using a SAAS solution like Iron.io to host the bus functionality for you. Iron.io is using what is called a push queue. With a push queue the message bus actively performs a request (push) to the registered listeners with the message content. This might work since it doesn't require you to do a curl request.
If none of the above is possible, your hands are tied. Another post which is quite relevant on the subject: Scalable, Delayed PHP Processing
I would try this, though I'm not 100% that Yii will work properly, but its relatively simple and worth a go:
public function actionCreate() {
$model = new Vacancies;
if (isset($_POST['Vacancies'])) {
$model->setAttributes($_POST['Vacancies']);
$model->save();
//I wish :)
}
HttpResponse::setContentType('text/html');
HttpResponse::setData($this->render('create', array( 'model' => $model), true);
HttpResponse::send();
flush(); // writes the response out to the client
if (isset($_POST['Vacancies'])) {
call_user_func_async('my_long_running_func',$model);
}
}
Here's an entirely different type of suggestion. What about registering for the onEndRequest event that is fired by CWebApplication's end() function?
public function end($status=0, $exit=true)
{
if($this->hasEventHandler('onEndRequest'))
$this->onEndRequest(new CEvent($this));
if($exit)
exit($status);
}
You'd need to register for the event and figure out how to pass your model in somehow, but the code would properly run after all the data has been flushed to the browser ...