Générer des pdf grâce à htmltopdf et smarty

Générer des pdf grâce à htmltopdf et smarty

Description

Créer des fichiers PDF, en se basant sur un modèle MVC c'est possible notamment par l'intermédiaire de la librairie Html2pdf qui génère du pdf à partir de codes html.
Dans ce tuto, nous allons donc voir comment cela est possible avec un modèle de facture simple à partir d'une base de donnée.

Introduction

Nous allons dans ce tuto, voir comment générer des pdf sur un modèle de facturation, disons-le M.V.C; Cooool ! non ?
Pour arriver à nos fins, nous allons donc utiliser pour l'essentiel le moteur de template smarty et html2pdf pour créer le PDF à partir des templates.

Niveau-requis

Des notions en objets et quelques notions avec smarty sont souhaitables.

Pré-requis

Il vous faut télécharger smarty si vous ne l'avez pas : ici
Et également html2pdf : ici

Présentation de html2pdf

Il s'agit d'une librairie crée par le français Laurent Minguet qui permet de convertir les pages HTML en PDF. Elle s'appuie pour le coté php5 sur TCPDF une autre librairie qui permet également la création de PDF de manière 'brute'.

Arborescence de notre site

Une fois les fichiers (smarty et html2pdf) téléchargés, on les décompresse de faite comme ci-dessous :
Libre à vous de changer les noms et la configuration après avoir entièrement lu le tuto.

Données pour générer la facture

CREATE TABLE IF NOT EXISTS 'detail_service' (
'id' int(10) unsigned NOT NULL AUTO_INCREMENT,
'id_invoice' int(10) unsigned NOT NULL,
'id_client' int(10) NOT NULL,
'libelle' varchar(100) NOT NULL,
'quantite' int(11) NOT NULL,
'tarif_unitaire' decimal(10,2) NOT NULL,
'tva' decimal(10,2) NOT NULL,
'tarif_total' decimal(10,2) NOT NULL,
'date' date NOT NULL,
PRIMARY KEY ('id')
KEY 'id_client' ('id_client')
) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=4 :

CREATE TABLE IF NOT EXISTS 'infos_client' (
'id' int(10) unsigned NOT NULL AUTO_INCREMENT,
'numero_client' int(10) unsigned NOT NULL,
'nom' char(20) NOT NULL,
'prenom' char(20) NOT NULL
'adresse' varchar(200) NOT NULL,
PRIMARY KEY ('id'),
UNIQUE KEY 'numero_client' ('numero_client')
) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=3 ;

INSERT INTO 'detail_service' ('id', 'id_invoice', 'id_client', 'libelle', 'quantite', 'tarif_unitaire', 'tva', 'tarif_total', 'date')
VALUES
(1, 1002, 1, 'Repassage', 2, '10.000', '19.60', '23.92', '2001-03-18'),
(2, 1002, 1, 'Livraison de courses', 3, '8.90', '19.60', '31.93', '2001-03-18'),
(3, 1003, 2, 'Jardinage', 1, '7.90', '19.60', '9.45', '2001-03-18');

INSERT INTO 'infos_client' ('id', 'numero_client', 'nom', 'prenom', 'adresse')
VALUES
(1, 200000, 'Client 1', 'prénom', 'rue albert <br/> appartement 6 <br/> batiment anaconda<br/> 22222 Lazaret <br/> Haute-PHP'),
(2, 200001, 'Client 2', 'prenom', 'rue alphonso');

Les valeurs sont bien entendues bidons et le nombre de table est limitée donc pas optimiser au mieux.

Première étape

On crée une classe pour récupérer les informations de la facture :

private $_link;

/* Connexion PDO */
public function __construct($dsn, $user, $passwd)
{
    $this->_link = new PDO($dsn, $user, $passwd);
}

public function __destruct()
{
    return $this->_link = false;
}

/* Affichage des erreurs */
public function displayErrors($msg)
{
    if(!empty($msg)) {
        $display = (!is_array($msg)) ? $msg : (implode('<br/>', $msg));
        throw new Exception($display);
    }
    return false
}

