TP : Les entités de notre blog

L'objectif de ce chapitre est de mettre en application tout ce que nous avons vu au cours de cette partie sur Doctrine2. Nous allons créer les entités Article et Blog, mais également adapter le contrôleur pour nous en servir. Enfin, nous verrons quelques astuces de développement Symfony2 au cours du TP.

Surtout, je vous invite à bien essayer de réfléchir par vous-mêmes avant de lire les codes que je donne. C'est ce mécanisme de recherche qui va vous faire progresser sur Symfony2, il serait dommage de s'en passer !

Bon TP !

Synthèse des entités

Entité Article

On a déjà pas mal traité l'entité Article au cours de cette partie. Pour l'instant, on a toujours le pseudo de l'auteur écrit en dur dans l'entité. Souvenez-vous, pour commencer, on n'a pas d'entité Utilisateur, on doit donc écrire le pseudo de l'auteur en dur dans les articles.

Voici donc la version finale de l'entité Article que vous devriez avoir :

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
<?php
// src/Sdz/BlogBundle/Entity/Article.php

namespace Sdz\BlogBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Gedmo\Mapping\Annotation as Gedmo;

/**
 * Sdz\BlogBundle\Entity\Article
 *
 * @ORM\Table()
 * @ORM\Entity(repositoryClass="Sdz\BlogBundle\Entity\ArticleRepository")
 * @ORM\HasLifecycleCallbacks()
 */
class Article
{
  /**
   * @var integer $id
   *
   * @ORM\Column(name="id", type="integer")
   * @ORM\Id
   * @ORM\GeneratedValue(strategy="AUTO")
   */
  private $id;

  /**
   * @var datetime $date
   *
   * @ORM\Column(name="date", type="datetime")
   */
  private $date;

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

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

  /**
   * @ORM\Column(name="publication", type="boolean")
   */
  private $publication;

  /**
   * @var text $contenu
   *
   * @ORM\Column(name="contenu", type="text")
   */
  private $contenu;

  /**
   * @ORM\Column(type="date", nullable=true)
   */
  private $dateEdition;

  /**
   * @Gedmo\Slug(fields={"titre"})
   * @ORM\Column(length=128, unique=true)
   */
  private $slug;

  /**
   * @ORM\OneToOne(targetEntity="Sdz\BlogBundle\Entity\Image", cascade={"persist", "remove"})
   */
  private $image;

  /**
   * @ORM\ManyToMany(targetEntity="Sdz\BlogBundle\Entity\Categorie", cascade={"persist"})
   */
  private $categories;

  /**
   * @ORM\OneToMany(targetEntity="Sdz\BlogBundle\Entity\Commentaire", mappedBy="article")
   */
  private $commentaires; // Ici commentaires prend un « s », car un article a plusieurs commentaires !


  public function __construct()
  {
    $this->date     = new \Datetime;
    $this->publication  = true;
    $this->categories   = new \Doctrine\Common\Collections\ArrayCollection();
    $this->commentaires = new \Doctrine\Common\Collections\ArrayCollection();
  }

  /**
   * @ORM\preUpdate
   * Callback pour mettre à jour la date d'édition à chaque modification de l'entité
   */
  public function updateDate()
  {
    $this->setDateEdition(new \Datetime());
  }

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

  /**
   * @param datetime $date
   * @return Article
   */
  public function setDate(\Datetime $date)
  {
    $this->date = $date;
    return $this;
  }

  /**
   * @return datetime
   */
  public function getDate()
  {
    return $this->date;
  }

  /**
   * @param string $titre
   * @return Article
   */
  public function setTitre($titre)
  {
    $this->titre = $titre;
    return $this;
  }

  /**
   * @return string
   */
  public function getTitre()
  {
    return $this->titre;
  }

  /**
   * @param text $contenu
   * @return Article
   */
  public function setContenu($contenu)
  {
    $this->contenu = $contenu;
    return $this;
  }

  /**
   * @return text
   */
  public function getContenu()
  {
    return $this->contenu;
  }

  /**
   * @param boolean $publication
   * @return Article
   */
  public function setPublication($publication)
  {
    $this->publication = $publication;
    return $this;
  }

  /**
   * @return boolean
   */
  public function getPublication()
  {
    return $this->publication;
  }

  /**
   * @param string $auteur
   * @return Article
   */
  public function setAuteur($auteur)
  {
    $this->auteur = $auteur;
    return $this;
  }

  /**
   * @return string
   */
  public function getAuteur()
  {
    return $this->auteur;
  }

  /**
   * @param Sdz\BlogBundle\Entity\Image $image
   * @return Article
   */
  public function setImage(\Sdz\BlogBundle\Entity\Image $image = null)
  {
    $this->image = $image;
    return $this;
  }

