Générer du PDF sous CakePHP v3

Bonjour à tous,

un nouveau tutorial ! cette fois-ci nous étudierons sur la possibilité de générer des fichiers PDF sous CakePHP v3. Le cas d’usage est le suivant :

Fournir un lien vers un fichier PDF de votre site codé avec CakePHP. Le fichier PDF est généré à la volée ou stocké sur le serveur. Le lien est de la forme /action/monfichier.pdf

Pour ce faire nous avons 2 solutions :

  • Utiliser un plugin dédié, comme CakePDF
  • soit coder nous-même la gestion du fichier PDF avec la librairie de notre choix

C’est pour la deuxième option que ce tutorial est prévu. L’utilisation de CakePDF fera l’objet d’un autre article.

Alors autant vous prévenir d’avance, on va taper dans le dur, sans forcément respecter les conventions de codage CakePHP. Néanmoins, le code qui vous sera présenté est simple à comprendre et facile à mettre en oeuvre. Nous ne créerons pas de plugins ou de composants, nous allons développer le code tel quel dans nos controller.

Prérequis

Avoir un environnement CakePHP opérationnel, c’est à dire installé, configuré et fonctionnel.

Etape 1 – Installer la librairie

Télécharger et installer la librairie PDF de votre choix : mPDF, DomPDf, TCPDF, HTML2PDF, etc… L’avantage de ma petite méthode c’est que n’importe quelle librairie PDF peut fonctionner, du moment qu’elle est prévue pour PHP.

Pour la suite de ce tutorial, j’utiliserai TCPDF.

Vous téléchargez la librairie TCPDF et vous l’installez dans l’emplacement suivant ;

/vendor/

vous devriez avoir une arborescence comme celle-ci dans vendor/ :

capture-tcpdf

Etape 2 – Configurer les routes

Il y’a 2 choses à faire pour que l’extension PDF puisse être interprétée par CakePHP. Tout d’abord dans le fichier routes.php, nous allons indiquer à notre « router » que notre application traitera les extension pdf, et ce sur toutes les routes. Pour ce faire, nous ajoutons dans routes.php la ligne suivante :

Router::extensions(['pdf']);

Si vous avez déjà une ligne Router::extensions, rajoutez l’extension ‘pdf’ dans le tableau existant.

A quoi cette ligne sert ?

Elle permet au « router » de prendre l’url demandée, et si l’extension « pdf » est détectée dans l’url, cette extension est enlevée et placée dans un tableau de retour. Quel est l’intérêt ? Et bien cela prend tout son sens avec le composant RequestHandler. Et nous arrivons à notre 2eme étape : paramétrer le RequestHandler.

Qu’est-ce que le RequestHandler ?

Dans notre cas, le RequestHandler permet d’analyser le type de requête reçue par CakePHP et fournit (entre autre) le type de retour attendu en fonction de la requête demandée (par exemple XML, JSON, etc.). Pour notre génération de PDF, RequestHandler récupère l’extension supprimée par Router::extensions, et va charger la vue et le layout de l’extension récupérée.

Par exemple, si votre lien est de type : /controller/action/1.pdf

RequestHandler va utiliser :

  • le layout situé dans /Template/Layout/pdf/default.ctp
  • la vue située dans /Template/controller/pdf/action.ctp

Pour déclarer notre requestHandler, nous modifions notre AppController.php (pour « globaliser » la gestion PDF) ou dans le controller approprié, et modifions la fonction « initialize » en chargeant le component :

public function initialize()
    {
        parent::initialize();
        $this->loadComponent('RequestHandler');
    }

 Etape 3 – Créer le layout et la vue

Vous l’aurez compris, maintenant que le RequestHandler sait gérer les liens avec l’extension PDF, il nous faut créer le layout et la vue correspondant à l’action qui nous intéresse.

Partons de l’idée que nous appelons l’url : « commandes/imprime_bon/1.pdf »

Comme nous l’avons dit plus haut, nous devons créer 2 répertoires « PDF » : un sous /Templates/layout et l’autre dans le répertoire de notre controller, donc dans « /Templates/commandes/ »

Dans le répertoire /Templates/layout/pdf, nous créons le template par défaut qui sera appelée par le controller : default.ctp

Contenu du default.ctp

C’est là où nous ne faisons pas exactement les choses comme il faudrait. La logique CakePHP voudrait que nous codions un composant pour produire un fichier PDF. Mais nous, nous allons faire plus simple, nous allons directement inclure la librairie TCPDF dans le default.ctp. Voici le code complet du default.ctp, avec les commentaire pour comprendre ce que l’on fait :

<?php
// inclusion de la librairie TCPDF
    require_once ROOT . DS . 'vendor' . DS . 'tecnick.com' . DS . 'tcpdf' . DS . 'tcpdf.php'; 

// Création d'un document TCPDF avec les variables par défaut
    $pdf = new TCPDF('L', 'mm', PDF_PAGE_FORMAT, TRUE, 'UTF-8', FALSE);
// Spécification de certains paramètres de TCPDF (içi on spécifie l'auteur par défaut)
    $pdf->SetCreator(PDF_CREATOR);

