Como posso executar um script PHP em segundo plano após o envio de um formulário?

104

Problema
Eu tenho um formulário que, quando enviado, irá executar um código básico para processar as informações enviadas e inseri-las em um banco de dados para exibição em um site de notificação. Além disso, tenho uma lista de pessoas que se inscreveram para receber essas notificações por e-mail e mensagem SMS. Esta lista é trivial como o momento (empurrando apenas cerca de 150), no entanto, é o suficiente para fazer com que demore mais de um minuto para percorrer toda a tabela de assinantes e enviar mais de 150 emails. (Os emails estão sendo enviados individualmente conforme solicitado pelos administradores do sistema de nosso servidor de email devido às políticas de email em massa.)

Durante esse tempo, o indivíduo que postou o alerta ficará sentado na última página do formulário por quase um minuto, sem qualquer reforço positivo de que sua notificação está sendo postada. Isso leva a outros problemas potenciais, todos com soluções possíveis que considero menos do que ideais.

  1. Primeiro, o autor da postagem pode pensar que o servidor está atrasado e clicar no botão 'Enviar' novamente, fazendo com que o script reinicie ou execute duas vezes. Eu poderia resolver isso usando JavaScript para desabilitar o botão e substituir o texto para dizer algo como 'Processando ...', no entanto, isso é menos do que o ideal porque o usuário ainda ficará preso na página durante a execução do script. (Além disso, se o JavaScript estiver desativado, o problema ainda existirá.)

  2. Em segundo lugar, o autor da postagem pode fechar a guia ou o navegador prematuramente após enviar o formulário. O script continuará em execução no servidor até tentar escrever de volta para o navegador, no entanto, se o usuário navegar para qualquer página em nosso domínio (enquanto o script ainda está em execução), o navegador para de carregar a página até que o script termine . (Isso só acontece quando uma guia ou janela do navegador é fechada e não todo o aplicativo do navegador.) Ainda assim, isso não é o ideal.

(Possível) Solução
Decidi dividir a parte "e-mail" do script em um arquivo separado que possa chamar depois que a notificação for postada. Originalmente, pensei em colocar isso na página de confirmação depois que a notificação foi postada com sucesso. No entanto, o usuário não saberá que este script está sendo executado e quaisquer anomalias não serão aparentes para ele; Este script não pode falhar.

Mas, e se eu puder executar esse script como um processo em segundo plano? Portanto, minha pergunta é a seguinte: Como posso executar um script PHP para acionar como um serviço de segundo plano e ser executado de forma completamente independente do que o usuário fez no nível do formulário?

EDIT: Isso não pode ser cron'ed. Ele deve ser executado no instante em que o formulário for enviado. Estas são notificações de alta prioridade. Além disso, os administradores de sistema que executam nossos servidores não permitem que o crons seja executado com mais frequência que 5 minutos.

Michael Irigoyen
fonte
Inicie um cron job e alerte o usuário em outra página que sua solicitação está sendo processada ou algo parecido.
Evan Mulawski
1
Você deseja executá-lo regularmente (todos os dias ou a cada hora) ou no envio? Eu acho que você poderia usar php, exec()mas nunca tentei isso.
Simon
2
Eu simplesmente uso ajaxe insiro sleep()com intervalo de tempo dentro do loop (eu uso foreach no meu caso) para executar o processo em segundo plano. Funciona perfeitamente no meu servidor. Não tenho certeza sobre os outros. Também estou adicionando ignore_user_abort()e set_time_limit()(com hora de término calculada) antes do loop para ter certeza de que o script não será interrompido pelo servidor que uso, mesmo de acordo com meu teste, o script que conclui a tarefa sem ignore_user_abort()e set_time_limit().
Ari
Eu vi que você já encontrou uma solução. Eu uso gearmancom php para lidar com tarefas em segundo plano. É perfeito para escalar se você se deparar com um grande processamento e cargas pesadas. Você deveria dar uma olhada nisso.
Andre Garcia
Siga esta resposta em [stackoverflow] ( stackoverflow.com/a/46617107/6417678 )
Joshy Francis

Respostas:

89