  /**
   * @return Sdz\BlogBundle\Entity\Image
   */
  public function getImage()
  {
    return $this->image;
  }

  /**
   * @param Sdz\BlogBundle\Entity\Categorie $categorie
   * @return Article
   */
  public function addCategorie(\Sdz\BlogBundle\Entity\Categorie $categorie)
  {
    $this->categories[] = $categorie;
    return $this;
  }

  /**
   * @param Sdz\BlogBundle\Entity\Categorie $categorie
   */
  public function removeCategorie(\Sdz\BlogBundle\Entity\Categorie $categorie)
  {
    $this->categories->removeElement($categorie);
  }

  /**
   * @return Doctrine\Common\Collections\Collection
   */
  public function getCategories()
  {
    return $this->categories;
  }

  /**
   * @param Sdz\BlogBundle\Entity\Commentaire $commentaire
   * @return Article
   */
  public function addCommentaire(\Sdz\BlogBundle\Entity\Commentaire $commentaire)
  {
    $this->commentaires[] = $commentaire;
    return $this;
  }

  /**
   * @param Sdz\BlogBundle\Entity\Commentaire $commentaire
   */
  public function removeCommentaire(\Sdz\BlogBundle\Entity\Commentaire $commentaire)
  {
    $this->commentaires->removeElement($commentaire);
  }

  /**
   * @return Doctrine\Common\Collections\Collection
   */
  public function getCommentaires()
  {
    return $this->commentaires;
  }

  /**
   * @param datetime $dateEdition
   * @return Article
   */
  public function setDateEdition(\Datetime $dateEdition)
  {
    $this->dateEdition = $dateEdition;
    return $this;
  }

  /**
   * @return date 
   */
  public function getDateEdition()
  {
    return $this->dateEdition;
  }

  /**
   * @param string $slug
   * @return Article
   */
  public function setSlug($slug)
  {
    $this->slug = $slug;
    return $this;
  }

  /**
   * @return string 
   */
  public function getSlug()
  {
    return $this->slug;
  }
}

Entité Image

L'entité Image est une entité très simple, qui nous servira par la suite pour l'upload d'images avec Symfony2. Sa particularité est qu'elle peut être liée à n'importe quelle autre entité : elle n'est pas du tout exclusive à l'entité Article. Si vous souhaitez ajouter des images ailleurs que dans des Article, il n'y aura aucun problème.

Voici son code, que vous devriez déjà avoir :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
<?php
// src/Sdz/BlogBundle/Entity/Image.php

namespace Sdz\BlogBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Table()
 * @ORM\Entity(repositoryClass="Sdz\BlogBundle\Entity\ImageRepository")
 */
class Image
{
  /**
   * @var integer $id
   *
   * @ORM\Column(name="id", type="integer")
   * @ORM\Id
   * @ORM\GeneratedValue(strategy="AUTO")
   */
  private $id;

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

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


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

  /**
   * @param string $url
   * @return Image
   */
  public function setUrl($url)
  {
    $this->url = $url;
    return $this;
  }

  /**
   * @return string 
   */
  public function getUrl()
  {
    return $this->url;
  }

  /**
   * @param string $alt
   * @return Image
   */
  public function setAlt($alt)
  {
    $this->alt = $alt;
    return $this;
  }

  /**
   * @return string 
   */
  public function getAlt()
  {
    return $this->alt;
  }
}

Entité Commentaire

L'entité Commentaire, bien que très simple, contient la relation avec l'entité Article, c'est elle la propriétaire. Voici son code :

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
<?php
// src/Sdz/Blog/Bundle/Entity/Commentaire.php

namespace Sdz\BlogBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Table()
 * @ORM\Entity(repositoryClass="Sdz\BlogBundle\Entity\CommentaireRepository")
 */
class Commentaire
{
  /**
   * @ORM\Column(name="id", type="integer")
   * @ORM\Id
   * @ORM\GeneratedValue(strategy="AUTO")
   */
  private $id;

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

  /**
   * @ORM\Column(name="contenu", type="text")
   */
  private $contenu;

  /**
   * @ORM\Column(name="date", type="datetime")
   */
  private $date;

  /**
   * @ORM\ManyToOne(targetEntity="Sdz\BlogBundle\Entity\Article", inversedBy="commentaires")
   * @ORM\JoinColumn(nullable=false)
   */
  private $article;

  public function __construct()
  {
    $this->date = new \Datetime();
  }

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

  /**
   * @param string $auteur
   * @return Commentaire
   */
  public function setAuteur($auteur)
  {
    $this->auteur = $auteur;
    return $this;
  }

  /**
   * @return string
   */
  public function getAuteur()
  {
    return $this->auteur;
  }

  /**
   * @param text $contenu
   * @return Commentaire
   */
  public function setContenu($contenu)
  {
    $this->contenu = $contenu;
    return $this;
  }

