Générer un document Word depuis un modèle

Le cadre est donné, nous allons maintenant voir comment il est possible de générer dynamiquement un document Word, en PHP, depuis un fichier modèle. Pour cet article, nous prendrons comme exemple un facture qu’il faut générer dynamiquement. Nous sommes tous d’accord pour dire que, dans l’absolu, une facture est le pire exemple possible…
En effet, qui souhaite envoyer à l’un de ses clients une facture en Word, format largement éditable et modifiable ?

Ceci étant, je peux d’un simple revers de la main anéantir cet argument en disant, par exemple, que je souhaite pouvoir ajouter un petit message personnalisé à chacun de mes clients.

Plus sérieusement, une facture est un document quasi-universellement reconnu, quel que soit le métier qu’on exerce, et cela constitue donc bien, selon moi, un exemple parfaitement pédagogique.

Le modèle

J’ai réalisé le document suivant directement dans Word; vous y trouverez surlignés en jaune les passages qui ont vocation a être dynamiquement générés.

Modèle de facture générée

cliquez sur l'image pour télécharger le fichier

Le fichier en question, bien que disposant d’une extension .doc, est enregistré au format xml de la version de Word 2003 :

Enregistrement au format XML Word 2003

Préparation du modèle

Nous allons transformer cette facture « type » en modèle réutilisable; cette opération est réalisée dans Word; elle consiste en la transformation de tout ce qui peut être modifié dynamiquement en « mots-clés » qui serviront à la fusion à venir; classiquement, j’identifie ces mots-clés en les précédant et suivant du signe @; de plus, ces mots-clés étant supposés constants, je les écris en majuscules (par exemple : @NOM_CLIENT@)

Nous ne nous intéresserons pas tout de suite au tableau de lignes de factures, nous verrons ça un peu plus loin dans cet article.

Ok, j’ai compris…ensuite, on fait quoi ?

Deux secondes, il y a une subtilité !! En effet, comme je le disais dans mon article précédent, Word se comporte un peu comme Dreamweaver à ses débuts (c’est à dire que ça a l’air propre, mais que derrière, c’est tout caca !); il est impératif d’écrire chacun des mots-clés d’une seule traite, faute de quoi Word risque d’écrire un paquet de trucs invisibles à l’intérieur des mots; ainsi, il n’est pas rare que @NOM_CLIENT@ devienne quelque chose comme : @NOM_CL</w:t><w:t>IENT@ , ce qui deviendra ensuite impossible à fusionner correctement.

Enfin du PHP

Passons maintenant au générateur; on va, toujours pour l’exemple, imaginer qu’on a déjà tout ce qu’il nous faut en terme d’objets $customer, $invoice et $invoiceLine (respectivement client, facture et ligne de facture).

//On récupère le contenu brut du fichier xml modèle
$myContent = file_get_contents("/cheminDuModele/facture.doc");

//On remplace les mots-clés, un à un
$myContent = str_replace("@NOM_CLIENT@",$customer->getName(),$myContent);
$myContent = str_replace("@ADRESSE_CLIENT@",$customer->getAddressLine(),$myContent);
$myContent = str_replace("@CP_CLIENT@",$customer->getPostalCode(),$myContent);
...

//On crée le fichier généré
$newFileHandler = fopen("/cheminDuNouveauFichier/nomDuFichier.doc","a");
fwrite($newFileHandler,$myContent);
fclose($newFileHandler);

//On a fini

Tout ça pour ça ? Vraiment ?

Réponse courte : « non » avec un « si »; réponse longue : « oui », avec un « mais ».

On peut bien sûr d’ores et déjà améliorer le concept; par exemple, les remplacements peuvent être mieux écrits :

$params = array(
                        "@NOM_CLIENT@"        => $customer->getName(),
                        "@ADRESSE_CLIENT@"  => $customer->getAddressLine(),
                        "@CP_CLIENT@"            => $customer->getPostalCode(),
                        ...
);

...

//au lieu des str_replace :
foreach($params as $key=>$value)
    $myContent = str_replace($key,$value,$myContent);

...

