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

 

 

 

image

 

 


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.

schema_MVC

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

Image utilisateur

image

logo_sf2

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.

Image utilisateur

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.
Image utilisateur
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

Note de versions et outils :

 

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 Symfony2

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 :

 

image

 


 

 

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" :

Nous allons donc utiliser ces 2 routes.
On pourrait les créer via le fichier routes.yaml de cette façon :

image

 

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

image

 

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 :

image 

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) :

image

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 :

image

 

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.

image


 

 

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.

imageEntité Emplacement :

image

imageEntité Louer :

image

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.

image

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;

image
 

    #[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

image

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"

 

image

Constatez, via PhpMyAdmin, les modifications sur la table emplacement.

imageimage

 

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.

image image

 

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 :image

Emplacement :image


 

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 :

image

 

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 :

image

 

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 :

image

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"

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 :

image


 

 

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" :

image

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 :

image

 

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 :

image

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.

image

 

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

image

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.

image

 

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 :

image

Il est donc clair maintenant que le préfixe admin sera ajouté à l’URL.

image

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 :

image


 

 

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 :

image

La table "annonces" sera ajoutée plus tard et servira pour d'autres fonctionnalités de l'application.