// On enlève l'entête et le pied de page
    $pdf->setPrintHeader(FALSE);
    $pdf->setPrintFooter(FALSE);

// On spécifie la fonte par défaut
    $pdf->SetDefaultMonospacedFont(PDF_FONT_MONOSPACED);

// On définit les marges
    $pdf->SetMargins(5, 37, PDF_MARGIN_RIGHT);
    $pdf->SetHeaderMargin(PDF_MARGIN_HEADER);
    $pdf->SetFooterMargin(5);

// On indique que le dépassement d'une page entraine automatiquement la création d'un saut de page et d'une nouvelle page
    $pdf->SetAutoPageBreak(TRUE, 5);

// ---------------------------------------------------------

// La fonte et la couleur à utiliser dans la page qui va être créée
    $pdf->SetFont('times', '', 10);
    $pdf->setColor('text', 0, 0, 0);
// On ajoute une page
    $pdf->AddPage();
// voilà l'astuce, on récupère la vue HTML créée par CakePHP pour alimenter notre fichier PDF
    $pdf->writeHTML($this->fetch('content'), TRUE, FALSE, TRUE, FALSE, '');
// on ferme la page
    $pdf->lastPage();
// On indique à TCPDF que le fichier doit être enregistré sur le serveur ($filename étant une variable que vous aurez pris soin de définir dans l'action de votre controller)
    $pdf->Output(APP . 'files' . DS . 'pdf' . DS . $filename . '.pdf', 'F');
?>

C’est un default.ctp de base, vous pouvez/devez configurer les options de TCPDF dans ce fichier. L’idée est de transformer l’output HTML de la vue pour la convertir en PDF.
Dans notre configuration, nous n’avons fait qu’enregistrer le fichier PDF sur le disque, mais le mieux serait d’envoyer le fichier vers le navigateur, pour cela vous pouvez utiliser :

$pdf->Output(APP . 'files' . DS . 'pdf' . DS . $filename . '.pdf', 'FI');

Il y’a des avantages à cette méthode, et des inconvénients :

  • L’avantage c’est que vous ne pouvez utiliser qu’une vue unique entre l’affichage HTML et l’affichage PDF, donc gain de temps.
  • L’inconvénient c’est que les librairies PDF interprètent (souvent) très mal les pages compliquées, avec des styles CSS3, etc. Et le résultat est quelquefois hasardeux. Dernier point : toutes vos images affichées doivent être en chemin absolu, et non en chemin relatif, car les librairies ne savent pas toujours lire les chemins relatifs.

Passons maintenant à notre vue imprime_bon.ctp :

<style>
    th {
        font-weight: bold;
    }
</style>
<table border="1" cellpadding="1" width="100%">
    <thead>
    <tr>
        <th>Date</th>
        <th width="150">Client</th>
        <th style="text-align:center;">Produit</th>
        <th style="text-align:center;">Qté</th>
        <th style="text-align:center;">Prix</th>
    </tr>
    </thead>
    <tbody>
    <?php foreach ($commandes as $commande) : ?>
        <tr>
            <td><?= date_format($commande->date, 'd-m-Y') ?></td>
            <td width="150"><?= $commande->user->nomcomplet ?></td>
            <td style="text-align:center;"><?= $commande->produit?></td>
            <td style="text-align:center;"><?= $commande->qte?></td>
            <td style="text-align:center;"><?= $commande->prix?></td>
        </tr>
    <?php endforeach; ?>
    </tbody>
</table>

C’est une vue minimaliste, juste pour l’exemple. Vous constatez que cette vue pourrait être utilisée pour l’affichage HTML, cela fonctionnerait parfaitement !

Etape 4 – Code Controller

Rien à faire côté controller ! c’est ça qui est magique

Conclusion

A partir de ce code de base, vous pouvez développer, par exemple en vérifiant l’extension PDF au niveau de l’action pour définir des variables en plus, etc.

La prochaine fois je vous montrerai un petit truc pour générer un fichier PDF de façon transparente pour l’utilisateur sans qu’il ne s’en rend compte, de cette façon il peut continuer sa navigation tranquillement sans être interrompu.

J’espère que cet article vous a plu, Je reste à votre disposition sur le forum Francophone de CakePHP si vous avez des questions.

A bientôt !