/* Total des totaux */
public function getTotal($id_invoice)
{
    if($this->_link) {
        $sth = $this->prepare('SELECT SUM(tarif_total) AS total_facture FROM detail_service WHERE id_invoice = :id_invoice');
        $sth->execute( array( ':id_invoice' => intval($id_invoice) ) );
        $err = $sth->errorInfo();
        if(!empty($err[1]))
            $this->displayErrors($err);
        return $sth->fetch();
    }
    $this->displayErrors('Connexion non initialisée');
}

/* Détails de la facture */
public function getDetails($id_invoice)
{
    if($this->_link) {
        $sth = $this->_link->prepare('SELECT ds.id_invoice, ds.id_client, de.libelle,
        ds.quantite, ds.tarif_unitaire, ds.tva, ds.tarif_total, ds.date,
        ic.id, ic.numero_client, ic.nom, ic.prenom, ic.adresse
        FROM detail_service ds
        LEFT JOIN infos_client ic
        ON ds.id_client = ic.id
        WHERE id_invoice = :id_invoice');
        $sth->execute( array( ':id_invoice' => intval($id_invoice) ) );
        $err = $sth->errorInfo();
        if(!empty($err[1]))
            $this->displayErrors($err);
        return $sth->fetchAll();
    }
    $this->displayErrors('Connexion non initialisée');
}

Les deux méthodes qui nous intéresse sont getTotal() et getDetails().

  • getTotal() : pour récupérer le total du montant de notre facture
  • getDetails() : pour récupérer les autres informations de notre facture

Toutes les deux prennent en arguments le numéro/ID de la facture.

On nommera le fichier : 'classe.invoice.php'

Deuxième étape - on crée nos modèles de templates

'style.tpl'

D'abord la feuille de style 'style.tpl', que personnellement j'ai choisi de donner comme extension '.tpl'.
Ce n'est pas une obligation !

{literal}
<style type="text/css">
<!--
table.tableLogo {
width:100%;
}
table.tableInfoClient {
width:100%;
text-align:right;
font-size:14px;
margin-top:10px;
}
table.tableInfoClient td{
width:76%;
padding-right:3px;
text-align:left;
padding-bottom:0px;
}
table.tableBar {
width:100%;
}
td.bar_1 {
width:13%;
text-align:left;
}
td.bar_1 div {
background:#777777;
height:1px;
float:left;
width:100%;
}
td.bar_2 {
width:16.5%;
text-align:left;
}
td.bar_2 div {
color:#777777;
font-weight:bold;
font-size:20px;
float:left;
font-style:italic;
}
td.bar_3 {
width:71%;
text-align:left;
}
td.bar_3 div {
background:#777777;
height:1px;
float:left;
width:100%;
}
table.tableAdresse {
border:1px;
width:100%;
font-size:14px;
margin-top:10px;
}
table.tableAdresse td {
border-width:1px;
padding:10px;
}
table.tableAdresse span {
color:#000000;
font-weight:bold;
font-size:14px;
}
table.tableDetails {
width:100%;
margin-top:15px;
border-top:1px;
border-left:1px;
border-bottom:1px;
}
table.tableDetails td {
font-size:14px;
padding:3px;
}
td.detailsTitre_1 {
font-weight:bold;
width:40%;
padding-bottom:10px;
border-right:1px;
text-align:left;
}
td.detailsTitre_2 {
font-weight:bold;
width:15%;
border-right:1px;
text-align:right;
}
td.details_1 {
border-right:1px;
text-align:left;
padding-bottom:2px;
}
td.details_2 {
border-right:1px;
text-align:right;
}
table.tableTotal {
width:100%;
font-size:14px;
}
td.total {
padding-top:3px;
padding-right:5px;
text-align:right;
width:100%;
}
table.page_footer {
width:100%;
border: none;
background-color:#DDDDFF;
border-top:solid 1mm #AAAADD;
padding:2mm;
}
table.page_footer td {
width:100%;
text-align:center;
font-size:14px;
}
table.page_footer span {
font-size:12px;
}
-->
</style>
{/literal}

Les balises {literal}{/literal} sont des fonctions natives à smarty pour lui indiquer qu'il doit utiliser le bloc tel quel.

'invoice.tpl'

<page backtop="5mm" backbottom="5mm" backleft="5mm" backright="5mm">
    <table class="tableLogo">
        <tr>
            <td align="left" valign="top">
                <img src="{$logo}" style="width:100px;height:137px;">
            </td>
        </tr>    
    </table>        

    <table class="tableInfoClient">
        <tr>
            <td>
                <table cellspacing="0" cellpadding="0">
                    <tr>
                        <td>
                            Numéro client : {$infos_facture[0]['numero_client']}
                        </td>
                    </tr>    
                    <tr>
                        <td>
                            Nom : {$infos_facture[0]['nom']|upper}
                        </td>
                    </tr>    
                    <tr>
                        <td>        
                            Prénom : {$infos_facture[0]['prenom']}
                        </td>
                    </tr>
                </table>
            </td>
            <td>
                <table cellspacing="0" cellpadding="0" style="padding-right:3px;">
                    <tr>
                        <td>
                            Date de la facture :
                        </td>
                        <td>
                            {$infos_facture[0]['date']|date_format:"%d.%m.%Y"}
                        </td>
                    </tr>
                </table>
            </td>
        </tr>
    </table>

    <table class="tableBar">
        <tr>
            <td class="bar_1">
                <div></div>
            </td>
            <td class="bar_2">
                <div>Facture client</div>
            </td>
            <td class="bar_3">
                <div></div>
            </td>
        </tr>
    </table>
    
    <table class="tableAdresse" align="center">
        <tr>
            <td>
                <span>
                    Adresse de prestation et de facturation :
                </span><br><br>
                    {$infos_facture[0]['adresse']|upper}
            </td>
        </tr>    
    </table>

    <table class="tableDetails" cellspacing="0">
        <tr>
            <td class="detailsTitre_1">
                Libellé
            </td>
            <td class="detailsTitre_2">
                Quantitée
            </td>
            <td class="detailsTitre_2">
                Prix unitaire
            </td>
            <td class="detailsTitre_2">
                T.V.A
            </td>
            <td class="detailsTitre_2">
                Prix total
            </td>
        </tr>
        
        {foreach from=$infos_facture key=keys item=value}
        <tr>
            <td class="details_1">
                {$infos_facture.$keys.libelle}
            </td>
            <td class="details_2">
                {$infos_facture.$keys.quantite}
            </td>
            <td class="details_2">
                {$infos_facture.$keys.tarif_unitaire} €
            </td>
            <td class="details_2">
                {$infos_facture.$keys.tva} %
            </td>
            <td class="details_2">
                {$infos_facture.$keys.tarif_total} €
            </td>
        </tr>
        {/foreach}
    </table>
    
   <table class="tableTotal" cellspacing="0" cellpadding="0">
        <tr>
            <td class="total">
                <strong>Total : {$total_facture[0]} €</strong>
            </td>
        </tr>
    </table>    
</page>
<page_footer>
    <table class="page_footer">
        <tr>
            <td>
                <strong>{$infos_societe['nom']} vous remercie de votre confiance !<br></strong>
                <span>
                    {$infos_societe['nom']|lower}, {$infos_societe['adresse']|lower} - numéro siren : {$infos_societe['siren']}
                </span>
            </td>
        </tr>
    </table>
</page_footer>

Troisième et dernière étape

On crée le fichier qui nous permettra de générer notre PDF

/* Initialisation */
$debug = true;
$id_facture = 1002; // numéro de la facture en cours
// Informations sur la société
$societe = array(
    'nom' =>'Société Alpha',
    'adresse' => 'Rue du chat vert',
    'siret' => '000.000.000'
);

/* Initialisation de smarty */
require_once('smarty/Smarty.class.php');
function SmartyLoadClass($className) {
    if (!class_exists($className, false))
        require_once(dirname-__FILE__).'/class.'.$className.'.php');
}
spl_autoload_register ('SmartyLoadClass');
    $smarty = new Smarty();
    $smarty->template_dir = dirname(__FILE__).'/gabarit_pdf';
    if($debug)
        $smarty->debug = false;

