Votre première application symfony2

Introduction

Logo Symfony

Depuis les années 2000, PHP est un langage qui fait fureur sur le web. Les points clés qui ont fait son succès sont sa simplicité et sa puissance. En effet, PHP est un langage très abordable qui permet de créer rapidement ses premières applications. Seulement cet avantage est également son plus gros point faible. Par défaut, PHP ne gère absolument pas le modèle MVC (modèle / vue / contrôleur) ce qui fait que dès que l'on veut aborder de gros développements en groupe on se rend compte qu'il devient périlleux de se lancer sans avoir un minimum de méthode. Dès lors, les frameworks se sont multipliés, avec chacun leurs avantages et leurs inconvénients. Nous allons aujourd'hui nous pencher sur un framework très puissant, il s'agit de symfony2. Si vous étiez développeur symfony dans sa version 1.X, vous allez vite constater que cette nouvelle version est vraiment très différente de sa précédente et qu'il va vous falloir un petit temps d'adaptation pour passer à cette nouvelle mouture.

Au cours de ce tutoriel vous allez donc apprendre à mettre en place un environnement de développement symfony2 puis à créer votre première application mettant en scène :

  • une base de données
  • une méthode permettant d'initialiser des données de départ pour votre application (fixtures)
  • un accès utilisateur avec authentification
  • une administration sommaire des enregistrements au sein de la base de données

Vous êtes confortablement installé dans votre canapé, prêt à faire votre première rencontre avec symfony2 ? C'est partit, suvez le guide !

A propos de ce tutorial

Au cours de ce tutorial, nous allons nous lancer dans la création d'une application simpliste pour vous montrer les différents points clés à connaître quant à la mise en place d'une application en Symfony2. Nous allons pour cela créer une vidéothèque pas à pas, de la création du bundle à la page d'administration des films.

Vous pouvez télécharger l'archive du projet en cliquant sur le lien suivant : Archive du projet. Gardez à l'esprit que ce tutorial devrais vous permettre d'écrire vous même votre application, cette archive vous servira uniquement si vous vous apercevez que quelque chose de fonctionne pas comme voulu.

Notions importantes avant de démarrer ce tutoriel

Fonctionnement du modèle MVC avec symfony

Le concept modèle / vue / contrôleur (MVC) permet aux différents intervenants qui vont travailler sur un même projet de travailler dans de bonnes conditions sans se marcher sur les plates bandes. Il permet comme son nom l'indque, de séparer, la gestion de la base de données, l'affichage dans le navigateur et le contrôle effectué par l'utilisateur (notemment les formulaires, la saisie d'une url, etc ...). Puisqu'un dessin vaut mieux qu'un long discours, voici une représentation du fonctionnement de Symfony.

Requête symfony

Vocabulaire à connaître

  • "Bundle" : Ce terme sera un des plus utilisés lors du travail avec le framework symfony2. Un bundle est en quelques sorte un module ou une fonctionnalité. Le coeur de symfony2 lui même est développé à partir d'un ensemble de bundles.
  • "Doctrine" : Doctrine est une librairie php qui n'a pas été développée par les éditeurs de symfony. En outre, c'est la librairie de gestion de base de données la plus mise en avant pour les développements symfony. C'est d'ailleurs celle que nous allons utiliser tout au long de ce tutoriel.
  • "Fixture" : Ce sont des bouts de scripts que l'on peut lancer en mode commande et qui vont générer un ensemble de données de départ dans la base de données. Si par exemple on veut fournir un ensemble de données pour certaines tables systèmes dans une application, on peut les créer à partir d'un script de fixtures.
  • "Twig" : Comme doctrine, twig est une librairie qui n'est pas propre à symfony, il s'agit là d'un système de template pour les applications PHP. Il existe 3 possiblités en symfony de créer nos vues : En PHP, en XML ou en TWIG. Dans ce tutoriel nous allons nous concentrer sur les templates en Twig

Installation de votre environnement de test

Nous allons passer les détails d'installation d'un serveur web. Si vous êtes ici, je pense que vous maitrisez déjà cette étape. Si ce n'est pas le cas et que vous travailliez sous windows, je vous conseil de regarder du côté de wamp ou easyPHP. Si en revanche vous travaillez sur un poste de travail linux, Internet regorge de tutos expliquant comment installer un serveur apache avec la prise en charge de php et mysql. Je vous laisse y jeter un oeil !

La première étape consiste à vous rendre sur le site de symfony2 afin de télécharger le framework (logique me direz vous !). Vous pouvez simplement cliquer le lien ci-dessous pour vous rendre sur la page de téléchargement.

Pour ce tutoriel nous allons prendre la version Symfony Standard with vendors dans sa version 2.3 qui est la version stable LTS actuelle.

Comme indiqué sur la page de téléchargement, il suffit de lancer la commande suivante une fois composer installé sur votre ordinateur:

composer create-project symfony/framework-standard-edition path/ "2.3.*"

Si vous ne savez pas comment utiliser composer, tout est expliqué sur la page de téléchargement de Symfony.

Une fois le framework fini de télécharger, le script vous posera quelques questions :

Some parameters are missing. Please provide them.
database_driver (pdo_mysql): 
database_host (127.0.0.1): 
database_port (null): 
database_name (symfony): videotheque
database_user (root): videotheque
database_password (null): password
mailer_transport (smtp): 
mailer_host (127.0.0.1): 
mailer_user (null): 
mailer_password (null): 
locale (en): fr
secret (ThisTokenIsNotSoSecretChangeIt): 

Vous n'êtes pas obligé de répondre car une fois Symfony2 installé, vous aurez accès à un assistant en ligne pour vous permettre de générer le fichier parameters.yml. Ici j'ai quand même complété l'assistant et on utilise une base de nom "videothèque" pour lesquels on a créé préalablement un accès avec les identifiants videotheque/password.

Une fois toutes les questions répondues, vous devriez donc vous retrouver avec la structure suivante :

Requête symfony

  • Le dossier principal que vous avez créé, contenant tout votre projet symfony
  • Le dossier "app" : C'est ici que se passera tout ce qui touche à la configuration de l'application, les identifiants de la base de données, le référencement des différents bundles, la configuration de la sécurité ou encore le routage des différentes urls à traiter.
  • Le dossier "src" : C'est l'emplacement qui va nous intéresser le plus, il contiendra, l'ensemble des controlleur, des vues ainsi que les modèles propres à notre application
  • Le dossier "vendor" : Ce dossier contiendra tous les bundle fournis avec symfony qui permettent de personnaliser votre framework selon vos besoins
  • Enfin le dossier "web" contiendra tous les fichiers qui doivent être accessibles publiquement comme par exemple les images, les fichiers css. Mais aussi et surtout, les points d'entrée de toutes les requêtes utilisateurs.

Lancement de votre application depuis le navigateur

Nous allons maintenant lancer le site tel qu'il est fourni par Symfony2 par défaut. Si nous revenons sur les fichiers contenus dans le dossier web, on peut voir 3 fichiers php :

  • "config.php" : Ce fichier est fourni par Symfony pour que vous puissiez vous assurer que votre serveur est correctement installé pour commencer à utiliser symfony. C'est le premier qu'on lance, nous y reviendrons juste après.

  • "app.php" : Ce fichier php permet de lancer l'application en mode production, c'est celui qui sera lancé par tous les utilisateurs finaux de l'application. Celui-ci n'affichera ni les erreurs php, ni les outils pour développeurs fournis par Symfony.

  • "app_dev.php" : Ce fichier va vous permettre de lancer votre application PHP en mode développeur. Ainsi pour chaque erreur, vous aurez un page de débogage détaillé. Vous aurez également accès à la barre d'aide au développement de symfony permettant à chaque instant de savoir les requêtes qui ont été lancées, le script php dans lequel vous vous trouvez, les mails qui ont été envoyés et bien d'autres choses très utiles.

Lancez maintenant le script config.php afin que symfony puisse vous dire ce qui est à améliorer sur votre serveur. Chez moi, l'url sera :

http://localhost/videotheque/web/config.php ATTENTION ! Si vous avez installé votre répertoire symfony sur un serveur autre que localhost, vous devez éditer le fichier config.php afin de supprimer la sécurité propre à l'utilisation du script à distance. Pour du développement Symfony, il est plus que conseillé d'avoir un environnement local afin d'accomplir les tests et lancer les commandes de génération de code fournies par symfony.

Requête symfony

En lançant le script, on s'aperçoit que Symfony a détecté de nombreuses erreurs qu'il nous faudra corriger avant de pouvoir aller plus loin.

Si aucun style n'apparaît sur la page de config. Vous aurez à lancer la commande suivante afin de générer les assets :

php app/console assets:install web

Le rapport de config contient 2 sections distinctes :

  • MAJORS PROBLEMS : Problèmes qu'il est impératif de résoudre sans quoi Symfony ne fonctionnera tout simplement pas

  • RECOMMANDATIONS : Il s'agit là principalement de conseils pour que votre application fonctionne dans les meilleures conditions. Si vous ne résolvez pas ces problèmes, votre application fonctionnera, mais pourra être plus lente qu'elle ne le devrait.

Une fois que toutes les erreurs seront résolues, vous pouvez cliquer sur le lien "Configure your Symfony Application online" afin de lancer l'assistant de configuration de Symfony. Répondez simplement à toutes les questions pour configurer notamment votre base de données.

  • Voici à quoi devrait ressembler notre fichier "videotheque/app/config/parameters.yml" :
parameters:
    database_driver: pdo_mysql
    database_host: 127.0.0.1
    database_port: null
    database_name: videotheque
    database_user: videotheque
    database_password: password
    mailer_transport: smtp
    mailer_host: 127.0.0.1
    mailer_user: null
    mailer_password: null
    locale: fr
    secret: aa10d5418e365126d2ec101f4bef5ff634
    database_path: null

Prenez un peu de temps pour faire le tour de la démo installée par défaut avec symfony. Chaque page de la démo est documentée et affiche la syntaxe utilisée pour arriver au résultat affiché. Dans le prochain chapitre nous allons supprimer toutes ces données de test.

Suppression des données de la démo et création de notre Bundle

Maintenant que l'on dispose d'un environnement fonctionnel pour développer notre application, il convient de faire un brin de ménage et de créer notre bundle principal. Commençons par le ménage ! Pour une explication détaillée, vous pouvez vous rendre sur la documentation en ligne à cette adresse. Voici donc les étapes :

  • On supprime le dossier "videotheque/src/Acme"
  • On supprime les entrées concernant "AcmeBundle" dans le fichier "videotheque/app/config/routing_dev.yml", il devrait rester le contenu suivant :
_wdt:
    resource: "@WebProfilerBundle/Resources/config/routing/wdt.xml"
    prefix:   /_wdt

_profiler:
    resource: "@WebProfilerBundle/Resources/config/routing/profiler.xml"
    prefix:   /_profiler

_configurator:
    resource: "@SensioDistributionBundle/Resources/config/routing/webconfigurator.xml"
    prefix:   /_configurator

_main:
    resource: routing.yml
  • On supprime le AcmeBundle de la liste des bundles dans le fichier "videotheque/app/AppKernel.php". La ligne à supprimer est la suivante :
