Huge export XLS | CSV | XML | JSON streamed via Symfony2

Ceci est une introduction

Faire un export XLS/CSV on l’a tous fait. Une requête SQL, le header qui va bien et en avant Guingamp ! Mais que ce passe t’il quand la table en question fait plus de 100K lignes ? Je vais vous le dire : PHP va faire la gueule, c’est loin d’être une soluce adaptée cette histoire ! Ho et puis amusez vous à utiliser Doctrine2 pour appeler une table aussi grosse sans limit, non sérieusement essayez c’est rigolo !

Woot ! Du stream comme sur DPStream !

Enfin pas exactement, mais l’idée est la même.

En quelques mots voilà comment ça se passe : d’abord on accède à la base de données,  on prépare notre requête SQL, on utilise les solutions d’itérations et d’écriture de données du bundle Exporter de Sonata Project (super bien foutu) et enfin on envoie tout via la fonction de Symfony : StreamedResponse.

Pour ce faire on commence par installer le bundle Sonata-project exporter qui va nous aider dans tout le process du traitement de la donnée

Dans le composer.json

    ...
    "require": {
        ...
        "sonata-project/exporter": "1.*@dev"
        ...
    },
    ...

Un petit coup de composer update !

php composer.phar update

Enfin le controller :

namespace Acme\DemoBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;

use Exporter\Source\DoctrineDBALConnectionSourceIterator;
use Exporter\Handler;
use Exporter\Writer\XlsWriter;
use Exporter\Writer\XmlWriter;
use Exporter\Writer\JsonWriter;
use Exporter\Writer\CsvWriter;


use Symfony\Component\HttpFoundation\StreamedResponse;

class DemoController extends Controller
{
    public function exportAction() {
        
        $doctrineDatabaseConnection = $this->get('database_connection');
    
        // HUGE request SQL
        $sqlQuery = 'SELECT * 
                     FROM HugeTable';
        
        // Preparing Source Data Iterator via DoctrineDBALConectionIterator
        $sourceIterator = new DoctrineDBALConnectionSourceIterator($doctrineDatabaseConnection, $sqlQuery);
        
       // Format, Content type and writer for XLS example (change this for CSV, JSON or XML)
        $format = 'xls'
        $contentType = 'application/vnd.ms-excel';
        $writer = new XlsWriter('php://output');
    
        $filename = sprintf(
            'streamed_huge_xls_%s_' . time() . '.%s',
            date('Y_m_d', strtotime('now')),
            $format
        );
        
        // Export the data using anonymous function for the streamed response
        $callback = function() use ($sourceIterator, $writer) {
            Handler::create($sourceIterator, $writer)->export();
        };
        
        // Using the symfony streamed response
        return new StreamedResponse($callback, 200, array(
            'Content-Type'        => $contentType,
            'Content-Disposition' => sprintf('attachment; filename=%s', $filename)
        ));
        
    }
}

Itérateur ? Fonction anonyme ? WTF

urkel

Bon c’est clairement un petit paragraphe pour ceux qui ne sont pas familiés avec ça.
Un itérateur est un design pattern qui permet de parcourir un objet de la même manière qu’un foreach. Sauf qu’il s’agit d’un foreach un peu plus évolué. Ici ce qui nous intéresse c’est  qu’il traite le gros paquet de données qu’on lui envoie en plein de petits paquets !
Concrètement au lieu de faire le gros bourrin à tous charger d’un coup dans la mémoire de PHP -au point de le faire tomber dans les pommes-, il va s’en occuper proprement bout par bout. Pour en savoir plus sur ce design pattern.

Il y a beaucoup à dire sur les fonctions anonymes ou fermeture(closure en anglais). Très simplement ce sont des petites fonctions qu’on utilise à la volée quand on en a besoin. Le mot clé use est là pour importer dans cette fonction des variables qui existent de base seulement dans le contexte de la méthode export(contexte parent). Ces petites fonctions sont donc accessibles à la volée avec des données importées d’un autre contexte. Bah tiens ça tombe bien c’est exactement ce dont a besoin pour le bon fonctionnement de la fonction de stream de Symfony ! Plus d’infos sur cette méthode.

Épilogue

Cette solution est inspirée du fonctionnement d’export de SonataAdmin. Le but étant de partager une solution robuste est très utile avec l’avantage ici d’une totale liberté sur les données exportées via une requête SQL. Vous pouvez mettre toute les requêtes que vous voulez, elle seront traitées en streaming et proprement formatées dans votre excel.
De plus cette solution n’a aucune dépendance à part le petit bundle exporter, on n’est donc pas obligé d’installer toute la solution admin de sonata pour en profiter sur n’importe quel projet !

Ceci dit je vous invite fortement à aller checker les bundles du Sonata Project si vous ne connaissez pas, ça vaut vraiment le détour.

Qui me parle ?

Je suis un dev. En ce moment je suis Backend Développeur / DevOps à Ubisoft. Je suis passionné du dev et expert en blagues. Je continue à te parler quotidiennement sur mon Twitter. Tu peux m'insulter à cette e-mail ou le faire directement dans les commentaires juste en dessous.

Commentaire(s)

  1. Merci pour le tuto, c’est ce qu’il me fallait, je n’avais jamais songé à un itérateur…, sur 5000 produits c’est comme quasi instantaté, j’imagine que ca aide ?! Sinon j’ai pensé au lieu de faire la query brute, j’utilise le queryBuilder()->select(…)->from(…)->getQuery->getSql() pour mieux intégrer le tout. Au plaisir de lire d’autres articles, vraiment fun et instructifs !

    1. En effet ça revient au même, mais ça à le mérite d’être plus maintenable !

      En fait j’ai mit du SQL brut pour que ça parle plus facilement à tout le monde.
      Merci pour la remarque!

  2. Bonjour et merci pour l’astuce. Ca fonctionne du tonnerre.
    J’avais toutefois 2 petites questions.

    1 comment gérer l’encodage de sortie du fichier CSV? Mes accents ne sont pas pris en compte.

    2. J’exporte des données issues de formulaire de contact. Certains champs ont des sauts de ligne.
    A l’export, les sauts de ligne sont conservés dans mon CSV.
    Comment puis-je les échapper pour le CSV?

    Je te remercie

T'en penses quoi ?

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