21

After a user submits a form, I want to render a view file, and right after that I want to initiate a background task to process five MS Excel files (each may have up to 2000 rows) , but in way so that the user don't have to wait for the process to finish in order to view the page. After the task is finished I will inform the user through an email.

I am using Symfony Framework 3. I have included my code below. Its not doing what I am trying to achieve. After submitting the form the page loads only when the entire background task is complete.

I am not sure but after googling a lot, I think The kernel.terminate Event could be useful here. But I can't seem to understand how to work with it.

Could you please tell me how to solve this problem ?

Here's my code:

I have created a Console Command:

class GreetCommand extends ContainerAwareCommand {

  protected function configure()
  {
    $this->setName('hello:world');
  }

  protected function execute(InputInterface $input, OutputInterface $output)
  {
   // My code to execute
  }

}

And in my controller I have

$process = new Process('ls -lsa');
$process->disableOutput();
$that = $this;

$process->start(function ($type, $buffer) use ($that) {
        $command = new GreetCommand();
        $command->setContainer($this->container);
        $input = new ArrayInput(array());
        $output = new NullOutput;
        $resultCode = $command->run($input, $output);

 });

 return $this->render('success.html.php',  array('msg' => 'Registraion Successful'));

Update

I have solved the problem using the connection-handling feature of PHP.

Thanks to this post

Community
  • 1
  • 1