$bundles[] = new Acme\DemoBundle\AcmeDemoBundle();
  • On supprime le dossier "web/bundles/acmedemo"
  • On remplace le contenu du fichier "videotheque/app/config/security.yml" par le contenu suivant (nous reviendrons sur ce dernier plus tard, dans une section dédiée) :
security:
    encoders:
        Symfony\Component\Security\Core\User\User: plaintext

    role_hierarchy:
        ROLE_ADMIN:       ROLE_USER
        ROLE_SUPER_ADMIN: [ROLE_USER, ROLE_ADMIN]

    providers:
        in_memory:
            memory:
                users:
                    user:  { password: userpass, roles: [ 'ROLE_USER' ] }
                    admin: { password: adminpass, roles: [ 'ROLE_ADMIN' ] }

    firewalls:
        dev:
            pattern:  ^/(_(profiler|wdt)|css|images|js)/
            security: false

        login:
            pattern:  ^/admin/login$
            security: false

        secured_area:
            anonymous: true
            pattern:    ^/admin
            form_login:
                check_path: _security_check
                login_path: _admin_login
            logout:
                path:   _admin_logout
                target: _admin

    access_control:
        - { path: ^/admin, roles: IS_AUTHENTICATED_ANONYMOUSLY }

Ca y est, on a supprimé tout ce qui permet de faire fonctionner la démo installée avec symfony2. Pour le moment le site ne marche plus, c'est tout à fait normal. Nous allons donc taper une ligne de commande dans une commande bash ou dos afin d'initialiser notre Bundle principal.

  • Ouvrez votre console bash ou dos, naviguez jusqu'au répertoire "videotheque" puis enfin tapez la commande suivante :

Pour dos :

cd "emplacement\du\repertoire\web"
php app/console generate:bundle --namespace=Iabsis/Bundle/VideothequeBundle --bundle-name="IabsisVideothequeBundle" --dir="src" --format="annotation" --structure

Pour système unix :

php app/console generate:bundle --namespace="Iabsis/VideothequeBundle" --bundle-name="IabsisVideothequeBundle" --dir="src" --format="annotation"  --structure

Le script va alors vous demander si vous confirmez la generation du bundle, il ne vous reste qu'à appuyer sur la touche entrée pour valider chacune des questions.

Si vous voulez plus de détails sur les différents arguments qu'on a utilisé pour la génération du bundle, vous pouvez consulter la documentation sur cette page : http://symfony.com/doc/current/bundles/SensioGeneratorBundle/commands/generate_bundle.html

Prenez soin de remplacer Iabsis par le nom de votre société ou bien votre pseudonyme habituel si vous êtes un particulier! Ce nom est nécessaire car Symfony gère ses Bundle en fonction du développeur. Ainsi, il est simple de voir la provenance d'un Bundle. Point important à signaler, il faut que le nom de votre bundle finisse par le mot clé "Bundle" sans quoi, symfony vous génèrera une erreur.

Requête symfony

Ouvrons le dossier "src" pour voir ce que symfony a généré pour nous. On voit que :

  • Un dossier principal a été créé au nom de notre société / pseudonyme que nous avons passé dans la commande contenant lui même un sous dossier "Iabsis" qui regroupera tous les bundles de "Iabsis".
  • Le dossier "VideothequeBundle" a bien été créé. C'est dans ce dossier que seront rangés la plupart des données de notre site. Les modèles seront stockés dans un dossier "Entity" que nous allons créer un peu plus tard, les templates HTML seront stockés dans le dossier "views" et enfin les contrôleurs seront rangés dans le dossier ... "Controller" !

Notre Bundle est créé, tout ce qu'il reste à faire, c'est d'indiquer à Symfony comment le trouver. Ensuite il faut lui dire qu'il doit le lancer par défaut lors de l'accès à notre site. Pour ce faire, nous allons avoir recours à 3 fichiers très importants de symfony2.

  • Pour ajouter notre bundle à Symfony2, on édite le fichier "app/appKernel.php" afin de rajouter les références à notre nouveau bundle. Normalement le script de génération du bundle l'a fait pour vous, si vous avez répondu "yes" à la question. Vous devriez donc avoir le contenu suivant dans votre fichier

Le fichier "app/appKernel.php"

use Symfony\Component\HttpKernel\Kernel;
use Symfony\Component\Config\Loader\LoaderInterface;

class AppKernel extends Kernel
{
    public function registerBundles()
    {
        $bundles = array(
            new Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
            new Symfony\Bundle\SecurityBundle\SecurityBundle(),
            new Symfony\Bundle\TwigBundle\TwigBundle(),
            new Symfony\Bundle\MonologBundle\MonologBundle(),
            new Symfony\Bundle\SwiftmailerBundle\SwiftmailerBundle(),
            new Symfony\Bundle\AsseticBundle\AsseticBundle(),
            new Doctrine\Bundle\DoctrineBundle\DoctrineBundle(),
            new Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle(),
            new AppBundle\AppBundle(),
            new Iabsis\VideothequeBundle\IabsisVideothequeBundle(),
        );

        if (in_array($this->getEnvironment(), array('dev', 'test'))) {
            $bundles[] = new Symfony\Bundle\WebProfilerBundle\WebProfilerBundle();
            $bundles[] = new Sensio\Bundle\DistributionBundle\SensioDistributionBundle();
            $bundles[] = new Sensio\Bundle\GeneratorBundle\SensioGeneratorBundle();
        }

        return $bundles;
    }

    public function registerContainerConfiguration(LoaderInterface $loader)
    {
        $loader->load(__DIR__.'/config/config_'.$this->getEnvironment().'.yml');
    }
}
  • Ensuite, on indique à Symfony qu'il doit lancer notre bundle lorsqu'on accède à la racine de notre site en éditant le fichier "app/config/routing.yml" (le script de génération du bundle l'a également fait pour vous si vous avez répondu "oui" à la question) :
iabsis_videotheque:
    resource: "@IabsisVideothequeBundle/Controller/"
    type:     annotation
    prefix:   /

Notez qu'il existe plusieurs syntaxe possible pour le fichier routing.yml. Dans cette version, Nous indiquons simplement le mode de routage (annotation) et le format d'url que l'on cherche à rediriger (ici /).

  • Puis pour finir, on édite le fichier "Iabsis/VideothequeBundle/Controller/DefaultController.php", celui-ci contient le code par défaut généré par la commande "generate". On peut y voir des commetaires php (annotations) qui disent à symfony comment réagir. Nous allons maintenant remplacer le contenu du fichier par le code suivant pour y mettre nos propres annotations.
<?php

namespace Iabsis\VideothequeBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;

class DefaultController extends Controller
{
    /**
     * @Route("/")
     * @Template()
     */
    public function indexAction()
    {
        return array('name' => "");
    }
}
?>
  • @Template sans argument veut dire que l'on utilise le template qui suit la logique du contrôleur.

Bon à savoir : Comme on se trouve dans le controleur Default et que l'action associée s'appelle index, Symfony va utiliser le template "IabsisVideothequeBundle:Default:index.html.twig". On pourrait aussi bien lui spécifier manuellement le template à choisir en utilisant la syntaxe suivante : @Template("IabsisVideothequeBundle:Default:index.html.twig").

  • @Route("/") : Veut dire que l'on veut déclencher la méthode qui se trouve juste en dessous, si on détecte que l'utilisateur va accéder à l'url "/" du site web.

Bon à savoir : On pourrait par exemple mettre @Route("/iabsis") si on voulait pouvoir accéder au script en tapant "/iabsis" au lieu de "/"

bq. Bon à savoir : A tout moment vous pouvez lancer la commande @php app/console router:debug@ afin de savoir quelles routes sont trouvées par symfony

Maintenant lorsque nous lançons notre application http://localhost/videotheque/web/app_dev.php/, on constate un message Hello! qui apparaît à l'écran ! Notre contrôleur s'est bien lancé et il utilise bien le template par défaut, à savoir : "views/Default/index.html.twig" du bundle "IabsisVideothequeBundle". Si vous éditez ce fichier et que vous modifiez le contenu, vous pourrez voir le résultat dans votre navigateur.

Mais nous reviendrons plus en détail sur la gestion des templates plus loin dans ce tutorial !

Quelques mots à propos de la sécurité

Pour notre environnement de test, nous avons installé un symfony simplement dans la racine web de notre serveur apache. En revanche, ceci constitue une ENORME faille de sécurité, et il est impensable de configurer notre site web de cette manière sur un serveur de production. Pour s'en rendre compte, il suffit de se rendre sur l'adresse suivante :

http://localhost/videotheque/app/config/parameters.yml

Si l'url rewriting n'est pas activé sur votre serveur, vous pourrez ainsi afficher vos identifiants en clair directement dans votre navigateur, IMPENSABLE !.

La bonne pratique consiste à placer votre dossier videotheque dans un emplacement non accessible depuis internet et ensuite de créer un hôte virtuel pointant vers le dossier "videotheque/web" afin de ne rendre accessible que ce dossier depuis le navigateur des clients. Si vous ne connaissez pas la notion de Virtual host et que vous utilisez apache, je vous conseil d'aller voir notre dossier sur les hôtes virtuels apache en cliquant sur le lien suivant :

Apache, les bonnes pratiques

Présentation de notre projet : Vidéothèque

Aujourd'hui, je me suis levé en sursaut et je me suis aperçu qu'il me manquait une application me permettant de gérer les bluray et les DVD dont je dispose. Il me faut absolument cette application, et bien sûr en quel langage vais-je le développer ? En php ! Et à l'aide de Symfony2 évidemment ! Avant de commencer mon projet il va me falloir faire des maquettes de ce que je veux développer. Une fois que j'aurais une idée précise de ce que je veux, je pourrais me lancer dans la création d'un modèle conceptuel de données, ce sera essentiel pour savoir où on veut aller tout au long du projet :). Ensuite seulement on attaquera le développement !

Maquettes

Dans un premier temps, je veux que quand je lance l'application dans mon navigateur j'arrive sur une page qui va m'afficher automatiquement :

  • la liste de mes films avec le titre
  • la pochette
  • la description
  • le genre
  • La liste des genres pour pouvoir trier par genre

Requête symfony

Il me faudra aussi une page d'administration pour ajouter / modifier / supprimer des films. celle-ci ne sera disponible que pour les utilisateurs authentifiés ! Je veux pas que n'importe qui vienne supprimer mes films !

Requête symfony

Représentation de la base de données

Voila nous y sommes, nous avons nos maquettes. D'après ce que l'on voit dessus on aura besoin de seulement 2 tables.

  • Une table Film
  • Une table Genre

Dans un premier temps on va gérer l'authentification avec un utilisateur et un mot de passe saisi directement dans le fichier de config. Plus tard, dans la section authentification, nous implémenterons une table User afin de vous montrer comment mettre en place une authentification à partir d'une base de données.

Requête symfony

Ca y est notre environnement de travail est près, notre application est bien modélisée. On va ENFIN pouvoir commencer à développer un peu ! Et ça commence tout de suite dans la prochaine section !

Mise en place de votre base de données avec doctrine

Maintenant que notre projet est défini et documenté, nous allons commencer à créer les "Entity". En fait, symfony2 utilise doctrine pour communiquer avec la base de données mysql. Doctrine est un ORM qui va nous permettre de traiter toutes nos tables comme de simples objets. Ainsi pour chacune des tables de notre application, nous devons créer sa représentation objet. Ensuite Doctrine fera presque tout le boulot à notre place. Génération des tables sql, récupération, modification et suppression des données dans la base de données, etc ....

La création des "Entity" est toutefois un peu particulière dans le sens où même les commentaires placés dans notre source php va servir à transmettre des données importantes à Doctrine pour qu'il puisse faire son boulot.

Les entitées vont être créées dans le dossier Entity, donc, commençons par créer un dossier "Entity" dans notre bundle : "videotheque/src/Iabsis/VideothequeBundle/Entity". Puis, l'application nécessitant 2 tables "Film" et "Genre", nous allons créer deux fichiers avec des noms originaux tel que Genre.php et Film.php à l'intérieur de ce nouveau dossier.

  • On commence par le fichier "Film.php". Observez bien de quelle manière nous allons implémenter le lien entre les tables "Film" et "Genre" :).