  /**
   * @return text
   */
  public function getContenu()
  {
    return $this->contenu;
  }

  /**
   * @param datetime $date
   * @return Commentaire
   */
  public function setDate(\Datetime $date)
  {
    $this->date = $date;
    return $this;
  }

  /**
   * @return datetime
   */
  public function getDate()
  {
    return $this->date;
  }

  /**
   * @param Sdz\BlogBundle\Entity\Article $article
   * @return Commentaire
   */
  public function setArticle(\Sdz\BlogBundle\Entity\Article $article)
  {
    $this->article = $article;
    return $this;
  }

  /**
   * @return Sdz\BlogBundle\Entity\Article
   */
  public function getArticle()
  {
    return $this->article;
  }
}

Entité Categorie

L'entité Categorie ne contient qu'un attribut nom (enfin, vous pouvez en rajouter de votre côté bien sûr !). La relation avec Article est contenue dans l'entité Article, qui en est la propriétaire. Voici son code, que vous devriez déjà avoir :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
<?php
// src/Sdz/BlogBundle/Entity/Categorie.php

namespace Sdz\BlogBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Table()
 * @ORM\Entity(repositoryClass="Sdz\BlogBundle\Entity\CategorieRepository")
 */
class Categorie
{
  /**
   * @var integer $id
   *
   * @ORM\Column(name="id", type="integer")
   * @ORM\Id
   * @ORM\GeneratedValue(strategy="AUTO")
   */
  private $id;

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


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

  /**
   * @param string $nom
   * @return Categorie
   */
  public function setNom($nom)
  {
    $this->nom = $nom;
    return $this;
  }

  /**
   * @return string 
   */
  public function getNom()
  {
    return $this->nom;
  }
}

Entités Competence et ArticleCompetence

L'entité Competence ne contient, au même titre que l'entité Categorie, qu'un attribut nom, mais vous pouvez bien sûr en rajouter selon vos besoins. Voici son code :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
<?php
// src/Sdz/BlogBundle/Entity/Competence.php

namespace Sdz\BlogBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Table()
 * @ORM\Entity(repositoryClass="Sdz\BlogBundle\Entity\CompetenceRepository")
 */
class Competence
{
  /**
   * @var integer $id
   *
   * @ORM\Column(name="id", type="integer")
   * @ORM\Id
   * @ORM\GeneratedValue(strategy="AUTO")
   */
  private $id;

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


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

  /**
   * @param string $nom
   * @return Competence
   */
  public function setNom($nom)
  {
    $this->nom = $nom;
    return $this;
  }

  /**
   * @return string 
   */
  public function getNom()
  {
    return $this->nom;
  }
}

L'entité ArticleCompetence est l'entité de relation entre Article et Competence. Elle contient les attributs $article et $competence qui permettent de faire la relation, ainsi que d'autres attributs pour caractériser la relation, ici j'ai utilisé un attribut niveau. Voici son code, vous pouvez bien entendu rajouter les attributs de relation que vous souhaitez :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
<?php
// src/Sdz/BlogBundle/Entity/ArticleCompetence.php

namespace Sdz\BlogBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 */
class ArticleCompetence
{
  /**
   * @ORM\Id
   * @ORM\ManyToOne(targetEntity="Sdz\BlogBundle\Entity\Article")
   */
  private $article;

  /**
   * @ORM\Id
   * @ORM\ManyToOne(targetEntity="Sdz\BlogBundle\Entity\Competence")
   */
  private $competence;

  /**
   * @ORM\Column()
   */
  private $niveau; // Ici j'ai un attribut de relation, que j'ai appelé « niveau »

  /**
   * @param string $niveau
   * @return Article_Competence
   */
  public function setNiveau($niveau)
  {
    $this->niveau = $niveau;
    return $this;
  }

  /**
   * @return string
   */
  public function getNiveau()
  {
    return $this->niveau;
  }

  /**
   * @param Sdz\BlogBundle\Entity\Article $article
   * @return ArticleCompetence
   */
  public function setArticle(\Sdz\BlogBundle\Entity\Article $article)
  {
    $this->article = $article;
    return $this;
  }

  /**
   * @return Sdz\BlogBundle\Entity\Article
   */
  public function getArticle()
  {
    return $this->article;
  }

  /**
   * @param Sdz\BlogBundle\Entity\Competence $competence
   * @return ArticleCompetence
   */
  public function setCompetence(\Sdz\BlogBundle\Entity\Competence $competence)
  {
    $this->competence = $competence;
    return $this;
  }

  /**
   * @return Sdz\BlogBundle\Entity\Competence
   */
  public function getCompetence()
  {
    return $this->competence;
  }
}

Et bien sûr…

