En programmation informatique, un framework est un kit de composants logiciels structurels, qui définissent les fondations ainsi que les grandes lignes de l’organisation de tout ou partie d’un logiciel (architecture). En programmation orientée objet un framework est typiquement composé de classes mères qui seront dérivées et étendues par héritage en fonction des besoins spécifiques à chaque logiciel qui utilise le framework.
Les framework sont utilisés pour modeler l’architecture des logiciels applicatifs, des applications web, des middleware et des composants logiciels. Les framework sont achetés par les ingénieurs, puis ajoutés comme partie intégrante des logiciels applicatifs mis sur le marché, ils sont par conséquent rarement achetés et installés séparément par un utilisateur final.
Un framework est un ensemble d’outils et de composants logiciels organisés conformément à un plan d’architecture et des design patterns. L’ensemble forme un squelette de programme. Il est souvent fourni sous la forme d’une bibliothèque logicielle, et accompagné du plan de l’architecture cible du framework. Avec un framework orienté objets, le programmeur qui utilise le framework pourra personnaliser les éléments principaux du programme par extension, en utilisant le mécanisme d’héritage: créer des nouvelles classes qui contiennent toutes les fonctionnalités que met en place le framework, et en plus ses fonctionnalités propres, créées par le programmeur en fonction des besoins spécifiques à son programme. Le mécanisme d’héritage permet également de transformer des fonctionnalités existant dans les classes du framework.
Un framework est conçu en vue d’aider les programmeurs dans leur travail. L’organisation du framework vise la productivité maximale du programmeur qui va l’utiliser – gage de baisse des coûts de construction du programme. Le contenu exact du framework est dicté par le type de programme et l’architecture cible pour lequel il est conçu.
Un framework n'est pas indispensable pour
réaliser une application web, mais il apporte ce qu'il faut pour
être bien plus efficace, rapide
et créer une application de qualité !
Un framework vous offre la possibilité de développer une
application dans les règles de l'art. Il permet de réaliser
une application structurée, maintenable et
évolutive. De plus, la flexibilité du framework vous
permet de vous concentrer entièrement sur votre application sans
trop se soucier du reste.
N'ayez crainte, vous maîtrisez toujours l'intégralité de votre
application. Le framework va vous apporter certaines briques de
base qui permettent de ne pas avoir à réinventer la roue.
· Abstraction de la base de données : un framework utilise PDO, une solution qui vous permet d'ajouter une couche d'abstraction entre votre base de données et votre code. Vous n'avez plus à vous soucier du type de base de données qui fonctionne derrière votre application. Un framework embarque aussi généralement un ORM (object relational mapper) qui permet d'installer une couche d'abstraction supplémentaire entre les appels à votre base de données et votre code. Vous pouvez ainsi faire des opérations courantes comme la récupération de données ou la sauvegarde d'un objet sans vous soucier du code SQL à écrire.
Abstraction de la base de données
· Gestion des formulaires : Le framework offre la possibilité de générer en grande partie tous les widgets HTML, il se charge de la validation du formulaire et de la sécurité CSRF (Cross-Site Request Forgery).
· Gestion d'utilisateurs : La plupart des frameworks ont tout ce qu'il faut pour pouvoir gérer l'authentification d'utilisateurs. Ils gèrent la connexion, la déconnexion, la création, la gestion des sessions et des droits.
· Gestion des erreurs et bien plus : Certains frameworks dont Symfony offrent de très bon outils de débogage ainsi qu'un "profiler" qui permet de vous renseigner sur tout ce qu'il se passe dans votre application (variables globales, requêtes de base de données, logs, temps de chargement, etc.).
· Internationalisation : Les frameworks vous permettent de créer facilement le multilingue pour vous et ce, de manière native la plupart du temps.
·
Moteur de template : De nombreux frameworks
intègrent un moteur de templates. Celui-ci permet de simplifier
grandement l'écriture de votre code HTML tout en étant propre et
efficace.
Symfony utilise le moteur
de template Twig
CakePHP |
CodeIgniter |
Laravel |
Symfony |
|
|||
CakePHP est un framework très simple d'utilisation. Facile à appréhender, il fournit un ensemble de briques de base solides et peut facilement être couplé à un ORM |
Code Igniter est un très bon framework qui se fait de plus en plus connaitre de part sa qualité. Il n'embarque pas d'ORM mais dispose de l'essentiel pour répondre à la plupart des demandes. |
En peu de temps, une communauté d'utilisateurs du framework Laravel s'est constituée, et il est devenu en 2016 le projet PHP le mieux noté de GitHub. Laravel reste pourtant basé sur son grand frère Symfony, pour au moins 30 % de ses lignes (utilisation de "Symfony component"). |
Symfony est un framework qui dispose d'un bon nombre de briques de base tout en laissant pas mal de libertés au développeur. Cependant, Symfony peut être délicat à aborder pour un débutant s'il n'a pas de bonnes connaissances en programmation orientée objet et une certaine maîtrise des technologies web est nécessaire pour en exploiter toute la puissance du produit. |
Lancé en 2005, Symfony est aujourd'hui un framework stable connu et reconnu à l'international. Symfony dispose aussi une communauté active de développeurs, intégrateurs, utilisateurs et d'autres contributeurs qui participent à l'enrichissement continu de l'outil.
https://sensiolabs.com/Derrière Symfony se cache une entreprise : Sensio, agence web créée il y a 12 ans. Oui, ce sont bien des français qui sont derrière Symfony, plus particulièrement Fabien Potencier et toute son équipe. Imaginé au départ pour ses propres besoins, le framework Symfony est aujourd'hui encore l'outil utilisé au quotidien par ses propres équipes. Symfony est un outil qui arrive à répondre aux exigences du monde professionnel.
Fabien Potencier,
lead développeur du projet Symfony
De nombreux sites et applications de
toutes tailles et de tous les types !
C'est par exemple le cas de Yahoo!,
Dailymotion, Opensky.com, Exercise.com et même des
applications telles que phpBB.
Symfony est tout ce
que vous attendez d'un framework : rapidité, flexibilité, des
composants réutilisables, etc.
De bonnes idées ont fait leur petit bout de chemin, c'est par
exemple le cas de l'injection de dépendances. Un concept tout droit
venu du monde du Java !
On retrouve aussi une "web debug toolbar" que l'ont peut retrouver
dans d'autre framework et qui apporte un une bonne amélioration de
la productivité des développeurs.
Lorsque vous utilisez Symfony, vous êtes assuré de ne jamais vous retrouvez seul. Une communauté se trouve à coté de vous ! Des conversations par mail sont partagées, des channels IRC existent pour que vous exposiez vos problèmes, de nombreux travaux sont publiés régulièrement et bien évidement, le Site du Zéro est là pour vous accompagner !
L'idée est de pas
vous enfermer dans Symfony ! Permettez-vous de construire des
applications qui répondent précisément à vos besoins !
Symfony tend à respecter un maximum de conventions de codage. Ce
n'est certes pas parfait, mais le framework s'efforce à respecter
des standards comme
PSR-0 et HTTP.
Enfin, Symfony est distribué sous licence Open Source
MIT, qui n'impose pas de contraintes et permet le
développement de l'Open Source ainsi que des applications
propriétaires.
Si vous souhaitez plus de ressource, le site principal de Symfony est accessible à l’adresse suivante : https://symfony.com/
Pour suivre le cours il est nécessaire de disposer de :
· Un éditeur de codes PHP – HTML – CSS – Javascript
Note de versions et outils :
Voir cours : "Atelier1_MiseEnPlaceConfigurationSymfony6.doc" visible à l'adresse https://btsrabelais22.fr/sio/sio2slam/Symfony6_MiseEnPlaceConfiguration-web/index.html
Symfony s'appuie sur l'architecture MVC. En voici les grandes lignes.
MVC signifie « Modèle / Vue / Contrôleur ». C'est un découpage très répandu pour développer les sites Internet, car il sépare les couches selon leur logique propre :
·
Le Contrôleur (ou Controller) : son rôle
est de générer la réponse à la requête HTTP demandée par notre
visiteur. Il est la couche qui se charge d'analyser et de traiter
la requête de l'utilisateur. Le contrôleur contient la logique de
notre site Internet et va se contenter « d'utiliser » les autres
composants : les modèles et les vues. Concrètement, un contrôleur
va récupérer, par exemple, les informations sur l'utilisateur
courant, vérifier qu'il a le droit de modifier tel article,
récupérer cet article et demander la page du formulaire d'édition
de l'article. C'est tout bête, avec quelques if()
, on s'en sort très bien.
·
Le Modèle (ou Model) : son rôle est de
gérer vos données et votre contenu. Reprenons l'exemple de
l'article. Lorsque je dis « le contrôleur récupère l'article », il
va en fait faire appel au modèle Article
et lui dire : « donne-moi
l'article portant l'id 5 ». C'est le modèle qui sait comment
récupérer cet article, généralement via une requête au serveur SQL,
mais ce pourrait être depuis un fichier texte ou ce que vous
voulez. Au final, il permet au contrôleur de manipuler les
articles, mais sans savoir comment les articles sont stockés,
gérés, etc. C'est une couche d'abstraction.
·
La Vue (ou View) : son rôle est
d'afficher les pages. Reprenons encore l'exemple de l'article. Ce
n'est pas le contrôleur qui affiche le formulaire, il ne fait
qu'appeler la bonne vue. Si nous avons une vue Formulaire
, les balises HTML du
formulaire d'édition de l'article y seront et au final le
contrôleur ne fera qu'afficher cette vue sans savoir vraiment ce
qu'il y a dedans. En pratique, c'est le designer d'un projet qui
travaille sur les vues. Séparer vues et contrôleurs permet aux
designers et développeurs PHP de travailler ensemble sans se
marcher dessus.
Au final, le contrôleur ne contient que du code très simple, car il se contente d'utiliser des modèles et des vues en leur attribuant des tâches précises. Il agit un peu comme un chef d'orchestre, qui n'agite qu'une baguette alors que ses musiciens jouent des instruments complexes.
Afin de bien visualiser tous les acteurs que nous avons vus à la page précédente voici le schéma du parcours complet d'une requête dans Symfony :
Parcours complet d'une requête dans Symfony sur un exemple de "plateforme d'annonces"
Textuellement, voici ce que cela donne :
1. Le
visiteur demande la page /platform
;
2. Le contrôleur frontal reçoit la requête, charge le Kernel et la lui transmet ;
3. Le
Kernel demande au Routeur quel contrôleur exécuter pour l'URL
/platform
. Ce
Routeur est un composant Symfony qui fait la correspondance entre
URL et contrôleurs, nous l'étudierons bien sûr dans un prochain
chapitre. Le Routeur fait donc son travail, et dit au Kernel qu'il
faut exécuter le contrôleur OCPlatform:Advert
;
4. Le
Kernel exécute donc ce contrôleur. Le contrôleur demande au modèle
Annonce
la
liste des annonces, puis la donne à la vue ListeAnnonces
pour qu'elle
construise la page HTML et la lui retourne. Une fois cela fini, le
contrôleur envoie au visiteur la page HTML complète.
Les couleurs permettent de distinguer les points où l'on intervient. En vert, les contrôleurs, modèle et vue, c'est ce qu'on devra développer nous-mêmes. En orange, le Kernel et le Routeur, c'est ce qu'on devra configurer. On ne touchera pas au contrôleur frontal, en gris.
Maintenant, il ne nous reste plus qu'à voir comment organiser concrètement notre code et sa configuration.
Cette application a pour premier objectif la "découverte" du framework Symfony6 à travers la "gestion" du port de Pontrieux. Elle évoluera régulièrement au cours de l'année.
Nous allons d'abord créer une page toute simple, la page d'accueil. Les informations figurant sur cette page seront statiques et ne proviendront pas d'une base de données. On y trouvera un peu de texte et un carrousel d'images du port de Pontrieux. Le style et le diaporama seront assurés par BootStrap :
Le fichier .env
Avant de construire l’application, il est important de connaitre un peu la configuration d'un projet Symfony. Si vous observez la structure de l’application dans VSCode, vous trouverez un dossier config à la racine du projet symf6_PortPontrieux.
Dans ce dossier, il y a une multitude de fichiers de configuration. Notamment, en ouvrant le sous-dossier packages, vous trouverez éventuellement les sous-dossiers dev, prod et test.
Ces dossiers correspondent à des configurations différentes : le mode développement (le mode où nous nous trouvons par défaut), le mode production et le mode test.
Vous découvrez aussi, à la base du dossier packages, un fichier framework.yaml.
YAML est un format de données très simple, basé sur des données au format clé : valeur.
Ouvrez ce fichier, vous découvrez la configuration par défaut du framework :
Nous pouvons remarquer dans la valeur de la clé secret, l’utilisation d’une valeur un peu particulière :
%env(APP_SECRET)%
Cette valeur fait référence à une variable d’environnement.
Toutes les variables d’environnement sont définies dans un seul fichier : .env, qui se trouve à la racine du projet Symf6_PortPontrieux.
Explorons le fichier .env :
L’avantage de l’utilisation de ce fichier est que toutes les variables d’environnement y sont regroupées. Vous n’avez pas à jongler avec l’ensemble des fichiers de configuration.
Un autre avantage est que vous pouvez avoir un fichier de configuration par environnement ; un .env.prod et un .env.test (qui est déjà créé par défaut), par exemple.
Vous pouvez également versionner (faire une version différente avec un outil comme Git : voir https://git-scm.com/) votre fichier .env pour partager votre configuration avec d’autres développeurs.
On remarque dans ce fichier une variable qui s’appelle : APP_ENV, qui est par défaut à dev. Cette variable est utilisée dans la configuration pour définir le mode de développement utilisé (ici nous sommes en mode dev par défaut).
Les variables d’environnement peuvent être utilisées dans toute l’application, grâce au helper env().
On appelle helper une fonction utilisable n’importe où dans le code.
Dans l’exemple du fichier framework.yaml ci-dessus, la variable d’environnement APP_SECRET est invoquée grâce à env(APP_SECRET).
Les % signifient qu’on utilise la valeur d’un paramètre en YAML. Nous y reviendrons plus tard.
Toutes les variables d’environnement présentes dans le fichier .env sont utilisables partout dans le code.
À titre d’exemple, créons la variable APP_AUTHOR dans le fichier .env :
APP_ENV=dev
APP_SECRET=053d04c9f2c414f9a22d032025dd8a6b
APP_AUTHOR=sio
Nous verrons par la suite, un exemple d’utilisation de cette variable dans un contrôleur.
1. Liste des variables d’environnement
On peut lister toutes les variables
d’environnement grâce à cette commande sur le terminal
([Ctrl] ù sur VSCode pour ouvrir une fenêtre du
terminal) :
php bin/
console
debug:container
--env-vars
2. Remplacement des variables d’environnement en local
Si vous devez remplacer une variable d’environnement localement sur votre machine tout en ne modifiant pas l’environnement global, vous pouvez le faire en créant un fichier .env.local.
.env.local remplace les valeurs par
défaut pour tous les environnements, mais uniquement sur la machine
qui contient le fichier.
Nous allons maintenant définir les routes de notre
application.
Pour définir les routes de toute application, il existe plusieurs
méthodes comme l'utilisation d'un fichier de configuration au
format yaml (.yaml), les annotations ou encore un fichier xml.
Nous allons privilégier dans ce tuto l'utilisation des
annotations.
Avant de définir les routes, énumérons d’abord quelques routes dont on pense avoir besoin pour l'application "Port de Pontrieux" :
Nous allons donc utiliser ces 2
routes.
On pourrait les créer via le fichier routes.yaml de cette façon
:
Ici il est donc précisé que le chemin par défaut : "localhost/Symf6_PortPontrieux/web/app_dev.php" lancera le contrôleur "AccueilController.php" et que l'on y trouvera impérativement la fonction indexAction.
Si on ajoute /annonces à l'url, on lancera le contrôleur "AnnoncesController.php" et on y trouvera la fonction listeAction.
Nous avons choisi de définir les routes via les annotations dans le contrôleur et non via le fichier de configuration routes.yaml. Nous allons donc créer les contrôleurs.
Le contrôleur est juste une méthode
qui va nous permettre de retourner une réponse. En gros le
contrôleur reçoit une requête Request
et renvoie une réponse
Response
.
Pour créer ce contrôleur, nous allons utiliser une ligne de
commande de Symfony (dans le terminal visualCode).
…\Symf6_PortPontrieux
> php bin/console make:controller AccueilController
Dans la console nous voyons que 2 fichiers ont été créés, le
fichier AccueilController.php
(
situé dans …/src/controller) donc et un fichier twig
(situé dans …/templates/accueil) qui nous servira pour la vue
(l'affichage). Pour le moment nous allons travailler sur le fichier
AccueilController.php, qui se trouve dans
src/Controller et l'ouvrir dans notre éditeur :
A noter la définition de la route entre #[ et ]. Ces remarques sont donc indispensables car nous avons choisi de définir les routes par annotation.
Si nous vérifions l'url localhost:8000/accueil nous obtenons maintenant (il faut que le serveur soit démarré : symfony server:start dans le terminal. Il est donc préférable d'avoir 2 terminaux sous VsCode) :
Ce n'est pas l'accueil espéré mais il n'y a plus d'erreurs. C'est le fichier index.html.twig du dossier "…/templates/accueil" qui est "renvoyé".
En Symfony 6, les templates se
trouvent dans le dossier templates qui se trouvent à la racine. Les
fichiers Twig ont une extension .twig
Si vous ouvrez le dossier templates, vous verrez un dossier accueil
et un fichier base.html.twig
. A l’intérieur du
dossier accueil se trouve un fichier index.html.twig
, ce fichier et le
dossier accueil ont été générés lorsque nous avons créé le
contrôleur AccueilController
. Pour le moment,
on ne se préoccupe pas du fichier base.html.twig
.
Nous allons dans un 1er temps modifier le fichier "index.html.twig" dans le dossier "…\www\Symf6_PortPontrieux\templates" pour obtenir uniquement :
<html>
<body>
<h2>Port de Pontrieux</h2>
<p>Situé dans le site exceptionnel de la vallée du Trieux.</p>
</body>
</html>
Pour obtenir :
Pour notre site nous allons utiliser BootStrap et JQuery pour assurer une mise en forme élaborée et responsive.
Voici respectivement les fichiers routes.yaml (du répertoire …\Symf6_PortPontrieux\config), AccueilController (…Symf6_PortPontrieux\src\Controller) et les fichiers pour la vue : base.html.twig (…\Symf6_PortPontrieux\templates) et index.html.twig (…\Symf6_PortPontrieux\templates\accueil) :
AccueilController.php (très légèrement modifié, le nom de la route a changé et est devenu accueil)
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class AccueilController extends AbstractController
{
#[Route('/accueil', name: 'accueil')]
public function index()
{
return $this->render('accueil/index.html.twig', [
'controller_name' => 'AccueilController',
]);
}
}
base.html.twig (du dossier templates)
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Gestion du port de Pontrieux</title>
{# Bloc que l'on pourra éventuellement modifié dans les templates finaux #}
{% block styles %}
<link rel="stylesheet" href="{{ asset('css/bootstrap.css') }}" type="text/css" />
<script src="{{ asset('js/jquery.js') }}"></script>
<script src="{{ asset('js/bootstrap.js') }}"></script>
<style>
.carousel-inner > .item > img,
.carousel-inner > .item > a > img
{
width: 70%;
margin: auto;
}
</style>
{% endblock %}
</head>
{% block body %}
{# Ici on va définir une banniere, un titre et un menu commun à toutes les pages #}
<nav class="navbar navbar-inverse">
<div class="container-fluid">
<div class="navbar-header">
<a class="navbar-brand" href="#">Port de Pontrieux</a>
</div>
<ul class="nav navbar-nav">
<li id='menu1'><a href="{{ path('accueil') }}">Accueil</a></li>
<li id='menu2'><a href="#">Animation</a></li>
<li id='menu3'><a href="#">Annonces</a></li>
{#<li id='menu4'><a href="{{ path('tarifs') }}">Tarifs</a></li>#}
<li id='menu5' class="dropdown">
<a class="dropdown-toggle" data-toggle="dropdown" href="#">Gestion des emplacements
<span class="caret"></span></a>
<ul class="dropdown-menu">
{#<li><a href="{{ path('emplacement') }}">Nouvel emplacement</a></li>#}
<li><a href="#">Nouvelle location</a></li>
</ul>
</li>
</ul>
</div>
</nav>
{# Ici se trouve le bloc que les vues pourront remplir #}
{% block PortPontrieux_body %}
{% endblock %}
{% endblock %}
A noter
: les 2 lignes en gras sont des "remarques"
twig. En effet, pour l'instant, les routes tarifs et emplacement
n'existent pas encore.
index.html.twig (du dossier templates/accueil)
{% extends "base.html.twig" %}
{% block PortPontrieux_body %}
<div id="myCarousel" class="carousel slide" data-ride="carousel">
<!-- Indicators -->
<ol class="carousel-indicators">
<li data-target="#myCarousel" data-slide-to="0" class="active"></li>
<li data-target="#myCarousel" data-slide-to="1"></li>
<li data-target="#myCarousel" data-slide-to="2"></li>
<li data-target="#myCarousel" data-slide-to="3"></li>
</ol>
<!-- Wrapper for slides -->
<div class="carousel-inner" role="listbox">
<div class="item active">
<img src="{{asset('images/port1.jpg')}}" alt="Image1 du port de Pontrieux">
<div class="carousel-caption">
<h2>Port de Pontrieux</h2><p>Situé dans le site exceptionnel de la vallée du Trieux.</p>
</div>
</div>
<div class="item">
<img src="{{asset('images/port2.jpg') }}" alt="Image2 du port de Pontrieux">
<div class="carousel-caption">
<h2>Port de Pontrieux</h2>
<p>Développer les activités fluviales et touristiques.</p>
</div>
</div>
<div class="item">
<img src="{{asset('images/port3.jpg') }}" alt="Image3 du port de Pontrieux">
<div class="carousel-caption">
<h2>Port de Pontrieux</h2>
<p>Assurer un accueil de qualité.</p>
</div>
</div>
<div class="item">
<img src="{{asset('images/port4.jpg') }}" alt="Image4 du port de Pontrieux">
<div class="carousel-caption">
<h2>Port de Pontrieux</h2><p>Garantir la préservation de l’environnement.</p>
</div>
</div>
</div>
<!-- Left and right controls -->
<a class="left carousel-control" href="#myCarousel" role="button" data-slide="prev">
<span class="glyphicon glyphicon-chevron-left" aria-hidden="true"></span>
<span class="sr-only">Previous</span></a>
<a class="right carousel-control" href="#myCarousel" role="button" data-slide="next">
<span class="glyphicon glyphicon-chevron-right" aria-hidden="true"></span>
<span class="sr-only">Next</span></a>
</div>
<script>
$(function() {
$('#menu1').attr('class', 'active');
});
</script>
{% endblock %}
Attention ! : Pour pouvoir bénéficier de bootstrap, jquery et des images il faut préalablement :
1. En ligne de commandes (sous le dossier Symf6_PortPontrieux) : composer require symfony/asset
2. Mettre les ressources (css, js, images…) dans Symf6_PortPontrieux/public.
Voici respectivement les fichiers TarifsController (…\Symf6_PortPontrieux\src\Controller) et le fichier pour la vue index.html.twig (...\Symf6_PortPontrieux\templates\tarifs). Un lien sera également modifié dans base.html.twig (le lien vers tarifs devient actif => retirer la remarque) :
TarifsController.php (les méthodes seront expliquées après avoir vu les entités et repository)
A générer avec, en lignes de commande :php bin/console make:controller TarifsController, puis à modifier pour obtenir :
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use App\Entity\Emplacement;
use Doctrine\Persistence\ManagerRegistry;
class TarifsController extends AbstractController
{
//La fonction index a été supprimée car inutile ici
#[Route('/tarifs', name: 'tarifs')]
public function liste(ManagerRegistry $doctrine)
{
$manager = $doctrine->getManager();
$rep = $manager->getRepository(Emplacement::class);
$lesEmplacements = $rep->findAll();
return $this->render('tarifs/index.html.twig', Array('lesEmplacements' => $lesEmplacements));
}
}
index.html.twig du dossier tarifs
{% extends "base.html.twig" %}
{% block PortPontrieux_body %}
<h1>Tableau des tarifs</h1>
<table class="table">
<thead>
<tr>
<th>Numero</th>
<th>Type</th>
<th>Profondeur</th>
<th>Prix</th>
<th>Disponible ?</th>
</tr>
</thead>
<tbody>
{% for emplacement in lesEmplacements %}
<tr {# si le nombre de passages dans la boucle est pair #} {% if loop.index is even %} class="success" {% else %} class="info" {% endif %}>
<td>{{emplacement.id}}</td>
<td>{{emplacement.type.situation}}</td>
<td>{{emplacement.type.profondeur}}</td>
<td>{{emplacement.type.prix}}</td>
{% if emplacement.disponible == 0 %}
<td>Oui</td>
{% else %}
<td>Non</td>
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
<script>
$(function() {
$('#menu4').attr('class', 'active');
});
</script>
{% endblock %}
Symfony6 est livré par défaut avec l'ORM Doctrine.
L'objectif d'un ORM (Object-Relation Mapper, soit en français « lien objet-relation ») est simple : se charger de l'enregistrement de nos données en nous faisant oublier que nous avons une base de données. Comment ? En s'occupant de tout !
Nous n'allons plus écrire de requêtes, ni créer de tables via phpMyAdmin. Dans notre code PHP, nous allons faire appel à Doctrine, l'ORM par défaut de Symfony6, pour faire tout cela.
Dans ORM, il y a la lettre O comme Objet. En effet, toutes nos données doivent être sous forme d'objets.
Pour notre exemple, nous allons créer les entités Type, Emplacement et Louer.
Pour cela nous allons ouvrir le
fichier .env
qui
se trouve à la racine du projet, vers la ligne 32 nous avons:
DATABASE_URL="postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=14&charset=utf8"
Ce que nous allons faire, c'est de créer un fichier
.env.local
à la racine de
notre projet (…/www/Symf6_PortPontrieux) et coller
uniquement cette ligne dans ce fichier. Il est préférable
d'utiliser ce nouveau fichier plutôt que de modifier
.env
car le fichier
.env
n'est pas
ignorer par git
,
ce qui veut donc dire que si vous publier votre code sur Github par
exemple, tout le monde aura accès à votre mot de passe, et cela
n'est évidemment pas envisageable pour des raisons évidentes de
sécurité. Par contre le fichier .env.local
est lui ignorer par
git
, il ne
quittera donc jamais votre ordinateur.
Nous allons configurer Doctrine pour utiliser le serveur de base de données de WAMP. C’est un serveur MySQL. L’adresse du serveur est : 127.0.0.1:3306. Le nom utilisateur est root au lieu de app, le mot de passe est vide.
Pour les utilisateurs de MAMP, l’adresse serveur est 127.0.0.1:8888 et le mot de passe est root.
Pour les utilisateurs de XAMPP, l’adresse est 127.0.0.1 ; le mot de passe est vide.
Le nom de la base de données, que nous allons créer juste après, sera symfony6_portpontrieux au lieu de db_name.
Ce qui donne (dans .env.local) :
DATABASE_URL="mysql://root:@127.0.0.1:3306/symfony6_portpontrieux?serverVersion=14&charset=utf8"
A partir de maintenant wamp ou xamp doit être démarré.
Nous allons ensuite créer la base de données avec :
…\www\Symf6_PortPontrieux>php bin/console doctrine:database:create
Après cette action on peut constater, via PhpMyAdmin, que la base symfony6_portpontrieux a été créée. Evidemment sans aucune table pour le moment mais nous allons y remédier.
Création des entités Type, Emplacement et Louer
La création d'une entité peut se faire directement en créant une classe mais c'est plus facile de la générer en mode console avec la commande :
php bin/console make:entity
Il faut ensuite saisir le nom de l'entity : Type
Attention : Si ceci ne fonctionne pas il faut saisir : composer install en ligne de commande
Symfony va non seulement créer l'entité mais aussi les attributs de celle-ci : situation, profondeur, prix.
Attention l'attribut id n'est pas à créer, symfony le fait automatiquement.
Si tout a bien fonctionné le fichier Type.php a été créé dans …/src/Entity et le fichier TypeRepository.php dans …/src/Repository. Le deuxième sert à récupérer les données et sera vu ultérieurement.
Dans le 1er on trouve Type.php :
<?php
namespace App\Entity;
use App\Repository\TypeRepository;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: TypeRepository::class)]
class Type
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 30)]
private ?string $situation = null;
#[ORM\Column]
private ?int $profondeur = null;
#[ORM\Column]
private ?float $prix = null;
public function getId(): ?int
{
return $this->id;
}
public function getSituation(): ?string
{
return $this->situation;
}
public function setSituation(string $situation): static
{
$this->situation = $situation;
return $this;
}
public function getProfondeur(): ?int
{
return $this->profondeur;
}
public function setProfondeur(int $profondeur): static
{
$this->profondeur = $profondeur;
return $this;
}
public function getPrix(): ?float
{
return $this->prix;
}
public function setPrix(float $prix): static
{
$this->prix = $prix;
return $this;
}
}
On peut constater que l'attribut id a été créé et que la configuration des attributs a bien été enregistrée sous forme d'annotation (en vert et commencant par #[ et terminant par ]). Les accesseurs ont aussi été générés.
Il faut ensuite procéder de la même façon pour créer les 2 autres entités : Emplacement et Louer.
2 solutions s'offrent à nous (on choisira la seconde, page 26) :
Solution 1 : Ne pas créer les relations lors de la création de l'entité
Dans un 1er temps on ne va donc pas créer les clés étrangères, elles le seront plus tard.
On va donc créer un seul attribut pour Emplacement et 5 pour Louer.
Entité Emplacement :
Entité Louer :
Privilégiez les noms sans _ (en raison des accesseurs générés). Contrairement à l'écran ci-dessus, créez donc plutôt les champs nomBateau, portAttache, dateArrivee, dateDepart.
Vous pouvez maintenant constater, via PhpMyAdmin que les tables n'ont toujours pas été créées. Pour se faire il faut maintenant saisir la commande :
php bin/console doctrine:schema:update --dump-sql
Symfony indique ici les commandes create qu'il va exécuter mais il ne le fait pas. Cela nous permet de contrôler si ces commandes sont correctes, si c'est le cas il faut maintenant saisir :
php bin/console doctrine:schema:update --force
Les tables sont maintenant créées. Une table messenger_messages peut également avoir été créée.
Les booléens sont devenus des tinyint de 1 caractère.
On pouvait également créer les tables de la base de données en utilisant les migrations. Il faut pour cela taper la commande : "php bin/consolemake:migration". Puis la commande :
"php bin/console doctrine:migrations:migrate"
Il nous reste maintenant à établir les relations entre les entités.
Etablir les relations entre les entités Type, Emplacement et Louer :
Il existe 3 types
de relation : One To One, Many To One (la plus courante) et Many To
Many
(voir cours Atelier4b_Doctrine_RelationEntreEntity).
Dans notre application, on ne va utiliser que Many To One. Entre Emplacement et Type et entre Louer et Emplacement.
Création de la relation entre Emplacement et Type
C'est une relation Many To One car un type peut être lié à plusieurs emplacements et à un emplacement n'est lié qu'un seul type.
Le propriétaire de cette relation est Emplacement car c'est cette entité qui contiendra le lien vers Type.
On pourrait donc ouvrir l'Entity Emplacement et effectuer les modifications suivantes (les modifications à opérer sont entourées ci-dessous) :
<?php
namespace App\Entity;
use App\Repository\EmplacementRepository;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: EmplacementRepository::class)]
class Emplacement
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
![]() |
#[ORM\ManyToOne(inversedBy: 'emplacements')]
#[ORM\JoinColumn(nullable: false)]
private ?Type $type = null;
#[ORM\Column]
private ?bool $disponible = null;
public function getId(): ?int
{
return $this->id;
}
public function isDisponible(): ?bool
{
return $this->disponible;
}
public function setDisponible(bool $disponible): static
{
$this->disponible = $disponible;
return $this;
}
}
Commentaires :
L'annotation JoinColumn avec son attribut nullable à false permet d'interdire la création d'un emplacement sans type. En effet, dans notre cas, un emplacement qui n'est rattaché à aucun type n'a pas de sens.
On vient d'ajouter un attribut, il faut donc créer le getter et le setter correspondants. On peut le faire manuellement mais le plus simple est de taper en ligne de commande :
php bin
/console make
:entity
--regenerate
Ouvrez Emplacement.php et vérifiez la création du getter et setter à la fin du script.
Mais la meilleure solution est de créer les relations (ici ManyToOne) lors de la création de l'Entity. Cela évite des erreurs lors de la rédaction des annotations (#[…]).
Solution 2 : Créer les relations dès la création de l'entité
Donc ici le mieux est de supprimer l'Entity Emplacement, si vous l'avez créé sans relation, et le Repository EmplacementRepository.
Et de recommencer la création de l'Entity Emplacement en précisant les relations cette fois :
C:\wamp64_3.3\www\symf6_PortPontrieux>php bin/console make:entity
Class name of the entity to create or update (e.g. OrangeGnome):
> Emplacement
created: src/Entity/Emplacement.php
created: src/Repository/EmplacementRepository.php
Entity generated! Now let's add some fields!
You can always add more fields later manually or by re-running this command.
New property name (press <return> to stop adding fields):
> type
Field type (enter ? to see all types) [string]:
> ManyToOne
What class should this entity be related to?:
> Type
Is the Emplacement.type property allowed to be null (nullable)? (yes/no) [yes]:
> no
Do you want to add a new property to Type so that you can access/update Emplacement objects from it - e.g. $type->getEmplacements()? (yes/no) [yes]:
>yes
A new property will also be added to the Type class so that you can access the related Emplacement objects from it.
New field name inside Type [emplacements]:
>emplacements
Do you want to activate orphanRemoval on your relationship?
A Emplacement is "orphaned" when it is removed from its related Type.
e.g. $type->removeEmplacement($emplacement)
NOTE: If a Emplacement may *change* from one Type to another, answer "no".
Do you want to automatically delete orphaned App\Entity\Emplacement objects (orphanRemoval)? (yes/no) [no]:
> yes
updated: src/Entity/Emplacement.php
updated: src/Entity/Type.php
Add another property? Enter the property name (or press <return> to stop adding fields):
> disponible
Field type (enter ? to see all types) [string]:
> boolean
Can this field be null in the database (nullable) (yes/no) [no]:
>
updated: src/Entity/Emplacement.php
Add another property? Enter the property name (or press <return> to stop adding fields):
Ici j'ai choisi d'avoir une relation inverse (voir les répercussions sur l'Entity Type) et de supprimer les emplacements devenus orphelins => si un type est supprimé alors les emplacements de ce type seront également supprimés.
Pour répercuter cette modification sur le modèle physique (la bdd donc) il faut saisir en ligne de commande :
php bin/console doctrine:schema:update --dump-sql
puis
php bin/console doctrine:schema:update –force
Ou
Taper la commande : "php bin/console make:migration". Puis la commande :
"php bin/console doctrine:migrations:migrate"
Constatez, via PhpMyAdmin, les modifications sur la table emplacement.
Création de la relation entre Louer et Emplacement
Pour ce faire, Il est préférable de supprimer l'Entity Louer (ainsi que le Repository associé), si vous l'avez créé sans relation, et de la recréer : php bin/console make:entity
Ne pas oublier la relation ManyToOne vers Emplacement
Pour répercuter cette modification sur le modèle physique (la bdd) il faut saisir en ligne de commande :
php bin/console doctrine:schema:update --dump-sql puis
php bin/console doctrine:schema:update –force
Ou faire les migrations avec les 2 commandes adéquates (voir ci-dessus, en rouge)
Constatez, via PhpMyAdmin, les modifications sur la table louer.
Attention : En cas de problème de migration (clé étrangère non générée dans la base par exemple), vous devez supprimer toutes les versions de migration dans le dossier "migrations" et la table correspondante "doctrine_migration_versions" sous phpmyadmin.
Et refaire la migration.
Vous pouvez maintenant faire un test en cliquant sur le menu tarifs. Ne pas oublier auparavant de modifier base.html.twig en rendant le lien vers tarifs actif (retirer les balises de remarque).
Récupérer les données des entités (Repository) :
L'une des principales fonctions de la couche Modèle dans une application MVC, c'est la récupération des données. Récupérer des données n'est pas toujours évident, surtout lorsqu'on veut récupérer seulement certaines données, les classer selon des critères, etc. Tout cela se fait grâce aux repositories, que nous allons étudier dans ce chapitre.
Un repository centralise tout ce qui touche à la récupération des entités. Concrètement donc, on ne doit pas faire la moindre requête SQL ailleurs que dans un repository, c'est la règle. On va donc y construire des méthodes pour récupérer une entité par son id, pour récupérer une liste d'entités suivant un critère spécifique, etc. Bref, à chaque fois que l'on veut récupérer des entités dans la base de données, on utilise le repository de l'entité correspondante.
Il existe un repository par entité. Cela permet de bien organiser son code. Bien sûr, cela n'empêche pas qu'un repository utilise plusieurs types d'entité, dans le cas d'une jointure par exemple.
Depuis un repository, il existe deux moyens de récupérer les entités : en utilisant du DQL et en utilisant le QueryBuilder. Dans ce tutoriel, nous n'utiliserons que le QueryBuilder.
Pour étudier la façon de récupérer les données et des exemples nous allons utiliser le document : Atelier4c_Doctrine_RecupererLesDonnees.
En étudiant les lignes placées en remarque dans les différents repository on peut comprendre en grande partie la façon d'interroger les données avec QueryBuilder.
EmplacementRepository.php : Copier à l'existant la fonction DQL et surtout la fonction QueryBuilder(mesEmplacementsDQL) (mesEmplacementsQueryBuilder).
<?php
namespace App\Repository;
use App\Entity\Emplacement;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
class EmplacementRepository extends ServiceEntityRepository{
public function mesEmplacementsDQL($em)
{
$req = $em->createQuery('SELECT e FROM PortPontrieuxBundle:Emplacement e');
$resultats = $req->getResult();
return $resultats;
}
public function mesEmplacementsQueryBuilder()
{
/*
// Méthode 1 : en passant par l'EntityManager
$queryBuilder = $this->_em->createQueryBuilder()
->select('e')
->from($this->_entityName, 'e');
*/
// Méthode 2 : en passant par le raccourci (préférable)
$queryBuilder = $this->createQueryBuilder('e');
// On récupère la Query à partir du QueryBuilder
$query = $queryBuilder->getQuery();
// On récupère les résultats à partir de la Query
$results = $query->getResult();
// On retourne ces résultats
return $results;
}
}
index.html.twig
{% extends "base.html.twig" %}
{% block PortPontrieux_body %} <h1>Tableau des tarifs</h1>
<table class="table">
<thead>
<tr>
<th>Numero</th>
<th>Type</th>
<th>Profondeur</th>
<th>Prix</th>
<th>Disponible ?</th>
</tr>
</thead>
<tbody>
{% for emplacement in lesEmplacements %}
<tr {# si le nombre de passages dans la boucle est pair #} {% if loop.index is even %} class="success" {% else %} class="info" {% endif %}>
<td>{{emplacement.id}}</td>
<td>{{emplacement.type.situation}}</td>
<td>{{emplacement.type.profondeur}}</td>
<td>{{emplacement.type.prix}}</td>
{% if emplacement.disponible == 0 %}
<td>Oui</td>
{% else %}
<td>Non</td>
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
<script>
$(function() {
$('#menu4').attr('class', 'active');
});
</script>
{% endblock %}
Remarque : Il faudrait ajouter quelques tuples dans la table type et emplacement pour constater le résultat.
Type : |
Emplacement : |
Objectif : Améliorer les connaissances sur la récupération des données via QueryBuilder et voir les routes (annotation dans le contrôleur) avec paramètre(s).
Résultat à obtenir :
5.1 Modifier le menu dans base.html.twig
Il faut d'abord ajouter 2 éléments dans le menu "gestion des emplacements"du fichier base.html.twig.
<ul class="dropdown-menu">
<li><a href="{{path('listeLocation', {'cote':'rue'}) }}">Visualiser locations coté rue</a></li>
<li><a href="{{path('listeLocation', {'cote':'rive'}) }}">Visualiser locations coté rive</a></li>
<li><a href="#">Nouvel emplacement</a></li>
</ul>
Le nom de la route sera listeLocation avec un paramètre qui sera, suivant le menu, "rue" ou "rive".
5.2 Créer le controleur LouerController
Il faut maintenant créer le contrôleur "LouerController.php" et y intégrer une fonction précédée des annotations précisant la route "listeLocation" et le paramètre attendu.
A générer avec, en lignes de commande : php bin/console make:controller LouerController, puis à modifier pour obtenir (les modifications sont en gras, ne pas oublier le use…) :
<?php
namespace App\Controller;
useSymfony\Bundle\FrameworkBundle\Controller\AbstractController;
useSymfony\Component\HttpFoundation\Response;
useSymfony\Component\Routing\Annotation\Route;
use Doctrine\Persistence\ManagerRegistry;
use App\Entity\Louer;
class LouerController extends AbstractController
{
#[Route('/ListeLocation/{cote}', name: 'listeLocation')]
public function listeLocation(ManagerRegistry $doctrine, $cote)
{
$manager = $doctrine->getManager();
$rep = $manager->getRepository(Louer::class);
$lesLocations = $rep->listeLocation($cote);
return $this->render('louer/index.html.twig', Array('lesLocations' => $lesLocations));
}
}
On voit ici que l'on fait appel à la fonction "listeLocation" du repository LouerRepository.
Celle-ci retourne un tableau de location, qui est ensuite injecté dans la vue louer/index.html.twig.
5.3 Modifier le repository LouerRepository
Il faut ajouter la fonction "listeLocation" dans "LouerRepository.php". Celle-ci attend un paramètre qui sera la situation de l'emplacement ("rue" ou "rive").
On désire afficher un tableau du style :
1ère solution : Faire des jointures dans la requête
On voit que les informations affichées proviennent de l'entité "Louer" (nomBateau, portAttache), "Emplacement" (id), "Type" (situation, profondeur). On sera dans l'obligation de faire 2 jointures (Louer vers Emplacement et Emplacement vers Type).
Ajout de la fonction "listeLocation" à la fin du fichier "LouerRepository" (en gras) :
<?php
.
.
.
public function listeLocation($cote)
{
//var_dump($cote);
$qb = $this->createQueryBuilder('l');
$qb->join('l.emplacement', 'e'); // Jointure avec Emplacement. Inutile de préciser avec quelle entité car c'est déja noté dans les annotatations de l'entité Louer
$qb->join('e.type', 't'); // Jointure avec Type. Inutile de préciser avec quelle entité car c'est déja noté dans les annotations de l'entité Emplacement
$qb->where('t.situation = :cote'); // Critère de sélection sur la situation (rue ou rive)
$qb->setParameter('cote', $cote);
$query = $qb->getQuery();
// On récupère les résultats à partir de la Query
$results = $query->getResult();
// On retourne ces résultats
return $results;
}
}
Modifier la vue index.html.twig du dossier templates/louer
{% extends 'base.html.twig' %}
{% block PortPontrieux_body %}
<h1>Tableau des emplacements coté </h1>
<table class="table">
<thead>
<tr>
<th>Numero</th>
<th>Type</th>
<th>Profondeur</th>
<th>Nom du bateau</th>
<th>Port d'attache</th>
</tr>
</thead>
<tbody>
{% for location in lesLocations %}
<tr {# si le nombre de passages dans la boucle est pair #} {% if loop.index is even %} class="success" {% else %} class="info" {% endif %}>
<td>{{location.emplacement.id}}</td>
<td>{{location.emplacement.type.situation}}</td>
<td>{{location.emplacement.type.profondeur}}</td>
<td>{{location.nomBateau}}</td>
<td>{{location.portAttache}}</td>
</tr>
{% endfor %}
</tbody>
</table>
<script>
$(function() {
$('#menu5').attr('class', 'active');
});
</script>
{% endblock %}
2ème solution : Sans jointure et en modifiant l'affichage (index.html.twig)
Comme dans l'Entity Louer on a un objet emplacement et dans Emplacement on a un objet type, on peut effectuer la restriction type == "rue" ou type == "rive" dans l'affichage twig et non dans la requête.
Ce qui entraine les modifications suivantes :
Dans LouerRepository (modifications en gras : mise des jointures en remarque)
// Méthode sans jointure
public function listeLocation($cote)
{
//var_dump($cote);
$qb = $this->createQueryBuilder('l');
//$qb->join('l.emplacement', 'e'); // Jointure avec Emplacement. Inutile de préciser avec quelle entité car c'est déja noté dans les annotatations de l'entité Louer
//$qb->join('e.type', 't'); // Jointure avec Type. Inutile de préciser avec quelle entité car c'est déja noté dans les annotatations de l'entité Emplacement
//$qb->where('t.situation = :cote'); // Critère de sélection sur la situation (rue ou rive)
//$qb->setParameter('cote', $cote);
$query = $qb->getQuery();
// On récupère les résultats à partir de la Query
$results = $query->getResult();
// On retourne ces résultats
return $results;
}
Et dans index.html.twig (condition ajoutée et affichage de la situation en 3ème ligne)
{% extends 'base.html.twig' %}
{% block PortPontrieux_body %}
<h1>Tableau des emplacements coté {{ cote }}</h1>
<table class="table">
<thead>
<tr>
<th>Numero</th>
<th>Type</th>
<th>Profondeur</th>
<th>Nom du bateau</th>
<th>Port d'attache</th>
</tr>
</thead>
<tbody>
{% for location in lesLocations %}
<tr {# si le nombre de passages dans la boucle est pair #} {% if loop.index is even %} class="success" {% else %} class="info" {% endif %}>
{% if (cote == location.emplacement.type.situation) %}
<td>{{location.emplacement.id}}</td>
<td>{{location.emplacement.type.situation}}</td>
<td>{{location.emplacement.type.profondeur}}</td>
<td>{{location.nomBateau}}</td>
<td>{{location.portAttache}}</td>
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
<script>
$(function() {
$('#menu5').attr('class', 'active');
});
</script>
{% endblock %}
On peut voir que la donnée cote a été ajoutée et utilisée en 3ème ligne et dans le if.
Il faut donc ajouter cette injection dans LouerController (modification en gras et grossie) :
<?php
namespace App\Controller;
useSymfony\Bundle\FrameworkBundle\Controller\AbstractController;
useSymfony\Component\HttpFoundation\Response;
useSymfony\Component\Routing\Annotation\Route;
useDoctrine\Persistence\ManagerRegistry;
use App\Entity\Louer;
class LouerController extends AbstractController
{
#[Route('/ListeLocation/{cote}', name: 'listeLocation')]
public function listeLocation(ManagerRegistry $doctrine, $cote)
{
$manager = $doctrine->getManager();
$rep = $manager->getRepository(Louer::class);
$lesLocations = $rep->listeLocation($cote);
return $this->render('louer/index.html.twig', Array('lesLocations' => $lesLocations, 'cote' => $cote));
}
}
Quelle que soit la solution adoptée, le résultat doit être équivalent :
Pour obtenir ce résultat il faut évidemment insérer quelques locations.
Avec Symfony, il existe une solution simple sous la forme d'un bundle publié par KNP Labs et appelé KNP Paginator Bundle.
Sa mise en œuvre est simple et permet de mettre en place une pagination gérée depuis le contrôleur et affichée dans la vue.
Nous allons exploiter ce service sur la page contenant la liste des tarifs. Elle a été créée dans les pages précédentes de ce tutoriel.
Nous allons créer des pages de seulement 3 emplacements pour pouvoir visualiser rapidement un résultat.
Tout d'abord il faut instaler KNP Paginator.
Afin d'intégrer KNP Paginator à notre projet, nous allons faire appel à composer au moyen de la commande suivante (à partir du dossier de base, exemple. : c:\wamp64\www\symf4_portpontrieux)
composer require knplabs/knp-paginator-bundle
Il y a parfois un problème de droits sur le dossier …var\cache. Dans ce cas supprimer manuellement ce dossier et relancer la commande précédente.
Exemple sur l'affichage des tarifs appliqués au port de Pontrieux (à partir de l'entité Emplacement)
TarifsController.php (modifications en gras)
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use App\Entity\Emplacement;
use Doctrine\Persistence\ManagerRegistry;
// Nécessaire pour la pagination
use Symfony\Component\HttpFoundation\Request; // Nous avons besoin d'accéder à la requête pour obtenir le numéro de page
use Knp\Component\Pager\PaginatorInterface; // Nous appelons le bundle KNP Paginator
class TarifsController extends AbstractController
{
//La fonction index a été supprimée car inutile ici
#[Route('/tarifs', name: 'tarifs')]
public function liste(ManagerRegistry $doctrine, Request $request, PaginatorInterface $paginator) // 2 parametres ajoutés
{
$manager = $doctrine->getManager();
$rep = $manager->getRepository(Emplacement::class);
$lesEmplacements = $rep->listeEmplacements();
$lesEmplacementsPagines = $paginator->paginate(
$lesEmplacements, // Requête contenant les données à paginer (ici nos emplacements)
$request->query->getInt('page', 1), // No de la page en cours (dans l'URL), 1 si aucune page
2 // Nombre de résultats par page
);
return $this->render('tarifs/index.html.twig', Array('lesEmplacements' => $lesEmplacementsPagines));
}
}
index.html.twig (modifications en gras)
{% extends "base.html.twig" %}
{% block PortPontrieux_body %}
<h1>Tableau des tarifs</h1>
<table class="table">
<thead>
<tr>
<th>Numero</th>
<th>Type</th>
<th>Profondeur</th>
<th>Prix</th>
<th>Disponible ?</th>
</tr>
</thead>
<tbody>
{% for emplacement in lesEmplacements %}
<tr {# si le nombre de passages dans la boucle est pair #} {% if loop.index is even %} class="success" {% else %} class="info" {% endif %}>
<td>{{emplacement.id}}</td>
<td>{{emplacement.type.situation}}</td>
<td>{{emplacement.type.profondeur}}</td>
<td>{{emplacement.type.prix}}</td>
{% if emplacement.disponible == 0 %}
<td>Oui</td>
{% else %}
<td>Non</td>
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
{{ knp_pagination_render(lesEmplacements) }}
<script>
$(function() {
$('#menu4').attr('class', 'active');
});
</script>
{% endblock %}
Un formulaire
se construit sur un objet existant, et son objectif est
d'hydrater cet objet. Il faut donc des objets avant de créer
des formulaires. Notre formulaire pour ajouter un emplacement va se
baser sur l'objet Emplacement
, objet que nous avons
construit lors d'une des parties précédentes.
Préalablement à la création du formulaire il va falloir modifier le fichier "base.html.twig" pour ajouter (ou activer) le sous menu "Nouvel emplacement" (l'ordre des sous-menus a également été modifié):
…
<body>
{% block body %}
{# Ici on va définir un titre et un menu commun à toutes les pages de l'appli. PortPontrieux #}
<nav class="navbar navbar-inverse">
<div class="container-fluid">
<div class="navbar-header">
<a class="navbar-brand" href="#">Port de Pontrieux</a>
</div>
<ul class="nav navbar-nav">
<li id='menu1'><a href="{{ path(accueil) }}">Accueil</a></li>
<li id='menu2'><a href="#">Animation</a></li>
<li id='menu3'><a href="#">Annonces</a></li>
<li id='menu4'><a href="{{ path(tarifs') }}">Tarifs</a></li>
<li id='menu5' class="dropdown">
<a class="dropdown-toggle" data-toggle="dropdown" href="#">Gestion des emplacements
<span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="{{path('emplacement') }}">Nouvel emplacement</a></li>
<li><a href="{{path('listeLocation', {'cote':'rue'}) }}">Visualiser location(s) coté rue</a></li>
<li><a href="{{path('listeLocation', {'cote':'rive'}) }}">Visualiser location(s) coté rive</a></li>
<li><a href="#">Nouvelle location</a></li>
</ul>
</li>
</ul>
</div>
</nav>
…
La route "emplacement" sera définie, comme habituellement, dans un contrôleur.
Ce contrôleur appelé "emplacementController.php" sera créé ultérieurement (page 42).
Effectuer la création de la définition du formulaire. Ici, création de EmplacementType (dans dossier Form) qui sera relié à l'entité Emplacement :
Ouvrez le terminal dans le dossier de votre projet et entrez la commande
php bin/console make:form
Il vous sera demandé d'entrer :
· Le nom de la classe du formulaire : entrer "EmplacementType"
Une fois validé, vous aurez un nouveau fichier dans "src/Form" qui se nomme "EmplacementType.php" et qui devrait ressembler à ceci :
<?php
namespace App\Form;
use App\Entity\Emplacement;
useSymfony\Component\Form\AbstractType;
useSymfony\Component\Form\FormBuilderInterface;
useSymfony\Component\OptionsResolver\OptionsResolver;
class EmplacementType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('disponible')
->add('type')
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Emplacement::class,
]);
}
}
Modifier ce fichier (EmplacementType.php qui se trouve dans le dossier src/form) pour obtenir ceci :
<?php
namespace App\Form;
use App\Entity\Emplacement;
useSymfony\Component\Form\AbstractType;
useSymfony\Component\Form\FormBuilderInterface;
useSymfony\Component\Form\Extension\Core\Type\CheckboxType;
useSymfony\Component\Form\Extension\Core\Type\DateTimeType;
useSymfony\Component\Form\Extension\Core\Type\SubmitType;
useSymfony\Component\Form\Extension\Core\Type\TextareaType;
useSymfony\Component\Form\Extension\Core\Type\TextType;
useSymfony\Component\OptionsResolver\OptionsResolver;
useSymfony\Bridge\Doctrine\Form\Type\EntityType;
class EmplacementType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('disponible', CheckboxType::class, array('required' => false));
$builder
->add('type', EntityType::class, array('class' => 'App\Entity\Type', 'choice_label' => 'situation', 'multiple' => false)); // Pour créer la selection du type
$builder->add('save', SubmitType::class);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Emplacement::class,
]);
}
}
Créer le contrôleur : "EmplacementController.php" en saisissant :
php
bin/console make:controller
EmplacementController
Effectuer les modifications pour obtenir :
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
// Ne pas oublier d'ajouter :
use App\Entity\Emplacement;
use App\Form\EmplacementType;
use Doctrine\Persistence\ManagerRegistry;
class EmplacementController extends AbstractController
{
#[Route('/emplacement', name: 'emplacement')]
public function inserer(Request $request, ManagerRegistry $doctrine)
{
// Création du formulaire
$emplacement = new Emplacement();
$form = $this->createForm(EmplacementType::class, $emplacement);
// Insertion dans la BDD si method POST ou affichage du formulaire dans le cas contraire
if ($request->getMethod() == 'POST')
{
$form->handleRequest($request); // remplissage du formulaire à partir de la requête Http
if ($form->isValid())
{
$em = $doctrine->getManager();
$em->persist($emplacement);
$em->flush();
$request->getSession()->getFlashBag()->add('info', 'Emplacement bien enregistré.');
return $this->redirectToRoute('accueil');
}
}
return $this->render('emplacement/index.html.twig', array('form' => $form->createView()));
}
}
Modifier la vue "index.html.twig" du dossier …/templates/emplacement/index.html.twig :
{% extends "base.html.twig" %}
{% block PortPontrieux_body %}
<h3>Formulaire de saisie des emplacements</h3>
<!-- On aurait pu remplacer toutes les lignes suivantes par <div class="well"> { form(form) } </div> -->
<!-- mais l'affichage aurait été plus sommaire -->
<div class="well">
{{ form_start(form, {'attr': {'class': 'form-horizontal'}}) }}
{# Les erreurs générales du formulaire. #}
{{ form_errors(form) }}
{# Génération du label + error + widget pour un champ. #}
{{ form_row(form.disponible) }}
{# Génération du label + error + widget pour un champ. #}
{{ form_row(form.type) }}
{# Pour le bouton, pas de label ni d'erreur, on affiche juste le widget #}
{{ form_widget(form.save, {'attr': {'class': 'btn btn-primary'}}) }}
{# Génération automatique des champs pas encore écrits.
Dans cet exemple, ce serait le champ CSRF (géré automatiquement par Symfony !)
et tous les champs cachés (type « hidden »). #}
{{ form_rest(form) }}
{# Fermeture de la balise <form> du formulaire HTML #}
{{ form_end(form) }}
</div>
<script>
$(function() {
$('#menu5').attr('class', 'active');
});
</script>
{% endblock %}
Il existe deux parties distinctes en matière de sécurité :
· l’authentification : comment un utilisateur peut s’authentifier pour accéder à une partie sécurisée de l’application
· l’autorisation : comment définir les droits pour un utilisateur (user, admin…) d’accéder à certaines ressources : routes, contrôleurs, méthodes, vues, entités…
8.1 L'authentification
Toute la sécurité de Symfony est concentrée dans un seul fichier : config/packages/security.yaml
Contenu de base de ce fichier (le vôtre peut être légèrement différent) :
security:
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
password_hashers:
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
providers:
users_in_memory: { memory: null }
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
lazy: true
provider: users_in_memory
# activate different ways to authenticate
# https://symfony.com/doc/current/security.html#the-firewall
# https://symfony.com/doc/current/security/impersonating_user.html
# switch_user: true
# Easy way to control access for large sections of your site
# Note: Only the *first* access control that matches will be used
access_control:
# - { path: ^/admin, roles: ROLE_ADMIN }
# - { path: ^/profile, roles: ROLE_USER }
when@test:
security:
password_hashers:
#By default, password hashers are resource intensive and take time. This is
#important to generate secure password hashes.In tests however, secure hashes
# are not important, waste resources and increase test times. The following
# reduces the work factor to the lowest possible values.
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface:
algorithm: auto
cost: 4 # Lowest possible value for bcrypt
time_cost: 3 # Lowest possible value for argon
memory_cost: 10 # Lowest possible value for argon
Explications :
Ce fichier contient déjà une sécurité mise en place où tout le monde peut accéder à l’application : lazy signifie que si on n’accède pas à une ressource protégée, la sécurité n’est pas activée. Elle n’est activée qu’au moment où on en a besoin (d’où le terme de lazy).
Si on revient sur le fichier security.yaml, on constate qu’il y a trois étiquettes.
La première, providers, concerne les fournisseurs d’utilisateurs. Cette rubrique va renseigner l’application sur comment et où trouver les identifiants des utilisateurs identifiés. Ici, par défaut, on utilise la méthode memory. Cette méthode est la plus simple puisqu’il s’agit de renseigner les utilisateurs directement dans le fichier de sécurité (déconseillé en mode prod).
La deuxième, firewalls, précise les pare-feu qui vont intervenir pour l’authentification d’un utilisateur. On peut cumuler plusieurs pare-feu. Dès qu’un pare-feu invalide un utilisateur, celui-ci ne peut pas accéder à la ressource demandée.
La troisième, access_control, précise les autorisations exigées pour accéder aux routes. Celles-ci seront détaillées dans la partie Autorisation.
Ici, nous avons par défaut deux pare-feu mis en place.
Le premier, dev, concerne les routes qui commencent par /_profiler, /_wdt, /css,/images,/js. Les routes qui cherchent à atteindre soit le web profiler, soit la webtool bar, soit un élément public de notre application (les images, le css ou le JavaScript) auront une sécurité désactivée (security : false), tout le monde peut y accéder.
Le deuxième concerne
toutes les autres routes. Le mode lazy empêche la création d’un
identifiant de session s’il n’y a pas besoin d’autorisation
(c’est-à-dire de la vérification explicite d’un privilège
utilisateur). Dans le cas contraire d’une ressource exigeant un
accès sécurisé, ce sont les utilisateurs définis dans
users_in_memory qui auront les autorisations d’y
accéder.
Nous verrons dans la partie Autorisation comment définir des rôles
d’utilisateurs pour accéder à une ressource.
Nous allons maintenant mettre en place une vraie politique de sécurité et d’authentification d’utilisateurs.
Travail à faire :
La première chose à faire est d’installer le package sécurité.
Sur le terminal, tapez la commande :
composer
require security
Tous les packages utiles à la sécurité devraient être installés.
Pour authentifier les utilisateurs, il nous faut une entité User. Surtout ne créez pas cette entité avec la commande habituelle make:entity, car user est une entité particulière, utilisez la commande :
php bin/
console make:user
Il faut ensuite répondre aux questions posées. Voici les réponses pour notre exemple :
Nous avons choisi de nous identifier avec le username (mais on aurait pu également choisir le mail). Il est également important de hasher le mot de passe (cryptage du mot de passe).
Les actions suivantes ont été effectuées :
· une entité a été créée : src/Entity/User.php
· un repository a été créé : src/Repository/UserRepository.php
· le fichier config/packages/security.yaml a été modifié
Voici maintenant le contenu du fichier "security.yaml" :
Une nouvelle étiquette passwords_hashers a été ajoutée. Elle précise le mode d’encodage utilisé pour le cryptage des mots de passe. Nous verrons plus tard comment l’utiliser.
Un nouveau provider a été ajouté : app_user_provider est un provider de type entity, c.a.d. que les utilisateurs authentifiés seront récupérés via une entité. Ils seront identifiés avec leur username.
Il est nécessaire maintenant de créer les routes, les contrôleurs et les vues qui vont permettre à des utilisateurs de s’identifier. En mode terminal, tapez la commande :
php bin/
console make:auth
Un système de questions/réponses vous demande des précisions sur la manière dont vous souhaitez que les utilisateurs s’identifient.
Il faut ensuite répondre aux questions posées. Voici les réponses pour notre exemple :
Nous choisissons une identification avec un formulaire de login.
Nous avons donné le nom CustomAuthenticator à notre authenticator (il faut saisir un nom, il n’y a pas de valeur par défaut).
Nous avons choisi le nom par défaut du contrôleur : SecurityController.
Nous avons demandé une route pour la déconnexion de l’utilisateur : /logout.
Les actions suivantes ont été exécutées :
· src/Security/CustomAuthenticator.php a été créé.
· config/packages/security.yaml a été mis à jour.
· src/Controller/SecurityController.php a été créé.
· templates/security/login.html.twig a été créé.
Il ne faut pas oublier de modifier la ligne 5 du fichier login.html.twig :
{% block PortPontrieux_body %}
Le fichier src/Security/CustomAuthenticator.php, qui a été créé pour authentifier les utilisateurs, va permettre également de définir la route à emprunter si l'authentification est correcte.
Il faut modifier dans ce fichier la méthode onAuthenticationSuccess().
En effet, en cas de succès d’authentification et si aucune route n’a été préalablement définie (c’est-à-dire l’utilisateur s’est directement connecté à l’URL/login), nous n’allons pas générer une erreur de type Exception, comme cela se fait pour l'instant, mais nous allons rediriger l’utilisateur vers la route prévue. Pour le moment nous allons rediriger vers la route "accueil".
Modification de la méthode onAuthenticationSuccess() en gras :
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
if ($targetPath = $this->getTargetPath($request->getSession(), $firewallName)) {
return new RedirectResponse($targetPath);
}
// For example:
// return new RedirectResponse($this->urlGenerator->generate('some_route'));
return new RedirectResponse($this->urlGenerator->generate('accueil'));
}
Pour tester le résultat exécutez la requête : locahost:8000/login ou
Modifier le fichier base.html.twig, en ajoutant la ligne en gras, pour compléter le menu :
…
<li id='menu5' class="dropdown">
<a class="dropdown-toggle" data-toggle="dropdown" href="#">Gestion des emplacements
<span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="{{path('emplacement') }}">Nouvel emplacement</a></li>
<li><a href="{{path('listeLocation', {'cote':'rue'}) }}">Visualiser location(s) coté rue</a></li>
<li><a href="{{path('listeLocation', {'cote':'rive'}) }}">Visualiser location(s) coté rive</a></li>
<li><a href="#">Nouvelle location</a></li>
</ul>
</li>
<li id='menu6'><a href="{{ path('app_login') }}">Connection</a></li>
…
On devrait obtenir cette page générée automatiquement :
Cette vue est templates/security/index.html.twig. On pourra la modifier pour obtenir un meilleur design.
Il ne reste plus qu’à créer des utilisateurs en base de données. L’entité User a été créée. Il suffit de générer la migration correspondante :
php bin/console doctrine:schema:update --dump-sql
puis
php bin/console doctrine:schema:update --force
On peut vérifier, via phpmyadmin que la table user a bien été créée.
Cette table user est évidemment vide pour l'instant.
Nous allons donc créer un nouveau contrôleur "RegisterController" qui nous permettra d’enregistrer des utilisateurs dans cette table user.
php bin/console make:controller RegisterController
Et le modifier pour obtenir le contenu suivant :
<?php
namespace App\Controller;
use App\Entity\User;
useSymfony\Bundle\FrameworkBundle\Controller\AbstractController;
useSymfony\Component\HttpFoundation\Request;
useSymfony\Component\Form\Extension\Core\Type\PasswordType;
useSymfony\Component\Form\Extension\Core\Type\RepeatedType;
useSymfony\Component\Form\Extension\Core\Type\ChoiceType;
useSymfony\Component\Form\Extension\Core\Type\SubmitType;
useSymfony\Component\Routing\Annotation\Route;
//use Symfony\Component\Security\Core\Encoder\UserPasswordHasherInterface;
useSymfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
useDoctrine\Persistence\ManagerRegistry;
class RegisterController extends AbstractController
{
#[Route('/register', name: 'app_register')]
public function register(Request $request, UserPasswordHasherInterface $passEncoder, ManagerRegistry $doctrine)
{
$form=$this->createFormBuilder()
->add('username')
->add('password', RepeatedType::class, [
'type'=>PasswordType::class,
'required'=>true,
'first_options'=>['label'=>'Mot de passe'],
'second_options'=>['label'=>'Confirmation Mot de passe'],
])
->add('roles', ChoiceType::class, [
'choices' => [
'ROLE_USER' => 'ROLE_USER',
'ROLE_ADMIN' => 'ROLE_ADMIN',
'ROLE_SUPER_ADMIN' => 'ROLE_SUPER_ADMIN',
],
'multiple'=>true
])
->add('enregistrer', SubmitType::class, ['attr'=>['class'=>'btn btn-success', ]])
->getForm();
$form->handleRequest($request);
if($request->isMethod('post') && $form->isValid())
{
$data=$form->getData();
$user=new User;
$user->setUsername($data['username']);
$user->setPassword($passEncoder->hashPassword($user,$data['password']));
$user->setRoles($data['roles']);
$em = $doctrine->getManager();
$em->persist($user);
$em->flush();
return $this->redirect($this->generateUrl('app_login'));
}
return $this->render('register/index.html.twig', ['form'=>$form->createView()]);
}
}
Le type ChoiceType permet d’avoir un menu à choix multiple pour choisir les rôles attribués à l’utilisateur. Ce sont ces rôles qui permettront de définir les ressources auxquelles il aura accès.
UserPasswordEncoderInterface permet de crypter le mot de passe avec l’algorithme de cryptage défini dans le fichier security.yaml.
Nous pouvons visualiser les routes générées par la commande :
php bin/
console debug:router
Parmi les routes se trouvent celles définies pour l’authentification :
Register ANY ANY ANY /register
app_login ANY ANY ANY /login
app_logout ANY ANY ANY /logout
Il faut modifier le fichier "…/templates/register/index.html.twig" de la façon suivante :
{% extends 'base.html.twig' %}
{% block title %}Hello RegisterController!{% endblock %}
{% block PortPontrieux_body %}
<style>
.example-wrapper { margin: 1em auto; max-width: 800px; width: 95%; font: 18px/1.5 sans-serif; }
.example-wrapper code { background: #F5F5F5; padding: 2px 6px; }
</style>
<h3>Formulaire de saisie des utilisateurs</h3>
<!-- On aurait pu remplacer toutes les lignes suivantes par <div class="well"> { form(form) } </div> -->
<!-- mais l'affichage aurait été plus sommaire -->
<div class="well">
{{ form_start(form, {'attr': {'class': 'form-horizontal'}}) }}
{# Les erreurs générales du formulaire. #}
{{ form_errors(form) }}
{# Génération du label + error + widget pour un champ. #}
{{ form_row(form.username) }}
{# Génération du label + error + widget pour un champ. #}
{{ form_row(form.password) }}
{# Génération du label + error + widget pour un champ. #}
{{ form_row(form.roles) }}
{# Pour le bouton, pas de label ni d'erreur, on affiche juste le widget #}
{{ form_widget(form.enregistrer, {'attr': {'class': 'btn btn-primary'}}) }}
{# Génération automatique des champs pas encore écrits.
Dans cet exemple, ce serait le champ CSRF (géré automatiquement par Symfony !)
et tous les champs cachés (type « hidden »). #}
{{ form_rest(form) }}
{# Fermeture de la balise <form> du formulaire HTML #}
{{ form_end(form) }}
</div>
{% endblock %}
Pour tester la création d'un utilisateur puis l’authentification, il faut saisir : localhost:8000/register.
Après avoir saisi les informations et cliquer sur le bouton <valider>, on est dirigé vers la vue d'authentification (username + password).
Si on saisit les bons identifiants de l’utilisateur enregistré, nous sommes redirigés vers la route accueil, comme nous l’avions précisé dans la classe CustomAuthenticator (page 49).
Il nous reste plus qu'à définir les autorisations. Quel utilisateur a accès à quelles ressources ?
8.1 L'autorisation
Actuellement, le firewall défini (main) laisse passer tout le monde :
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
lazy: true
provider: app_user_provider
custom_authenticator: App\Security\CustomAuthenticator
logout:
path: app_logout
# where to redirect after logout
# target: app_any_route
Lazy : true signifie simplement qu’il n’y a pas besoin de démarrer une session d’autorisation dans le cas où il n’y a pas une demande explicite d’autorisation. La session qui permettra d’avoir un identifiant de session pour la sécurité sera créée dynamiquement lors de l’accès aux ressources qui sont protégées.
Autrement dit, tout le monde a accès à toutes les ressources de notre application pour l’instant. En effet, nous souhaitons donner accès aux pages Front de notre application sans activer la sécurité. Ce sont uniquement les pages d’administration (à créer ultérieurement) qui devront être protégées.
Nous allons définir des autorisations d’accès à certaines ressources.
Il y a 4 types d’autorisations :
· access_control : permet de définir des autorisations au niveau des routes.
· accès contrôleur : permet de définir des autorisations au niveau d’un contrôleur.
· accès action : permet de définir des autorisations au niveau des méthodes d’un contrôleur.
· accès vue : permet de définir des autorisations au niveau des vues.
Nous allons commencer par les autorisations access_control.
Pour faire les tests commencez par créer (via ../register) un utilisateur role_user et un autre role_user et role_administrateur.
8.1.1. access_control
Pour l’instant, tous nos utilisateurs ont accès à la route register définie dans le contrôleur RegisterController.
Pour utiliser l’identification mise en place sur cette route, nous allons nous servir dans un 1er temps des access_control.
Ceux-ci sont définis dans le fichier config/packages/security.yaml. Vous trouvez ces lignes :
access_control:
# - { path: ^/admin, roles: ROLE_ADMIN }
# - { path: ^/profile, roles: ROLE_USER }
Il suffit de les décommenter et de définir les routes et les rôles nécessaires pour y accéder.
On peut écrire, par exemple :
access_control:
- { path: ^/register: ROLE_ADMIN }
Il pourrait y avoir plusieurs fonctions et donc plusieurs routes dans RegisterController et cela nécessiterait autant de lignes dans access_control.
Il y a pourtant un moyen plus simple pour définir un access_control sur un ensemble de routes.
Par exemple, pour définir un access_control sur toutes les routes qui pointeraient vers le contrôleur AdminController, nous préfixerions toutes les routes du contrôleur par admin.
Nous allons donc, dans src/Controller/RegisterController, ajoutez cette annotation en haut de la classe :
#[Route('/admin')]
class RegisterController extends AbstractController
{
…
}
Cela n'aurait rien changer à l’accès à ces routes dans les templates …/index.html.twig (pas encore fait), car ils utilisent le nom de la route et pas le chemin ({{ path('register') }}.
Par contre, si vous saisissez l’adresse "localhost:8000/register" dans la barre de votre navigateur, vous aurez ce résultat :
Il est donc clair maintenant que le préfixe admin sera ajouté à l’URL.
Sachant cela on va modifier nos access_control dans le fichier config/packages/security.yaml, on peut définir un seul access_control pour toutes les routes de RegisterController :
access_control:
- { path: ^/admin, roles: ROLE_ADMIN }
N'oubliez pas de
garder le - devant { path… }
Le symbole ^ signifie "commence
par", c’est-à-dire que toutes les routes qui commencent par /admin
sont contrôlées par la sécurité. Par contre, on ne met pas le $ car
on veut que toutes les routes commencent par /admin, quels que
soient les caractères à la suite. Les autres routes sont
accessibles à tout le monde.
Donc cette fois, si vous essayez de vous diriger vers une route admin/… (pour l'instant, simplement admin/register) avec un utilisateur qui a pour rôle uniquement ROLE_USER, vous tomberez sur cette page d’erreur :
Cela dit, ce n’est pas très agréable d’avoir ce qui s’apparente à une erreur en mode production même pour un problème de droit. Il serait plus judicieux de rediriger l’utilisateur vers la page de login en cas de non-autorisation.
La classe qui génère l’exception s’appelle AccessDeniedHandler. Il est possible de développer sa propre classe AccessDeniedHandler pour la rendre moins "agressive".
Pour cela créons la classe AccessDeniedHandler.php dans le dossier src/Security :
<?php
namespace App\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Http\Authorization\AccessDeniedHandlerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
class AccessDeniedHandler extends AbstractController implements AccessDeniedHandlerInterface
{
public function handle(Request $request, AccessDeniedException $accessDeniedException)
{
return $this->redirect($this->generateUrl('app_login'));
}
}
Nous héritons de la classe AbstractController pour pouvoir bénéficier de la redirection.
En cas d’erreur, nous redirigeons l’utilisateur vers la route dont le nom est app_login.
C'est ce nom de route que nous avions défini dans le fichier : src/Controller/SecurityController.
Il ne reste plus qu’à déclarer cette classe comme étant le service pour gérer les exceptions d’autorisations.
Cela se passe dans le fichier config/packages/security.yaml. Ajoutez dans le firewall main l’étiquette access_denied_handler de la façon suivante (en gras et en grand):
main:
lazy: true
provider: app_user_provider
custom_authenticator: App\Security\CustomAuthenticator
logout:
path: app_logout
access_denied_handler: App\Security\AccessDeniedHandler
Cette fois, si vous essayez d’atteindre la route admin/register, vous serez redirigé vers le formulaire de login. Ce serait quand même bien d'ajouter un message Flash Bag pour signaler la nécessité d’avoir le rôle admin pour s’identifier.
Voici la classe AccessDeniedHandler modifiée (3 lignes ajoutées en gras et en grand) :
<?php
namespace App\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Http\Authorization\AccessDeniedHandlerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
class AccessDeniedHandler extends AbstractController implements AccessDeniedHandlerInterface
{
public function handle(Request $request, AccessDeniedException $accessDeniedException)
{
$this->addFlash('message', 'Vous n\'avez pas les droits suffisants pour accéder à cette page');
$this->addFlash('statut', 'danger');
return $this->redirect($this->generateUrl('app_login'));
}
}
Si vous essayez d’atteindre la route admin/register avec un utilisateur qui a le rôle ROLE_USER, vous tomberez sur la page de login avec le message d’erreur affiché :
C’est exactement ce qui était souhaité.
Pour le logout, vous pouvez rediriger l'application après une déconnection en modifiant le fichier "security.yaml" comme suit :
…
logout:
path: app_logout
# where to redirect after logout
target: accueil
…
8.1.3. Accès action
Afin de protéger une seule méthode d’un contrôleur (une action), vous pouvez ajouter :
$this->denyAccessUnlessGranted('ROLE_ADMIN');
Au tout début de la méthode "à protéger".
Exemple sur la méthode register() :
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
class RegisterController extends AbstractController
{
#[Route('/register', name: 'register')]
public function register(Request $request, UserPasswordEncoderInterface $passEncoder)
{
// On autorise l'acces uniquement à l'administrateur
$this->denyAccessUnlessGranted('ROLE_ADMIN');
. . .
}
Supprimez les annotations de sécurité sur le contrôleur et essayez de vous rendre sur la route admin/register avec un utilisateur qui a pour rôle ROLE_USER, vous serez de nouveau redirigé vers la route login.
8.1.4. Accès vue
Il est également possible d’ajouter des contrôles d’autorisation
dans les vues. Cela va être utile pour ajouter un bouton d'ajout de
nouvel utilisateur dans le formulaire
templates/security/login.html.twig.
Ce bouton ne devrait apparaitre que si l'utilisateur possède le
ROLE_ADMIN.
Il suffit d’ajouter la syntaxe Twig suivante :
{%
if is_granted(
"ROLE_ADMIN") %}
...
{ %
endif %}
Dans notre template "…/security/login.html.twig", encapsulez le bouton de la façon suivante :
</form>
{% if is_granted("ROLE_ADMIN") %}
<a class="btn btn-info mb-2"href="{{ path('app_register') }}">
Insertion d'un nouvel utilisateur
</a>
{% endif %}
Vous pouvez tester en étant connecté en tant que ROLE_USER puis en tant que ROLE_ADMIN.
La base de données finale MySql "symfony6_portpontrieux", comportera les tables suivantes :
La table "annonces" sera ajoutée plus tard et servira pour d'autres fonctionnalités de l'application.