Génération de fichier PDF sur Symfony 2 avec le bundle KnpSnappyBundle

Génération de fichier PDF dans une application Symfony 2.

Publié le 13/09/2015

Nous allons générer des fichiers PDF avec KnpSnappyBundle, ce dernier se base sur Wkhtmltopdf et nécessite Xvfb fonctionner. On part du principe qu'on est sur une Debian 7.x.

1 - Installation de Wkhtmltopdf et Xvfb


su
apt-get update
apt-get install wkhtmltopdf xvfb

2 - Configuration du wrapper

nano /usr/bin/wkhtmltopdf.sh


EOF
#!/bin/sh
xvfb-run -a wkhtmltopdf $@

Rendre le fichier exécutable :

chmod +x /usr/bin/wkhtmltopdf.sh

3 - Installation de KnpSnappyBundle

Ajout du require dans le fichier /composer.json; ajouter les lignes suivantes :

"knplabs/knp-snappy-bundle": "^1.3"

Mettre à jour le vendors :

php composer.phar update

Activation du bundle dans le fichier /app/AppKernel.php, ajouter la ligne suivante dans la méthode registerBundles() :

new Knp\Bundle\SnappyBundle\KnpSnappyBundle(),

Configuration du Bundle :

nano app/config/config.yml


knp_snappy:
    pdf:
        enabled:    true
        binary:     /usr/bin/wkhtmltopdf.sh
        options:    []

4 - Exemple de génération de PDF

Je vais partir du projet Jobmanager pour l'exemple, le sources sont disponibles sur GitHub.

Nous allons utiliser le bundle dans un couple controller/vue qui va générer un fichier PDF à partir de la vue.

4.1 - Création de la route

nano src/Jobmanager/MainBundle/Resources/config/routing/cv.yml


jobmanager_main_bundle_cv_generate_pdf:
    path: /cv/generate-pdf
    defaults: { _controller: JobmanagerMainBundle:Cv:generatePdf }
    requirements:
        _method: GET

4.2 - Modifications du controller

Ajout d'une méthode generatePdfAction() au controller :

nano src/Jobmanager/MainBundle/Controller/CVController.php


public function generatePdfAction()
    {
        $em = $this->getDoctrine()->getManager();
        $cvPath = '/var/www/jobmanager/web/docs/cv.pdf';

        // init memcache obj
        $cache = $this->get('memcache.default');

        // retrieve tutorials
        if ($cache->get('tutorials_list')) {
            $tutorials = $cache->get('tutorials_list');
        } else {
            $tutorials = $em->getRepository('JobmanagerMainBundle:Tutorial')->getLastPublishedTutorialsByDate(3);
            $cache->delete('tutorials_list');
            $cache->set('tutorials_list', $tutorials, null, self::ONE_DAY);
        }

        // retrieve candidate

        $candidate = $em->getRepository('JobmanagerMainBundle:Candidate')->find(1);

        // retrieve candidate
        $candidate = $em->getRepository('JobmanagerMainBundle:Candidate')->find(1);

        // calculate age
        $birthDate = $candidate->getBirthdate()->format('m/d/Y');

        // explode the date to get month, day and year
        $birthDate = explode("/", $birthDate);

        // get age from date or birthdate
        $age = (date("md", date("U", mktime(0, 0, 0, $birthDate[0], $birthDate[1], $birthDate[2]))) > date("md")
            ? ((date("Y") - $birthDate[2]) - 1)
            : (date("Y") - $birthDate[2]));

        // retrieve categories competence
        $categoriesCompetence = $em->getRepository('JobmanagerMainBundle:CategoryCompetence')->getAllCategoryWithCvCompetence();

        // retrieve experiences
        $experiences = $em->getRepository('JobmanagerMainBundle:JobExperience')->getJobExperiencesByDate();

        // remove old cv
        if (file_exists($cvPath)) {
            unlink($cvPath);
        }

        // generate pdf
        $this->get('knp_snappy.pdf')->generateFromHtml(
            $this->renderView('JobmanagerCmsBundle:Cv:cv-pdf.html.twig',array(
                    'candidate'  => $candidate,
                    'tutorials' => $tutorials,
                    'age' => $age,
                    'categoriesCompetence' => $categoriesCompetence,
                    'experiences' => $experiences
                )
            ),
            $cvPath
        );

        // set flash bag message
        $this->get('session')->getFlashBag()->add('notice', 'CV généré');

        // send redirection
        return $this->redirect($this->generateUrl('jobmanager_main_bundle_admin_index'));
    }

NDLR : Pour l'exemple la vue du cv n'étant pas terminée, elle suffira pour l'exemple. Ne pas tenir compte de l'appel du footer et du header ;).

4.3 - Création de la vue

nano src/Jobmanager/CmsBundle/Resources/Cv/cv-pdf.html.twig


