LucileDT
LucileDT

Reputation: 636

How to execute a Symfony command in background from a controller

I have a command which take a long time to run (it generates a big file).

I would like to use a controller to start it in background and don't wait for the end of its execution to render a view.

Is it possible? If yes, how?

I though the Process class would be useful but the documentation says:

If a Response is sent before a child process had a chance to complete, the server process will be killed (depending on your OS). It means that your task will be stopped right away. Running an asynchronous process is not the same as running a process that survives its parent process.

Upvotes: 0

Views: 2725

Answers (1)

LucileDT
LucileDT

Reputation: 636

I solved my problem using the Messenger component as @msg suggested in comments.

To do so, I had to:

  • install the Messenger component by doing composer require symfony/messenger
  • create a custom log entity to track the file generation
  • create a custom Message and a custom MessageHandler for my file generation
  • dispatch the Message in my controller view
  • move my command code to a service method
  • call the service method in my MessageHandler
  • run bin/console messenger:consume -vv to handle the messages

Here is my code:

Custom log entity

I use it to show in my views if a file is being generated and to let the user download the file if its generation is complete

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity(repositoryClass="App\Repository\MyLogForTheBigFileRepository")
 */
class MyLogForTheBigFile
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\Column(type="datetime")
     */
    private $generationDateStart;

    /**
     * @ORM\Column(type="datetime", nullable=true)
     */
    private $generationDateEnd;

    /**
     * @ORM\Column(type="string", length=200, nullable=true)
     */
    private $filename;

    /**
     * @ORM\ManyToOne(targetEntity="App\Entity\User")
     * @ORM\JoinColumn(nullable=false)
     */
    private $generator;

    public function __construct() { }

    // getters and setters for the attributes
    // ...
    // ...
}

Controller

I get the form submission and dispatch a message which will run the file generation

/**
 * @return views
 * @param Request $request The request.
 * @Route("/generate/big-file", name="generate_big_file")
 */
public function generateBigFileAction(
    Request $request,
    MessageBusInterface $messageBus,
    MyFileService $myFileService
)
{
    // Entity manager
    $em = $this->getDoctrine()->getManager();

    // Creating an empty Form Data Object
    $myFormOptionsFDO = new MyFormOptionsFDO();

    // Form creation
    $myForm = $this->createForm(
        MyFormType::class,
        $myFormOptionsFDO
    );

    $myForm->handleRequest($request);

    // Submit
    if ($myForm->isSubmitted() && $myForm->isValid())
    {
        $myOption = $myFormOptionsFDO->getOption();

        // Creating the database log using a custom entity 
        $myFileGenerationDate = new \DateTime();
        $myLogForTheBigFile = new MyLogForTheBigFile();
        $myLogForTheBigFile->setGenerationDateStart($myFileGenerationDate);
        $myLogForTheBigFile->setGenerator($this->getUser());
        $myLogForTheBigFile->setOption($myOption);

        // Save that the file is being generated using the custom entity
        $em->persist($myLogForTheBigFile);
        $em->flush();

        $messageBus->dispatch(
                new GenerateBigFileMessage(
                        $myLogForTheBigFile->getId(),
                        $this->getUser()->getId()
        ));

        $this->addFlash(
                'success', 'Big file generation started...'
        );

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

    return $this->render('Files/generate-big-file.html.twig', [
        'form' => $myForm->createView(),
    ]);
}

Message

Used to pass data to the service


namespace App\Message;


class GenerateBigFileMessage
{
    private $myLogForTheBigFileId;
    private $userId;

    public function __construct(int $myLogForTheBigFileId, int $userId)
    {
        $this->myLogForTheBigFileId = $myLogForTheBigFileId;
        $this->userId = $userId;
    }

    public function getMyLogForTheBigFileId(): int
    {
        return $this->myLogForTheBigFileId;
    }

    public function getUserId(): int
    {
        return $this->userId;
    }
}

Message handler

Handle the message and run the service

namespace App\MessageHandler;

use App\Service\MyFileService;
use App\Message\GenerateBigFileMessage;
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;

class GenerateBigFileMessageHandler implements MessageHandlerInterface
{
    private $myFileService;

    public function __construct(MyFileService $myFileService)
    {
        $this->myFileService = $myFileService;
    }

    public function __invoke(GenerateBigFileMessage $generateBigFileMessage)
    {
        $myLogForTheBigFileId = $generateBigFileMessage->getMyLogForTheBigFileId();
        $userId = $generateBigFileMessage->getUserId();
        $this->myFileService->generateBigFile($myLogForTheBigFileId, $userId);
    }
}

Service

Generate the big file and update the logger

public function generateBigFile($myLogForTheBigFileId, $userId)
{
    // Get the user asking for the generation
    $user = $this->em->getRepository(User::class)->find($userId);

    // Get the log object corresponding to this generation
    $myLogForTheBigFile = $this->em->getRepository(MyLogForTheBigFile::class)->find($myLogForTheBigFileId);
    $myOption = $myLogForTheBigFile->getOption();

    // Generate the file
    $fullFilename = 'my_file.pdf';
    // ...
    // ...

    // Update the log
    $myLogForTheBigFile->setGenerationDateEnd(new \DateTime());
    $myLogForTheBigFile->setFilename($fullFilename);

    $this->em->persist($myLogForTheBigFile);
    $this->em->flush();
}

Upvotes: 2

Related Questions