<?php
# Fichier Film.php

/*******************************************************************************************
 *  Namespace dans lequel se trouvera notre objet "Film" .
 * Les namespace servent à définir un espace de noms dans lesquels seront stockés notre objet.
 * Ici on dit que notre classe Genre fait partit de l'espace de Nom Entity, 
 * ainsi  Symfony saura qu'il s'agit bien d'une entité.
 * 
 * Dès lors qu'on utilisera l'instruction "use Iabsis\Bundle\VideothequeBundle\Entity\Film" dans un fichier PHP, 
 * on pourra accéder à notre entité sans utiliser à chaque fois une référence complète vers l'objet !
 * 
 * On pourra donc faire new Film() au lieu de new Iabsis\Bundle\VideothequeBundle\Entity\Film();
 ********************************************************************************************/

namespace Iabsis\VideothequeBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/*******************************************************************************************
 * 
 * Ci-dessous, nous allons décrire notre table, telle qu'elle sera gérée par Doctrine.
 * 
 * Vous allez voir des commentaires faisant apparaître le mot clé @ORM,
 * ces balises sont très importantes, elles permettent principalement de définir de quel
 * type de champ il s'agit. Ainsi Doctrine saura comment créé ce champ dans la base
 * de données de votre choix.
 * 
 *******************************************************************************************/
/**
 * @ORM\Entity
 */
class Film
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="IDENTITY")
     */
    protected $id;

    /**
     * Bidirectional 
     *
     * @ORM\ManyToMany(targetEntity="Genre", inversedBy="listeDesFilms")
     * @ORM\JoinTable(name="_assoc_film_genre",
     *   joinColumns={@ORM\JoinColumn(name="user_id", referencedColumnName="id")},
     *   inverseJoinColumns={@ORM\JoinColumn(name="film_id", referencedColumnName="id")}
     * )
     */
    protected $listeDesGenres;

    /**
     * @ORM\Column(type="string", length=90)
     */
    protected $titre;

    /**
     * @ORM\Column(type="text")
     */
    protected $description;
}
?>
  • On continue avec la table "Genre"
<?php
# Fichier Genre.php

namespace Iabsis\VideothequeBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 */
class Genre
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="IDENTITY")
     */
    protected $id;

    /**
     * Bidirectional 
     *
     * @ORM\ManyToMany(targetEntity="Film", mappedBy="listeDesGenres")
     */
    protected $listeDesFilms;

    /**
     * @ORM\Column(type="string", length=90)
     */
    protected $label;
}
?>

Maintenant que nous avons créé nos entitées, et si, comme moi, vous êtes un peu fainéant sur les bords et que vous aimez profiter de la génération de code automatique, vous allez être heureux. Symfony2 est livré avec un ensemble de commandes qui vont s'occuper de créer le code sql, de créer vos tables mais également de générer les getters et les setters pour tous les champs que vous avez créés dans vos objets de type Entity, c'est beau ! :)

  • Tout d'abord, on demande à symfony de créer la base de données en fonction des paramètres passés à l'étape de la configuration de symfony
    php app/console doctrine:database:create
  • Ensuite on crée les getters / setters pour toutes nos Entity.
    php app/console  doctrine:generate:entities Iabsis/VideothequeBundle/Entity/