Si vous avez ajouté et/ou modifié des entités, n'oubliez pas de mettre à jour votre base de données ! Vérifiez les requêtes avec php app/console doctrine:schema:update --dump-sql, puis exécutez-les avec --force.

Adaptation du contrôleur

Théorie

Maintenant que l'on a nos entités, on va enfin pouvoir adapter notre contrôleur Blog pour qu'il récupère et modifie des vrais articles dans la base de données, et non plus nos articles statiques définis à la va-vite.

Pour cela, il y a très peu de modifications à réaliser : voici encore un exemple du code découplé que Symfony2 nous permet de réaliser ! En effet, il vous suffit de modifier les quatre endroits où on avait écrit un article en dur dans le contrôleur. Modifiez ces quatre endroits en utilisant bien le repository de l'entité Article, seules les méthodes findAll() et find() vont nous servir pour le moment.

Attention, je vous demande également de faire attention au cas où l'article demandé n'existe pas. Si on essaie d'aller à la page /blog/article/4 alors que l'article d'id 4 n'existe pas, je veux une erreur correctement gérée ! On a déjà vu le déclenchement d'une erreur 404 lorsque le paramètre page de la page d'accueil n'était pas valide, reprenez ce comportement.

À la fin le contrôleur ne sera pas entièrement opérationnel, car il nous manque toujours la gestion des formulaires. Mais il sera déjà mieux avancé !

Et bien sûr, n'hésitez pas à nettoyer tous les codes de tests qu'on a pu utiliser lors de cette partie pour jouer avec les entités, maintenant on doit avoir un vrai contrôleur qui ne fait que son rôle.

Pratique

Il n'y a vraiment rien de compliqué dans notre nouveau contrôleur, le voici :

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
<?php

// src/Sdz/BlogBundle/Controller/BlogController.php

namespace Sdz\BlogBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sdz\BlogBundle\Entity\Article;

class BlogController extends Controller
{
  public function indexAction($page)
  {
    // On ne sait pas combien de pages il y a
    // Mais on sait qu'une page doit être supérieure ou égale à 1
    // Bien sûr pour le moment on ne se sert pas (encore !) de cette variable
    if ($page < 1) {
      // On déclenche une exception NotFoundHttpException 
      // Cela va afficher la page d'erreur 404
      // On pourra la personnaliser plus tard
      throw $this->createNotFoundException('Page inexistante (page = '.$page.')');
    }

    // Pour récupérer la liste de tous les articles : on utilise findAll()
    $articles = $this->getDoctrine()
                     ->getManager()
                     ->getRepository('SdzBlogBundle:Article')
                     ->findAll();

    // L'appel de la vue ne change pas
    return $this->render('SdzBlogBundle:Blog:index.html.twig', array(
      'articles' => $articles
    ));
  }

  public function voirAction($id)
  {
    // On récupère l'EntityManager
    $em = $this->getDoctrine()
               ->getManager();

    // Pour récupérer un article unique : on utilise find()
    $article = $em->getRepository('SdzBlogBundle:Article')
                  ->find($id);

    if ($article === null) {
      throw $this->createNotFoundException('Article[id='.$id.'] inexistant.');
    }

    // On récupère les articleCompetence pour l'article $article
    $liste_articleCompetence = $em->getRepository('SdzBlogBundle:ArticleCompetence')
                                  ->findByArticle($article->getId());

    // Puis modifiez la ligne du render comme ceci, pour prendre en compte les variables :
    return $this->render('SdzBlogBundle:Blog:voir.html.twig', array(
      'article'                 => $article,
      'liste_articleCompetence' => $liste_articleCompetence,
      // Pas besoin de passer les commentaires à la vue, on pourra y accéder via {{ article.commentaires }}
      // 'liste_commentaires'   => $article->getCommentaires()
    ));
  }

  public function ajouterAction()
  {
    // La gestion d'un formulaire est particulière, mais l'idée est la suivante :

    if ($this->get('request')->getMethod() == 'POST') {
      // Ici, on s'occupera de la création et de la gestion du formulaire

      $this->get('session')->getFlashBag()->add('info', 'Article bien enregistré');

      // Puis on redirige vers la page de visualisation de cet article
      return $this->redirect( $this->generateUrl('sdzblog_voir', array('id' => 1)) );
    }

    // Si on n'est pas en POST, alors on affiche le formulaire
    return $this->render('SdzBlogBundle:Blog:ajouter.html.twig');
  }

  public function modifierAction($id)
  {
    // On récupère l'EntityManager
    $em = $this->getDoctrine()
               ->getEntityManager();

    // On récupère l'entité correspondant à l'id $id
    $article = $em->getRepository('SdzBlogBundle:Article')
                  ->find($id);

    // Si l'article n'existe pas, on affiche une erreur 404
    if ($article == null) {
      throw $this->createNotFoundException('Article[id='.$id.'] inexistant');
    }

    // Ici, on s'occupera de la création et de la gestion du formulaire

    return $this->render('SdzBlogBundle:Blog:modifier.html.twig', array(
      'article' => $article
    ));
  }