/* On récupère les informations de la facture */
try {
    $invoice = new Invoice('mysql:host=DBSERVER;dbname=DBNAME', 'DBUSER', 'DBPASSWD');
    $infos = $invoice->getDetails($id_facture);
    if(empty($infos))
        throw new Exception('Impossible de récupérer la facture demandée');
}
catch(Exception $catch) {
    if($debug)
        die('<pre>' . $catch . '</pre>');
    die('<pre>' . $catch->getMessage() . '</pre>');
}

/* Assignation des données */
$smarty->assign(array(
    'tpl_dir' => $smarty->template_dir.'invoice/',
    'infos_facture' => $infos,
    'total_facture' => $invoice->getTotal($id_facture),
    'infos_societe' => $societe,
    'logo' => 'logo.png'
));

/* Affichage des templates */
$smarty->display('invoice/css/style.tpl');
$smarty->display('invoice/invoice.tpl');
$smarty->display('invoice/footer.tpl');
// On récupère le contenu de la page en sortie
$content_html = ob_get_clean();

/* On génère le PDF */
require(dirname(__FILE__).'/html2pdf/html2pdf.class.php');
try {
    $marges = array(2, 2, 2, 2);
    $html2pdf = new HTML2PDF('P', 'A4', 'fr', false, 'ISO-8859-1', $marges);
    $html2pdf->setDefaultFont('times');
    $html2pdf->pdf->SetAuthor($societe['nom']);
    $html2pdf->writeHTML($content_html);
    //if($debug)
    //    $html2pdf->setModeDebug()
    $html2pdf->Output('facture_'.$id_facture.'.pdf');
}
catch(HTML2PDF_exception $catch) {
    if($debug)
        die('<pre>' . $catch . '</pre>');
    die('Impossible de générer la facture demandée');
}

