How can I run a PHP script in the background after a form is submitted?
Asked Answered
C

16

115

Problem
I have a form that, when submitted, will run basic code to process the information submitted and insert it into a database for display on a notification website. In addition, I have a list of people who have signed up to receive these notifications via email and SMS message. This list is trivial as the moment (only pushing about 150), however it's enough to cause it takes upwards of a minute to cycle through the entire table of subscribers and send out 150+ emails. (The emails are being sent individually as requested by the system administrators of our email server because of mass email policies.)

During this time, the individual who posted the alert will sit on the last page of the form for almost a minute without any positive reinforcement that their notification is being posted. This leads to other potential problems, all that have possible solutions that I feel are less than ideal.

  1. First, the poster might think the server is lagging and click the 'Submit' button again, causing the script to start over or run twice. I could solve this by using JavaScript to disable the button and replace the text to say something like 'Processing...', however this is less than ideal because the user will still be stuck on the page for the length of the script execution. (Also, if JavaScript is disabled, this problem still exists.)

  2. Second, the poster might close the tab or the browser prematurely after submitting the form. The script will keeping running on the server until it tries to write back to the browser, however if the user then browses to any page within our domain (while the script is still running), the browser hangs loading the page until the script has ended. (This only happens when a tab or window of the browser is closed and not the entire browser application.) Still, this is less than ideal.

(Possible) Solution
I've decided I want to break out the "email" part of the script into a separate file I can call after the notification has been posted. I originally thought of putting this on the confirmation page after the notification has been successfully posted. However, the user will not know this script is running and any anomalies will not be apparent to them; This script cannot fail.

But, what if I can run this script as a background process? So, my question is this: How can I execute a PHP script to trigger as a background service and run completely independent of what the user has done at the form level?

EDIT: This cannot be cron'ed. It must run the instant the form is submitted. These are high-priority notifications. In addition, the system administrators running our servers disallow crons from running any more frequently than 5 minutes.

