Hey salut, bienvenue dans cette série sur Symfony, dans la dernière partie, nous avons créé nos routes et contrôleurs, nous avons aussi vu comment retourner du texte depuis un contrôleur en utilisant la classe Response
de Symfony. Dans ce tutoriel, nous allons encore aller plus loin, nous allons parler des vues et voir comment retourner une page HTML.
Tu te rappelles je t'avais dit que Symfony repose sur l'architecture MVC (Modèle - Vue - Contrôleur), nous avons déjà parler du contrôleur qui prend une requête la traite et construit la vue pour la renvoyer à l'utilisateur, nous allons donc parler de la vue aujourd'hui.
Nous allons continuer à travailler sur le codebase du dernier tutoriel, si tu ne l'as pas, il est disponible sur Github, tu peux le cloner en utilisant git
:
$ git clone https://github.com/kaherecode/symfony-ticketing-app.git
$ cd symfony-ticketing-app
$ git checkout controllers
$ composer install
La troisième ligne va te permettre de te placer exactement sur le code nécessaire pour continuer sur ce tutoriel et ensuite tu installes les dépendances avec composer
.
Le moteur de template Twig
Symfony utilise un moteur de template qui s'appelle Twig. L’intérêt d'utiliser un moteur de template c'est la clarté du code, on écrira pas du PHP dans du HTML. Par exemple, disons que nous avons un tableau d'objet utilisateur et on veut afficher le nom de chaque utilisateur dans une liste ordonnée, en PHP, on aurait fait:
<ol>
<?php foreach ($users as $user) : ?>
<li><?= $user->getName() ?></li>
<?php endforeach; ?>
</ol>
Et avec Twig, voilà ce qu'on aurait fait:
<ol>
{% for user in users %}
<li>{{ user.name }}</li>
{% endfor %}
</ol>
Alors, ce n'est pas vraiment évident avec ce petit bout de code, mais maintenant imagine le cas d'un grand projet, ça aurait beaucoup plus complexe.
Une autre particularité de Twig, c'est sa rapidité, tous les templates sont compilés en PHP et mis en cache, et seulement quand tu changes le code d’un template, Twig le recompile et le remet encore en cache, ce qui est très pratique en production.
Pour utiliser Twig, il faut retenir ces trois syntaxes:
{{ ... }}
Afficher quelque chose, une variable par exemple{% ... %}
Faire quelque chose, définir une variable, faire une boucle, les structures conditionnelles{# ... #}
Commenter du texte
En Symfony (depuis la version 4), les templates se trouvent dans le dossier templates
qui se trouvent à la racine et les fichiers Twig ont une extension .twig
Si tu ouvres le dossier templates
, tu verras deux sous dossiers core
et event
et un fichier base.html.twig
. A l’intérieur du sous dossier core
se trouve un fichier index.html.twig
, si tu te rappelles bien, ce fichier et le dossier core
ont été généré lorsque nous avons créé la classe CoreController
, c'est aussi le cas pour le sous dossier event
avec la classe EventController
. Pour le moment, on laisse tomber le fichier base.html.twig
, tu ne le supprimes pas, on le garde juste à côté un instant.
Ouvre donc le fichier index.html.twig
qui se trouve dans templates/core
, nous allons remplacer son contenu par ce qui suit:
{# templates/core/index.html.twig #}
<h1>Hello toi dev Symfony, ceci est une vue twig! Bravo!</h1>
Nous avons notre vue, il faut maintenant faire le lien avec le contrôleur pour lui dire de retourner cette page à chaque fois que l'utilisateur visite la page d'accueil de notre site, nous allons donc faire cela dans la fonction homepage()
de la classe CoreController.php
.
Pour rappel, voici le contrôleur homepage()
:
<?php // src/Controller/CoreController.php
namespace App\Controller;
// ...
class CoreController extends AbstractController
{
/**
* @Route("/", name="homepage")
*/
public function homepage(): Response
{
return new Response("
<h1>Hello toi dev Symfony,</h1>
<h2>Bienvenue sur Kaherecode!</h2>
");
}
// ...
}
Le contrôleur retourne un objet Response
pour l'instant, vu que nous l’avons déjà créer une vue, nous allons donc l'utiliser:
<?php // src/Controller/CoreController.php
namespace App\Controller;
// ...
class CoreController extends AbstractController
{
/**
* @Route("/", name="homepage")
*/
public function homepage(): Response
{
return $this->render('core/index.html.twig');
}
// ...
}
Si tu te rends maintenant sur la page d'accueil de notre site, tu dois avoir la page:
Rendu avec Twig
Et voilà, on a notre première vue, c'est vraiment fort. Nous utilisons la méthode render()
de la classe AbstractController
de Symfony, et nous lui envoyons en paramètre la vue à afficher, le chemin vers notre template. Symfony recherche les templates dans le dossier templates
qui se trouve à la racine de notre projet, donc nous utilisons le chemin en fonction de ce dossier.
Voyons maintenant comment envoyer un paramètre à une vue twig, comme dans le cas de notre contrôleur EventController::show($id)
qui va afficher l'id que nous recevons depuis l'URL sur la page. Voilà actuellement le contenu de la fonction show()
:
<?php // src/Controller/EventController.php
namespace App\Controller;
// ...
class EventController extends AbstractController
{
/**
* @Route("/events/{id}", name="show_event", requirements={"id"="\d+"})
*/
public function show($id): Response
{
return new Response("<h1>Affichage de l'événement {$id}.</h1>");
}
// ...
}
Nous allons donc modifier cette fonction pour retourner une vue twig et envoyer la variable $id
à cette vue:
<?php // src/Controller/EventController.php
namespace App\Controller;
// ...
class EventController extends AbstractController
{
/**
* @Route("/events/{id}", name="show_event", requirements={"id"="\d+"})
*/
public function show($id): Response
{
return $this->render('event/show.html.twig', ['event_id' => $id]);
}
// ...
}
Nous utilisons toujours la méthode render()
, mais cette fois ci, nous lui envoyons un deuxième paramètre, un tableau associatif dans lequel on a une clé event_id
qui a comme valeur la variable $id
que nous récupérons depuis l'URL de la requête. À quoi sert ce tableau? À envoyer des paramètres à notre vue, tu te rappelles de ce que fait le contrôleur? Prendre une requête, aller chercher les modèles dont la vue a besoin, construire la vue puis retourner une réponse au client. Ici, prend la variable $id
comme un objet que nous venons de récupérer en base de données, nous voulons donc afficher cet objet dans notre vue Twig, il faut l'envoyer à la vue, on utilise donc ce deuxième paramètre pour envoyer l'objet ($id
) au template show.html.twig
.
Nous allons maintenant créer le fichier event/show.html.twig
dans le dossier templates:
{# templates/event/show.html.twig #}
<h1>Affichage de l'événement {{ event_id }} dans une vue twig.</h1>
Si je me rends maintenant sur l'URL https://127.0.0.1:8000/events/35, j'ai la page:
Et voilà! Dans la vue, nous utilisons la clé du tableau que nous avons défini comme deuxième paramètre de la méthode render()
du contrôleur, et chaque clé dans ce tableau représente le nom d'une variable dans la vue associée. En twig, nous utilisons {{ nom_variable }}
pour afficher une variable.
Ça y est, un peu de HTML, du CSS par ci, et on a notre application.
Jusque là, nos pages sont, comment dire, moches, mais nous allons y travailler, dans un petit instant. Avant, laisse moi te présenter une autre grande fonctionnalité de twig que tu va kiffer.
Héritage de templates
Généralement, sur une application ou site web, nous avons des blocs qui ne changent pas sur toutes les pages, c'est le cas de la barre de navigation et/ou du pied de page. Avec Twig, nous n'allons pas redéfinir ces blocs sur toutes le pages, nous allons définir un template mère qui va contenir l'architecture de base de nos pages et dont les pages de notre site vont hérités. Le template mère va définir des blocs (block
) dans lesquels on pourra rajouter du contenu spécifique pour chaque page. Cela fonctionne comme l’héritage en PHP, on peut redéfinir les méthodes publics de la classe mère.
Dans notre cas, nous allons utiliser le fichier templates/base.html.twig
comme template mère, il va donc contenir la barre de navigation de notre site et définir un bloc pour le contenu de nos pages.
{# templates/base.html.twig #}
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
{% block stylesheets %}
{# Bootstrap CSS #}
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
{% endblock %}
<title>{% block title %}Symfony Ticketing App{% endblock %}</title>
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<a class="navbar-brand" href="{{ path('homepage') }}">Symfony Ticketing App</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#mobileMenu" aria-controls="mobileMenu" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="mobileMenu">
<ul class="navbar-nav ml-auto mt-2 mt-lg-0">
<li class="nav-item active">
<a class="nav-link" href="{{ path('homepage') }}">Accueil</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Connexion</a>
</li>
</ul>
</div>
</nav>
{% block content %}
{# Le contenu de chaque page sera ici #}
{% endblock %}
{% block scripts %}
{# jQuery first, then Popper.js, then Bootstrap JS #}
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>
{% endblock %}
</body>
</html>
Ce fichier défini le template de base de toutes les pages de notre site, toutes nos pages devront donc hériter de ce fichier. Nous définissons ici 4 blocs (block
): title
, stylesheets
, content
et scripts
. Les blocs vont nous servir à définir des sections dans lesquels le template enfant va pouvoir écrire. Tout le contenu qui ne se trouve pas dans un bloc est une méthode privé, on ne peut donc pas le modifier dans les templates enfants, ce qui veut donc dire qu'on ne peut pas modifier le contenu de la balise header
dans les templates enfants, parce que ceux-ci ne sont pas définis dans un bloc. Maintenant pour pouvoir voir les modifications, nous allons faire hériter notre page d'accueil (core/index.html.twig
) de ce template, nous aurons donc:
{# templates/core/index.html.twig #}
{% extends 'base.html.twig' %}
<h1>Hello toi dev Symfony, ceci est une vue twig! Bravo!</h1>
On utilise extends
comme en PHP aussi. Nous mettons juste le nom du fichier, le chemin de base pour tous les templates c'est le dossier templates
, le fichier base.html.twig
se trouve directement dans ce dossier, pas dans un sous dossier, donc on ne spécifie aucun chemin ici. Si on actualise la page d'accueil on a:
Oouppsss, une erreur! Alors ça dit quoi? "Un template qui hérite d'un autre ne peut pas définir du contenu en dehors d'un bloc", le message d'erreur est très clair, nous affichons la balise h1
en dehors d'un bloc. Le bloc content
que nous avons défini dans le fichier base.html.twig
va servir à contenir tout le contenu de nos pages, nous allons donc l'utiliser comme ceci:
{# templates/core/index.html.twig #}
{% extends 'base.html.twig' %}
{% block content %}
<h1>Hello toi dev Symfony, ceci est une vue twig! Bravo!</h1>
{% endblock %}
Si tu actualises maintenant la page d'accueil:
Yesssss, nous avons bien la barre de navigation, avec le texte qui se trouve dans le bloc content
, t'es un génie!
Et si tu définis un autre bloc en dehors du bloc content
, ce dernier ne sera pas afficher. Tu peux par contre définir un bloc dans le bloc content
, mais rappelle toi, ça ne sert à rien de juste créer des blocs si ce n'est pas pour les réutiliser dans un template enfant.
Nous allons donc faire la même chose pour le template event/show.html.twig
:
{# templates/event/show.html.twig #}
{% extends 'base.html.twig' %}
{% block content %}
<h1>Affichage de l'événement {{ event_id }} dans une vue twig.</h1>
{% endblock %}
Comme nous l'avons précédemment dit, il est possible de modifier ou d'écraser le contenu de tous les blocs du template parent dans les templates fils. Le bloc title
par exemple, permet de définir le titre d'une page, nous avons une valeur par défaut qui est "Symfony Ticketing App", nous voulons maintenant que le titre de nos pages ressemble à ceci "Accueil | Symfony Ticketing App", c'est à dire le nom de la page, puis le nom de l'application. Mais comment faire celà? Il faut redéfinir le bloc title
dans le template fils et lui mettre une valeur. Voyons voir:
{# templates/core/index.html.twig #}
{% extends 'base.html.twig' %}
{% block title %}Accueil{% endblock %}
{% block content %}
<h1>Hello toi dev Symfony, ceci est une vue twig! Bravo!</h1>
{% endblock %}
Je modifie ici le template core/index.html.twig
de la page d'accueil, regardons donc ce que ça donne dans le navigateur:
UH OH! On a seulement "Accueil", mais si tu réfléchis tu te dis que c'est normal, on n'a pas demandé à ré-utiliser le contenu du bloc dans le template parent, c'est comme quand tu redéfini le constructeur d'une classe en PHP, si tu veux utiliser le constructeur de la classe mère, il faut le spécifier avec parent::__construct()
, ça va aussi être le cas ici, il faut donc faire:
{# templates/core/index.html.twig #}
{% extends 'base.html.twig' %}
{% block title %}Accueil | {{ parent() }}{% endblock %}
{% block content %}
<h1>Hello toi dev Symfony, ceci est une vue twig! Bravo!</h1>
{% endblock %}
Voilà, nous avons un template base.html.twig
dont toutes les pages de notre application vont hériter.
Inclure un template
L'inclusion de template va nous permettre d'injecter un template dans un autre. C'est différent de l'héritage dans le sens ou avec l'héritage, le template fils va hériter de la structure même du template parent, avec l'inclusion, on prend un template qu'on injecte à un emplacement spécifique d'un autre template. Prenons l'exemple d'un formulaire pour éditer un événement, ce formulaire il est le même sur la page d'ajout d'un événement que sur la page pour modifier un événement, pour éviter donc de le redéfinir dans ces deux pages, nous allons sortir juste le formulaire, le mettre dans un fichier et ensuite l'inclure dans ces deux pages. Nous verrons cet exemple plus tard, quand nous parlerons des formulaires. Pour illustrer l'inclusion ici, je vais sortir l'en-tête et le pied de page de nos pages, les balises header
et footer
dans deux fichiers différents et après les inclures dans le fichier base.html.twig
.
Pour organiser un peu le dossier templates
, je vais créer un sous dossier includes
dans lequel je vais mettre les fichiers à inclure. Je vais donc créer les fichiers includes/_header.html.twig
et includes/_footer.html.twig
, je précède le nom des fichiers à inclure d'un underscore (_
), c'est juste un autre moyen de savoir que ce sont des templates à inclure, j'aime me faciliter la vie, parce que dans quelques mois, je risque de me perdre sur du code que j'ai écris. Voici donc le contenu du fichier includes/_header.html.twig
:
{# templates/includes/_header.html.twig #}
<header>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<a class="navbar-brand" href="{{ path('homepage') }}">Symfony Ticketing App</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#mobileMenu" aria-controls="mobileMenu" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="mobileMenu">
<ul class="navbar-nav ml-auto mt-2 mt-lg-0">
<li class="nav-item active">
<a class="nav-link" href="{{ path('homepage') }}">Accueil</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Connexion</a>
</li>
</ul>
</div>
</nav>
</header>
Puis on l'inclus dans le template base.html.twig
:
{# templates/base.html.twig #}
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
{% block stylesheets %}
{# Bootstrap CSS #}
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
{% endblock %}
<title>{% block title %}Symfony Ticketing App{% endblock %}</title>
</head>
<body>
{% include 'includes/_header.html.twig' %}
{% block content %}
{# Le contenu de chaque page sera ici #}
{% endblock %}
{% include 'includes/_footer.html.twig' %}
{% block scripts %}
{# jQuery first, then Popper.js, then Bootstrap JS #}
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>
{% endblock %}
</body>
</html>
Nous utilisons cette fois-ci le mot clé include
puis nous indiquons le chemin vers le fichier à inclure.
Le template est un concept super large et je te conseille de faire un tour sur la documentation et aussi sur Twig.
J'ai un peu modifier les fichiers core/index.html.twig
et event/show.html.twig
pour y mettre un peu de contenu, le tout se trouve sur le dépôt Github.
Et l'affichage d'un événement:
Maintenant que nous avons nos vues, nous allons nous limiter là. La prochaine fois nous allons parler de doctrine et des entités. A très bientôt.
Participe à la discussion