Fazendo algumas experiências com exec e shell_execeu ter descoberto uma solução que funcionou perfeitamente! Eu escolho usar shell_execpara poder registrar todos os processos de notificação que acontecem (ou não). ( shell_execretorna como uma string e isso era mais fácil do que usar exec, atribuindo a saída a uma variável e, em seguida, abrindo um arquivo para gravar.)

Estou usando a seguinte linha para invocar o script de e-mail:

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

É importante observar o &no final do comando (como apontado por @netcoder). Este comando UNIX executa um processo em segundo plano.

As variáveis ​​extras entre aspas simples após o caminho para o script são definidas como $_SERVER['argv']variáveis ​​que posso chamar dentro do meu script.

O script de e-mail então sai para o meu arquivo de log usando o >>e vai gerar algo assim:

[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
fonte
Obrigado. Isso salvou minha vida.
Liam Bailey
O que é isso $post_iddepois do caminho do script php?
Perocat de
@Perocat é uma variável que você está enviando para o script. Neste caso está enviando um ID que será um parâmetro para o script que está sendo chamado.
Chique,
Duvido que funcione perfeitamente, consulte stackoverflow.com/questions/2212635/…
symcbean
1
Há uma sugestão de edição pendente para escapar $post_id; Eu prefiro lançá-lo diretamente para um número: (int) $post_id. (Chamando sua atenção para decidir o que é melhor.)
Al.G.
19

Em servidores Linux / Unix, você pode executar uma tarefa em segundo plano usando 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);

O &ser a parte importante aqui. O script continuará mesmo se o script original tiver terminado.

netcoder
fonte
Isso funciona quando eu não ligo proc_closequando a tarefa é concluída. proc_closefaz o script travar por cerca de 15 a 25 segundos. Eu precisaria manter um log usando as informações do pipe, então eu tenho que abrir um arquivo a ser escrito, fechá-lo e, em seguida, fechar a conexão proc. Então, infelizmente, essa solução não funciona para mim.
Michael Irigoyen
3
Claro que você não liga proc_close, esse é o ponto! E sim, você pode enviar para um arquivo com proc_open. Veja a resposta atualizada.
netcoder
@netcoder: e se eu não quiser armazenar o resultado em nenhum arquivo. quais mudanças são necessárias no stdout?
naggarwal11
9

De todas as respostas, nenhuma considerou a ridiculamente fácil fastcgi_finish_request função , que quando chamada, libera toda a saída restante para o navegador e fecha a sessão Fastcgi e a conexão HTTP, enquanto permite que o script seja executado em segundo plano.

Um exemplo:

<?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
fonte
isso é exatamente o que eu estava procurando, em shell_exec eu tenho que transferir de alguma forma os dados postados para o script executado, o que é uma tarefa demorada no meu caso e também torna as coisas muito complicadas.
Aurangzeb,
7

PHP exec("php script.php")pode fazer isso.

Do Manual :

Se um programa é iniciado com esta função, para que ele continue em execução em segundo plano, a saída do programa deve ser redirecionada para um arquivo ou outro fluxo de saída. Não fazer isso fará com que o PHP trave até que a execução do programa termine.

Portanto, se você redirecionar a saída para um arquivo de log (o que é uma boa idéia de qualquer maneira), seu script de chamada não travará e seu script de email será executado em bg.

Simon
fonte
1
Obrigado, mas isso não funcionou. O script ainda "trava" enquanto processa o segundo arquivo.
Michael Irigoyen
Veja o elogio php.net/manual/en/function.exec.php#80582 Parece que isso ocorre porque Safe_mode está habilitado. Existe uma solução alternativa no UNIX.
Simon
Infelizmente, o modo de segurança não está habilitado em nossa instalação. É estranho que ainda esteja pendurado.
Michael Irigoyen
6

E por que não fazer uma solicitação HTTP no script e ignorar a resposta?

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

Se você fizer sua solicitação no script, você precisará chamar o seu servidor web que o executará em segundo plano e você poderá (no script principal) mostrar uma mensagem informando ao usuário que o script está sendo executado.

Twister
fonte
4

Que tal agora?

  1. Seu script PHP que contém o formulário salva uma bandeira ou algum valor em um banco de dados ou arquivo.
  2. Um segundo script PHP pesquisa esse valor periodicamente e, se tiver sido definido, aciona o script Email de maneira síncrona.

