Découverte de Symfony à
travers la conception du site "gestion du port de
Pontrieux"

I – Framework PHP
Définition d’un Framework (Source Wiki) :
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.
2°) Les avantages d’un
Framework
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
II – Présentation de Symfony
?
1°) Pourquoi Symfony ?
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.
|
2°) Sur le site de l’éditeur
:
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.
Qui est derrière tout ça ?
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
Qui utilise 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.
De l'innovation
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.
Avec Symfony, vous n'êtes jamais seul
!
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 !
Utiliser, ré-utiliser, exploiter sans
limites
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.
3°) Ressources
Si vous souhaitez plus de ressource, le site principal de
Symfony est accessible à l’adresse suivante : https://symfony.com/
II – Introduction à
Symfony
Pour suivre le cours il est
nécessaire de disposer de :
·
Un éditeur de codes PHP – HTML – CSS – Javascript
- Des connaissances dans les langages de
programmation WEB – PHP – HTML – CSS – Javascript
- Un serveur WAMP (préconisé) ou LAMP ou
XAMP
Note de versions et outils :
- Version Symfony : 5 ou 6
- Version PHP : 8.0.3 ou supérieur
- Editeur HTML – PHP : NotePad++, StudioCode
(ou tout autre éditeur comme sublime, atom…)
- Framework jQuery et jQuery UI
- Tweeter BootStrap
- Nom du projet : "Symf6_PortPontrieux" – Un
dossier portant ce nom est créé à la racine du serveur Apache :
"www/Symf6_PortPontrieux /" – Page d'accueil visible à l'adresse :
"localhost:8000"
1°) Téléchargement &
installation
Voir cours : "Atelier1_MiseEnPlaceConfigurationSymfony6.doc"
visible à l'adresse
https://btsrabelais22.fr/sio/sio2slam/Symfony6_MiseEnPlaceConfiguration-web/index.html
III – Notions de bases
1°) Symfony et le modèle
MVC
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.
2°) Parcours d'une requête dans Symfony
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.
IV –Projet Symfony – Réalisation
du site "Port de Pontrieux"
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.
A- Création de la page
d'accueil
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
:

Etape 0 (préambule) : L'environnement de
l'application
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.
Etape 1 : Etablir les routes du site
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"
:
- L’accueil: c’est le point d'entrée de notre
application et nous allons y afficher un carrousel de photos.
- L'affichage des annonces du port.
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.
Etape 2 : Configurer la page d'accueil
(contrôleur et vues)
Créer le contrôleur
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é".
Créer les vues
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.
Etape 4 : Créer la page Tarifs
(contrôleur et vue)
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
%}
La couche métier : les entités (Entity)
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 :
|
Etape 5 : Visualiser les locations
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.
Etape 6 : Modifier la page Tarifs
(contrôleur et vue) pour ajouter une pagination
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.
Installation de 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.
Modification du controleur
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 %}
Etape 7 : Formulaire – "Ajout
emplacement"
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"
- Le nom de l'entité liée au formulaire :
entrer "Emplacement"
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 %}
Etape 8 : Sécurisez l'accès de l'application
web
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.