0

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.

2
  • 2
    Take a look to the Messenger component instead. Commented May 11, 2020 at 16:48
  • 2
    There are no good reasons to trigger a long running, usually unkillable command via request directly (since every request would generate a new one, which could kill the server while providing NO benefit). Instead, you might use a worker process that is run on the server, on its own, all the time, to which you can send the task of building your file, which then can call the command or whatever as you normally would, and when you kill the worker, you kill the processes. you can also do parallel stuff, but a worker IMHO is the way to go. comm with the worker via messenger component is nice. Commented May 11, 2020 at 17:54

1 Answer 1

2

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();
}
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.