Comment puis-je exécuter un script PHP en arrière-plan après l'envoi d'un formulaire?

104

Problème
J'ai un formulaire qui, une fois soumis, exécutera un code de base pour traiter les informations soumises et l'insérer dans une base de données pour l'afficher sur un site Web de notification. De plus, j'ai une liste de personnes qui se sont inscrites pour recevoir ces notifications par e-mail et SMS. Cette liste est triviale pour le moment (seulement environ 150), mais elle suffit pour que cela prenne plus d'une minute pour parcourir toute la table des abonnés et envoyer plus de 150 e-mails. (Les e-mails sont envoyés individuellement à la demande des administrateurs système de notre serveur de messagerie en raison des politiques de messagerie de masse.)

Pendant ce temps, l'individu qui a publié l'alerte restera assis sur la dernière page du formulaire pendant près d'une minute sans aucune confirmation positive que sa notification est affichée. Cela conduit à d'autres problèmes potentiels, tout ce qui a des solutions possibles que je trouve loin d'être idéales.

  1. Tout d'abord, l'affiche peut penser que le serveur est à la traîne et cliquer à nouveau sur le bouton «Soumettre», provoquant le redémarrage ou l'exécution du script deux fois. Je pourrais résoudre ce problème en utilisant JavaScript pour désactiver le bouton et remplacer le texte pour dire quelque chose comme «Traitement ...», mais ce n'est pas idéal car l'utilisateur sera toujours bloqué sur la page pendant toute la durée de l'exécution du script. (De plus, si JavaScript est désactivé, ce problème existe toujours.)

  2. Deuxièmement, l'affiche peut fermer l'onglet ou le navigateur prématurément après avoir soumis le formulaire. Le script continuera à s'exécuter sur le serveur jusqu'à ce qu'il essaie de réécrire dans le navigateur, mais si l'utilisateur navigue ensuite sur une page de notre domaine (pendant que le script est toujours en cours d'exécution), le navigateur suspend le chargement de la page jusqu'à ce que le script soit terminé . (Cela ne se produit que lorsqu'un onglet ou une fenêtre du navigateur est fermé et non pas l'ensemble de l'application du navigateur.) Pourtant, c'est loin d'être idéal.

(Possible) Solution
J'ai décidé de séparer la partie "e-mail" du script dans un fichier séparé que je pourrai appeler une fois la notification publiée. J'ai initialement pensé à le mettre sur la page de confirmation une fois la notification publiée avec succès. Cependant, l'utilisateur ne saura pas que ce script est en cours d'exécution et aucune anomalie ne lui sera apparente; Ce script ne peut pas échouer.

Mais que se passe-t-il si je peux exécuter ce script en tant que processus d'arrière-plan? Donc, ma question est la suivante: comment puis-je exécuter un script PHP pour se déclencher en tant que service d'arrière-plan et s'exécuter complètement indépendamment de ce que l'utilisateur a fait au niveau du formulaire?

EDIT: Cela ne peut pas être cron'ed. Il doit s'exécuter au moment où le formulaire est soumis. Ce sont des notifications hautement prioritaires. De plus, les administrateurs système exécutant nos serveurs interdisent aux crons de s'exécuter plus de 5 minutes.

Michael Irigoyen
la source
Démarrez une tâche cron et alertez l'utilisateur sur une autre page que sa demande est en cours de traitement, ou quelque chose du genre.
Evan Mulawski
1
Voulez-vous l'exécuter régulièrement (tous les jours ou toutes les heures) ou sur soumission? Je suppose que vous pourriez utiliser php exec()mais je n'ai jamais essayé cela.
Simon le
2
J'utilise simplement ajaxet j'insère sleep()avec un intervalle de temps dans la boucle (j'utilise foreach dans mon cas) pour exécuter le processus en arrière-plan. Cela fonctionne parfaitement sur mon serveur. Pas sûr des autres. J'ajoute également ignore_user_abort()et set_time_limit()(avec l'heure de fin calculée) avant la boucle pour m'assurer que le script ne sera pas arrêté par le serveur que j'utilise, même selon mon test, le script complétant la tâche sans ignore_user_abort()et set_time_limit().
Ari
J'ai vu que vous aviez déjà trouvé une solution. J'utilise gearmanavec php pour gérer les tâches en arrière-plan. Il est parfait pour évoluer si vous rencontrez un traitement fastidieux et des charges lourdes. Vous devriez y jeter un œil.
Andre Garcia
Suivez cette réponse sur [stackoverflow] ( stackoverflow.com/a/46617107/6417678 )
Joshy Francis

Réponses:

89

Faire quelques expérimentations avec execet shell_execj'ai découvert une solution qui fonctionnait parfaitement! Je choisis d'utiliser shell_execafin de pouvoir enregistrer chaque processus de notification qui se produit (ou non). ( shell_execretourne sous forme de chaîne et c'était plus facile que d'utiliser exec, d'assigner la sortie à une variable puis d'ouvrir un fichier dans lequel écrire.)

J'utilise la ligne suivante pour appeler le script d'e-mail:

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

Il est important de noter le &à la fin de la commande (comme indiqué par @netcoder). Cette commande UNIX exécute un processus en arrière-plan.

Les variables supplémentaires entourées de guillemets simples après le chemin du script sont définies comme des $_SERVER['argv']variables que je peux appeler dans mon script.

Le script de courrier électronique sort ensuite dans mon fichier journal à l'aide de >>et produira quelque chose comme ceci:

[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)
Michael Irigoyen
la source
Merci. Cela m'a sauvé la vie.
Liam Bailey
Qu'est-ce que c'est $post_idaprès le chemin du script php?
Perocat
@Perocat c'est une variable que vous envoyez au script. Dans ce cas, il envoie un ID qui sera un paramètre du script appelé.
Chique
Je doute que cela fonctionne parfaitement, voir stackoverflow.com/questions/2212635/…
symcbean
1
Il y a une suggestion d'édition en attente pour échapper $post_id; Je préférerais jeter directement à un numéro: (int) $post_id. (Appel à votre attention pour décider ce qui est le mieux.)
Al.G.
19

Sur les serveurs Linux / Unix, vous pouvez exécuter un travail en arrière-plan en utilisant 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);

L' &être le plus important ici. Le script continuera même si le script d'origine est terminé.

netcoder
la source
Cela fonctionne lorsque je n'appelle pas proc_closelorsque la tâche est terminée. proc_closefait que le script se bloque environ 15 à 25 secondes. J'aurais besoin de garder un journal en utilisant les informations de pipe, donc je dois ouvrir un fichier sur lequel écrire, le fermer, puis fermer la connexion proc. Donc, malheureusement, cette solution ne fonctionne pas pour moi.
Michael Irigoyen
3
Bien sûr, vous n'appelez pas proc_close, c'est le point! Et oui, vous pouvez rediriger vers un fichier avec proc_open. Voir la réponse mise à jour.
netcoder
@netcoder: que faire si je ne veux pas stocker le résultat dans un fichier. quels changements sont nécessaires dans stdout?
naggarwal11
9

De toutes les réponses, aucune n'a considéré la fonction ridiculement facile fastcgi_finish_request , qui, lorsqu'elle est appelée, vide toute la sortie restante vers le navigateur et ferme la session Fastcgi et la connexion HTTP, tout en laissant le script s'exécuter en arrière-plan.

Un exemple:

<?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,
Danogentili
la source
c'est exactement ce que je cherchais, dans shell_exec, je dois en quelque sorte transférer les données publiées dans un script exécuté, ce qui en soi est une tâche fastidieuse dans mon cas et rend également les choses très compliquées.
Aurangzeb
7

PHP exec("php script.php")peut le faire.

À partir du manuel :

Si un programme est démarré avec cette fonction, pour qu'il continue à s'exécuter en arrière-plan, la sortie du programme doit être redirigée vers un fichier ou un autre flux de sortie. Ne pas le faire entraînera le blocage de PHP jusqu'à la fin de l'exécution du programme.

Donc, si vous redirigez la sortie vers un fichier journal (ce qui est une bonne idée de toute façon), votre script d'appel ne se bloquera pas et votre script de courrier électronique s'exécutera dans bg.

Simon
la source
1
Merci, mais cela n'a pas fonctionné. Le script «se bloque» toujours pendant qu'il traite le deuxième fichier.
Michael Irigoyen
Regardez la recommandation php.net/manual/en/function.exec.php#80582 On dirait que c'est parce que Safe_mode est activé. Il existe une solution de contournement sous unix.
Simon
Malheureusement, le mode sans échec n'est pas activé sur notre installation. C'est étrange qu'il soit toujours suspendu.
Michael Irigoyen
6

Et pourquoi ne pas faire une requête HTTP sur le script et ignorer la réponse?

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

Si vous faites votre demande sur le script, vous devez appeler votre serveur Web qui l'exécutera en arrière-plan et vous pourrez (dans votre script principal) afficher un message indiquant à l'utilisateur que le script est en cours d'exécution.

Tornade
la source
4

Que dis-tu de ça?

  1. Votre script PHP qui contient le formulaire enregistre un indicateur ou une valeur dans une base de données ou un fichier.
  2. Un deuxième script PHP interroge périodiquement cette valeur et si elle a été définie, il déclenche le script Email de manière synchrone.

Ce second script PHP doit être configuré pour s'exécuter en tant que cron.

Adarshr
la source
Merci, mais j'ai mis à jour la question d'origine. Les Cron ne sont pas une option dans ce cas.
Michael Irigoyen
4

Comme je sais que vous ne pouvez pas le faire de manière simple (voir fork exec, etc. (ne fonctionne pas sous Windows)), vous pouvez peut-être inverser l'approche, utiliser l'arrière-plan du navigateur en postant le formulaire en ajax, donc si le message est toujours travail, vous n'avez pas de temps d'attente.
Cela peut aider même si vous devez faire une longue élaboration.

À propos de l'envoi de courrier, il est toujours suggéré d'utiliser un spouleur, peut être un serveur smtp local et rapide qui accepte vos demandes et les spoule vers le vrai MTA ou les met tout dans une base de données, plutôt que d'utiliser un cron qui spoule la file d'attente.
Le cron peut être sur une autre machine appelant le spouleur en tant qu'URL externe:

* * * * * wget -O /dev/null http://www.example.com/spooler.php
Ivan Buttinoni
la source
3

Le travail cron en arrière-plan semble être une bonne idée pour cela.

Vous aurez besoin d'un accès ssh à la machine pour exécuter le script en tant que cron.

$ php scriptname.php pour l'exécuter.

Ian
la source
1
pas besoin de ssh. De nombreux hôtes n'autorisent pas ssh, mais ont le support cron.
Teson le
Merci, mais j'ai mis à jour la question d'origine. Les Cron ne sont pas une option dans ce cas.
Michael Irigoyen
3

Si vous pouvez accéder au serveur via ssh et exécuter vos propres scripts, vous pouvez créer un simple serveur fifo en utilisant php (bien que vous devrez recompiler php avec le posixsupport de fork).

Le serveur peut vraiment être écrit dans n'importe quoi, vous pouvez probablement le faire facilement en python.

Ou la solution la plus simple serait d'envoyer une HttpRequest et de ne pas lire les données de retour, mais le serveur pourrait détruire le script avant la fin du traitement.

Exemple de serveur:

<?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);
?>

Exemple de 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)));
}
?>
OneOfOne
la source
3

