Exporter des données au format Excel (.xls)
Nous allons voir comment exporter des données au format Excel. Nous imaginerons que cette fonctionnalité sera réservée à l’administrateur du site. C’est l’occasion d’aborder trois points intéressants :
- L’utilisation d’un Helper, très inspiré de celui disponible sur la Bakery mais avec quelques améliorations
- La manipulation de fichiers avec la classe
File - La protection de fichiers destinés au téléchargement grâce à une Vue de type
Media
1. La demande
Notre client nous impose les contraintes suivantes :- pouvoir exporter la liste des membres inscrits sur son site sous forme de fichier Excel
- l’application doit stocker tous les exports effectués
- chaque liste ne doit pouvoir être téléchargée que par un administrateur
2. Les actions du Contrôleur Members
Nous déterminons les actions dont nous avons besoin pour satisfaire la demande :
adminexportsindex: liste des fichiers Excel existantsadminexport: collecte des membres inscrits et enregistrement sous forme de fichier Exceladminexportdownload: téléchargement d’un fichier existantadminexport_delete: suppression d’un fichier d’export
Le préfixe admin_, associé à la mise en place de la restriction d’accès avec le composant Auth, nous garantit que seul un administrateur logué peut accèder à ces quatre actions.
2.1 Liste des fichiers existants
Nous devons veiller à ce que les fichiers qui seront créés ne soient pas directement accessibles par un utilisateur non logué comme administrateur. Le meilleur moyen est d’enregistrer ces fichiers dans un répertoire en dehors de la racine de l’hébergement, et de gérer leur téléchargement grâce à une Vue de type Media. Mais commençons par lister les fichiers disponibles, que nous placerons dans le répertoire {app}/exports (donc hors du webroot) :
// {app}/controllers/members_controller.php function admin_exports_index() { App::import('Core', 'Folder'); $dir = new Folder('..'.DS.'exports'); $data = $dir->find('.+\.xls'); rsort($data); $this->set(compact('data')); }
Cette action importe la classe de gestion de répertoires fournie avec Cake, Folder. Nous l’instancions avec le chemin d’accès au répertoire de destination, et nous utilisons la méthode Folder::find avec une expression régulière pour trouver tous les fichiers dont l’extension est « .xls ». Nous classons la liste de fichiers trouvés par ordre alphabétique inverse, et passons le tableau à la Vue.
La vue correspondante est très simple :
// {app}/members/admin_exports_index.ctp
<h3>Historique des exports</h3>
<table>
<thead><?php
echo $html->tableHeaders(
array("Fichier", "Action")
); ?></thead>
<tbody>
<?php foreach($data as $row): ?>
<tr>
<td><?php echo $row; ?></td>
<td>
<?php echo $html->link("Télécharger", 'export_download/'.$row); ?>
<?php echo $html->link("Supprimer", 'export_delete/'.$row, null, "Etes-vous sûr de vouloir supprimer ?"); ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<p><?php echo $html->link("Créer un nouvel export", 'export'); ?></p>Nous parcourons la liste des fichiers trouvés en faisant un lien vers les actions adminexportdownload et adminexportdelete, auxquelles nous passons en paramètre le nom du fichier. Nous terminons par un lien vers le script de création d’un nouvel export.
2.2 Création d’un nouvel export
Cette action va utiliser le Helper Xls suivant :
// {app}/views/helpers/xls.php
<?php
class XlsHelper extends AppHelper
{
/**
* Buffer. Chaque indice du tableau contient une ligne du fichier.
*
* @var array
*/
var $rows = array();
/**
* Ajoute un nombre
*
* @param int $row Abscisse
* @param int $col Ordonnée
* @param int $val Valeur numérique
* @return string Valeur encodée
*/
function addNumber($row, $col, $val)
{
$out = pack("sssss", 0x203, 14, $row, $col, 0x0);
$out .= pack("d", $val);
return $out;
}
/**
* Ajoute une chaine de caractères
*
* @param int $row Abscisse
* @param int $col Ordonnée
* @param int $val Chaine de caractères
* @return string Valeur encodée
*/
function addString($row, $col, $val)
{
$length = strlen($val);
$out = pack("ssssss", 0x204, 8 + $length, $row, $col, 0x0, $length);
$out .= $val;
return $out;
}
/**
* Ajoute une ligne dans le buffer
*
* @param array $data Tableau de valeurs
*/
function addRow($data = array())
{
$out = '';
$row = count($this->rows);
foreach($data as $col => $val)
{
if(is_numeric($val))
{
$out .= $this->addNumber($row, $col, $val);
}
else
{
$out .= $this->addString($row, $col, "{$val}");
}
}
$this->rows[] = $out;
}
/**
* Retourne buffer encodé, avec l'entête et la fin de fichier.
*
* @return string Contenu du fichier
*/
function render()
{
$out = pack("ssssss", 0x809, 0x8, 0x0, 0x10, 0x0, 0x0);
$out .= join('', $this->rows);
$out .= pack("ss", 0x0A, 0x00);
return $out;
}
}
?>Nous prenons soin d’y faire appel dans le Contrôleur Members :
// {app}/controllers/members_controller.php var $helpers = array('Xls'); function admin_export() { $data = $this->Member->find('all'); $this->set(compact('data')); $filename = 'export_' . strftime('%Y-%m-%d') . '.xls'; $this->autoLayout = false; App::import('Core', 'File'); $file = new File('..'.DS.'exports'.DS.$filename, true); $file->write($this->render()); $file->close(); $this->Session->setFlash("Nouveau fichier disponible.", 'message_ok'); $this->redirect($this->referer()); }
Voyons pas à pas l’exécution de cette action :
- nous commençons par récupérer la liste des membres inscrits et la passons à la Vue ;
- nous créons le nom du fichier à partir de la date du jour ;
- nous empêchons CakePHP d’ajouter le layout autour de la Vue avec
$this->autoLayout = false;; - nous importons la classe de gestion de fichiers,
File, fournie avec CakePHP ; - nous créons le nouveau fichier en mode écriture (attention si le fichier existe déjà il sera écrasé) ;
- nous écrivons le résultat de la vue dans le fichier ;
- nous redirigeons sur l’index des fichiers avec un message de succès.
// {app}/views/members/admin_export.ctp // Entêtes $xls->addRow(array("ID", "Email", "Date d'inscription")); // Données foreach($data as $row) { $xls->addRow(array( $row['Member']['id'], $row['Member']['email'], $row['Member']['created'] )); } // Rendu echo $xls->render();
2.3 Télécharger un fichier
Nous l’avons vu, nos fichiers sont enregistrés en dehors du webroot, rendant impossible le téléchargement direct. Nous allons utiliser une Vue de type Media pour servir le fichier à télécharger :
// {app}/controllers/members_controller.php function admin_export_download($filename) { $this->view = 'Media'; $params = array( 'path' => 'exports' . DS, 'id' => $filename, 'name' => substr($filename, 0, strpos($filename, '.xls')), 'extension' => 'xls', 'download' => true ); $this->set($params); }
La Vue Media attend la variable $params, un tableau dont les entrées sont les suivantes :
path: chemin d’accès au répertoire où se trouvent les fichiers. Notons qu’ici nous partons de la racine de l’application et non du webroot, et qu’il faut terminer par le séparateur de répertoires ;id: nom réel du fichier sur le serveurname: nom final du fichier proposé au téléchargement, sans l’extension. Nous choisissons de garder le nom réel du fichier, mais nous aurions tout aussi bien pu imposer n’importe quel autre nom ;extension: l’extension du fichier proposé au téléchargement. Là encore nous pourrions imposer une autre extension ;download: sitrue, force le téléchargement (ouverture d’une fenêtre du navigateur qui va proposer d’ouvrir ou de télécharger le fichier). Sifalse, le navigateur va essayer de charger l’application locale associée au type du fichier.
2.4 Suppression d’un fichier
Il ne nous reste qu’à proposer la possibilité de supprimer un fichier, là encore tirons parti de la classe File :
// {app}/controllers/members_controller.php function admin_delete_file($filename) { App::import('Core', 'File'); $file = new File('..' . DS . 'exports' . DS . $filename); if(!$file->delete()) { $this->Session->setFlash("Impossible de supprimer le fichier '{$filename}'.", 'message_error'); } else { $this->Session->setFlash("Fichier '{$filename}' supprimé.", 'message_ok'); } $this->redirect($this->referer()); }
Commentaires
22 janvier 2009 à 0:02
[...] Exporter des données au format Excel (.xls) [...]
6 mars 2009 à 15:22
Bonjour et merci pour ce tuto, je l’ai adapté pour créer et télécharger le fichier en une seule passe.
Est-ce possible de supprimer le fichier juste après l’avoir téléchargé ?
J’ai également un problème d’encodage des caractères, mais je ne sais pas si ça vient de l’export ou de la configuration de mon OpenOffice.
merci
2 juin 2009 à 16:53
Salut,
Tuto pratique… mais perfectible à mon sens.
D’abord on peut très bien écrire les fichiers dans un répertoire du webroot et donc proposer un lien direct de téléchargement, sans avoir besoin de créer la fonction de download !
Voici ce que je fais :
Ensuite, comme Elianora, j’ai de nombreux problèmes d’accents dans le fichier Excel généré et je n’utilise pas OpenOffice. Apparemment, par défaut, c’est encodé en iso…
Enfin, il me semblerait utile de pouvoir faire chaque nouvel export, en prenant comme point de départ le premier inscrit après la date du précédent export, au lieu de récupérer tous les enregistrements de la table à chaque fois.
Je suis en train de réfléchir à un système pour conserver quelque part le dernier inscrit de l’export qui vient d’être effectué, je reviendrai ici pour expliquer comment je m’y prends.
2 juin 2009 à 22:59
La contrainte ici était que les fichiers créés ne devaient surtout pas être accessibles librement (donc pas dans le webroot).
Je n’ai pas constaté le problème d’accents, l’export ici ne concernant que des adresses email. Si tu trouves la parade, n’hésite pas !
Bien évidemment la logique de collecte des données avant l’export reste à la charge du développeur, ce n’était là qu’un exemple simplifié. Le coeur du sujet était avant tout l’export au format excel et la protection contre le download direct.
Merci pour tes retours en tout cas, toujours intéressants.
5 juin 2009 à 9:11
Oui, désolé pour mon emportement, en relisant ton tuto et juste après avoir déposé mon commentaire, je me suis aperçu que c’était volontaire de créer les exports hors de la racine web ;o)
Bon, sinon, j’ai trouvé une première astuce pour les accents : dans la vue adminexport, j’ai simplement ajouté des utf8decode() autour des données susceptibles de contenir des accents ! Il y a d’autres techniques, visiblement en envoyant des headers dans la vue qui gère le download, mais comme je ne l’utilise pas, j’ai pas été plus loin.
Je cherche maintenant une manière élégante de ne pas reprendre toute la base à chaque export, mais de repartir à la dernière ligne exportée précédemment. J’indiquerai ici le résultat de mes recherches ;o)
5 juin 2009 à 11:22
Après de nombreux essais infructueux pour lire le fichier Excel créé lors du dernier export (trop de problème avec le format XLS notamment une fois mis en prod sur du Linux), j’ai choisi une petite astuce qui vaut ce qu’elle vaut !
En fait, à chaque export, j’enregistre le dernier id dans un fichier texte et si l’utilisateur souhaite ensuite faire un export partiel, je lis cet id dans le fichier texte et je l’utilise comme condition au find() :
4 août 2009 à 10:19
Un immense MERCI pour ce tuto qui est vraiment très utile, je n’aurai vraiment pas su par où commencer tout seul. (Je n’ai pas fini de code mon extraction mais ça commence déjà à fonctionner, c’est rassurant !!)
3 février 2010 à 12:23
Bonjour, Le fichier xls créé ne contient pas les données qui devrait y être mais des mots qu’on ne peut pas lire; alors que j’ai bien suivi le tuto; quelqu’un peut m’aider svp; merci