Exporter des données dans un fichier csv avec Symfony

Share Button

Read the English version
Récemment, j’ai dû exporter un gros volume de données dans un fichier csv. C’est facile et rapide à faire si vous ne vous souciez pas de la mémoire utilisée et de l’expérience utilisateur. Je voulais que la consommation de mémoire n’augmente pas en fonction du volume de données.

Je me suis inspiré de cet article de blog mais, malgré ce qui est écrit dans le billet, des tests ont montré que la consommation de mémoire augmentait rapidement si le volume de données augmentait.

C’est pourquoi j’ai amélioré le script en utilisant la classe StreamedResponse. Cette solution a également l’avantage de générer/télécharger le fichier petit à petit afin que l’utilisateur n’ait pas à attendre la fin du processus.

Exporter des données en quantité, peu de mémoire consommée et un fichier généré à la volée, objectif accompli.

<?php

namespace Obtao\AcmeBundle\Controller;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Symfony\Bundle\FrameworkBundle\Controller\Controller
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\StreamedResponse;

class AcmeController extends Controller
{
    /**
     * @Route("/export-jedis", name="export_jedi")
     * @Template()
     */
    public function exportAction()
    {
        // récupère le conteneur de services pour le passer à la closure
        $container = $this->container;
        $response = new StreamedResponse(function() use($container) {

            $em = $container->get('doctrine')->getManager();

            // La méthode getExportQuery retourne une Query qui est utilisée pour récupérer
            // tous les objets (lignes du fichier csv) dont vous avez besoin. La méthode iterate
            // est utilisée pour limiter la consommation de mémoire
            $results = $em->getRepository('ObtaoAcmeBundle:Jedi')->getExportQuery()->iterate();
            $handle = fopen('php://output', 'r+');

            while (false !== ($row = $results->next())) {
                // ajoute une ligne au fichier csv. Vous devrez implémenter la méthode toArray()
                // pour transformer votre objet en tableau
                fputcsv($handle, $row[0]->toArray());
                // utilisé pour limiter la consommation de mémoire
                $em->detach($row[0]);
            }

            fclose($handle);
        });

        $response->headers->set('Content-Type', 'application/force-download');
        $response->headers->set('Content-Disposition','attachment; filename="export.csv"');

        return $response;
    }

Share Button

7 thoughts on “Exporter des données dans un fichier csv avec Symfony

  1. Bonjour,

    La fonction fputscv prend un tableau en argument et comme il s’agit d’un fichier csv, il n’y a aucune mise en forme. Inutile d’utiliser un template ici.

  2. Comment puis lancer cette génération en ligne de commande ? En commencant par ‘php’ et non un cURL ou un wget..
    Merci de ton aide.

  3. Pour ceux qui ont vu ma dernière question, voici une bonne piste de réflexion.
    (à cause du filtre anti-spam)-> symfony.com/ fr / doc / current /cookbook /console /console_command.html
    Sinon je n’ai pas trouvé de doc sur getExportQuery.
    Est-ce que l’impact d’un export est aussi important pour penser à ‘affiner’ le tir comme ça ? Sinon un dump de base ne suffisait-il pas ? Je dis ça je dis rien, mais exporter des données complexes sans typage ou autre ça me semble un peu risqué.
    Quoiqu’il en soit la réflexion de cet article est franchement intéressante.

    • Bonjour, getExportQuery est une méthode custom qui retourne un QueryBuilder. Vous pouvez y mettre ce dont vous avez besoin.
      Un export d’un gros volume de données peut être assez gourmand si on veut retourner des résultats calculés ou aller chercher dans les jointures. Le but n’est pas d’exporter simplement une table mais d’afficher des données qui ont du sens. Par exemple, on ne veut pas afficher l’id de la catégorie mais son nom traduit selon la locale de l’utilisateur. L’usage de la StreamedResponse dans ce cas est justifié et même nécessaire. Après effectivement, selon la nature des données exportées, il peut y avoir des solutions plus simples à mettre en place. A chaque besoin sa solution.

  4. Hello,

    Merci pour ce petit guide, quelques petites remarques cependant, si tu veux l’améliorer:

    - D’après la RFC 2616, “application/force-download” n’est pas reconnu comme Content-Type. “application/octet-stream” serait, à mon sens, plus approprié ici.

    - Symfony propose une méthode pour mettre en place le header ‘Content-Disposition’:

    $disposition = $response->headers->makeDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, $filename);
    $response->headers->set(‘Content-Disposition’, $disposition);

    - Pourquoi ne pas envoyer les headers le plus tôt possible ?

    $response = new StreamedResponse();
    $response->headers->set(‘Content-Type’, ‘application/octet-stream’);
    /* Défini les autres headers… */

    $response->sendHeaders();

    $em = $this->getDoctrine()->getManager();
    $response->setCallback(function () use ($em) {
    /* Utilise l’EntityManager ici… */
    });

    $response->sendContent();

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

Protected by WP Anti Spam