Search code examples
phpsymfony

How to force a download using Symfony 6's forward function?


The problem is to send data from one controller to another controller using Symfony's forward function and download CSV file.

In my first controller, I perform a query bounded by dates supplied by the operator.

#[Route('/admin/fuente/exportar0/', name: 'app_exportar0_fuente', methods: ['GET', 'POST'])]
    public function exportar0(Request $request, FuenteRepository $fuenteRepository, CSVResponse $CSVResponse): Response
    {
           $defaultData = [];
    $form = $this->createFormBuilder($defaultData, ['method' => Request::METHOD_GET])
            
        ->add('fecha_desde', DateType::class, array(
                'label'    => 'Fecha desde?: ', 
                'input'=>'string', 'required' => true,  
            'widget' => 'single_text',
        'attr' => array('style' => 'width: 120px'), 

            ))
        ->add('fecha_hasta', DateType::class, array(
                'label'    => 'Fecha hasta?: ', 
                'input'=>'string', 'required' => true, 
              'widget' => 'single_text',
             'attr' => array('style' => 'width: 120px'),

                ))
         ->add('save', SubmitType::class, ['label' => 'Buscar'])

        ->getForm();

        $form->handleRequest($request);
       
        

      if($form->isSubmitted()&&$form->isValid()){

        // datafecha is an array with "fecha_desde", and "fecha_hasta" keys 
        
       $datafechas = $form->getData();

       $noticias = $fuenteRepository->findTodosCSV($datafechas);
       
       $response = $this->forward('App\Controller\FuenteController::csvAction', [
       
        'noticias' => $noticias
    ]);

      return $response;
      

      }
      return $this->render('fuente/entrefechas.html.twig', array(
        'form' => $form->createView() ));
     
    }

In the second controller, I use the result of the query from the previous controller to generate a csv file and download it.

 #[Route('/admin/fuente/csv/', name: 'app_csv_fuente', methods: ['GET', 'POST'])]
            public function csvAction(array $noticias ): Response
    {           

        $data=array();
       
        $data[0]= array("Id","Diario","Link", "Palabras","Título","Contenido","Fecha","Mes", "Año",
        "Protagonista", "Dinámica Conflictual", "Sector", "Sub sector", "Departamento","Pertenencia",
        "Agrupación", "Agregación", "Antagonista", "Actor Estatal","Nivel de Gobierno", "Participación",
        "Protagonista", "Respuesta Patronal 1","Respuesta Patronal 2", "Respuesta Estado 1", "Respuesta Estado 2",
   "Demanda 1 nombre", "Demanda 1 principal","Demanda 2 nombre","Demanda 2 principal", 
    "Demanda 3 nombre", "Demanda 3 principal", "Actor emergente",
     "Formato","Formato 2","Formato 3",     
);
       $i=1;
     
        foreach ($noticias as $noticia) {
         
         $data[$i]= array(
              $noticia['id'],
               $noticia['diario'],
               $noticia['link'],
               $noticia['palabras'],
               $noticia['titulo'],
               $noticia['contenido'],
               $noticia['fecha']->format('d-m-Y'),
               $noticia['fecha']->format('F'),
               $noticia['fecha']->format('Y'),
               $noticia['protagonista_name'],
               $noticia['actividad_name'],  
               $noticia['sector_name'],
               $noticia['subsector_name'],
               $noticia['Departamento'],
               $noticia['Pertenencia'],
               $noticia['Agrupacion'],
               $noticia['Agregacion'],
               $noticia['Antagonista'],
               $noticia['Actorestatal'],
               $noticia['Nivelgobierno'],
               $noticia['Participacion'],
               $noticia['protagonista_name'],
               $noticia['respuestapatronal_name'],
               $noticia['respuestapatronal1_name'],
               $noticia['respuestaestado_name'],
               $noticia['respuestaestado1_name'],
               $noticia['demanda1_name'],
               $noticia['principal1_name'],
               $noticia['demanda2_name'],
               $noticia['principal2_name'],
               $noticia['demanda3_name'],
               $noticia['principal3_name'],
               $noticia['Actoremergente'],
               $noticia['protesta_name'],
               $noticia['protesta_name2'],
               $noticia['protesta_name3'] ,        
       ); 
         $i++;
           }  
       
            
              $fp = fopen('php://memory','w', "w");
              foreach($data as $fields){
                fputcsv($fp, $fields, ';');
              }
              rewind($fp);

              $response = new Response(stream_get_contents($fp));
              fclose($fp);
              $response->headers->set('Content-Type', 'text/csv; charset=UTF-8');
              $response->headers->set('Content-Encoding', 'UTF-8');
              $response->headers->set('Cache-Control', 'private');
              
              $response->headers->set('Content-Disposition', 'attachment; filename="noticias.csv"');
              $response->sendHeaders();
            
              
              return $response;

            }           