Le moyen le plus simple d'exécuter un script PHP en arrière-plan est

php script.php >/dev/null &

Le script s'exécutera en arrière-plan et la page atteindra également la page d'action plus rapidement.

Sandeep Dhanda
la source
2

En supposant que vous exécutez sur une plate-forme * nix, utilisez cronet l' phpexécutable.

ÉDITER:

Il y a déjà un certain nombre de questions demandant "exécuter php sans cron" sur SO. En voici une:

Planifier des scripts sans utiliser CRON

Cela dit, la réponse exec () ci-dessus semble très prometteuse :)

Norman épineux
la source
Merci, mais j'ai mis à jour la question d'origine. Les Cron ne sont pas une option dans ce cas.
Michael Irigoyen
2

Si vous êtes sous Windows, recherchez proc_openou popen...

Mais si nous sommes sur le même serveur "Linux" exécutant cpanel, alors c'est la bonne approche:

#!/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
}
?>

N'utilisez pas fork()ou curlsi vous doutez de pouvoir les gérer, c'est comme abuser de votre serveur

Enfin, sur le script.phpfichier qui est appelé ci-dessus, prenez note de ceci assurez-vous d'avoir écrit:

<?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

?>
je suis ArbZ
la source
Désolé de l'éditer par la suite car j'utilise mon téléphone portable et en effet c'est beaucoup plus difficile que d'écrire un script exec (). Lol;)
je suis ArbZ
2