Et si on veut vraiment jouer les durs :

// au lieur du foreach précédent
$myContent = str_replace(array_keys($params),$params,$myContent);

Mais, pour répondre à la question, alors : oui…tout ça pour ça.

Et pour le tableau de lignes ?

Pour le tableau, il faut gruger un peu…on trouve aujourd’hui de nombreuses documentations sur le WordML sur internet; lorsque j’ai commencé à générer du Word, il n’y en avait pas et j’ai souvent dû recourir au système D; et je dois admettre que ce système a son intérêt…

Étape 1 – Comprendre ce que le logiciel fait

Vous ouvrez word, vous créez un tableau identique à celui du modèle (nombre de colonnes, largeur des colonnes, etc.), mais vous ne remplissez que l’en-tête. Par ailleurs, vous laissez l’ensemble du document complètement vide. Enregistrez votre fichier au format XML et ouvrez-le dans votre éditeur préféré (Textmate pour Mac, UltraEdit pour PC, vi pour Linux).

Reprenez votre fichier word, ajoutez une seule ligne dans le tableau (sans rien modifier par ailleurs), enregistrez votre nouveau document au format XML, et ouvrez-le dans votre éditeur préféré.

Comparez les deux documents et tentez de reverse-engineerer le processus.

Je l’ai fait pour vous, et voici la différence :

<w:tr wsp:rsidR="00C35C97" wsp:rsidRPr="005945CA">
    <w:tc>
        <w:tcPr>
            <w:tcW w:w="7338" w:type="dxa"/>
        </w:tcPr>
        <w:p wsp:rsidR="00C35C97" wsp:rsidRPr="005945CA" wsp:rsidRDefault="00C35C97" wsp:rsidP="00C35C97"/>
        <w:p wsp:rsidR="00C35C97" wsp:rsidRPr="005945CA" wsp:rsidRDefault="00C35C97" wsp:rsidP="00C35C97">
            <w:r wsp:rsidRPr="005945CA">
                <w:t>
                    TEXTE DE LA CELLULE 1
                </w:t>
            </w:r>
        </w:p>
    </w:tc>
    <w:tc>
        <w:tcPr>
            <w:tcW w:w="1868" w:type="dxa"/>
        </w:tcPr>
        <w:p wsp:rsidR="00C35C97" wsp:rsidRPr="005945CA" wsp:rsidRDefault="00C35C97" wsp:rsidP="00C35C97">
            <w:pPr>
                <w:jc w:val="right"/>
            </w:pPr>
        </w:p>
        <w:p wsp:rsidR="00C35C97" wsp:rsidRPr="005945CA" wsp:rsidRDefault="00C35C97" wsp:rsidP="00C35C97">
            <w:pPr>
                <w:jc w:val="right"/>
            </w:pPr>
            <w:r wsp:rsidRPr="005945CA">
                <w:t>
                    LE MONTANT
                </w:t>
            </w:r>
        </w:p>
        <w:p wsp:rsidR="00C35C97" wsp:rsidRPr="005945CA" wsp:rsidRDefault="00C35C97" wsp:rsidP="00C35C97">
            <w:pPr>
                <w:jc w:val="right"/>
            </w:pPr>
        </w:p>
    </w:tc>
</w:tr>

En regardant de près, on retrouve à peu près certains tags relativement équivalents en HTML : <w:tr /> pour une ligne de tableau, <w:p /> pour un paragraphe, <w:t> pour du texte…

Maintenant, ouvrez votre modèle de fichier word XML dans votre éditeur préféré et supprimez l’ensemble des lignes de tableau hors en-tête, et remplacez-le par un mot-clé du type @INVOICE_LINES@; enregistrez.

Attention, à partir de là, votre fichier modèle n’est plus lisible dans Word.

Ensuite, prenez tout le bloc précédent correspondant à une ligne donnée du tableau et affectez-le à une variable PHP, par exemple $lineModel.  (faites bien attention aux guillemets, aux apostrophes, et aux caractères d’échappement)

Enfin, dans votre code PHP :

//On affecte le modèle de ligne
$lineModel = '...';