Cary answered 7/1, 2011 at 15:5 Comment(5)
Start a cron job and alert the user on another page that their request is being processed, or something like that.Ormuz
Do you want to run it regularly (every day or every hour) or on submit? I guess you could use php's exec() but I never tried this.Streamlet
I simply use ajax and inserting sleep() with time interval inside the loop (I use foreach in my case) to run background process. It works perfectly in my server. Not sure about the others. I am also adding ignore_user_abort() and set_time_limit() (with calculated end time) before the loop to make sure that the script won't be stopped by the server I use, even according to my test, the script completing the task without ignore_user_abort() and set_time_limit().Masticatory
I saw you already came up with a solution. I use gearman with php to deal with background tasks. It's perfect to scale if you came across with big time processing and heavy loads. You should take a look into it.Contagious
Follow this answer on[ stackoverflow](https://mcmap.net/q/107492/-php-execute-a-background-process)Charley
C
102

Doing some experimentation with exec and shell_exec I have uncovered a solution that worked perfectly! I choose to use shell_exec so I can log every notification process that happens (or doesn't). (shell_exec returns as a string and this was easier than using exec, assigning the output to a variable and then opening a file to write to.)

I'm using the following line to invoke the email script:

shell_exec("/path/to/php /path/to/send_notifications.php '".$post_id."' 'alert' >> /path/to/alert_log/paging.log &");

It is important to notice the & at the end of the command (as pointed out by @netcoder). This UNIX command runs a process in the background.

The extra variables surrounded in single quotes after the path to the script are set as $_SERVER['argv'] variables that I can call within my script.

The email script then outputs to my log file using the >> and will output something like this:

[2011-01-07 11:01:26] Alert Notifications Sent for http://alerts.illinoisstate.edu/2049 (SCRIPT: 38.71 seconds)
[2011-01-07 11:01:34] CRITICAL ERROR: Alert Notifications NOT sent for http://alerts.illinoisstate.edu/2049 (SCRIPT: 23.12 seconds)
Cary answered 7/1, 2011 at 17:19 Comment(5)
What is that $post_id after the path of the php script?Feudist
@Feudist it is a variable that you are sending to the script. In this case it is sending an ID which will be a parameter to the script being called.Longboat
I doubt this does work perfectly, see #2213135Jitney
There's a pending edit suggestion to escape$post_id; I would rather cast it directly to a number: (int) $post_id. (Calling out for your attention to decide which is better.)Utgardloki
Thanks, using it now and works fine! One suggestion: as the variables might come from (unsafe) user input, you might want to use escapeshellarg(), e.g. making your example shell_exec("/path/to/php /path/to/send_notifications.php " . escapeshellarg($post_id) . " 'alert' >> /path/to/alert_log/paging.log &"); to prevent a possible command injection (owasp.org/www-community/attacks/Command_Injection)Rockefeller
M
22

Of all the answers, none considered the ridiculously easy fastcgi_finish_request function, that when called, flushes all remaining output to the browser and closes the Fastcgi session and the HTTP connection, while letting the script run in the background.

Example:

<?php
header('Content-Type: application/json');
echo json_encode(['ok' => true]);
fastcgi_finish_request(); // The user is now disconnected from the script

// Do stuff with received data

Note: Due to a wontfix quirk calling flush() after fastcgi_finish_request will cause it to exit without warning/error.

You may wish to call ignore_user_abort(true) beforehand to supress this behavior, or simply avoid calling flush() after you've intentionally closed the connection :)

$connected = true;

// Stuff...

fastcgi_finish_request();
$connected = false;

// ...
if ($connected) {
    flush();
}

Or

ignore_user_abort(true);
fastcgi_finish_request();
// Accidental flush()es won't do harm (even if you really shouldn't be calling flush() if you know you've disconnected from the user)
flush();
Mangan answered 1/6, 2017 at 11:2 Comment(3)
this is exactly what I was looking for, in shell_exec I have to somehow transfer the posted data to executed script which itself is a time consuming task in my case and also makes things very complicated.Jacy
This function is not defined for me. Is it part of some optional module?Scammony
@Scammony It is enabled by default when using php-fpm, it's not available when using apache (unless you're using php-fpm with apache, in which case it will still be available).Mangan
O
20

On Linux/Unix servers, you can execute a job in the background by using proc_open:

$descriptorspec = array(
   array('pipe', 'r'),               // stdin
   array('file', 'myfile.txt', 'a'), // stdout
   array('pipe', 'w'),               // stderr
);

$proc = proc_open('php email_script.php &', $descriptorspec, $pipes);

The & being the important bit here. The script will continue even if the original script has ended.

Orsini answered 7/1, 2011 at 15:16 Comment(3)
This works when I do not call proc_close when the task has been completed. proc_close makes the script hang about 15 to 25 seconds. I would need to keep a log using the pipe information, so I have to open a file to be written to, close it, and then close the proc connection. So, unfortunately, this solution does not work for me.Cary
Of course you don't call proc_close, that's the point! And yes, you can pipe to a file with proc_open. See updated answer.Orsini
@Orsini : what if I don't want to store the result in any file. what changes are required in stdout ?Morph
S
8

PHP exec("php script.php") can do it.

From the Manual:

If a program is started with this function, in order for it to continue running in the background, the output of the program must be redirected to a file or another output stream. Failing to do so will cause PHP to hang until the execution of the program ends.

So if you redirect the output to a log file (what is a good idea anyways), your calling script will not hang and your email script will run in bg.

Streamlet answered 7/1, 2011 at 15:13 Comment(4)
Thanks, but this did not work. The script still "hangs" while it processes the second file.Cary
Look at the commend php.net/manual/en/function.exec.php#80582 Seems as this is because Safe_mode is enabled. There is a workaround under unix.Streamlet
Unfortunately, safe mode is not enabled on our installation. It's odd that it is still hanging.Cary
This works: exec($cmd . " > /path/to/file &");Reedy
M
6

And why not making a HTTP Request on the script and ignoring the response ?

http://php.net/manual/en/function.httprequest-send.php

If you make your request on the script you need to call your webserver will run it in background and you can (in your main script) show a message telling the user that the script is running.

Menses answered 7/1, 2011 at 15:13 Comment(0)
A
5

The simpler way to run a PHP script in background is

php script.php >/dev/null &

The script will run in background and the page will also reach the action page faster.

Agley answered 7/2, 2013 at 12:45 Comment(1)
I have used: nohup php script.php >/dev/null & and it is the only solution that worked for meIconology
A
4

How about this?

  1. Your PHP script that holds the form saves a flag or some value into a database or file.
  2. A second PHP script polls for this value periodically and if it's been set, it triggers the Email script in a synchronous manner.

This second PHP script should be set to run as a cron.

Acrylonitrile answered 7/1, 2011 at 15:9 Comment(1)
Thank you, but I've update the original question. Cron's are not an option in this case.Cary
L
4

As I know you cannot do this in easy way (see fork exec etc (don't work under windows)), may be you can reverse the approach, use the background of the browser posting the form in ajax, so if the post still work you've no wait time.
This can help even if you have to do some long elaboration.

About sending mail it's always suggest to use a spooler, may be a local & quick smtp server that accept your requests and the spool them to the real MTA or put all in a DB, than use a cron that spool the queue.
The cron may be on another machine calling the spooler as external url:

* * * * * wget -O /dev/null http://www.example.com/spooler.php
Leandra answered 7/1, 2011 at 15:25 Comment(0)
E
3

Background cron job sounds like a good idea for this.

You'll need ssh access to the machine to run the script as a cron.

$ php scriptname.php to run it.

Ecumenical answered 7/1, 2011 at 15:8 Comment(2)
no need for ssh. Many hosts disallow ssh, but have cron-support.Wenoa
Thank you, but I've update the original question. Cron's are not an option in this case.Cary
W
3

Assuming you are running on a *nix platform, use cron and the php executable.

EDIT:

There are quite a number of questions asking for "running php without cron" on SO already. Here's one:

Schedule scripts without using CRON

That said, the exec() answer above sounds very promising :)

Waffle answered 7/1, 2011 at 15:8 Comment(1)
Thank you, but I've update the original question. Cron's are not an option in this case.Cary
S
3

If you can access the server over ssh and can run your own scripts you can make a simple fifo server using php (although you will have to recompile php with posix support for fork).

The server can be written in anything really, you probably can easily do it in python.

Or the simplest solution would be sending an HttpRequest and not reading the return data but the server might destroy the script before it finish processing.

Example server :

<?php
define('FIFO_PATH', '/home/user/input.queue');
define('FORK_COUNT', 10);

if(file_exists(FIFO_PATH)) {
    die(FIFO_PATH . ' exists, please delete it and try again.' . "\n");
}

if(!file_exists(FIFO_PATH) && !posix_mkfifo(FIFO_PATH, 0666)){
    die('Couldn\'t create the listening fifo.' . "\n");
}

$pids = array();
$fp = fopen(FIFO_PATH, 'r+');
for($i = 0; $i < FORK_COUNT; ++$i) {
    $pids[$i] = pcntl_fork();
    if(!$pids[$i]) {
        echo "process(" . posix_getpid() . ", id=$i)\n";
        while(true) {
            $line = chop(fgets($fp));
            if($line == 'quit' || $line === false) break;
            echo "processing (" . posix_getpid() . ", id=$i) :: $line\n";
        //  $data = json_decode($line);
        //  processData($data);
        }
        exit();
    }
}
fclose($fp);
foreach($pids as $pid){
    pcntl_waitpid($pid, $status);
}
unlink(FIFO_PATH);
?>

Example client :

<?php
define('FIFO_PATH', '/home/user/input.queue');
if(!file_exists(FIFO_PATH)) {
    die(FIFO_PATH . ' doesn\'t exist, please make sure the fifo server is running.' . "\n");
}

function postToQueue($data) {
    $fp = fopen(FIFO_PATH, 'w+');
    stream_set_blocking($fp, false); //don't block
    $data = json_encode($data) . "\n";
    if(fwrite($fp, $data) != strlen($data)) {
        echo "Couldn't the server might be dead or there's a bug somewhere\n";
    }
    fclose($fp);
}
$i = 1000;
while(--$i) {
    postToQueue(array('xx'=>21, 'yy' => array(1,2,3)));
}
?>
Schuler answered 7/1, 2011 at 16:26 Comment(0)
I
3

If you're on Windows, research proc_open or popen...

But if we're on the same server "Linux" running cpanel then this is the right approach:

#!/usr/bin/php 
<?php
$pid = shell_exec("nohup nice php -f            
'path/to/your/script.php' /dev/null 2>&1 & echo $!");
While(exec("ps $pid"))
{ //you can also have a streamer here like fprintf,        
 // or fgets
}
?>

Don't use fork() or curl if you doubt you can handle them, it's just like abusing your server

Lastly, on the script.php file which is called above, take note of this make sure you wrote:

<?php
ignore_user_abort(TRUE);
set_time_limit(0);
ob_start();
// <-- really optional but this is pure php

//Code to be tested on background

ob_flush(); flush(); 
//this two do the output process if you need some.        
//then to make all the logic possible


str_repeat(" ",1500); 
//.for progress bars or loading images

sleep(2); //standard limit

?>
Imbed answered 9/5, 2014 at 19:47 Comment(1)
Sorry for editing it subsequently for im using my mobile phone and indeed it's way more harder than writing an exec() script. Lol ;)Imbed
T
3

For background worker, I think you should try this technique. It will help to call as many as pages you like all pages will run at once independently without waiting for each page response as asynchronous.

form_action_page.php

<?php
post_async("http://localhost/projectname/testpage.php", "Keywordname=testValue");
// post_async("http://localhost/projectname/testpage.php", "Keywordname=testValue2");
// post_async("http://localhost/projectname/otherpage.php", "Keywordname=anyValue");
// call as many as pages you like all pages will run at once //independently without waiting for each page response as asynchronous.

// Your form db insertion or other code goes here do what ever you want //above code will work as background job this line will direct hit before //above lines response     

/*
 * Executes a PHP page asynchronously so the current page does not have to wait for it to     finish running.
 */
function post_async($url,$params)
{
    $post_string = $params;

    $parts = parse_url($url);

    $fp = fsockopen($parts['host'],
        isset($parts['port'])?$parts['port']:80,
        $errno, $errstr, 30);

    $out = "GET ".$parts['path']."?$post_string"." HTTP/1.1\r\n";//you can use POST instead of GET if you like
    $out .= "Host: ".$parts['host']."\r\n";
    $out .= "Content-Type: application/x-www-form-urlencoded\r\n";
    $out .= "Content-Length: ".strlen($post_string)."\r\n";
    $out .= "Connection: Close\r\n\r\n";

    fwrite($fp, $out);
    fclose($fp);
}
?>

testpage.php

<?php
echo $_REQUEST["Keywordname"];//case1 Output > testValue
// here do your background operations it will not halt main page
?>

P.S: if you want to send url parameters as loop then follow this answer: https://mcmap.net/q/159693/-how-to-run-the-php-code-asynchronous

Treadwell answered 19/12, 2016 at 15:42 Comment(0)
V
0

In my case I have 3 params, one of them is string (mensaje):

exec("C:\wamp\bin\php\php5.5.12\php.exe C:/test/N/trunk/api/v1/Process.php $idTest2 $idTest3 \"$mensaje\" >> c:/log.log &");

In my Process.php I have this code:

if (!isset($argv[1]) || !isset($argv[2]) || !isset($argv[3]))
{   
    die("Error.");
} 

$idCurso = $argv[1];
$idDestino = $argv[2];
$mensaje = $argv[3];
Vagabondage answered 23/6, 2015 at 18:58 Comment(0)
S
0

This works for me. try this:

exec("php asyn.php > /dev/null 2>/dev/null &);
Sander answered 25/8, 2018 at 10:22 Comment(1)
what if I have post values? from a form and I send it via ajax and serialize() function. for example, I have index.php with form and I send value via ajax to index2.php I want to perform sending data in index2.php in the background what is you suggest? thanksSpurlock
W
0

Use Amphp to execute jobs in parallel & asynchronously.

Install the library

composer require amphp/parallel-functions

Code sample

<?php

require "vendor/autoload.php";

use Amp\Promise;
use Amp\ParallelFunctions;


echo 'started</br>';

$promises[1] = ParallelFunctions\parallel(function (){
    // Send Email
})();

$promises[2] = ParallelFunctions\parallel(function (){
    // Send SMS
})();


Promise\wait(Promise\all($promises));

echo 'finished';

Fo your use case, You can do something like below

<?php

use function Amp\ParallelFunctions\parallelMap;
use function Amp\Promise\wait;

$responses = wait(parallelMap([
    '[email protected]',
    '[email protected]',
    '[email protected]',
], function ($to) {
    return send_mail($to);
}));
Whist answered 21/4, 2020 at 8:14 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.