  public function supprimerAction($id)
  {
    // On récupère l'EntityManager
    $em = $this->getDoctrine()
               ->getEntityManager();

    // On récupère l'entité correspondant à l'id $id
    $article = $em->getRepository('SdzBlogBundle:Article')
                  ->find($id);

    // Si l'article n'existe pas, on affiche une erreur 404
    if ($article == null) {
      throw $this->createNotFoundException('Article[id='.$id.'] inexistant');
    }

    if ($this->get('request')->getMethod() == 'POST') {
      // Si la requête est en POST, on supprimera l'article

      $this->get('session')->getFlashBag()->add('info', 'Article bien supprimé');

      // Puis on redirige vers l'accueil
      return $this->redirect( $this->generateUrl('sdzblog_accueil') );
    }

    // Si la requête est en GET, on affiche une page de confirmation avant de supprimer
    return $this->render('SdzBlogBundle:Blog:supprimer.html.twig', array(
      'article' => $article
    ));
  }

  public function menuAction($nombre)
  {
    $liste = $this->getDoctrine()
                  ->getManager()
                  ->getRepository('SdzBlogBundle:Article')
                  ->findBy(
                    array(),          // Pas de critère
                    array('date' => 'desc'), // On trie par date décroissante
                    $nombre,         // On sélectionne $nombre articles
                    0                // À partir du premier
                  );

    return $this->render('SdzBlogBundle:Blog:menu.html.twig', array(
      'liste_articles' => $liste // C'est ici tout l'intérêt : le contrôleur passe les variables nécessaires au template !
    ));
  }
}

Amélioration du contrôleur

Le contrôleur qu'on vient de faire n'est pas encore parfait, on peut faire encore mieux !

Je pense notamment à deux points en particulier :

  • Le premier est l'utilisation implicite d'un ParamConverter pour éviter de vérifier à chaque fois l'existence d'un article (l'erreur 404 qu'on déclenche le cas échéant) ;
  • Le deuxième est la pagination des articles sur la page d'accueil.

L'utilisation d'un ParamConverter

Le ParamConverter est une notion très simple : c'est un convertisseur de paramètre. Vous voilà bien avancés, n'est-ce pas ? :p

Plus sérieusement, c'est vraiment cela. Il existe un chapitre dédié sur l'utilisation du ParamConverter dans la partie astuces. Mais sachez qu'il va convertir le paramètre en entrée du contrôleur dans une autre forme. Typiquement, nous avons le paramètre $id en entrée de certaines de nos actions, que nous voulons transformer directement en entité Article. Pour cela, rien de plus simple, il faut modifier la définition des méthodes du contrôleur comme suit :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<?php
// src/Sdz/BlogBundle/Controller/BlogController.php

// La définition :

  public function voirAction($id)

// Devient :

  public function voirAction(Article $article)

Bien entendu, n'oubliez pas de rajouter en début de fichier use Sdz\BlogBundle\Entity\Article; afin de pouvoir utiliser Article.

Et grâce aux mécanismes internes de Symfony2, vous retrouvez directement dans la variable $article l'article correspondant à l'id $id. Cela nous permet de supprimer l'utilisation du repository pour récupérer l'article, mais également le test de l'existence de l'article. Ces deux points sont fait automatiquement par le ParamConverter de Doctrine, et cela simplifie énormément nos méthodes. Voyez par vous-mêmes ce que devient la méthode voirAction du contrôleur :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
// src/Sdz/BlogBundle/Controller/BlogController.php

  public function voirAction(Article $article)
  {
    // À ce stade, la variable $article contient une instance de la classe Article
    // Avec l'id correspondant à l'id contenu dans la route !

    // On récupère ensuite les articleCompetence pour l'article $article
    // On doit le faire à la main pour l'instant, car la relation est unidirectionnelle
    // C'est-à-dire que $article->getArticleCompetences() n'existe pas !
    $listeArticleCompetence = $this->getDoctrine()
                                   ->getManager()
                                   ->getRepository('SdzBlogBundle:ArticleCompetence')
                                   ->findByArticle($article->getId());

    return $this->render('SdzBlogBundle:Blog:voir.html.twig', array(
      'article'                 => $article,
      'listeArticleCompetence'  => $listeArticleCompetence
    ));
  }

Merveilleux, non ? La méthode est réduite à son strict minimum, sans rien enlever en termes de fonctionnalité. Essayez avec un article qui n'existe pas : /blog/article/5123123 ; nous avons bien une erreur 404.

Utiliser une jointure pour récupérer les articles