Allez voir le code généré dans vos fichiers Film.php et Genre.php. tous les getters / setters ont dû être ajoutés directement dans les objets que vous avez écrit ! Pour chaque fichier modifié, symfony a créé une copie de sauvegarde du fichier suffixé par le caractère ~. Une fois que vous vous êtes assurés que vos fichiers modifiés sont justes, vous pouvez (et devez) supprimer ces fichiers de sauvegarde. Si vous ne le faites pas, symfony va vous générer des erreurs car il trouvera plusieurs fois les même objets.

  • La prochaine commande demande à symfony de créer les tables dans la base de données
    php app/console doctrine:schema:create
  • Oh non catastrophe ! Je voulais mettre une longueur de titre de 120 caractères, et non pas 90 pour les titres de mes films :(. Pas de panique, tout est prévu. Il suffit de vous rendre dans votre fichier Film.php et de remplacer
     /**
     * @orm\Column(type="string", length=90)
     */
    protected $titre;

Par le code suivant :

     /**
     * @orm\Column(type="string", length=120)
     */
    protected $titre;
  • Lancer alors la commande suivante afin de mettre à jour la structure de votre base de données
    php app/console doctrine:schema:update --force

Et voilà, c'est aussi simple que ça !

ASTUCE: Si vous voulez afficher le code sql qui sera générer plutôt que de le lancer automatiquement, vous pouvez lancer la commande php app/console doctrine:schema:update --dump-sql".

Créez votre set de données initial (fixtures)

Les fixtures sont une fonctionnalité très intéressante de Symfony2, elles permettent de facilement créer des données qui seront importées dans la base de données à partir d'une commande bash. Malheureusement cette fonctionnalité n'est pas disponible dans le package de base fourni par Symfony. Toutefois on peut trouver les bundles concernés sur le site des Bundle Symfony. La méthode la plus simple pour installer les fixtures au sein de symfony est d'utiliser composer. Voici les étapes à suivre :

  • Installer git sur votre système
  • Installation de la librairie composer.phar. Vous pouvez télécharger le fichier .phar à cette adresse : https://packagist.org/. Cette étape doit déjà être faite puisque nous avons télécharger Symfony avec Composer.
  • Ajouter la ligne "doctrine/doctrine-fixtures-bundle": "dev-master" dans la section "require" de votre fichier "composer.json".
  • Ouvrez une commande bash ou dos
  • Placez vous dans le dossier du projet à l'aide de la commande cd Dossier_du_projet
  • Executez la commande php composer.phar update
  • Si tout s'est bien passé, le dossier doctrine-fixtures-bundle a dû se créé dans le dossier "vendor/doctrine"
  • Ajouter la ligne new Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle() dans la liste des bundles de votre fichier app/AppKernel.php afin de déclarer celui-ci.

Création d'une fixture pour notre application videotheque

Maintenant que tout est en place, nous allons pouvoir créer notre fichier de fixtures. Pour que Symfony puisse les trouver, il faut créer un dossier particulier dans notre Bundle. Donc rendez-vous dans le répertoire de votre bundle "VideothequeBundle" et créez le dossier "DataFixtures". A l'interieur de ce dossier créez un nouveau dossier "ORM". Dans ce dosier, vous pouvez créer des fichiers php de n'importe quel nom, ils seront reconnus par Symfony.

  • Commençons par créer le fichier "LoadingFixtures.php"
<?php
# Fichier videotheque/src/Iabsis/Bundle/VideothequeBundle/DataFixtures/ORM/LoadingFixtures.php
/* Les Fixtures doivent êtres stockées dans le namespace suivant */
namespace  Iabsis\VideothequeBundle\DataFixtures\ORM;

/*
 *  On a besoin de recourir à l'interface FixtureInterface pour définir une fixture alors on le déclare
 * Si nous n'avions pas mis ce use, on aurait dû taper
 * class LoadingFixtures implements Doctrine\Common\DataFixtures\FixtureInterface pour l'implémentation
 * de l'interface FixtureInterface, ce qui avouons-le n'est pas toujours très pratique, surtout si on
 * veut utiliser plusieurs fois l'objet / interface en question.
 */
use Doctrine\Common\DataFixtures\FixtureInterface;
use Doctrine\Common\Persistence\ObjectManager;

/*
 * Nous aurons besoin de nos entités Genre et Film également, on le déclare donc ici aussi...
 */
use Iabsis\VideothequeBundle\Entity\Genre;
use Iabsis\VideothequeBundle\Entity\Film;

/*
 * Les fixtures sont des objets qui doivent obligatoireemnt implémenter l'interface FixtureInterface
 */
class LoadingFixtures implements FixtureInterface
{
    public function load(ObjectManager $manager)
    {
        // Création d'un Genre "Horreur"
        $Horreur = new Genre();
        $Horreur->setLabel("Horreur");
        // Enregistrment dans la base de données
        $manager->persist($Horreur);
        $manager->flush();

        // Création d'un genre Action
        $Action = new Genre();
        $Action->setLabel("Action");
        // Enregistrment dans la base de données
        $manager->persist($Action);
        $manager->flush();

        // Création d'un genre Aventure
        $Aventure = new Genre();
        $Aventure->setLabel("Aventure");
        // Enregistrment dans la base de données
        $manager->persist($Aventure);
        $manager->flush();

        // Création d'un genre Science fiction
        $Science_fiction = new Genre();
        $Science_fiction->setLabel("Science fiction");
        // Enregistrment dans la base de données
        $manager->persist($Science_fiction);
        $manager->flush();

        // On crée le film Matrix !
        $Film = new Film();
        $Film->setTitre("Matrix");
        $Film->setDescription("Un super film ou neo va se révéler être l'élu. Sa mission sera de sauver l'humanité de la matrix. Mais ... Qu'est ce que la matrice ?");
        $Film->getListeDesGenres()->add($Action); 
        $Film->getListeDesGenres()->add($Science_fiction); 
        // Enregistrment dans la base de données
        $manager->persist($Film);
        $manager->flush();   
    }        
}
?>
  • Puis on lance la commande symfony pour lancer les fixtures
php app/console doctrine:fixtures:load

Vous pouvez vous rendre dans votre SGBD et afficher le contenu des tables Genre et _assoc_film_genre et Film pour vérifier que les données ont bien été créées dans la base de données

Création des templates html pour l'affichage de votre site

Etant donné que nous ne sommes pas là pour faire un cours de design, nous allons simplement utilisé un framework css bien connu, à savoir Twitter Boostrap que nous allons décompresser dans le dossier "videotheque/web/bundles/iabsisvideotheque". Vous pouvez télécharger le zip en cliquant sur ce lien.

Vous êtes sûrement très pressé de voir votre site commencer à prendre forme. C'est précisément ce que nous allons faire dans ce chapitre. Toutefois, je vous conseille vivement d'aller faire un tour sur cette page http://symfony.com/doc/current/book/templating.html afin de bien comprendre la syntaxe des templates Twig que nous allons utiliser tout au long de ce chapitre. Il est essentiel de comprendre au minimum la notion d'héritage des templates ainsi que des balises {{ ... }} et {% ... %}. Si vous utilisez un IDE, il est fort probable qu'il existe un plugin pour la complétion Twig. Je ne saurais que trop vous conseiller de l'installer

Lors de la création de notre Bundle Videotheque, nous en étions resté à un message Hello! qui s'affiche lors du chargement de notre application web. Nous avons vu que le fichier qui affiche se message est en fait le fichier twig situé dans le dossier "videotheque/src/Iabsis/VideothequeBundle/Ressources/views/Default/index.html.twig". La bonne méthode concernant les template consiste à faire un fichier de squelette global que nous allons importer dans chacun des templates spécifiques à chaque page. Pour tester que tout fonctionne correctement, nous allons d'abord mettre un simple contenu HTML figé (donc aucun contenu récupéré de notre base de données, afin de nous assurer que notre template marchera comme voulu !

  • Editons donc ce fichier et remplaçons son contenu par le suivant :
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Bootstrap, from Twitter</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta name="description" content="">
    <meta name="author" content="">

    <!-- Le styles -->
    <link href="{{ asset('bundles/iabsisvideotheque/css/bootstrap.css') }}" rel="stylesheet">
    <link href="{{ asset('bundles/iabsisvideotheque/css/videotheque-spec.css') }}" rel="stylesheet">
    <link href="{{ asset('bundles/iabsisvideotheque/css/font-awesome.min.css') }}" rel="stylesheet">
    <style type="text/css">
      body {
        padding-top: 60px;
        padding-bottom: 40px;
      }
      .sidebar-nav {
        padding: 9px 0;
      }

    </style>
    <link href="{{ asset('bundles/iabsisvideotheque/css/bootstrap-responsive.css') }}" rel="stylesheet">

  </head>

  <body>
    <div class="container-fluid">
      <div class="row-fluid">
        <div class="span3">
          <div class="well sidebar-nav">
            <ul class="nav nav-list">
              <li class="nav-header">Tri par genre</li>
              <li class="active"><a href="#">Tous <span class="badge badge-info">300</span></a></li>
              <li><a href="#">Horreur <span class="badge badge-info">10</span></a></li>
              <li><a href="#">Action <span class="badge badge-info">10</span></a></a></li>
              <li><a href="#">test <span class="badge badge-info">10</span></a></a></li>
            </ul>
          </div><!--/.well -->
        </div><!--/span sidebar-->

        <div class="span9">
          <div class="hero-unit">
            <div class="block">
                <h2>Film 1</h2>
                <div class="tags-list"><span class="badge">Action</span><span class="badge">Science fiction</span></div>
                <div class="illustration">
                    <span class="fa fa-ban"></span>
                </div>
                <div class="description">
                    Donec id elit non mi porta gravida at eget metus. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Etiam porta sem malesuada magna mollis euismod. Donec sed odio dui.
                </div>
                <div class="read-more">
                    <a class="btn btn-primary btn-small">Voir</a>
                </div>
            </div>

            <div class="block">
                <h2>Film 3</h2>
                <div class="tags-list"><span class="badge">Action</span><span class="badge">Science fiction</span></div>
                <div class="illustration">
                    <span class="fa fa-ban"></span>
                </div>
                <div class="description">
                    Donec id elit non mi porta gravida at eget metus. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Etiam porta sem malesuada magna mollis euismod. Donec sed odio dui.
                </div>
                <div class="read-more">
                    <a class="btn btn-primary btn-small">Read more</a>
                </div>
            </div>

            <div class="block">
                <h2>Film 3</h2>
                <div class="tags-list"><span class="badge">Action</span><span class="badge">Science fiction</span></div>
                <div class="illustration">
                    <span class="fa fa-ban"></span>
                </div>
                <div class="description">
                    Donec id elit non mi porta gravida at eget metus. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Etiam porta sem malesuada magna mollis euismod. Donec sed odio dui.
                </div>
                <div class="read-more">
                    <a class="btn btn-primary btn-small">Read more</a>
                </div>
            </div>

            <div class="block">
                <h2>Film 4</h2>
                <div class="tags-list"><span class="badge">Action</span><span class="badge">Science fiction</span></div>
                <div class="illustration">
                    <span class="fa fa-ban"></span>
                </div>
                <div class="description">
                    Donec id elit non mi porta gravida at eget metus. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Etiam porta sem malesuada magna mollis euismod. Donec sed odio dui.
                </div>
                <div class="read-more">
                    <a class="btn btn-primary btn-small">Read more</a>
                </div>
            </div>

          </div>
        </div><!--/span-->
      </div><!--/row-->

      <hr>

      <footer>
        <p>© Iabsis 2015</p>
      </footer>

    </div><!--/.fluid-container-->

    <!-- Le javascript
    ================================================== -->
    <!-- Placed at the end of the document so the pages load faster -->
    <script src="{{ asset('bundles/iabsisvideotheque/js/jquery.min.js') }}"></script>
    <script src="{{ asset('bundles/iabsisvideotheque/js/bootstrap.min.js') }}"></script>
    <script src="{{ asset('bundles/iabsisvideotheque/js/app.js') }}"></script>

  </body>
</html>
  • Si tout c'est bien passé, vous devriez voir la maquette s'afficher avec des données placées en dur dans le fichier tel qu'affiché sur la capture suivante :

Requête symfony

Maintenant qu'on sait que notre code html et notre CSS vont s'afficher dans les bonnes conditions une fois des données insérées, on peut commencer à remplacer les données qu'on a écrites en dur par des balises variable au format twig !

Pour bien faire les choses, notre fichier "index.html.twig" sera notre squelette générale, puis nous ferons pour chaque page de notre site, un template dédié qui héritera du fichier "index.html.twig".

Si vous avez jeté un oeil sur le site de symfony comme je vous l'ai dit tout en haut de ce chapitre, vous connaissez un petit peu la syntaxe twig. Vous devriez alors essayer de modifier vous même le fichier "index.html.twig" pour y ajouter des balises variables par vous même avant de simplement récupérer le bout de code que je vous donne jsute en dessous :).

Partie publique

Vous avez assez cherché ? Bien entendu, il y a une infinité de solutions, voici maintenant la solution que je vous propose pour le thème de la section principale. J'ai décidé de séparer un maximum les différents blocs afin de pouvoir, au besoin les utiliser dans une autre page (évidemment dans notre exemple il y aura très peu de bouts de templates qui seront repris dans divers page. Mais sur un projet plus volumineux, cela s'avérera très utile, voir primordial. Le code du template se séparera donc de cette manière :

  • base.html.twig : Ce fichier servira de "squelette" générale. On y intégrera le css, les divers balises de positionnement de notre site (header, footer, sidebar, contenu, ...).
  • videotheque_home.html.twig : Ce fichier va hériter de la page index.html.twig et remplacer tous les bouts de code dont le squelette a besoin dans le cas de l'affichage du site (partie non admin).
  • sidebar_genres.html.twig : Fichier qui permet de générer le menu de la partie principale du site (à savoir la possibilité de sélectionner les genres)

Tous les fichiers seront stockés dans le dossier "videotheque/src/Iabsis/VideothequeBundle/Ressources/views/Default/".

Le fichier base.html.twig

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Bootstrap, from Twitter</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta name="description" content="">
    <meta name="author" content="">

    <!-- Le styles -->
    <link href="{{ asset('bundles/iabsisvideotheque/css/bootstrap.css') }}" rel="stylesheet">
    <link href="{{ asset('bundles/iabsisvideotheque/css/videotheque-spec.css') }}" rel="stylesheet">
    <link href="{{ asset('bundles/iabsisvideotheque/css/font-awesome.min.css') }}" rel="stylesheet">
    <style type="text/css">
      body {
        padding-top: 60px;
        padding-bottom: 40px;
      }
      .sidebar-nav {
        padding: 9px 0;
      }

    </style>
    <link href="{{ asset('bundles/iabsisvideotheque/css/bootstrap-responsive.css') }}" rel="stylesheet">

  </head>

  <body>
    <div class="container-fluid">
      <div class="row-fluid">
        <div class="span3">
          <div class="well sidebar-nav">
            {% block sidebar %}{% endblock %}
          </div><!--/.well -->
        </div><!--/span sidebar-->

        <div class="span9">
          <div class="hero-unit">
            {% block body %}{% endblock %}

          </div>
        </div><!--/span-->
      </div><!--/row-->

      <hr>

      <footer>
        <p>© Iabsis 2015</p>
      </footer>

    </div><!--/.fluid-container-->

    <!-- Le javascript
    ================================================== -->
    <!-- Placed at the end of the document so the pages load faster -->
    <script src="{{ asset('bundles/iabsisvideotheque/js/jquery.js') }}"></script>
    <script src="{{ asset('bundles/iabsisvideotheque/js/bootstrap-transition.js') }}"></script>
    <script src="{{ asset('bundles/iabsisvideotheque/js/bootstrap-alert.js') }}"></script>
    <script src="{{ asset('bundles/iabsisvideotheque/js/bootstrap-modal.js') }}"></script>
    <script src="{{ asset('bundles/iabsisvideotheque/js/bootstrap-dropdown.js') }}"></script>
    <script src="{{ asset('bundles/iabsisvideotheque/js/bootstrap-scrollspy.js') }}"></script>
    <script src="{{ asset('bundles/iabsisvideotheque/js/bootstrap-tab.js') }}"></script>
    <script src="{{ asset('bundles/iabsisvideotheque/js/bootstrap-tooltip.js') }}"></script>
    <script src="{{ asset('bundles/iabsisvideotheque/js/bootstrap-popover.js') }}"></script>
    <script src="{{ asset('bundles/iabsisvideotheque/js/bootstrap-button.js') }}"></script>
    <script src="{{ asset('bundles/iabsisvideotheque/js/bootstrap-collapse.js') }}"></script>
    <script src="{{ asset('bundles/iabsisvideotheque/js/bootstrap-carousel.js') }}"></script>
    <script src="{{ asset('bundles/iabsisvideotheque/js/bootstrap-typeahead.js') }}"></script>

  </body>
</html>

Le fichier videotheque_home.html.twig

{% extends 'IabsisVideothequeBundle:Default:base.html.twig' %}

{% use 'IabsisVideothequeBundle:Default:sidebar_genres.html.twig' %}

{% block body %}
    {% for film in films %}
        <div class="block">
            <h2>{{ film.titre }}</h2>
            <div class="tags-list">{% for Genre in film.listeDesGenres %}{% if loop.index > 1 %},{% endif %} <span class="badge">{{ Genre.label }}</span>{% endfor %}</div>
            <div class="illustration">
                {% if film.illustration is defined %}
                    <img src="{{ film.illustration }}" />
                {% else %}
                    <span class="fa fa-ban"></span>
                {% endif %}
            </div>
            <div class="description">
                {{ film.description }}
            </div>
            <div class="read-more">
                <a class="btn btn-primary btn-small" href="#">Voir</a>
            </div>
        </div>
    {% else %}<div class="alert">Désolé, il n'y a aucun film dans cette section</div>
    {% endfor %}
{% endblock %}

Le fichier sidebar_genres.html.twig

{% block sidebar %}
    <ul class="nav nav-list">
        <li class="nav-header">Tri par genre</li>
        <li {% if selected_genre < 1 %}class="active"{% endif %}><a href="{{ path('_index') }}">Tous <span class="badge badge-info">{{ total_nb_films }}</span></a></li>
        {% for genre in genres %}
            <li {% if genre['genre'].id == selected_genre %}class="active"{% endif %}><a href="{{ path('_filter_by_genre', { 'label': genre['genre'].label }) }}">{{ genre['genre'].label }} <span class="badge badge-info">{{ genre['nbConcernedFilms'] }}</span></a></li>
        {% endfor %}
    </ul>
{% endblock %}

Modification de la route

Comme on le voit dans le fichier "sidebar_genres.html.twig", pour sélectionner une section, on va générer une url avec la règle _index que l'on va créer juste en-dessous dans le fichier "src/Iabsis/VideothequeBundle/Controller/DefaultController.php".

  • On édite le fichier "src/Iabsis/VideothequeBundle/Controller/DefaultController.php". On remplace alors les informations de routes (commentaires situés juste au dessus de la déclaration de la méthode indexAction) par le code suivant :
    /**
    * @Route("/", name="_index", defaults={"label"=""})
    * @Route("/filter-by-genre/{label}", name="_filter_by_genre", defaults={"label"=""})
    * @Template()
    */
  • Petite explication : on spécifie à symfony que les url / ou /filter-by-genre/nimporte-quel-texte sera toujours redirigé vers la fonction déclarée juste en dessous. En revanche tout ce qui est après le slash doit être transmit à notre action dans la variable $label.

  • Les name="_index" et name="_filter_by_genre" spécifient en quelque sorte le nom de la route qui pourra être utilisé dans les templates twig par exemple pour générer une url correspondant à cette route.

  • Si on regarde à la fin de la ligne "defaults", on voit que si rien n'est passé après le /, on définit la variable $label à une chaine vide. On aurait tout aussi bien pu mettre "Science fiction" afin de spécifier que par défaut, si aucun genre de film n'a été saisi, on affiche les films de Science fiction.

Récupération des données passées par l'url.

Vous allez maintenant me dire que c'est bien de pouvoir récupérer les variables dans une url mais ... comment diable les récupère-t-on ? C'est assez simple, et c'est dans notre action que ça va se passer. Juste en dessous du commentaire précédemment modifié, vous voyez la déclaration de la méthode indexAction. Tout ce que vous avez à faire c'est éditer celle-ci et lui rajouter un paramètre $label. Ainsi les labels récupérés dans les url seront directement transmis à cette variable. Facile n'est-ce pas !

    public function indexAction($label)

Les repositories de l'entité Genre

On sait dors et déjà que notre action devra accéder aux entités Film et Genre. On peut donc tout de suite le déclarer en haut de notre fichier et créer les "use" en conséquence. Comme tout à l'heure, juste en dessous de namespace on va ajouter les 2 lignes suivantes :

use Iabsis\VideothequeBundle\Entity\Genre;
use Iabsis\VideothequeBundle\Entity\Film;

La récupération des genres avec le nombre de films concernés va nécessiter une requête particulière. En effet, la méthode findAll() fourni par le repository par défaut des entités ne permet que de récupérer la liste des Genres mais en aucun cas le total des films concernés. Comme il n'est pas propre de rajouter des requêtes sql dans un contrôleur et encore moins dans une entité, nous allons devoir procéder autrement. Heureusement, Symfony a prévu cette situation et vous permet de créer un repository personnalisés pour chacune de vos entités en héritant du repository par défaut.

Pour définir un repository différent pour notre Entity Genre, il va nous falloir éditer le fichier Genre.php dans votre dossier Entity et modifier les lignes

    /**
     * @ORM\Entity
     */

Par

    /**
     * @ORM\Entity(repositoryClass="Iabsis\VideothequeBundle\Entity\GenreRepository")
     */

Et enfin, créez le fichier "GenreRepository.php" dans le dossier "Entity", avec le contenu suivant

<?php

namespace Iabsis\VideothequeBundle\Entity;

use Doctrine\ORM\EntityRepository;

class GenreRepository extends EntityRepository
{
    public function fetchAllWithFilmsCount()
    {
       /* Création de la requète avec le query builder */
        $queryBuilder = $this->_em->createQueryBuilder();
        $queryBuilder->select("g as genre, count(f) as nbConcernedFilms")
                     ->from("IabsisVideothequeBundle:Genre", "g")
                     ->leftJoin("g.listeDesFilms", "f")
                     ->groupBy("g");
       /* On retourne les enregistrements, il s'agira d'un table multidimentionnel
        * Le premier paramètre "genre" contiendra notre entité genre
        * Le 2ème paramètre nbConcernedFilms contient le nombre de films du genre
        * On aurait pu tout simplement faire un findAll() en y mettant une jointure et compter
        * simplement le nombre de genre associés, mais cela chargerait les données des genres qui
        * nous sont ici inutiles.
        */
        return $queryBuilder->getQuery()->getResult();

    }
}

?>

Les repositories de l'entité Film

Pour les films, nous auront besoin de récupérer tous les Films faisant partit d'un certain genre. Nous allons donc de la même manière créer un repository FilmRepository permettant de créer la méthode findByGenre($id_genre).

  • Comme tout à l'heure on modifie le Film.php et on remplace
    /**
     * @ORM\Entity
     */

Par

    /**
     * @ORM\Entity(repositoryClass="Iabsis\VideothequeBundle\Entity\FilmRepository")
     */
  • Puis on crée le fichier "FilmRepository.php" dans le dossier "Entity", avec le contenu suivant
<?php

namespace Iabsis\VideothequeBundle\Entity;

use Doctrine\ORM\EntityRepository;

class FilmRepository extends EntityRepository
{
    /**
     * Retourne la liste des films correspondant au genre passé en paramètre (id)
     * Si aucun genre n'est spécifié, la liste de tous les films est renvoyée.
     * @param int $idGenre Id du genre à rechercher
     * @return Iabsis\Bundle\VideothequeBundle\Entity\Film[] Liste des films du genre demandé
     */
    public function findByGenre($idGenre = 0)
    {
        /* Création de la requète avec le query builder */
        $queryBuilder = $this->_em->createQueryBuilder();
        $queryBuilder->select("f, g")
                     ->from("IabsisVideothequeBundle:Film", "f")
                     ->leftJoin("f.listeDesGenres", "g");

        /* Si on reçoit un id de genre valide alors on recherche les Films de ce genre là uniquement */
        if ((int)$idGenre > 0) {
            $queryBuilder->where("g.id=:idGenre")->setParameter("idGenre", (int)$idGenre);
        }
        /* Puis on retourne la liste des films du genre demandé */
        return $queryBuilder->getQuery()->getResult();
    }

    /**
     * Retourne le nombre de film au total
     * @return int Renvoie le nombre de films total
     */
    public function countAll()
    {
        /*
         * On aurait tendance à vouloir faire un count() d'un fetchAll() sur le FilmRepository()
         * Cette solution foncitonne mais demande à doctrine de charger toutes les données en mémoire.
         * Si on a des milliers de films, doctrine va alors créer des milliers d'objets Film dans la ram
         * ce qui n'est pas du tout optimisé.
         * 
         * En faisant un count comme ceci, aucune données n'est chargée en mémoire, une simple requête sql est 
         * lancée et le nombre est alors renvoyé.
         */

        /* Création de la requète count avec le query builder */
        $queryBuilder = $this->_em->createQueryBuilder();
        $queryBuilder->select("count(f) as total")
                     ->from("IabsisVideothequeBundle:Film", "f");

        /**
        * Comme le seul résultat qui nous intéresse est le count et pas du tout l'entité,
        * On utilise getSingleScalarResult() afin de renvoyer une valeur scalaire unique
        */
        return $queryBuilder->getQuery()->getSingleScalarResult();
    }
}

?>

Affichage de la liste des films

Ça y est, on a tous nos repositories et on peut simplement, grâce à ceux-ci, générer le contenu de notre page depuis notre action indexAction. Voici donc comment on va créer le contenu de notre page. Tout est expliqué dans les nombreux commentaires du code qui suit. Ouvrez donc votre fichier "DefaultController" et remplacer votre méthode indexAction par la suivante :

    public function indexAction($label)
    {
        /* Tableau qui va stocker toutes les données à remplacer dans le template twig */
        $variables = array();

        // Récupération de l'entity manager qui va nous permettre de gérer les entités.
        $entityManager = $this->getDoctrine()->getManager();

        // On recherche dans la table Genre l'enregistrement qui correspond au label reçut
        // par l'url et on stocke l'objet Genre dans une variable
        $selectedGenre = $entityManager
                            ->getRepository("IabsisVideothequeBundle:Genre")
                            ->findOneBy(array("label" => $label));

        if ($selectedGenre) {
            // Si le genre passé par l'url existe bien, on passe l'id à notre template
            $variables['selected_genre'] = $selectedGenre->getId();
        } else {
            // Sinon on renvoie 0, aucun genre n'a été sélectionné
            $variables['selected_genre'] = null;
        }

        // Récupération de la liste des films grâce à notre méthode findByGenre.
        $listeDeFilms = $entityManager
                            ->getRepository("IabsisVideothequeBundle:Film")
                            ->findByGenre($variables['selected_genre']);

        $variables['films'] = $listeDeFilms;

        // On récupère le nombre total de films en comptant simplement le resultat de la recheche de tous les films
        $variables['total_nb_films'] = $entityManager->getRepository("IabsisVideothequeBundle:Film")->countAll();

        /* On récupère la liste des genres avec le nombre de films associés pour notre sidebar */
        $variables['genres'] = $entityManager
                                    ->getRepository('IabsisVideothequeBundle:Genre')
                                    ->fetchAllWithFilmsCount();

        return $this->render('IabsisVideothequeBundle:Default:videotheque_home.html.twig', $variables);
    }

Si vous avez bien suivi toutes les étapes, vous devriez avoir votre application qui s'affiche correctement et les films que vous aviez ajouté avec les fixtures devraient apparaître dans les différents Genres.

Génération CRUD (partie admin)

Pour la partie admin, nous allons voir comment utiliser une fonciton extrèmement puissante de Symfony2 qui vous permettra de gagner énormément de temps, à savoir la génération automatique d'un CRUD basé sur nos entités.

A savoir : Qu'est ce que c'est un CRUD ? Il s'agit de l'acronyme pour Create / Read / Update / Delete. Vous l'aurez compris, il s'agit de l'ensemble des pages permettant d'afficher la liste des enregistrements d'une table avec la possibilité de les afficher / modifier / supprimer.

Pour générer l'ensemble des pages pour nos 2 entités "Genre" et "Film", il suffit de taper les 2 commandes suivantes :

php app/console generate:doctrine:crud --entity=IabsisVideothequeBundle:Genre --format=annotation --with-write --no-interaction
php app/console generate:doctrine:crud --entity=IabsisVideothequeBundle:Film --format=annotation --with-write --no-interaction

Vous verrez alors que Symfony2 a automatiquement créé les "vues" dans le dossier "views" de votre bundle et les "controlleurs" des 2 entités, à savoir respectivement les 2 fichiers "GenreController" et "FilmController" dans le dossier "Controller". Il a également créé les formulaires dans le dossier "Form" sur lesquels nous reviendrons plus tard.

Un petit problème toutefois, si vous essayez d'éditer un genre, vous verrez que Symfony génère une erreur car il ne sait pas comment afficher représenter un film de manière lisible pour un humain. Nous allons donc devoir créer une méthode __toString() dans nos entités "Genre" et "Film" afin de dire à PHP comment afficher ces objets à l'écran.

on rajoute donc dans le fichier "Entity/Genre.php" la méthode suivante :

/**
 * Affichage d'une entité Genre avec echo
 * @return string Représentation du genre
 */
public function __toString()
{
    return $this->label;
}

Puis dans le fichier "Entity/Film.php" :

/**
 * Affichage d'une entité Film avec echo
 * @return string Représentation du film
 */
public function __toString()
{
    return $this->titre;
}

Modification du fichier routing.yml

Si vous regarder les fichiers controlleurs, vous verrez que par défaut, les urls pour y accéder sont "/genre" et "/film". Ce type d'url va entrer en conflit avec les routes de notre première étape nous permettant d'afficher les films par genre. De plus nous souhaitons que les pages d'administrations se trouvent sous l'arborescende "/admin/page_divers".

Pour se faire, nous allons devoir éditer le fichier routing.yml afin de lui spécifier que les controller "FilmController" et "GenreController" doivent être accessible depuis "/admin/genre" et "/admin/film". On spécifie le prefix "/admin". Comme dans le controlleur, la route parente est "/genre" et "/film", nous aurrons bien "/admin/genre" et "/admin/film" avec le prefix.

Ajoutons donc les 2 blocs suivant à la fin de notre fichier routing.yml :

_admin_genre:
    resource: "@IabsisVideothequeBundle/Controller/GenreController.php"
    type:     annotation
    prefix:   /admin

_admin_film:
    resource: "@IabsisVideothequeBundle/Controller/FilmController.php"
    type:     annotation
    prefix:   /admin

Astuce : Vous pouvez taper la commande suivante dans la racine de votre projet afin de voir la liste des urls possibles de votre projet : php app/console router:debug.

La commande de debuging nous donne donc bien actuellement les urls suivantes :

    Name                     Method Scheme Host Path
    _wdt                     ANY    ANY    ANY  /_wdt/{token}
    _profiler_home           ANY    ANY    ANY  /_profiler/
    _profiler_search         ANY    ANY    ANY  /_profiler/search
    _profiler_search_bar     ANY    ANY    ANY  /_profiler/search_bar
    _profiler_purge          ANY    ANY    ANY  /_profiler/purge
    _profiler_info           ANY    ANY    ANY  /_profiler/info/{about}
    _profiler_phpinfo        ANY    ANY    ANY  /_profiler/phpinfo
    _profiler_search_results ANY    ANY    ANY  /_profiler/{token}/search/results
    _profiler                ANY    ANY    ANY  /_profiler/{token}
    _profiler_router         ANY    ANY    ANY  /_profiler/{token}/router
    _profiler_exception      ANY    ANY    ANY  /_profiler/{token}/exception
    _profiler_exception_css  ANY    ANY    ANY  /_profiler/{token}/exception.css
    _configurator_home       ANY    ANY    ANY  /_configurator/
    _configurator_step       ANY    ANY    ANY  /_configurator/step/{index}
    _configurator_final      ANY    ANY    ANY  /_configurator/final
    _index                   ANY    ANY    ANY  /filter-by-genre/{label}
    genre                    GET    ANY    ANY  /admin/genre/
    genre_create             POST   ANY    ANY  /admin/genre/
    genre_new                GET    ANY    ANY  /admin/genre/new
    genre_show               GET    ANY    ANY  /admin/genre/{id}
    genre_edit               GET    ANY    ANY  /admin/genre/{id}/edit
    genre_update             PUT    ANY    ANY  /admin/genre/{id}
    genre_delete             DELETE ANY    ANY  /admin/genre/{id}
    film                     GET    ANY    ANY  /admin/film/
    film_create              POST   ANY    ANY  /admin/film/
    film_new                 GET    ANY    ANY  /admin/film/new
    film_show                GET    ANY    ANY  /admin/film/{id}
    film_edit                GET    ANY    ANY  /admin/film/{id}/edit
    film_update              PUT    ANY    ANY  /admin/film/{id}
    film_delete              DELETE ANY    ANY  /admin/film/{id}

Si vous vous rendez sur les urls "/admin/genre" et "/admin/film", vous verrez que nous avons bien des pages fonctionnelles pour administrer nos genres et nos films. Cela nous a pris 2min pour générer un backoffice pour nos films et nos genres ! Bien sûr il nous faut encore appliquer les templates afin d'avoir un design correct mais avouez que Symfony nous a déjà bien mâché le travail !

Amélioration de l'apparence du backoffice

Si vous ouvrez les fichiers templates des vues générées, vous verrez que par défaut elles étendent le fichier {% extends '::base.html.twig' %}. Ce fichier se trouve dans le dossier app/Resources/views/base.html.twig. Comme vous l'aurez compris, il est préférable d'utiliser ce fichier pour l'ensemble de nos pages twig afin que toutes les pages générées automatiquement fonctionnent correctement. Nous allons donc :

  • copier le contenu du fichier "base.html.twig" de notre bundle dans le fichier "base.html.twig" du core. Vous pouvez ensuite supprimer le fichier base.html.twig de votre bundle.
  • ModifieR ensuite le fichier "views/Default/videotheque_home.html.twig" de votre bundle afin qu'il étende le fichier base.html.twig du core : {% extends '::base.html.twig' %}.
  • Enfin, ajoutez les classes que vous désirez sur les boutons et tableaux afin d'avoir un thème sympa. (voir les classes bootstrap sur le site de twitter bootrap mais ce n'est pas important pour ce tutoriel).

Création d'une page d'accueil pour l'administration

Nous avons donc nos routes pour les genres et pour les films mais il serait quand même mieux d'avoir accès à une page "/admin" disposant d'un menu pour se rendre sur les différentes sections. On n'a pas envie d'avoir à retenir toutes les url par coeur. Pour cela rien de plus simple !

Ajout de la route /admin au routing.yml

Comme d'habitude, on ouvre notre fichier routing.yml et on ajoute la règle suivante (avant les 2 autres blocks admin).

iabsis_videotheque_admin:
    path: /admin
    defaults: { _controller: IabsisVideothequeBundle:Admin:index}

Comme nous le savons maintenant, ce que dit cette règle c'est : Lancer la méthode indexAction() du fichier AdminController.php stocké dans le dossier "Controller" du bundle "IabsisVideothequeBundle". Ce fichier n'existe pas pour le moment.

Création du contrôleur

  • On crée le fichier "AdminController.php" dans le dossier "Controller".
<?php

namespace Iabsis\VideothequeBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;

class AdminController extends Controller
{
    /**
     * @Template()
     */
    public function indexAction()
    {
        /**
         * Comme @Template() est vide, il va chercher par défaut le fichier dans videothequeBundle/views/Admin/index.html.twig
         */
        return array();
    }
}

?>

L'action va ouvrir le fichier template videothequeBundle/views/Admin/index.html.twig". Nous allons donc devoir le créer.

Création du template pour la page d'administration par défaut

  • Créez le dossier "Admin" dans votre dossier "views"
  • Créez le fichier "admin_home.html.twig" à l'intérieur de ce dossier avec le contenu suivant :
{% extends '::base.html.twig' %}
{% block title %}Administration{% endblock %}

{% block body %} <div>Sélectionnez une section dans le menu de gauche :)</div>{% endblock %}