The problem is that the download does not work but the csv file is created! When I apply this function to a download, it does not occur but the file is generated somewhere in memory or server space. How do I know? If I look for the Ajax section in the bottom toolbar, I can see the list of files generated but not downloaded. If we click on some of these links in the function bar, the download is achieved there. The challenge is how to achieve a direct download


Solution

  • I found the solution and it does not correspond to any of the files presented in the question above. Let's see the full story and the solution:

    I need to make a query and then download the result in a CSV file.

    This process involves four files of my project. The first file is the controller FuenteController.php. The second file is the template where I define a date range for the query, and I named it: entrefechas.html.twig The third file is the FuenteRepository.php repository where I process the query. The fourth file is the function that generates the CSV and that I defined as a service: CSVResponse.php located in the Service folder of Symfony 6.4. Below are the files in the order they work

      #[Route('/admin/fuente/exportar0/', name: 'app_exportar0_fuente', methods: ['GET', 'POST'])]
        public function exportar0(Request $request, FuenteRepository $fuenteRepository, ): Response
        {
               $defaultData = [];
        $form = $this->createFormBuilder($defaultData, ['method' => Request::METHOD_GET])
                
            ->add('fecha_desde', DateType::class, array(
                    'label'    => 'Fecha desde?: ', 
                    'input'=>'string', 'required' => true,  
                'widget' => 'single_text',
            'attr' => array('style' => 'width: 120px'), 
    
                ))
            ->add('fecha_hasta', DateType::class, array(
                    'label'    => 'Fecha hasta?: ', 
                    'input'=>'string', 'required' => true, 
                  'widget' => 'single_text',
                 'attr' => array('style' => 'width: 120px'),
    
                    ))
             ->add('save', SubmitType::class, ['label' => 'Buscar'])
    
            ->getForm();
    
            $form->handleRequest($request);
           
            
    
          if($form->isSubmitted()&&$form->isValid()){
    
            // datafecha is an array with "fecha_desde", and "fecha_hasta" keys 
            
           $datafechas = $form->getData();
    
           $noticias = $fuenteRepository->findTodosCSV($datafechas);
           
           $response = $this->forward('App\Controller\FuenteController::csvAction', [
           
           'datafechas' => $datafechas
          
        ]);
    return $response;
    
              }
              return $this->render('fuente/entrefechas.html.twig', array(
                'form' => $form->createView() ));
             
            }
    

    The original thing about the previous function is that the result is not sent to a Twig file but to another controller, using the Symfony forward function The template for the query is:

      {% block body %}
        {{ form_start(form, {'attr': {'data-turbo': 'false'}}) }}  
      {{ form_widget(form) }}
    {{ form_end(form) }}  
     
    {% endblock %}
    

    A fundamental detail of this template is the attribute that we added to disable AJAX here, since otherwise the CSV file that will be generated will not be downloaded (my problem). The function that processes the result of the query is the following:

      #[Route('/admin/fuente/csv/', name: 'app_csv_fuente', methods: ['GET', 'POST'])]
            public function csvAction(FuenteRepository $fuenteRepository, array $datafechas, )
        {
         
            $noticias = $fuenteRepository->findTodosCSV($datafechas);
    
            $data=array();
           
            $data[0]= array("Id","Diario","Link", "Palabras","Título","Contenido","Fecha","Mes", "Año",
            "Protagonista", "Dinámica Conflictual", "Sector", "Sub sector", "Departamento","Pertenencia",
            "Agrupación", "Agregación", "Antagonista", "Actor Estatal","Nivel de Gobierno", "Participación",
            "Protagonista", "Organización Agregada","Respuesta Patronal 1","Respuesta Patronal 2", "Respuesta Estado 1", "Respuesta Estado 2",
       "Demanda 1 nombre", "Demanda 1 principal","Demanda 2 nombre","Demanda 2 principal", 
        "Demanda 3 nombre", "Demanda 3 principal", "Actor emergente",
         "Formato","Formato 2","Formato 3",     
    );
           $i=1;
         
            foreach ($noticias as $noticia) {
             
             $data[$i]= array(
                  $noticia['id'],
                   $noticia['diario'],
                   $noticia['link'],
                   $noticia['palabras'],
                   $noticia['titulo'],
                   $noticia['contenido'],
                   $noticia['fecha']->format('d-m-Y'),
                   $noticia['fecha']->format('F'),
                   $noticia['fecha']->format('Y'),
                   $noticia['protagonista_name'],
                   $noticia['actividad_name'],  
                   $noticia['sector_name'],
                   $noticia['subsector_name'],
                   $noticia['Departamento'],
                   $noticia['Pertenencia'],
                   $noticia['Agrupacion'],
                   $noticia['Agregacion'],
                   $noticia['Antagonista'],
                   $noticia['Actorestatal'],
                   $noticia['Nivelgobierno'],
                   $noticia['Participacion'],
                   $noticia['protagonista_name'],
                   $noticia['protagonista_organizacion'],
                   $noticia['respuestapatronal_name'],
                   $noticia['respuestapatronal1_name'],
                   $noticia['respuestaestado_name'],
                   $noticia['respuestaestado1_name'],
                   $noticia['demanda1_name'],
                   $noticia['principal1_name'],
                   $noticia['demanda2_name'],
                   $noticia['principal2_name'],
                   $noticia['demanda3_name'],
                   $noticia['principal3_name'],
                   $noticia['Actoremergente'],
                   $noticia['protesta_name'],
                   $noticia['protesta_name2'],
                   $noticia['protesta_name3'] ,        
           ); 
             $i++;
               }  
              
               $response = new CSVResponse( $data);
                return $response;
    
                }           
    

    This is completed by calling CSVResponse.php in the penultimate line.

     Here the function in the Service folder:
    
    namespace App\Service;
    use Symfony\Component\HttpFoundation\Response;
    
    class CSVResponse extends Response
    {
        protected $data;
        protected $filename = 'export.csv';
        public function __construct($data = array(), $status = 200, $headers = array())
        {
            parent::__construct('', $status, $headers);
            $this->setData($data);
        }
        public function setData(array $data)
        {
            $output = fopen('php://memory', 'w');
            foreach ($data as $row) {
                fputcsv($output, array_map("utf8_decode",$row), ';');
                
            }
            rewind($output);
            $this->data = '';
            while ($line = fgets($output)) {
                $this->data .= $line;
            }
            $this->data .= fgets($output);
    
            fclose($output);
    
    // Make sure nothing else is sent, our file is done
    
            return $this->update();
        }
        public function getFilename()
        {
            return $this->filename;
        }
        public function setFilename($filename)
        {
            $this->filename = $filename;
            return $this->update();
        }
        protected function update()
        {
            $this->headers->set('Content-Disposition', sprintf('attachment; filename="%s"', $this->filename));
            if (!$this->headers->has('Content-Type')) {
                $this->headers->set('Content-Type', 'text/csv'); 
                
            }
            return $this->setContent($this->data);
        }
    }