Actuellement sur la page d'accueil, avec l'action indexAction(), on ne récupère que les articles en eux-mêmes. Comme on en a parlé dans les précédents chapitres, cela veut dire que dans la boucle pour afficher les articles on ne peut pas utiliser les informations sur les relations (dans notre cas, les attributs $image, $categories et $commentaires). Enfin, on peut bien entendu les utiliser via $article->getImage(), mais dans ce cas une requête sera générée pour aller récupérée l'image… à chaque itération de la boucle sur les articles !

Ce comportement est bien sûr à proscrire, car le nombre de requêtes SQL va monter en flèche et ce n'est pas bon du tout pour les performances. Il faut donc modifier la requête initiale qui récupère les articles, pour y rajouter des jointures qui vont récupérer en une seule requête les articles ainsi que leurs entités jointes.

Tout d'abord, on va créer une méthode getArticles() dans le repository de l'entité Article, une version toute simple qui ne fait que récupérer les entités ordonnées :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<?php
// src/Sdz/BlogBundle/Entity/ArticleRepository.php

namespace Sdz\BlogBundle\Entity;

use Doctrine\ORM\EntityRepository;

class ArticleRepository extends EntityRepository
{
  public function getArticles()
  {
    $query = $this->createQueryBuilder('a')
                  ->orderBy('a.date', 'DESC')
                  ->getQuery();

    return $query->getResult();
  }
}

Adaptons ensuite le contrôleur pour utiliser cette nouvelle méthode :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?php
// src/Sdz/BlogBundle/Controller/BlogController.php

class BlogController extends Controller
{
  public function indexAction($page)
  {
    // On ne sait pas combien de pages il y a
    // Mais on sait qu'une page doit être supérieure ou égale à 1
    // Bien sûr pour le moment on ne se sert pas (encore !) de cette variable
    if ($page < 1) {
      // On déclenche une exception NotFoundHttpException
      // Cela va afficher la page d'erreur 404
      // On pourra la personnaliser plus tard
      throw $this->createNotFoundException('Page inexistante (page = '.$page.')');
    }

    // Pour récupérer la liste de tous les articles : on utilise notre nouvelle méthode
    $articles = $this->getDoctrine()
                     ->getManager()
                     ->getRepository('SdzBlogBundle:Article')
                     ->getArticles();

    // L'appel de la vue ne change pas
    return $this->render('SdzBlogBundle:Blog:index.html.twig', array(
      'articles' => $articles
    ));
  }

  // …
}

Maintenant, il nous faut mettre en place les jointures dans la méthode getArticles(), afin de charger toutes les informations sur les articles et éviter les dizaines de requêtes supplémentaires.

Dans notre exemple, nous allons afficher les données de l'entité image et des entités categories liées à chaque article. Il nous faut donc rajouter les jointures sur ces deux entités. On a déjà vu comment faire ces jointures, n'hésitez pas à essayer de les faire de votre côté avant de regarder ce code :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
<?php
// src/Sdz/BlogBundle/Entity/ArticleRepository.php

// …

  public function getArticles($nombreParPage, $page)
  {
    $query = $this->createQueryBuilder('a')
                  // On joint sur l'attribut image
                  ->leftJoin('a.image', 'i')
                    ->addSelect('i')
                  // On joint sur l'attribut categories
                  ->leftJoin('a.categories', 'c')
                    ->addSelect('c')
                  ->orderBy('a.date', 'DESC')
                  ->getQuery();

    return $query->getResult();
  }

Comme vous pouvez le voir, les jointures se font simplement en utilisant les attributs existants de l'entité racine, ici l'entité Article. On rajoute donc juste les leftJoin() et les addSelect(), afin que Doctrine n'oublie pas de sélectionner les données qu'il joint. C'est tout ! Vous pouvez maintenant utiliser un $article->getImage() sans déclencher de nouvelle requête.

Soit dit en passant, ces jointures peuvent justifier la mise en place d'une relation bidirectionnelle. En effet, dans l'état actuel on ne peut pas récupérer les informations des compétences liées à un article par exemple, car l'entité Article n'a pas d'attribut ArticleCompetences, donc pas de ->leftJoin() possible. C'est l'entité ArticleCompetence qui est propriétaire de la relation unidirectionnelle. Si vous voulez afficher les compétences, vous devez commencer par rendre la relation bidirectionnelle. N'hésitez pas à le faire, c'est un bon entraînement !

La pagination des articles sur la page d'accueil

Paginer manuellement les résultats d'une requête n'est pas trop compliqué, il faut juste faire un peu de mathématiques à l'aide des variables suivantes :

  • Nombre total d'articles ;
  • Nombre d'articles à afficher par page ;
  • La page courante.