{% use 'IabsisVideothequeBundle:Admin:sidebar_admin.html.twig' %}
  • On voit que cette fois-ci, le fichier template pour la sidebar est un nouveau fichier puisque le menu en tant qu'administrateur n'est plus le même, on crée donc le fichier "sidebar_admin.html.twig" dans le dossier "views/Admin".
{% block sidebar %}
<ul id="menu-genres" class="nav nav-list">
    <li class="nav-header">Administrer</li>
    <li {% if section|default('') == "genre" %}class="selected"{% endif %}><a href="{{ path('genre') }}">Genres</a></li>
    <li {% if section|default('') == "film" %}class="selected"{% endif %}><a href="{{ path('film') }}">Films</a></li>
</ul>    
{% endblock %}
  • Il ne reste plus qu'à insérer un {% use 'IabsisVideothequeBundle:Admin:sidebar_admin.html.twig' %} dans chacun des fichiers templates des dossiers "Film" et "Genre" de nos "views" afin qu'ils affichent tous notre sidebar d'administration.

Les formulaires en Symfony

Comme nous avons utilisé le générateur CRUD, nous n'avons pas eu à nous préoccuper de l'écriture des formulaires, mais nous allons quand même aborder le sujet car il est important de bien comprendre comment marche ce composant. En effet, nous ne voulons pas forcément toujours utiliser le générateur embarqué de Symfony2. Pafois nous avons besoin de formulaire complètement personnalisé. Voyons donc le fonctionnement en détail afin de vous adez à comprendre.