{% extends "JobmanagerCmsBundle::layout.html.twig" %}

{% block title %}CV - Pierre-Antoine Foulquier | Développeur Web - Administrateur système{% endblock %}

{% block description_keywords %}
    <meta name="description" content="CV de Pierre-Antoine Foulquier, développeur web et adminisrateur système : compétences orientées dev-back, devops sur le framework PHP Symfony 2." />
    <meta name="keywords" content="CV, développeur, web, administrateur système, back-end, devops, symfony 2, PHP" />
{% endblock %}

{% block fb_meta_og %}
    <meta property="og:title" content="CV - Pierre-Antoine Foulquier | Développeur Web - Administrateur système"/>
    <meta property="og:type" content="website"/>
    <meta property="og:url" content="http://www.foulquier.info/competences/cv/">
    <meta property="og:image" content="http://www.foulquier.info/images/fb-og-picto.jpg"/>
    <meta property="og:description" content="CV de Pierre-Antoine Foulquier, développeur web et administrateur système : compétences orientées dev-back, devops sur le framework PHP Symfony 2."/>
    <meta property="og:site_name" content="Pierre-Antoine Foulquier | Développeur Web - Administrateur système"/>
    <meta property="fb:admins" content="100004078454329"/>
{% endblock %}

{% block content %}
    <div class="row">
        <div class="col-xs-12 col-sm-12 col-md-12">
            <div id="cv-wrapper">
                <section class="header-cv">
                    <h1 class="title-02">{{ candidate.firstname }} {{ candidate.lastname }}</h1>
                    <p>{{ age }} ans</p>
                    <p>{{ candidate.jobname }}</p>
                </section>
                <section class="bloc-tech-cv">
                    <h2 class="title-02">Compétences techniques</h2>
                    {% for category in categoriesCompetence %}
                        <div class="category-tech">
                            <p>
                                <strong>{{ category.title }} :</strong>
                                {% for competence in category.competences %}
                                    {% if competence.isOnCv == true %}
                                        {% if loop.last %}
                                            {{ competence.name }}.
                                        {% else %}
                                            {{ competence.name }},
                                        {% endif %}
                                    {% endif %}
                                {% endfor %}
                            </p>
                        </div>
                    {% endfor %}

                </section>
                <section class="language">
                    <h2 class="title-02">Langues</h2>
                    <div class="bloc-language">
                        <p><strong>Anglais :</strong> Courant</p>
                    </div>
                    <div class="bloc-language">
                        <p><strong>Allemand :</strong> Bilingue</p>
                    </div>
                </section>
                <section class="formation">
                    <h2 class="title-02">Formations</h2>
                    <div class="bloc-formation">
                        <p><strong>2012 :</strong> Formation PHP développeur à IP FORMATION (280h) - Paris 12e</p>
                    </div>
                    <div class="bloc-formation">
                        <p><strong>2011 :</strong> Formation PHP développeur (niveau II) à l’IFOCOP (900h) - Paris 11e</p>
                    </div>
                </section>
                <section class="experience">
                    <h2 class="title-02">Expériences</h2>
                    {% for experience in experiences %}
                        <div class="bloc-experience">
                            <h3 class="company-name">
                                <strong>{{ experience.companyName }}</strong>
                            </h3>
                            <h4 class="title-experience">{{ experience.titleJob }}</h4>
                            <h4 class="date-experience">{{ experience.dateBegin|date('d/m/Y') }} - {{ experience.dateEnd|date('d/m/Y') }}</h4>
                            <p>{{ experience.description }}</p>
                            {% for key, project in experience.projects %}
                                <div class="bloc-project">
                                    <h5 class="title-project">
                                        <strong>Projet {{ key+1 }} : {{ project.name }}</strong>
                                    </h5>
                                    <div class="project-description">{{ project.description|raw }}</div>
                                    <h6 class="sub-title-project">
                                        <strong>Domaine d'intervention :</strong>
                                    </h6>
                                    <div class="domain-intervention">{{ project.domainIntervention|raw }}</div>
                                    <h6 class="sub-title-project">
                                        <strong>Environnement technique :</strong>
                                    </h6>
                                    <div class="technical-environnement">{{ project.technicalEnvironnement|raw }}</div>
                                    {% if project.url is not null %}
                                        <p>
                                            <a href="{{ project.url }}" target="_blank"><strong>Lien :</strong> {{ project.url }}</a>
                                        </p>
                                    {% endif %}
                                </div>
                            {% endfor %}
                        </div>
                    {% endfor %}
                </section>
            </div>
        </div>
    </div>
{% endblock %}

On peut tester en appelant l'url /admin/cv/generate-pdf. Le résultat sera disponible à l'emplacement suivant : /var/www/jobmanager/web/docs/cv.pdf