...

//On a déjà fait les autres str_replace, si on veut...

$invoiceContent = "";
foreach($invoiceLines as $aLine) {
    //On veut conserver $lineModel, alors on passe par une variable temporaire
    $tmp = $lineModel;
    $tmp = str_replace("@INVOICE_LINE_DESCRIPTION@",$aLine->getDescription(),$tmp);
    $tmp = str_replace("@INVOICE_LINE_AMOUNT@",number_format($aLine->getAmount(),2,","," "),$tmp);
    $invoiceContent .= $tmp;
}
$myContent = str_replace("@INVOICE_LINES@",$invoiceContent,$myContent);

...

Bilan ?

Voilà une méthode relativement simple pour générer des documents Word depuis un modèle. Il est très facile de générer rapidement tout type de document, principalement des documents dits « à texte », comme des contrats, des polices d’assurance, etc.

Ceci étant, si on veut vendre notre sauce avec un peu plus de plus value, on pourra se poser la question, dans le prochain article, toujours dans le cadre d’un document généré via modèle, de :

  • comment intégrer des images dans un document Word ?
  • comment permettre à l’utilisateur de fabriquer ses propres modèles, en limitant les risques d’erreurs ?
 
 

14 réponses à to “Générer un document Word depuis un modèle”

  • Gaëlle:

    Super tutoriel! Merci, exactement ce que je cherchais.

  • Olivier:

    Merci beaucoup pour solution simple.
    Ayant eu trop de problème avec les formats XML (ca ne marchait pas bien à cause de différentes versions Word), j’ai appliqué cette méthode à des fichiers RTF : impec !!!

  • Avec plaisir. En effet, même si Microsoft a fait de nombreux efforts ces dernières années pour tenter de mettre à disposition un standard utilisable et exploitable (avec plus ou moins de succès et plus ou moins d’explications), le rechercher/remplacer reste une méthode simple…Attention toutefois, lorsqu’on utilise du XML (WordML est du XML), il y a des caractères dont il faut proprement gérer le remplacement : & , < , > . Par ailleurs, attention également aux problèmes d’encodage (quand on a un fichier encodé en ISO, déclaré en UTF8, et qu’on tente des remplacements dans un fichier Word CP 1512, on peut avoir des problèmes d’accents; ça semble insoluble comme problème tant qu’on ne considère pas l’ensemble des fichiers concernés, autant dans ce qu’ils décrivent que de la façon dont ils sont écrits — et si en plus on se fait des passages Windows < --> Linux, c’est la fin de tout…)

  • Olivier:

    Mon problème était que certains de mes clients utilisent des versions Word < 2003 :)
    Et là, le RTF m'a sauvé. Du coup, je l'utilise systématiquement.

  • Paul Verstraten:

    Bravo Mister Bruce
    c’est tout ce que j’aime : quelqu’un qui n’a pas peur de regarder comment ça se passe dans la cuisine et qui, avec un peu de logique, trouve une solution simple, élégante et efficace.
    Petite question : des idées comme cela pourrait se faire avec Libre Office 4 (juste une idée, pas plus). Et comment faire une génération Word à la queue-leu-leu (genre 40 factures, avec saut de page si nécessaire)

    J’en profite : super ton didacticiel sur IBAN ! Quand je vois tous ces sites fleurir autour du SEPA, c’est un peu comme avec le bug de l’an 2000 : beaucoup se font des gros sous sur un non-évènement (enfin, il suffit de réfléchir un peu…)

    Merci encore, et bon vent

    Paul V

  • openzero:

    merci beaucoup…mais j’arrive pas à insérer des caractères arabe..j’ai des ???? à la place…pouvez vous m’aider ?

  • Je n’ai jamais essayé des caractères étrangers (arabe, hébreu, chinois, japonais…), mais en règle générale, les problèmes peuvent survenir de deux endroits seulement :
    - le texte que vous insérez n’est pas correctement interprété;
    - le fichier final n’est pas correctement encodé.
    (ça peut également être les deux à la fois).

    Mon conseil est le suivant :
    - prenez un document word vide, écrivez deux-trois caractères en arabe et sauvegardez-le en XML (évitez de faire des modifications, des annulations, des choses comme ça, chacune laisse une trace dans le fichier XML – vous écrivez directement vos caractères — copiez-collez au besoin — et vous sauvez);
    - ouvrez votre document word dans un éditeur de texte brut (type notepad) et cherchez les mentions d’encodage (UTF-8 a priori); une fois la balise correspondante trouvée, remplacez la balise de votre modèle par celle-ci.
    - re-testez ! si ça ne fonctionne toujours pas, utilisez la fonction utf8_encode($monTexte) en PHP (ou utf8_decode, c’est selon) et le résultat final devrait être bon.

    Si ça fonctionne, merci de mettre votre solution en commentaire.

  • Yohan gillet:

    Merci beaucoup pour ce tuto. cela va vraiment m’aider par contre je rencontre le problème suivant: J’arrive à bien généré avec le modèle de la facture que tu propose mais quand je créer mon propre modèle, la génération du document s’effectue mais je ne peut pas l’ouvrir. Je ne voit pas ou est le problème. Pouvez vous m’aider?

  • Cela fait longtemps que je n’ai pas testé, donc je ne sais pas ce qu’il en est des nouvelles versions de Word. En revanche, mon expérience est la suivante : lorsque le fichier refuse de s’ouvrir, c’est qu’il y a un problème XML dans le fichier généré. Il faut voir si Word crache une erreur quelconque (étape 1). Ensuite, le cas échéant, valider le document généré par un XML validator (il y en a plein sur le web). Enfin, ouvrir dans un éditeur de texte brut et partir à la recherche de caractères invisibles (sur Windows, les caractères de retour à la ligne sont très chiants à retrouver, sachant de plus qu’il y en a 2 : un pour la fin de ligne, et un pour annoncer la nouvelle ligne).

    Si après tout ça ça ne marche pas, il faut re-générer en commençant par générer le modèle sans rien y apporter de modifications, puis en ajoutant les modifications une à une jusqu’à ce que ça plante…C’est un peu con, mais ça fonctionne.

    Pour info, depuis, il y a des logiciels qui génèrent proprement du Word en PHP (par contre, il faut programmer le contenu, ce qui est un peu lourd si on veut juste faire de la génération d’après modèle).
    J’avais utilisé le concept de cet article dans une SSII pour pouvoir générer à la volée les CV de tous les consultants à partir d’une base de données qui contenait leurs infos. Ca nous faisait gagner un temps considérable.

  • Yohan gillet:

    Merci de m’avoir répondus. Et oui tu avais bien raison j’avais un problème au niveau du xml. Ceci est réglé. J’ai bien vu qu’il existait des librairie telle que PHPWORD qui permettent de générer du word à partir de php. Le seul problème est qu’il fonctionne bien avec un model seulement si se n’est du texte. Cela ne passe pas avec une image. c’est pour cela que je me suis intéressé à ton tuto. (je n’en suis pas encore arrivé à on tuto sur la génération d’une image mais i m’a l’air prometteur!!)
    En attendant je te remercie de m’avoir répondu.

  • jsb:

    Bonjour,

    Super pratique, la grande class !

    Par contre, j’ai eu un souci avec @NUM_CHRONO@ lorsqu’il a du le remplacer par sa valeur. Après « quelques » recherches, j’ai du inscrire NUM_CHRONO dans mon document WORD, le transformer en XML, puis via Notepad, rechercher la chaine de caractère « NUM_CHRONO » et rajouter les deux @.

    Encore merci.

  • Emi:

    Très bon tuto mais j’ai un problème avec l’insertion des lignes de tableau. Puis je avoir une copie du document wordxml pour les tableaux. Merci d’avance!!!

  • mymy:

    Merci pour ce tuto… Mais après avoir essayé, je n’obtiens rien dans mon document .doc…

  • Il y a une solution PHP pour fabriquer un DOCX, SLSX, PPTX à partir d’un modèle : c’est OpenTBS.

    http://tinybutstrong.com/opentbs.php