Este segundo script PHP deve ser configurado para ser executado como um cron.

Adarshr
fonte
Obrigado, mas eu atualizei a pergunta original. Cron's não são uma opção neste caso.
Michael Irigoyen
4

Como eu sei que você não pode fazer isso de maneira fácil (ver fork exec etc (não funciona no windows)), pode ser que você pode inverter a abordagem, use o fundo do navegador postando o formulário em ajax, portanto, se o post ainda trabalho você não tem tempo de espera.
Isso pode ajudar mesmo que você precise fazer uma longa elaboração.

Sobre o envio de email é sempre sugerido usar um spooler, pode ser um servidor smtp local & quick que aceita seus pedidos e os envia para o MTA real ou coloca tudo em um banco de dados, do que usar um cron que faz o spool da fila.
O cron pode estar em outra máquina chamando o spooler como url externo:

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

O cron job em segundo plano parece uma boa ideia para isso.

Você precisará de acesso ssh à máquina para executar o script como um cron.

$ php scriptname.php para executá-lo.

Ian
fonte
1
não há necessidade de ssh. Muitos hosts não permitem o ssh, mas têm suporte para cron.
Teson
Obrigado, mas eu atualizei a pergunta original. Cron's não são uma opção neste caso.
Michael Irigoyen
3

Se você pode acessar o servidor via ssh e pode executar seus próprios scripts, você pode fazer um servidor fifo simples usando php (embora você tenha que recompilar o php com posixsuporte parafork ).

O servidor pode ser escrito em qualquer coisa, você provavelmente pode fazer isso facilmente em python.

Ou a solução mais simples seria enviar um HttpRequest e não ler os dados de retorno, mas o servidor pode destruir o script antes de terminar o processamento.

Servidor de exemplo:

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

Cliente de exemplo:

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

A maneira mais simples de executar um script PHP em segundo plano é

php script.php >/dev/null &

O script será executado em segundo plano e a página também alcançará a página de ação mais rapidamente.

Sandeep Dhanda
fonte
2

Supondo que você esteja executando em uma plataforma * nix, use crone o phpexecutável.

EDITAR:

Já existem várias questões pedindo para "rodar php sem cron" no SO. Aqui está um:

Agende scripts sem usar CRON

Dito isso, a resposta exec () acima parece muito promissora :)

Spiny Norman
fonte
Obrigado, mas eu atualizei a pergunta original. Cron's não são uma opção neste caso.
Michael Irigoyen
2

Se você estiver no Windows, pesquise proc_openou popen...

Mas se estivermos no mesmo servidor "Linux" executando cpanel, então esta é a abordagem certa:

#!/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ão use fork()ou curlse você duvida que pode lidar com eles, é como abusar do seu servidor

Por último, no script.phparquivo chamado acima, tome nota disso, certifique-se de ter escrito:

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

?>
eu sou ArbZ
fonte
Desculpe por editá-lo posteriormente porque estou usando meu telefone celular e, de fato, é muito mais difícil do que escrever um script exec (). Lol;)
sou ArbZ
2

para o trabalhador em segundo plano, acho que você deve tentar essa técnica. Ela ajudará a chamar quantas páginas quiser, todas as páginas serão executadas de uma vez, independentemente, sem esperar por cada resposta de página como assíncrona.

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: se você deseja enviar parâmetros de url como loop, siga esta resposta: https://stackoverflow.com/a/41225209/6295712

Hassan Saeed
fonte
0

No meu caso, tenho 3 parâmetros, um deles é 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 &");

No meu Process.php, tenho este código:

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

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

Isso funciona para mim. tirem isso

exec(“php asyn.php”.” > /dev/null 2>/dev/null &“);
Raj
fonte
e se eu tiver valores de postagem? de um formulário e envio via ajax e serialize()função. por exemplo, tenho index.php com form e envio valor via ajax para index2.php quero realizar o envio de dados em index2.php em background o que você sugere? obrigado
khalid JA
0

Use Amphp para executar trabalhos em paralelo e de forma assíncrona.

Instale a biblioteca

composer require amphp/parallel-functions

Amostra de código

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

Para o seu caso de uso, você pode fazer algo como a seguir

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