Cependant, c'est un comportement assez classique et en bon développeurs que nous sommes, trouvons une méthode plus simple et déjà prête ! Il existe en effet un paginateur intégré dans Doctrine2, qui permet de faire tout cela très simplement. Intégrons-le dans notre méthode getArticles() comme ceci :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?php
// src/Sdz/BlogBundle/Entity/ArticleRepository.php

namespace Sdz\BlogBundle\Entity;

use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Tools\Pagination\Paginator;

class ArticleRepository extends EntityRepository
{
  // On ajoute deux arguments : le nombre d'articles par page, ainsi que la page courante
  public function getArticles($nombreParPage, $page)
  {
    // On déplace la vérification du numéro de page dans cette méthode
    if ($page < 1) {
      throw new \InvalidArgumentException('L\'argument $page ne peut être inférieur à 1 (valeur : "'.$page.'").');
    }

    // La construction de la requête reste inchangée
    $query = $this->createQueryBuilder('a')
                  ->leftJoin('a.image', 'i')
                    ->addSelect('i')
                  ->leftJoin('a.categories', 'cat')
                    ->addSelect('cat')
                  ->orderBy('a.date', 'DESC')
                  ->getQuery();

    // On définit l'article à partir duquel commencer la liste
    $query->setFirstResult(($page-1) * $nombreParPage)
    // Ainsi que le nombre d'articles à afficher
          ->setMaxResults($nombreParPage);

    // Enfin, on retourne l'objet Paginator correspondant à la requête construite
    // (n'oubliez pas le use correspondant en début de fichier)
    return new Paginator($query);
  }
}

Maintenant que cette méthode est fonctionnelle pour la pagination, je vous invite à adapter le contrôleur pour prendre en compte cette pagination. Il faut donc utiliser correctement la méthode du repository que l'on vient de détailler, mais également donner à la vue toutes les données dont elle a besoin pour afficher une liste des pages existantes.

Au travail ! Encore une fois, faites l'effort de la réaliser de votre côté. Évidemment, je vous donne la solution, mais si vous n'avez pas essayé de chercher, vous ne progresserez pas. Courage !

Il existe bien entendu différentes manières de le faire, mais voici le code du contrôleur que je vous propose :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<?php
// src/Sdz/BlogBundle/Controller/BlogController.php

  public function indexAction($page)
  {
    $articles = $this->getDoctrine()
                     ->getManager()
                     ->getRepository('SdzBlogBundle:Article')
                     ->getArticles(3, $page); // 3 articles par page : c'est totalement arbitraire !

    // On ajoute ici les variables page et nb_page à la vue
    return $this->render('SdzBlogBundle:Blog:index.html.twig', array(
      'articles'   => $articles,
      'page'       => $page,
      'nombrePage' => ceil(count($articles)/3)
    ));
  }

C'est tout ! En effet, rappelez-vous l'architecture MVC, toute la logique de récupération des données est dans la couche Modèle : ici notre repository. Notre contrôleur est donc réduit à son strict minimum ; la couche Modèle, grâce à un Doctrine2 généreux en fonctionnalités, fait tout le travail.

Attention à une petite subtilité. Ici, la variable $articles contient une instance de Paginator. Concrètement, c'est une liste d'articles, dans notre cas une liste de 3 articles (on a mis cette valeur en dur). Vous pouvez l'utiliser avec un simple foreach par exemple. Cependant, pour obtenir le nombre de pages vous voyez qu'on a utilisé un count($articles) : ce count ne retourne pas 3, mais le nombre total d'articles dans la base de données ! Cela est possible avec les objets qui implémentent l'interface Countable de PHP.