black_belt
  • 6,601
  • 36
  • 121
  • 185
  • 3
    It would be useful to update your post with a little bit of info regarding the implementation of connection-handling feature! – billias Nov 06 '16 at 09:10
  • @mpilliador - you could simply create an event listener on [kernel.terminate](http://symfony.com/doc/current/reference/events.html#kernel-terminate) or run process asynchronously (see Denis Alimov answer below). – Marek Dec 29 '16 at 10:01

7 Answers7

26

Running Processes Asynchronously

You can also start the subprocess and then let it run asynchronously, retrieving output and the status in your main process whenever you need it. Use the start() method to start an asynchronous process

documentation

so, to start your command asynchronously you should create new process with command and start it

$process = new Process('php bin/console hello:word');
$process->start();

Consider to change this to full paths like \usr\bin\php \var\www\html\bin\console hello:word

Also there is good bundle cocur/background-process you may use it, or at least read the docs to find out how it works.

Denis Alimov
  • 2,861
  • 1
  • 18
  • 38
  • Thank you very much for your answer. I already tried the above code, but its still waiting for the process to finish and then rendering the page. Do you think its because I am on windows, may be if I move to Linux it would work? – black_belt Jun 10 '16 at 12:21
  • 1
    @black_belt yep, this maybe the cause of this. – Denis Alimov Jun 10 '16 at 12:35
  • 2
    @black_belt, no. it is not because you are on windows. Your main process is the application that launches the process. The output stream of sub-process is attached to the main process and it will block application process from termination until child press finishes. – lazycommit Nov 08 '17 at 16:04
  • 2
    This answer does not sound right. It says in the doc: "If a Response is sent before a child process had a chance to complete, the server process will be killed (depending on your OS)." That's the opposite of what the question asked for. – Alexander Rechsteiner May 22 '18 at 14:31
  • 1
    @Alexander Rechsteiner you are looking at Symfony 4 documentation. 2 years ago we were talking about Symfony 2 I guess. – Denis Alimov May 22 '18 at 15:29
  • @DenisAlimov: I'm not blaming you but it seems that this answer is now outdated and suggests something that is explicitly advised against in the documentation that is linked. – Alexander Rechsteiner May 22 '18 at 16:17
  • @Alexander Rechsteiner Yes, looks so – Denis Alimov May 22 '18 at 19:56
  • Official docs say: "Running an asynchronous process is not the same as running a process that survives its parent process." And then: *If you want your process to survive the request/response cycle, you can take advantage of the kernel.terminate event, and run your command synchronously inside this event. Be aware that kernel.terminate is called only if you use PHP-FPM.* So, if you're running PHP as a module in Windows (like it is in WAMP), the background process won't work. You need to have the PHP running as PHP-FPM to have it work. I'm not sure is it enough to have PHP running as FCGI. – userfuser Jul 22 '20 at 11:22
9

I am a bit late to the game, but I just found a solution for this problem using the fromShellCommandLine() method:

use Symfony\Component\Process\Process;

Process::fromShellCommandline('/usr/bin/php /var/www/bin/console hello:world')->start();

This way it is possible to start a new process/run a command asynchronously.

Michal
  • 91
  • 1
  • 2
7

For using in controller:

use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpKernel\Event\PostResponseEvent;

$myVar = new MyObject();
$this->get('event_dispatcher')->addListener(KernelEvents::TERMINATE, function(PostResponseEvent $event) use($myVar) {
    //You logic here
    $request = $event->getRequest();
    $test = $myVar->getMyStuff();
});

But it is not a good practice, please read about normal registering event listeners

kernel.terminate event will be dispatched after sending the response to user.

Arthur
  • 2,869
  • 14
  • 25
5

There is a bundle called AsyncServiceCallBundle which allows you to call your service's methods in background.

You can refer this answer for more details about how it is done internally. Everything you need is to invoke your service's method as follows:

$this->get('krlove.async')->call('service_id', 'method', [$arg1, $arg2]);
Andrii Mishchenko
  • 2,626
  • 21
  • 19
5

Simple use Symfony Process Options

create_new_console

From Symfony Process Source:

 /**
  * Defines options to pass to the underlying proc_open().
  *
  * @see https://php.net/proc_open for the options supported by PHP.
  *
  * Enabling the "create_new_console" option allows a subprocess to continue
  * to run after the main process exited, on both Windows and *nix
  */
  public function setOptions(array $options)

So lets do it this way:

$process = new Process($cmds);
$process->setOptions(['create_new_console' => true]);
$process->start();
teddds
  • 51
  • 1
  • 2
  • On windows, it launches command prompt. Is there any way to avoid opening command prompt. – Dinesh Belkare Feb 25 '22 at 10:23
  • I couldn't find a way from PHP. Anyway, even if you close the prompt manually, your process will still keep running. So perhaps it's possible by analyzing windows taskmanager and calling some specific windows task command in shell. Have a look at https://superuser.com/a/1189987 – teddds Feb 26 '22 at 14:09
4

I guess what you want is to store the necessary data in a database and then have a cronjob/queue execute the actual command, instead of trying to execute it directly from your controller.

Add something like the following to your /etc/crontab to let it run your command every 5 minutes

*/5 * * * * root /usr/bin/php /path/to/bin/console hello:world

Then, let your command query the database for the stored data and have it process the excel files

rvanlaarhoven
  • 591
  • 6
  • 16
0

Even though it's already answered,
I post here my working solution if any one is need of.
In my Controller

public function exportDossiersAction(Request $request)
{    
    $form = $this->createForm(ExportType::class, null, []);
    $form->handleRequest($request);

    if ($form->isSubmitted() && $form->isValid()) {
        $filters = ["status" => $form->get('status')->getData()];

        $phpBinaryFinder = new PhpExecutableFinder();
        $phpBinaryPath = $phpBinaryFinder->find();
        $projectRoot = $this->get('kernel')->getProjectDir();

        $process = new Process([$phpBinaryPath, $projectRoot . '/bin/console', 'myapp:files:export', json_encode($filters)]);
        $process->setTimeout(36000); //10min

        // let the process run in the background
        $this->get('event_dispatcher')->addListener(KernelEvents::TERMINATE, function() use($process) {
            $process->start();
            $process->wait();
        });

       // render this messsage in twig
        $this->addFlash("success", "The export files is initiated");

        return $this->redirectToRoute('home');
     }

   return $this->render('$PATH/export.html.twig', [
        'form' => $form->createView()
    ]);
}
Theva
  • 873
  • 8
  • 15