Info : Pour une documentation plus exhaustive des formulaires dans Symfony2 vous pouvez vous rendre sur la documentation officielle qui est très bien expliquée : http://symfony.com/fr/doc/2.3/book/forms.html

Création d'un formulaire simple

Ouvez le fichier Controller/GenreController.php et observez le code, vous allez voir qu'il y a plusieurs méthode pour générer un formulaire dépendant notamment de la réutilisabilité qu'on souhaite lui donner.

Si on veut un formulaire simple permettant par exemple de supprimer un enregistrement, nous pouvons simplement générer un formulaire de la manière suivante :

// Création d'un formulaire
$this->createFormBuilder()
     // qui pointe vers la route 'film_delete' avec en argument l'id de l'objet à supprimer
     ->setAction($this->generateUrl('film_delete', array('id' => $id)))
     // Le formulaire aura la propriété methode="DELETE"
     ->setMethod('DELETE')
     // Ajouter un bouton de soumission avec pour label 'Delete'
     ->add('submit', 'submit', array('label' => 'Delete'))
     // Finaliser le formulaire (c'est l'objet renvoyé par getForm()->getView() que l'on devra renvoyer au template twig pour affichage)
     ->getForm();

Création d'un formulaire complexe

Si on souhaite un formulaire plus complexe avec de nombreux champs et une meilleure réutilisabilité, la meilleure méthode est de passer par un objet de formulaire. Voici comment sont gérés les formulaires complexes

Création de la classe pour représenter le formulaire