Avantages

Le principal avantage est bien entendu la séparation des codes PHP et XHTML. Ce qui permet de générer des pdf sans trop faire appel aux nombreuses méthodes qu'incorporent les librairies PDF en générale.

Contraintes

- La génération des PDF est un peu longue. Ceci n'est pas du à smarty mais bel et bien à html2pdf. En effet, pour générer le PDF à partir des codes html celui doit retraiter les codes html. Nous nous trouvons donc au final avec 6 couches d'analyse et de traitement pour générer un PDF :
Préparation template->Traitement template->Sortie template->Préparation PDF->Adaptation->Sortie PDF
La partie la plus longue en terme de temps est bien entendu `Adaptation'.
- Html2Pdf ne traite pas le CSS 3.0 (bien qu'il ne soit pas officiellement sortie) et à du mal également avec certains attributs du CSS 2.0.

Conclusion

Si vous n'êtes pas trop familier avec les méthodes brutes, ou dans un souci de déploiement rapide cette méthode conviendra surement. Mais dans un souci de rapidité vaut mieux passer par la méthode brute.
Remarque : Pour pallier aux lenteurs, on peut toujours sauvegarder les fichiers pdf sur le serveur et faire appel à ceux-ci directement au lieu de les générer systématiquement. Dans ce cas notre méthode conviendra parfaitement.

Autres informations

- Vous pouvez télécharger une partie des fichiers du tuto ici, renommer-le fichier en '.zip'
- Wiki de html2pdf : http://wiki.spipu.net/doku.php?id=html2pdf:fr:v4:accueil
- Documentation de smarty : http://www.smarty.net/docsv2/fr/
- Pas de demande d'aide ici, mais sur le Forum

A voir également
Ce document intitulé « Générer des pdf grâce à htmltopdf et smarty » issu de CodeS SourceS (codes-sources.commentcamarche.net) est mis à disposition sous les termes de la licence Creative Commons. Vous pouvez copier, modifier des copies de cette page, dans les conditions fixées par la licence, tant que cette note apparaît clairement.
Rejoignez-nous