Enfin, il nous reste juste la vue à adapter. Voici ce que je peux vous proposer, j'ai juste rajouté l'affichage de la liste des pages possibles :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
{# src/Sdz/BlogBundle/Resources/views/Blog/index.html.twig #}

  {# … le reste de la vue #}

  <div class="pagination">
    <ul>
      {# On utilise la fonction range(a, b) qui crée un tableau de valeurs entre a et b #}
      {% for p in range(1, nombrePage) %}
        <li{% if p == page %} class="active"{% endif %}>
          <a href="{{ path('sdzblog_accueil', {'page': p}) }}">{{ p }}</a>
        </li>
      {% endfor %}
    </ul>
  </div>

Ce qui donne le résultat visible à la figure suivante.

Nos articles et la pagination s'affichent

Bingo !

Améliorer les vues

Dans cette section, on s'est surtout intéressés au contrôleur. Mais on peut également améliorer nos vues, en les mutualisant pour certaines actions par exemple. L'idée que j'ai en tête, c'est d'avoir une unique vue article.html.twig qu'on pourra utiliser sur la page d'accueil ainsi que sur la page d'un article. Bien sûr, ce n'est valable que si vous voulez la même présentation pour les deux, ce qui est mon cas.

Essayez donc de créer un article.html.twig générique, et de l'inclure depuis les deux autres vues.

Article.html.twig

Dans cette vue générique, on va intégrer l'affichage des informations de l'image et des catégories dont on a fait la jointure au début de cette section. Voici ma version :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
{# src/Sdz/BlogBundle/Resources/views/Blog/article.html.twig #}

{# On utilise une variable temporaire, qu'on définit à false si elle n'est pas déjà définie #}
{% set accueil = accueil|default(false) %}

<h2>
  {# On vérifie qu'une image est bien associée à l'article #}
  {% if article.image is not null %}
    <img src="{{ asset(article.image.url) }}" alt="{{ article.image.alt }}" />
  {% endif %}

  {# Si on est sur la page d'accueil, on fait un lien vers l'article, sinon non #}
  {% if accueil %}
    <a href="{{ path('sdzblog_voir', {'id': article.id} ) }}">{{ article.titre }}</a>
  {% else %}
    {{ article.titre }}
  {% endif %}
</h2>

<i>Le {{ article.date|date('d/m/Y') }}, par {{ article.auteur }}.</i>

<div class="well">
  {{ article.contenu }}
</div>

{# On affiche les catégories éventuelles #}
{% if article.categories.count > 0 %}
  <div class="well well-small">
    <p><i>
      Catégories :
      {% for categorie in article.categories %}
        {{ categorie.nom }}{% if not loop.last %}, {% endif %}
      {% endfor %}
    </i></p>
  </div>
{% endif %}

Ce que vous pouvez faire pour améliorer cette vue :

  • Utiliser le slug plutôt que l'id pour les liens vers les articles, histoire d'avoir un meilleur référencement ;
  • Utiliser un bundle de Markdown pour mettre en forme le texte du contenu de l'article ;
  • Utiliser les microdata pour améliorer la compréhension de votre page par les moteurs de recherche, notamment avec le format Article.

N'hésitez pas à traiter ces points vous-mêmes pour vous entraîner et pour améliorer le rendu de votre blog !

Index.html.twig

Dans cette vue, il faut juste modifier la liste afin d'utiliser la vue précédente pour afficher chaque article. Voici ma version :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
{# src/Sdz/BlogBundle/Resources/views/Blog/index.html.twig #}

{# … La liste devient : #}
<ul>
  {% for article in articles %}
    {# On inclut la vue à chaque itération dans la boucle #}
    {% include "SdzBlogBundle:Blog:article.html.twig" with {'accueil': true} %}
    <hr />
  {% else %}
    <p>Pas (encore !) d'articles</p>
  {% endfor %}
</ul>

{# … #}

Il faut toujours vérifier que la vue incluse aura les variables qu'elle attend. Ici, la vue article.html.twig utilise la variable {{ article }}, il faut donc :

  • Que cette variable existe dans la vue qui l'inclut ;
  • Ou que la vue qui l'inclut précise cette variable dans le tableau : {{ include "…" with {'article': … } }}.

Nous sommes dans le premier cas, la variable {{ article }} est définie par le for sur la liste des articles. Par contre, la variable accueil est transmise via le with.

Voir.html.twig

Dans cette vue, il faut juste remplacer l'affichage en dur de l'article par l'inclusion de notre nouvelle vue. Voici ma version :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
{# src/Sdz/BlogBundle/Resources/views/Blog/voir.html.twig #}

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

{% block title %}
  Lecture d'un article - {{ parent() }}
{% endblock %}

{% block sdzblog_body %}

  {# Ici, on inclut la vue #}
  {% include "SdzBlogBundle:Blog:article.html.twig" %}

  <p>
    <a href="{{ path('sdzblog_accueil') }}" class="btn">
      <i class="icon-chevron-left"></i>
      Retour à la liste
    </a>
    <a href="{{ path('sdzblog_modifier', {'id': article.id}) }}" class="btn">
      <i class="icon-edit"></i>
      Modifier l'article
    </a>
    <a href="{{ path('sdzblog_supprimer', {'id': article.id}) }}" class="btn">
      <i class="icon-trash"></i>
      Supprimer l'article
    </a>
  </p>

{% endblock %}

Pour conclure

Et voilà, le premier TP du tutoriel s'achève ici. J'espère que vous avez pu exploiter toutes les connaissances que vous avez pu acquérir jusqu'ici, et qu'il vous a aidé à vous sentir plus à l'aise.

La prochaine partie du tutoriel va vous emmener plus loin avec Symfony2, pour connaître tous les détails qui vous permettront de créer votre site internet de toutes pièces. À bientôt !


En résumé

  • Vous savez maintenant construire vos entités ;
  • Vous savez maintenant développer un contrôleur abouti ;
  • Vous savez maintenant faire des jointures et plus encore dans vos repositories ;
  • Vous savez maintenant inclure habilement des vues pour éviter de dupliquer du code ;
  • Bravo !