Tout d'abord on crée une classe de formulaire qui étend la classe "AbstractType" dans le dossier "Form" à l'image du fichier GenreController.php que nous allons examiner ci-dessous (j'ai ajouté des commentaires pour vous aider à comprendre).

<?php

namespace Iabsis\VideothequeBundle\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;

class GenreType extends AbstractType
{
    /**
     * @param FormBuilderInterface $builder
     * @param array $options
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            // Ajout d'un champ pour le label. Aucun argument n'a été spécifié, cela veut dire qu'on laisse Symfony déterminer
            // automatiquement le meilleur type de champ à utiliser. Comme il s'agit d'un petit varchar, Symfony va utiliser
            // un champ de type texte.
            // Voyons comment détailler manuellement le champ label
            //    - Le premier paramètre désigne la propriété de l'entité qu'on souhaite modifier
            //    - Le 2ème paramètre défini le type de champ de formulaire à utiliser (la liste est disponible sur http://symfony.com/fr/doc/current/reference/forms/types.html)
            //    - Le 3ème paramètre sert à spécifier des options diverses pour le champ de formulaire comme par exemple un label personnalisé
            ->add('label', 'text', array('label' => "Genre de film"))
            // Ajout du champ 'listeDesFilms' permettant de choisir directement les films associés à ce genre
            // Si on ne spécifie pas d'argument, Symfony va générer automatiquement le type de champ adéquat (ici un select box multiple permettant de choisir un ou plusieurs films)
            // Cette syntaxe équivaut à la suivante :
            //     ->add('listeDesFilms', 'entity', array( 
            //       'class' => 'IabsisVideothequeBundle:Film',
            //       'multiple' => true,
            //       'label' => "Liste des films"
            //    )); 
            //  
            ->add('listeDesFilms');
    }

    /**
     * @param OptionsResolverInterface $resolver
     */
    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'Iabsis\VideothequeBundle\Entity\Genre'
        ));
    }

    /**
     * @return string
     */
    public function getName()
    {
        // Identifiant du formulaire
        return 'iabsis_videothequebundle_genre';
    }
}

?>

Initialisation du formulaire

L'instanciation d'un formulaire doctrine (ici le formulaire d'édition d'un genre) se fait de la manière suivante :

    private function createEditForm(Genre $entity)
    {
        // Le premier paramètre est un objet instancié de votre classe que vous avez créé dans le dossier **"Form"**
        // Le deuxième paramètre représente un objet de Type Genre qui représente l'entité que vous voulez modifier (avant application des données envoyées par le formulaire)
        // Le troisième paramètre représente comme pour les formulaires simples, les propriétés du formulaire
        $form = $this->createForm(new GenreType(), $entity, array(
            'action' => $this->generateUrl('genre_update', array('id' => $entity->getId())),
            'method' => 'PUT',
        ));

        $form->add('submit', 'submit', array('label' => 'Update'));

        return $form;
    }

Affichage du formulaire dans le template twig

Pour afficher un formulaire dans la page twig, il y a 2 choses à savoir :

Tout d'abord il faut renvoyer le formulaire au template comme ceci :

    return array(
        // Ici on renvoie le formulaire à la vue grâce à la méthode ->createView()
        'edit_form'   => $editForm->createView(),
    );

Appliquer les données saisies à l'entité

Une fois le formulaire instancié de la manière voulue, il ne reste plus qu'à récupérer les informations saisies par l'utilisateur et les appliquer à l'entité. Ici l'exemple de l'édition d'un Genre (visible dans le fichier GenreController.php) :

    // $editForm est le formulaire instancié avec l'entité à modifier
    // La méthode handleRequest() va simplement mettre à jour l'objet Genre avec les données saisies par l'utilisateur (uniquement en mémoire pas dans la base)
    $editForm->handleRequest($request);

    // On teste si le formulaire est valide 
    if ($editForm->isValid()) {
        // Si oui, on sauvegarde l'entité dans la base de données
        $em->flush();

        // Puis on redirige vers la page de son choix
        return $this->redirect($this->generateUrl('genre_edit', array('id' => $id)));
    }

La validation des formulaires

Il est possible de personnaliser les champs avec des règles afin que Symfony se charge d'afficher des erreurs aux utilisateurs lorsque des données ont été mal saisies. Par défaut le seul paramètre qui est géré par le CRUD de symfony c'est le "required". Mais si vous désactiver la gestion des attributs html5 de votre navigateur vous vous rendrez compte que si vous validez un Genre sans libellé, vous aurez une erreur mysql spécifiant qu'il est impossible d'enregistrer un film avec une valeur de titre NULL :

SQLSTATE[23000]: Integrity constraint violation: 1048 Column 'label' cannot be null

Pas très élégant n'est-ce pas. Nous allons donc voir comment gérer ce problème de manière plus sympa pour les utilisateurs. Et pour faire ça nous allons nous pencher sur les Asserts. Comme d'habitude pour les curieux, la documentation complète est disponnible ici : http://symfony.com/fr/doc/2.3/book/validation.html.

Pour cet exemple, rien de plus simple :

  • Ouvrez l'entité Genre.php
  • Ajoutez le "user" adéquate dans le le fichier :
use Symfony\Component\Validator\Constraints as Assert;
  • Repérer la propriété $label et changer son commentaire pour ajouter les propriétés assert voulues (ici par exemple on veut empêcher les valeurs vides, de plus on veut que la taille du libellé soit comprise entre 2 et 50 caractères).
/**
 * @ORM\Column(type="string", length=90)
 * @Assert\NotBlank()
 * @Assert\Length(
 *      min = "2",
 *      max = "50",
 *      minMessage = "Le genre doit faire au moins {{ limit }} caractères",
 *      maxMessage = "Le genre ne peut pas être plus long que {{ limit }} caractères"
 * )
 */
protected $label;
  • Et c'est tout ! Essayez maintenant d'éditer un genre et de ne pas lui mettre de valeur ou de lui mettre un libellé de 1 caractère, vous verrez que symfony affiche proprement les erreurs.

Evidemment il existe toute sorte de validation plus complexes. Les possibilité sont tellement nombreuses qu'il est impossible de lister tout ça au sein de ce tuto d'initiation. Je vous encourage à aller lire le tutoriel que je vous ai mis en lien juste au début du chapitre si vous voulez en savoir plus.

Mise en place de l'authentification

Dans ce chapitre nous allons aborder les notions d'authentification au sein des applications symfony. Nous allons voir deux approches différentes. L'utilisation d'un utilisateur enregistré directement dans la configuration de symfony dans un premier temps, puis, dans un second temps, nous allons créer une authentification basée sur une table utilisateurs.

Ce tutoriel ne prétend aucunement montrer toutes les possibilités du système d'authentification de Symfony, celui-ci étant très complet. En revanche, vous pourrez apprendre les bases pour mettre en place une authentification basique sur un site web. Pour avoir tous les détails sur l'authentification, vous pouvez aller voir la page suivante : http://symfony.com/doc/current/book/security.html

Configuration dite "en dur"

Modification du fichier routing.yml

Première étape, nous avons besoin de créer 2 routes, une pour afficher le formulaire de login et une que symfony utilisera pour vérifier les droits.

On corrige donc le fichier "routing.yml". On utilise un block par controller pour pouvoir gérer l'ordre des routes de façon explicite.

    _security:
        resource: "@IabsisVideothequeBundle/Controller/SecurityController.php"
        type:     annotation

    # Cette route sera gérée automatiquement par symfony, il faut juste la déclarer
    _security_login_check:
        pattern: /login_check

    # Cette route sera gérée automatiquement par symfony, il faut juste la déclarer
    _security_logout:
        pattern: /logout

    _admin:
        resource: "@IabsisVideothequeBundle/Controller/AdminController.php"
        type:     annotation

    _genre:
        resource: "@IabsisVideothequeBundle/Controller/GenreController.php"
        type:     annotation

    _film:
        resource: "@IabsisVideothequeBundle/Controller/FilmController.php"
        type:     annotation

    _default:
        resource: "@IabsisVideothequeBundle/Controller/DefaultController.php"
        type:     annotation

Modification du security.yml

Dans cette configuration on spécifie :

  • "provider" : source de laquelle proviennent les utilisateurs. Ici on crée donc l'utilisateur "admin" avec pour mot de passe "admin".
  • "firewalls" : c'est là qu'on définit notamment vers où doivent être redirigés les utilisteurs non authentifiés quand ils se trouvent dans une section non autorisée du site.
  • "access_control" : C'est la section qui va nous permettre dire quel type d'utilisateur est alloué à visiter telle section. Ici on voit que pour accéder à toutes les urls de type /admin.*, l'utilisateur doit disposer des droits ROLE_ADMIN. Pour le reste du site en revanche, ils peuvent accéder de manière anonyme.

  • "encoders" : Permet de définir de quelle manière va être crypté le mot de passe de l'utilisateur. Ici, pour une question de simplicité nous n'allons utiliser le type textplain qui nous permet de saisir les mots de passes en clair dans le fichier de config.
    security:
        encoders:
            Symfony\Component\Security\Core\User\User: plaintext

        role_hierarchy:
            ROLE_ADMIN:       ROLE_USER

        providers:
            in_memory:
                memory:
                    users:
                        user:  { password: userpass, roles: [ 'ROLE_USER' ] }
                        admin: { password: adminpass, roles: [ 'ROLE_ADMIN' ] }

        firewalls:           

            secured_area:
                pattern:    ^/
                anonymous: ~
                form_login:
                    login_path: /login
                    check_path: /login_check
                logout:
                    path:   _security_logout
                    target: _index

        access_control:
            - { path: ^/admin, roles: ROLE_ADMIN }

À ce stade, si vous essayez d'aller sur votre application vous aurez accès à la partie principale mais pas à la section d'administration. Pour le moment, si vous allez sur la section admin, vous aurez une erreur puisque les routes ont été déclarées mais nous n'avons pas créer de contrôleur pour réceptionner ces routes.

h4. Création du contrôleur

Le contrôleur qui va afficher le forumlaire peut paraître un peu complexe de prime abord, mais au final, ce bout de code est repris telquel dans la plupart des authentification Symfony.

Créez donc le fichier "SecurityController.php" dans votre dossier Controller et ajoutez-y le contenu suivant :

<?php
namespace Iabsis\VideothequeBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\Security\Core\SecurityContext;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;

class SecurityController extends Controller
{
    /**
     * @Route("/login", name="_security_login")
     * @Template()
     */
    public function loginAction()
    {
        // ON récupère les erreurs d'authentification si le formulaire a été passé avec de mauvaises informations
        if ($this->get('request')->attributes->has(SecurityContext::AUTHENTICATION_ERROR)) {
            $error = $this->get('request')->attributes->get(SecurityContext::AUTHENTICATION_ERROR);
        } else {
            $error = $this->get('request')->getSession()->get(SecurityContext::AUTHENTICATION_ERROR);
        }

        return $this->render('IabsisVideothequeBundle:Security:login.html.twig', array(
            // On envoie à notre vue le login qu'a saisi l'utilisateur précédemment
            'last_username' => $this->get('request')->getSession()->get(SecurityContext::LAST_USERNAME),
            // Et les erreurs qu'il y a eut lors de la validation du formulaire
            'error'         => $error,
        ));
    }
}

?>