pour le travailleur d'arrière-plan, je pense que vous devriez essayer cette technique, cela vous aidera à appeler autant de pages que vous aimez toutes les pages fonctionneront à la fois indépendamment sans attendre que chaque page de réponse soit asynchrone.

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     
                ?>
                <?php

                /*
                 * 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

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

PS: si vous souhaitez envoyer des paramètres d'url en boucle, suivez cette réponse: https://stackoverflow.com/a/41225209/6295712

Hassan Saeed
la source
0

Dans mon cas, j'ai 3 paramètres, l'un d'eux est 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 &");

Dans mon Process.php j'ai ce code:

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

$idCurso = $argv[1];
$idDestino = $argv[2];
$mensaje = $argv[3];
Hernaldo Gonzalez
la source
0

Cela fonctionne pour moi. tyr ceci

exec(“php asyn.php”.” > /dev/null 2>/dev/null &“);
Raj
la source
et si j'ai des valeurs de publication? à partir d'un formulaire et je l'envoie via ajax et serialize()function. par exemple, j'ai index.php avec formulaire et j'envoie une valeur via ajax à index2.php Je veux effectuer l'envoi de données dans index2.php en arrière-plan que suggérez-vous? merci
khalid JA
0

Utilisez Amphp pour exécuter des travaux en parallèle et de manière asynchrone.

Installez la bibliothèque

composer require amphp/parallel-functions

Exemple de code

<?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';

Pour votre cas d'utilisation, vous pouvez faire quelque chose comme ci-dessous

<?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);
}));
SkyRar
la source