如何使用Codeigniter在单独的过程中发送电子邮件

时间:2019-06-26 18:25:55

标签: php email codeigniter-3

我在Web应用程序中具有错误报告功能,并且当用户发布错误时,电子邮件会发送到开发人员电子邮件。当前的工作流程类似于user input -> db write -> send email -> redirect

为了防止电子邮件功能阻止其余的用户流程,我想将其分开,以便电子邮件发送流程可以完成他们的任务,但用户可以继续工作。所以就像

user input -> db write --> redirect
                       \-> send email

我想到的第一件事是,我发送了一个ajax请求来启动电子邮件过程,以便错误报告和电子邮件功能可以异步工作,但是我很好奇是否有更好的方法可以做到这一点。

2 个答案:

答案 0 :(得分:1)

根据您要获得的复杂程度,可以采用不同的方法。 PHP确实具有过程控制方面的内容,但是特别是如果您的站点将由共享的托管服务提供商托管,则无法保证您的托管环境中该功能的可用性。

就确保跨各种托管环境的兼容性而言,执行此操作的更简便的方法之一可能是启动一个新的“要发送的邮件”数据库表,并为要发送的每个邮件记录一行。然后在服务器上设置一个cron任务,该任务运行一个脚本,该脚本从该表中提取所有行,发送电子邮件,然后从表中删除该行。使用CodeIgniter,如果您像这样调用index.php脚本

php -f index.php foo bar

它将调用与通过标准路由系统调用/foo/bar路径时将调用的方法相同的方法(例如,bar控制器的Foo方法)。然后,在该方法内部,您可以使用if (!is_cli()) { exit(); }来确保未从Web请求中调用该方法,并可以使用flock()来确保您没有一次同时发送电子邮件的两个进程(以防挂起或花费很长时间)。从“要发送的邮件”表中选择所有内容,逐行进行遍历,发送邮件,进行一些日志记录(如果适用),然后删除该行。 (More info on doing CLI scripts on CodeIgniter.

然后设置一个cron任务以每X分钟运行一次此脚本,也许可以设置某种方式来立即检查该表,然后确保它不会变得太大(这表明邮件发送脚本失败了。 ),然后就可以了。

如上所述,有更多聪明的方法可以实现这样的功能,但这是一种广泛兼容且万无一失的方法。

答案 1 :(得分:1)

可以从正在处理Web服务器请求的PHP脚本中派生一个进程,但是您应该注意以下几点:

  • 您的原始PHP流程和分叉的流程可能会共享许多数据对象。虽然可以在子进程和父进程中复制某些变量并对其进行独立操作,但某些变量(例如db连接或文件句柄等)可能引用相同的计算资源。您应注意避免重复使用资源变量。让孩子重新连接到您的数据库并重新打开文件等。
  • 如果进程启动,则可能无法连接到db或邮件网关等。您可能应该使此分叉的子进程循环尝试完成其目标,然后在尝试了一定次数后最终死亡做重要的事情。
  • 关于多线程,竞争条件,资源使用等的警告可能还有十几种。请记住,分叉进程意味着您正在消耗更多资源,这些资源可能被其他进程使用,并且您可能会得到难以解决的怪异问题。

所有这些,我很幸运使用exec命令的组合来分叉一个单独的进程,然后调用posix_setsid函数,以便分叉的子进程可以完全分离即使父进程完成或apache重新启动或执行其他操作,也可以从apache本身继续运行。

使用此代码自担风险。

从您的CodeIgniter脚本中,您可以使用exec派生一个子进程,如下所示:

// the PARENT process (a CodeIgniter script running in response to http request)
// put this code in a controller or something

// here's a command to run the PHP executable in the background on some file
// routing the output and error messages to another file
$cmd = "/usr/bin/php /path/to/child-script.php > /tmp/foo/out.txt 2>&1 & echo \$!";

// will contain an array of output lines from the exec command
$cmd_output = NULL;

// will contain the success/failure code returned by the OS.
// Will be zero for a valid command, non-zero otherwise
$cmd_result = NULL; 

// $return will contain the last line from the result of
// the command which should be the PID of the process we have spawned
$cmd_return = exec ( $cmd, $cmd_output, $cmd_result );

注意:您可以改而使用pcntl_fork来完成该过程。我之所以没有这个原因,是因为这意味着子进程将继承 codeigniter父脚本中的每个变量,这可能导致一些令人困惑的行为。例如,如果一个进程更改了db连接,则该更改可能会突然出现在另一个脚本中,依此类推。此处使用exec()更彻底地将这两个脚本分开,并使事情变得更简单。如果要在$ cmd上面更改以运行CodeIgniter控制器,请参见CodeIgniter from the Command Line

在子进程中,必须调用posix_setsid(),以便子脚本将其自身与父脚本分离。如果不这样做,则子进程可能会在父进程完成(或被杀死)或Web服务器重启或崩溃等情况时被杀死。

这是子进程中的代码,它要求通过disable_functions指令在PHP.ini中安装,启用和禁用POSIX扩展。在Web服务器的PHP.ini中,经常会(出于充分的理由)禁用此扩展,但有时仍可用于CLI脚本。使用exec启动命令行过程的另一个很好的理由。

// child-script.php
// CHILD process. Put your emailing code or other long-running junk in here
// because this is a brand-new, separate process, you might need to load configuration files, connect to the db, etc.

if (!extension_loaded("posix")){
    throw new Exception("This script requires posix extension");
}

// NOTE: the output and results of this file will be completely unavailable to the parent script and you might never know what happens if you don't write a log file or something. Consider opening a log file somewhere

// process id of this process, should match contents of $cmd_return above
$mypid = posix_getpid();

// session id -- not to be confused with php session id
// apparently this call will make this process a 'session leader'
$sid = posix_setsid();
if ($sid < 0) {
    throw new Exception("setsid failed, returned $sid");
} 

// this output should be routed by the exec command above to
// /tmp/foo/out.txt
echo "setsid success, sid=$sid\n";

// PUT YOUR EMAILING CODE HERE