Template twig

Pour afficher le formulaire, nous avons maintenant besoin d'un template que nous allons mettre comme les autres dans le dossier "videotheque/src/Iabsis/videothequeBundle/Ressources/Views". A l'intérieur de ce dossier, on crée un sous dossier "Security" puis on y met un nouveau fichier "login.html.twig".

{% extends '::base.html.twig' %}
{% block title %}Authentification requise{% endblock %}

{% block body %}
    {% if error %}
        <div class="error">{{ error.message }}</div>
    {% endif %}

    <form action="{{ path('_security_login_check') }}" method="POST">

        <input type="hidden" name="_target_path" value="{{ url('iabsis_videotheque_admin') }}" />

        <div>
            <label for="username">Username:</label>
            <input type="text" id="username" name="_username" value="{{ last_username }}" />
        </div>

        <div>
            <label for="password">Password:</label>
            <input type="password" id="password" name="_password" />
        </div>  

        <input type="submit" name="login" value="submit" />

    </form>
{% endblock %}

Rendez-vous maintenant sur votre page d'administration et vérifiez que l'authentification fonctionne

http://videotheque.local/web/app_dev.php/admin

Authentification à partir d'une table user

Pour créer une authentification basée sur une table user, nous pouvons reprendre l'essentiel du code qu'on a mis en place dans l'authentification classique. Le template et les routes restent exactement les mêmes.

En revanche nous allons devoir modifier le fichier security.yml et ajouter 2 nouvelles tables. Une table User (logique me direz vous) et une table Role qui permet de définir des niveau d'accès pour nos utilisateurs

Modification du fichier security.yml

Commençons donc par le commencement, 2 solutions s'offrent alors à nous. On peut soit garder l'authentification en dur et en plus rajouter l'authentification par base de données. Symfony étant tout à fait capable de gérer plusieurs mode d'authentification simultanée. Ou alors on peut choisir de n'autoriser que les utilisateurs que nous mettrons dans notre base de données. Cette approche est celle que nous allons voir dans ce chapitre.

Ouvrez votre fichier security.yml et remplacez son contenu par celui-ci

security:
    encoders:
        Iabsis\VideothequeBundle\Entity\User:
          algorithm: sha512
          encode-as-base64: true
          iterations: 10

    role_hierarchy:
        ROLE_ADMIN:       ROLE_USER

    providers:
        main:
          entity: { class: IabsisVideothequeBundle:User, property: login }

    firewalls:           

        secured_area:
            pattern:    ^/
            anonymous: ~
            form_login:
                login_path: /login
                check_path: /login_check
            logout:
                path:   _security_logout
                target: _index

    access_control:
        - { path: ^/admin, roles: ROLE_ADMIN }

Comme vous l'aurez remarqué, les changements ne sont pas énormes. Nous avons :

  • Remplacé l'encoder qui spécifie le type d'encodage que nous allons utiliser pour stocker les mots de passe dans la base de données. Ici on encoder en SHA512, 10x de suite avant de mettre le résultat dans la base de données.
  • Remplacé également le provider en spécifiant cette fois qu'il s'agit d'une entité (table), plus précisément la table User de notre bundle. Enfin "property" est le champ utilisé pour le nom d'utilisateur, nous choisissons le champ login.

Les entités User et Role

Ces entités seront créées comme n'importe quelle entité, à ceci près qu'elles devront implémenter une interface bien précise.

  • Concernant la table "User", elle va devoir implémenter l'interface "UserInterface" dont voici une représentation ci-dessous juste à titre d'information :
    interface UserInterface
    {
        function getRoles();
        function getPassword();
        function getSalt();
        function getUsername();
        function eraseCredentials();
        function equals(UserInterface $user);
    }

Note: il est aussi possible d'implémenter l'interface "AdvancedUserInterface" qui intègre des notions plus avancées comme la gestion des expirations. Mais nous ne les utiliserons pas ici.

  • Ensuite, concernant la table "Role", elle doit implémenter quant à elle l'interface "RoleInterface" dont voici également une représentation :
    interface RoleInterface
    {
        public function getRole();
    }

Créons mantenant les enttés "User" et "Role" en gardant bien en tête leurs interfaces respectives :

Fichier "User.php"

<?php

namespace Iabsis\VideothequeBundle\Entity;

use \Symfony\Component\Security\Core\User\UserInterface;
use \Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 */
class User implements UserInterface, \Serializable
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="IDENTITY")
     */
    protected $id;

    /**
     * @ORM\Column(type="string", length=255)
     */
    protected $login;

    /**
     * @ORM\Column(type="string", length=255)
     */
    protected $password;

    /**
     * @ORM\Column(type="string", length=255)
     *
     * @var string salt
     */
    protected $salt;

    /**
     * @ORM\ManyToMany(targetEntity="Role")
     * @ORM\JoinTable(name="_assoc_user_role",
     *     joinColumns={@ORM\JoinColumn(name="user_id", referencedColumnName="id")},
     *     inverseJoinColumns={@ORM\JoinColumn(name="role_id", referencedColumnName="id")}
     * )
     *
     * @var ArrayCollection $userRoles
     */
    protected $userRoles;

    public function __construct()
    {
        $this->createdAt = new \DateTime();
        $this->userRoles = new ArrayCollection();
        $this->updatedAt = new \DateTime();
    }

    /**
     * Gets an array of roles.
     * 
     * @return array An array of Role objects
     */
    public function getRoles()
    {
        return $this->getUserRoles()->toArray();
    }

    /**
     * Gets the user roles.
     *
     * @return ArrayCollection A Doctrine ArrayCollection
     */
    public function getUserRoles()
    {
        return $this->userRoles;
    }

    /**
     * Gets the user password.
     *
     * @return string The password.
     */
    public function getPassword()
    {
        return $this->password;
    }

    /**
     * Sets the user password.
     *
     * @param string $value The password.
     */
    public function setPassword($value)
    {
        $this->password = $value;
    }

    /**
     * Gets the user salt.
     *
     * @return string The salt.
     */
    public function getSalt()
    {
        return $this->salt;
    }

    /**
     * Sets the user salt.
     *
     * @param string $value The salt.
     */
    public function setSalt($value)
    {
        $this->salt = $value;
    }

    /**
     * Gets the username.
     *
     * @return string The username.
     */
    public function getUsername()
    {
        return $this->login;
    }

    /**
     * Sets the username.
     *
     * @param string $value The username.
     */
    public function setUsername($value)
    {
        $this->login = $value;
    }

    /**
     * Erases the user credentials.
     */
    public function eraseCredentials()
    {

    }

    /**
     * Renvoie l'objet au format serialisé
     * @return string
     */
    public function serialize()
    {
        return \json_encode(array($this->id, $this->login, $this->password, $this->salt, $this->userRoles));
    }

    /**
     * Renseigne les valeurs de l'objet à partir d'une chaine serialisée
     * @param type $serialized
     */
    public function unserialize($serialized)
    {
        list($this->id, $this->login, $this->password, $this->salt, $this->userRoles) = \json_decode($serialized);
    }
}

?>

Fichier "Role.php"

<?php

namespace Iabsis\VideothequeBundle\Entity;

use \Symfony\Component\Security\Core\Role\RoleInterface;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 * @ORM\Table(name="Role")
 */
class Role implements RoleInterface
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     *
     * @var integer $id
     */
    protected $id;

    /**
     * @ORM\Column(type="string", length=255)
     *
     * @var string $name
     */
    protected $name;

    /**
     * Gets the id.
     *
     * @return integer The id.
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * Gets the role name.
     *
     * @return string The name.
     */
    public function getName()
    {
        return $this->name;
    }

    /**
     * Sets the role name.
     *
     * @param string $value The name.
     */
    public function setName($value)
    {
        $this->name = $value;
    }

    /**
     * Gets the DateTime the role was created.
     *
     * @return DateTime A DateTime object.
     */
    public function getCreatedAt()
    {
        return $this->createdAt;
    }

    /**
     * Consturcts a new instance of Role.
     */
    public function __construct()
    {
        $this->createdAt = new \DateTime();
    }

    /**
     * Implementation of getRole for the RoleInterface.
     * 
     * @return string The role.
     */
    public function getRole()
    {
        return $this->getName();
    }
}

?>

Et pour finir, on lance la commande symfony pour créer nos nouvelles tables dans notre base de données

php app/console doctrine:schema:update --force

Ajouter un utilisateur

Pour simplifier les choses et éviter d'avoir à créer une interface de gestion des utilisateurs, nous allons maintenant créer un utilisateur directement dans notre fichier de fixtures. Vous vous souvenez ? Nous avons créé un fichier de fixture pour créer nos premiers films. Ouvrons à nouveau le fichier "LoadingFixtures.php" pour y insérer le code adéquat.

Au début du fichier (en dessous des autres "use ...;", rajoutez les lignes suivantes pour pouvoir accéder à nos entités "User" et "Role".

use Iabsis\VideothequeBundle\Entity\Role;
use Iabsis\VideothequeBundle\Entity\User;

# il nous faut ce namespace pour la gestion du cryptage du mot de passe
use Symfony\Component\Security\Core\Encoder\MessageDigestPasswordEncoder;

A la fin de notre méthode load, nous allons donc ajouter le code suivant (tout est bien expliqué à nouveau dans les commentaires) :

    // Create the roles
    $role_user = new Role();
    $role_user->setName("ROLE_USER");
    $manager->persist($role_user);
    $manager->flush();

    $role_admin = new Role();
    $role_admin->setName("ROLE_ADMIN");
    $manager->persist($role_admin);
    $manager->flush();

    // create a user
    $user = new User();

    // On donne le login Admin à notre nouvel utilisateur
    $user->setUsername('admin');

    // On cré un salt pour amélioré la sécurité
    $user->setSalt(md5(time()));

    // On crée un mot de passe (attention, comme vous pouvez le voir, il faut utiliser les même paramètres
    // que spécifiés dans le fichier security.yml, à savoir SHA512 avec 10 itérations.
    $encoder = new MessageDigestPasswordEncoder('sha512', true, 10);
    // On crée donc le mot de passe "admin" à partir de l'encodage choisi au-dessus
    $password = $encoder->encodePassword('password', $user->getSalt());
    // On applique le mot de passe à l'utilisateur
    $user->setPassword($password);

    $user->getUserRoles()->add($role_admin);

    $manager->persist($user);
    $manager->flush();

Il ne nous reste plus qu'à lancer notre fixtures pour créer notre utilisateur dans la base de données.

Attention, lorsque vous lancez le script de fixtures, les anciennes données que vous avez saisies sont entièrement réinitialisées et les données présentes dans votre fichier de fixtures seront créées.

Lancez donc la commande suivante :

php app/console doctrine:fixtures:load

Vous devriez maintenant pouvoir vous connecter à votre interface d'administration avec les identifiants enregistrés dans votre base de données, à savoir admin / password.

A propos de l'auteur

Gilles Hemmerlé

Développeur d'applications et web designer pour la société Iabsis

Passionné d'informatique et notamment de développement depuis plus de 15 ans. Il s'est spécialisé dans le domaine du web et notamment dans le développement PHP / Mysql.