Comments: 6

  1. Moïse Wicht says:

    Salut.

    Bon tutoriel !

    Mon problème c’est que j’ai déjà mon arborescence de créée, je travaille en template/admin et une vue est déjà créée.

    De plus , je ne comprends pas l’intérêt de tout passer depuis le layout, car je récupère des valeurs depuis le controller et ce serait depuis là que je devrais préparer le PDF.

    Est-ce que tu serais disposé à me donner un coup de main ? Car je ne peux pas utiliser les routes du coup…
    Merci d’avance

  2. cyberbobjr says:

    Disons que le minimum c’est le layout, car c’est le layout qui permet de générer le PDF.
    Après la vue peut être partagée entre un output PDF ou un output HTML.
    Si tu veux plus de renseignements, n’hésites pas à aller sur le forum Cakephp-fr.com

  3. rcnchris says:

    Bonjour,

    Merci pour ton tutoriel.

    Mais malheureusement je n’arrive pas à le faire fonctionner… Mon objectif final est relativement simple, je veux générer un pdf à partir d’une vue index.ctp en cliquant sur un lien présent sur cette même page.

    J’utilise wkhtmltopdf dans le default.ctp. Voici le code simplifié au maximum… (ce code fonctionne dans un environnement « non Cake »)

    $pdf = new \mikehaertl\wkhtmlto\Pdf();
    $options = [
    ‘encoding’ => ‘utf-8’,
    ‘orientation’ => ‘Portrait’, // ou Landscape
    ];
    $pdf->setOptions($options);
    $pdf->addToc([‘toc-header-text’ => ‘Index’]);
    $pdf->addPage($this->fetch(‘content’));
    $pdf->send();

    Comme tu l’auras compris je suis donc sur la page index de mon controller « Users » et j’ai généré un lien comme ceci : /users/liste/liste.pdf.

    Mon problème est qu’il cherche toujours la méthode « liste » dans le controller Users… « The action liste is not defined in UsersController ».

    J’espère t’avoir donné les bons éléments… Puis-je compter sur ton aide à l’occasion ?

    Encore merci pour ton article !

    • cyberbobjr says:

      Salut,
      effectivement si tu indiques /users/liste dans ton url, il cherchera toujours la fonction « liste » dans ton controller. Tu as 2 solutions pour résoudre le problème :
      1°) tu créé une route « /users/liste » qui pointe vers « index », en prenant bien soin d’ajouter la gestion des extensions PDF dans ton fichier de routes (pour que ce soit le layout du PDF qui soit utilisé et non le layout default)
      ou 2°) tu créé une fonction « liste » dans ton controllers « users », cette fonction liste utilisera la vue « index » mais avec la gestion de l’extension PDF, de cette façon avec l’extension PDF ce sera le layout PDF qui sera utilisé,
      A+

      • rcnchris says:

        Bonjour et merci beaucoup pour ton retour,

        Mais je suis contraint de t’embêter de nouveau… car il y a du mieux. L’apercu PDF s’affiche mais avec un message « Echec du chargement du document PDF »

        Je vais essayer d’être plus complet cette fois-ci :

        – Je suis sur Debian 8.2, Apache, PHP 5.6.23 et Cake 3.2.12 (machine virtuelle)
        – wkhtmltopdf installé en version 0.12.2.1 (with patched qt)

        – Dans le routes.php j’ai ajouté cette ligne : $routes->addExtensions([‘pdf’]);
        Ce qui a pour effet de modifier les routes suivantes comme ceci :
        extensions())>
        _controller:index / pdf
        _controller:_action / pdf

        – Dans Layout/pdf/default.ctp :
        J’ai une entête HTML simplifiée avec un lien vers un fichier css qui est la : webroot/css/pdf.css
        Puis dans le body j’ai le code PHP suivant :

        $pdf = new \mikehaertl\wkhtmlto\Pdf();
        $options = [
        ‘title’ => ‘Facture – ‘.$faker->randomNumber(5),
        ‘encoding’ => ‘utf-8’,
        ‘orientation’ => ‘Portrait’,
        ‘page-size’ => ‘A4’,

        ];
        $pdf->setOptions($options);
        $pdf->addPage($this->fetch(‘content’));
        $pdf->addToc([‘toc-header-text’ => ‘Index’]);
        $pdf->send();

        Aucun javascript dans la page.

        – Je suis sur une page de vue (j’ai changé…) d’un utilisateur appcake/users/view/1
        – J’ai généré un lien qui pointe là : users/viewpdf/1 (sans le .pdf à la fin)

        – Arborescence Templates/Users :
        pdf/viewpdf.ctp
        index.ctp
        view.ctp

        – La méthode viewpdf dans UsersController :

        public function viewpdf($id = null)
        {
        $user = $this->Users->get($id, [
        ‘contain’ => [‘Civilites’, ‘Groupes’, ‘Images’, ‘Articles’, ‘Bookmarks’, ‘Todos’]
        ]);
        $action = ‘view’;
        $icon = $this->icons[$action];
        $comment = $this->getCommentTable();
        $title = $user->FullName;
        $this->set(compact(‘user’, ‘title’, ‘icon’, ‘comment’, ‘action’));
        $this->set(‘_serialize’, [‘user’]);

        $this->response->type(‘pdf’);
        return $this->response;
        }

        Suite à ton retour j’ai ajouté les deux dernières lignes, ce qui a eu pour effet d’afficher l’aperçu PDF mais avec le message d’erreur cité plus haut.

        – Enfin… Le fichier viewpdf.ctp est un copier/coller du fichier view.ctp, dans lequel j’ai enlevé le code PHP propre au layout « normal » de toutes mes pages.

        J’espère que ces éléments pourront t’éclairer car je dois t’avouer que je deviens barge avec ces PDF sous Cake… Alors que tout le reste est d’une simplicité enfantine… Comprends pas 🙁

        Merci pour ton aide, elle m’est précieuse.

  4. sahib says:

    vous n’aurez pas un example qui marche et merci

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *