Phpmailer和异步发送电子邮件

3

在向您提问之前,我阅读了其他问题,因为其他答案没有解决我的问题。

我有一个使用PHP编写的定制CMS。例如,如果我插入一个新的支付,脚本会向所有管理员用户发送通知:

function insert_payment() {
// CODE TO INSERT PAYMENT INSIDE MYSQL DB
$sql_payment = "INSERT INTO payments ($amount, ...) VALUES (?, ...);"
...
// NOTIFY ALL ADMINS
foreach ( $array_emails as $email ) {
   send_email_to_admin($email, $subject, $body);
  }


  // redirect to dashboard
  header("Location: " . $homepage);

以下是send_email_to_admin()函数的示例:

function send_email_to_admin($email, $subject, $body) {
       // return example:
       // $result = array(
       //      "Error" = true or false
       //      "DateTimeOfSent" = datetime
       //      "Email" = string
       //)
       // SAVE RESULTS IN MYSQL DB ( I need to register results to be sure email are sent without errors...table can be then seen to a specific pages under admin panel of cms)

       $mail = new PHPMailer;
       ...
       ...
       if(!$mail->send()) {
  $result = array("Error" => true, "DateTimeOfSent" => date("Y-m-d"), "Email" => $mail);
} else {
  $result = array("Error" => false, "DateTimeOfSent" => date("Y-m-d"), "Email" => $mail);
}

       $sql_result = "INSERT INTO SentResult ("Error", "DateTimeSent", "Email", ...) VALUES ( $result['Error'], $result['DateTimeOfSent'], $result['Email'] )"
       ...
       //end function
}

现在如果我只有1或2个管理员,那没问题...但是如果我有很多管理员,等待每个发送的结果的时间间隔就不好了。
如果可能的话,我想将foreach循环传递给子进程,可以异步处理整个SENDING和SAVING到MYSQL的循环结果。
因此,header("Location: " . $homepage) 可以立即执行。
一些额外的信息:
  1. 我使用托管服务器,因此无法安装软件包和库。
  2. 我只能使用默认PHP配置提供的函数。
  3. 我无法使用cronjob队列方法,因为我的主机不提供免费服务。
  4. 我希望解决方案适用于基于IIS的Windows服务器和Linux服务器。
我想要一个小脚本示例,基于我的代码,因为我从未在php中使用过异步方法,也不知道任何关于它的东西:(
对不起我的英语。

每个管理员的电子邮件地址都一样吗?如果是,只需在“收件人”字段中发送一个带有所有不同电子邮件地址的电子邮件。 - ADyson
$body是为每个管理员个性化定制的。但这只是一个例子...我的目标是在所有情况下实现异步传递方法。我不寻求使用phpmailer同时使用多个地址的解决方法... - itajackass
可能会有所帮助,无论是否涉及cURL部分,请参考https://dev59.com/11oV5IYBdhLWcg3wgu5H。此外,还可以查看https://blog.programster.org/php-async-curl-requests。 - ADyson
1个回答

2
你可以实现一个队列,使用curl调用异步处理这个队列。
不要直接从函数`send_email_to_admin()`发送电子邮件,而是将新数据集插入到专用的SQL表`EmailQueue`中。接下来,编写一个递归函数,处理这个队列(所有等待发送的电子邮件),直到表`EmailQueue`为空为止。
插入付款:
...
// NOTIFY ALL ADMINS
foreach ( $array_emails as $email ) {
   queue_email($email, $subject, $body);
}

curl_process_email_queue();
...

进行CURL调用,从父脚本中分离(来源):

function curl_process_email_queue() {
  $c = curl_init();
  curl_setopt($c, CURLOPT_URL, $url/send_queued_emails.php);
  curl_setopt($c, CURLOPT_FOLLOWLOCATION, true);  // Follow the redirects (needed for mod_rewrite)
  curl_setopt($c, CURLOPT_HEADER, false);         // Don't retrieve headers
  curl_setopt($c, CURLOPT_NOBODY, true);          // Don't retrieve the body
  curl_setopt($c, CURLOPT_RETURNTRANSFER, true);  // Return from curl_exec rather than echoing
  curl_setopt($c, CURLOPT_FRESH_CONNECT, true);   // Always ensure the connection is fresh

  // Timeout super fast once connected, so it goes into async.
  curl_setopt( $c, CURLOPT_TIMEOUT, 1 );

  return curl_exec( $c );
}

排队发送电子邮件:

function queue_email($email, $subject, $body) {
    $sql = "INSERT INTO emailQueue ("email", "subject", "body") VALUES ($email, $subject, $body)";
    ...
};

将 PHP send_queued_emails.php 脚本拆分,通过 cURL 调用 URL 实际发送已排队的电子邮件(递归,直至队列为空):

<?php

// close connection early, but keep executing script
// https://dev59.com/IHVC5IYBdhLWcg3w9GA9#141026
ob_end_clean();
header("Connection: close");
ignore_user_abort(true);
ob_start();
echo('Some status message');
$size = ob_get_length();
header("Content-Length: $size");
header("Content-Encoding: none");
ob_end_flush();
flush();
// connection is closed at this point

// start actual processing here
send_queued_emails();

function send_queued_emails() {
    // avoid concurrent access
    $sql = 'START TRANSACTION'; 
    mysqli_query($sql);

    // read one item from the queue
    $sql = 'SELECT "id", email", "subject", "body" FROM emailQueue LIMIT 1';
    $result = mysqli_query($sql);

    // if no more datasets are found, exit the function
    if (!$result || (mysqli_num_rows($result) == 0))
      return; 

    // mail the queried data
    $mail = new PHPMailer;
    ...

    // optionally write the result back to database
    $sql_result = 'INSERT INTO SentResult ... ';
    mysqli_query($sql);

    // delete the email from the queue
    $sql = 'DELETE FROM emailQueue WHERE "id"=...';
    mysqli_query($sql);

    // commit transaction
    $sql = 'COMMIT'; 
    mysqli_query($sql);

    // recursively call the function
    send_queued_emails();
};

为提高可靠性,您可能需要使用事务,以防止send_queued_emails.php脚本的并发调用问题。有关其他选项,请参见PHP 5.4中异步进程的方法
编辑:根据此线程的建议,“尽早关闭连接,但保持执行脚本”,这应该使您甚至可以设置更高的cURL调用超时时间。
编辑2:根据itajackass(请参阅评论)的建议添加header("Content-Encoding: none");

哇,这是一个很棒的答案。我会在我的本地主机上尝试它。我现在不知道我的托管服务器是否可用cURL,所以...告诉我是否插入像if( function_exists('curl_version'))这样的检查是个好主意。如果为真,请执行您发布的方法。如果为假...继续使用我的原始脚本...这样做有好处可以防止任何兼容性问题吗? - itajackass
Klee,你能澄清一下这个异步部分在哪里吗?PHP中的cURL默认不是异步的。 - ADyson
@klee500,我看到了提到的文章...我认为还需要添加header("Content-Encoding: none")来防止启用gzip选项。 - itajackass
大家好,这里是我的另一个账户@itajackass。我试过脚本,但不幸的是脚本从来没有运行“send_queued_emails()”。它会在之前停止。如果我注释掉//ob_end_flush()和//flush(),脚本就可以一直运行到最后... - Giuseppe Lodi Rizzini
嗨,我解决了这个问题。这只是一个打字错误,PHP在递归脚本中没有显示!谢谢! - itajackass
显示剩余5条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接