Cette page uniquementToutes les pages
Propulsé par GitBook
1 sur 35

Symfony-6-B.U.T.

Loading...

Semestre 3

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Semestre 4

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Autres ressources

Loading...

Loading...

Loading...

Loading...

Séance 0 : Docker et Installation

Séance 2 : Architecture de Symfony & première page

Depuis la version 4, Symfony à grandement simplifié la structure de ses répertoires et à normalisé les appellations pour coïncider avec la pratique de la majorité des framework.

Symfony à également abandonné la notion de Bundle, qui était nécessaire pour développer son projet.

Architecture globale

Arborescence de base d'un projet Symfony
  • bin/ : contient la console

  • config/ : contient les configurations des différents bundles, les routes, la sécurité et les services

  • public/ : c'est le répertoire public et accessible du site. Contient le contrôleur frontal "index.php" et les "assets" (css, js, images, ...)

  • src/ : contient votre projet (M et C du MVC)

  • templates/ : contient les vues (V du MVC)

  • var/ : contient les logs, le cache

  • vendor/ : contient les sources de bundles tiers et de Symfony

Configuration

Vous remarques des fichiers au format yaml, le format par défaut pour la configuration de Symfony. Vous pouvez aussi remarquer les répertoires dev, prod et test. Ces répertoires permettent de facilement différencier une configuration pour un environnement de développement, pour le site en ligne, ou encore pour un contexte de test.

Par exemple, lorsque l'on développe, on ne souhaite pas réellement envoyer les mails aux usagers, par contre, on souhaite les visualiser. On peut donc configurer ce comportement dans le répertoire dev, pour le bundle de gestion des emails. Un autre exemple est la gestion des logs. En dev, on ne souhaite pas les conserver, mais les afficher, en production, on ne souhaite surtout pas les afficher, mais les stocker dans des fichiers.

Le concept est donc de définir le comportement global quelque soit l'environnement dans le fichier à la racine de config/packages, et de spécifier un comportement en fonction de l'environnement dans les répertoire dev, prod ou test.

Src

Dans ce répertoire se trouve toute la logique de l'application, les contrôleurs, les modèles, nos classes spécifiques, les services, ...

  • Controller/ : Tous les contrôleurs de l'application, accessibles par des routes

  • Entity/ : Les classes spécifiques pour la gestion de l'application, des données et des API

  • Migrations/ : Contient les fichiers permettant la mise à jour de la base de données pour chaque modification effectuée sur la structure.

  • Repository/ : Est très généralement lié à une entité, et permet d'ajouter des requêtes spécifiques.

Tous ces points seront détaillés dans les parties suivantes.

Principe général de fonctionnement de Symfony

Première page avec Symfony

Pour créer une page dans Symfony, il faut, au minimum :

  • Une route : pour faire le lien entre une URL et une méthode d'un contrôleur

  • Un contrôleur : qui contient des méthodes, chacune, en générale, associée à une route

  • Une méthode : permet l'execution d'une action précise, généralement en lien avec une route

Premier controller

Ce code est votre premier contrôleur (à déposer dans src/Controller). Ce contrôleur est composé d'une méthode qui calcul un nombre aléatoire et retourne une réponse. Ce code n'utilise pas directement les vues de Symfony, et ne fonctionne pas (en tout cas il n'est pas possible de l'appeler), car il n'est pas lié à une route.

Définir une route

Nous allons utiliser les Attributes, qui permettent une syntaxe plus simple, et une proximité entre la définition de la route et la définition de la méthode (il existe d'autres façons de faire, qui peuvent avoir leurs avantages et leurs inconvenients et dépendre des pratiques de l'entreprise).

Les Attributes, contrairement aux annotations, ne nécessitent pas de dépendances supplémentaires. En effet les Attributes sont maintenant natifs en PHP 8.0, et sont supportés par Symfony depuis la version 5.1.

Modifions le contrôleur précédent en intégrant directement la route sous forme d'Attribute.

Pour accéder a cette route, il faut maintenant taper l'URL suivante : http://localhost:8000/lucky/number si vous executez le serveur de développement de Symfony. Sinon, il faut taper l'URL suivante : http://localhost:8000/public/index.php/lucky/number. La partie locahost:8000 est à adapter en fonction de votre configuration.

Si vous testez cette URL, vous devriez voir affiché le nombre aléatoire. Cependant, regardez attentivement et vous verrez que ce qui est renvoyé n'est pas du HTML, mais du texte brut. C'est parce que nous n'utilisons pas encore les vues de Symfony, et que nous retournons directement une réponse sans plus d'information que cela sur le format retourné.

Ajout d'une vue

Cette solution fonctionne, mais écrire tout le code "HTML" dans la méthode n'est pas très pratique. Nous devons donc écrire des vues. Par défaut, Symfony utilise . Pour cela, il faut l'installer

Il faut ensuite modifier le contrôleur pour utiliser les vues.

Il faut maintenant écrire la vue.

Et Voilà !

Création avec la console

Symfony propose un outil qui se nomme "maker" qui permet de générer du code pour nous. Le code produit est très générique, ne correspond pas forcément exactement à vos besoin, mais permet d'avoir une première base de travail et une structure pour vos différents fichiers.

Pour installer le maker (si vous n'avez pas installé un projet avec toutes les dépendances --wepabb), en étant dans votre projet :

composer require maker --dev

ou

symfony composer require maker --dev

Ensuite pour utiliser le maker, dans le projet saisir la commande suivante

bin/console make:controller NomDuController

Cette commande va générer un controller nommé "NomDuController" (dans src/controller) et la vue associée dans le repertoire templates/NomDuController, avec une méthode index.

Pour ajouter d'autres méthodes, vous devrez le faire directement dans le controller, le maker ne permet pas d'en ajouter dans un fichier existant.

Twig
Arborescence du répertoire de configuration
Configuration classique du répertoire src/
Principe général de fonctionnement de Symfony (source : https://symfony.com/doc/current/introduction/http_fundamentals.html#the-symfony-application-flow)

Séance 1 : Introduction & Eco-système Symfony

La présentation : https://presentations.davidannebicque.fr/r319.html

Présentation

Découvrir et appréhender un framework PHP web.

Pré-requis

  • PHP

  • Programmation Orientée Objet

  • Structure MVC

  • Base de données

Le concept du MVC

Le concept de programmation MVC consiste à séparer les différentes parties d'un projet web en trois parties distinctes : le modèle, la vue et le contrôleur. Le modèle représente les données et la logique métier, la vue représente l'interface utilisateur et le contrôleur gère les interactions entre le modèle et la vue. Le contrôleur reçoit les demandes de l'utilisateur, récupère les données nécessaires auprès du modèle et les transmet à la vue pour affichage.

L'intérêt d'utiliser le concept de programmation MVC est de séparer les différentes parties d'un projet web en trois parties distinctes, ce qui permet une meilleure organisation et une plus grande facilité de maintenance du code. En séparant la logique métier, l'interface utilisateur et les interactions entre les deux, il est plus facile de faire des modifications sans affecter les autres parties du code. Cela permet également une meilleure collaboration entre les développeurs travaillant sur le même projet.

C: Controller / Contrôleur

C'est lui qui reçoit l'interaction (la demande/request) du visiteur. Il se charge de récupérer les éléments nécessaires auprès du/des modèle(s). Il transmets toutes les données nécessaires à la vue.

V: View / Vue

C'est lui qui apporte la réponse (response/render) au visiteur. Une vue peut être une page web, un fichier pdf, ... Ne se préoccupe que de l'affiche des informations, n'assure aucun traitement

M: Model / Modèle

C'est lui qui s'occupe de récupérer et préparer les données. Le modèle peut être en lien avec une base de données. Le modèle peut être en lien avec des API. Le modèle prépare les données pour qu'elles soient facilement manipulables par la vue.

Notion de Framework

Définition générale

En programmation informatique, un framework ou structure logicielle est un ensemble cohérent de composants logiciels structurels, qui sert à créer les fondations ainsi que les grandes lignes de tout ou d’une partie d’un logiciel (architecture). Un framework se distingue d’une simple bibliothèque logicielle principalement par :

  • son caractère générique,

  • faiblement spécialisé,

  • contrairement à certaines bibliothèques ;

Un framework peut à ce titre être constitué de plusieurs bibliothèques chacune spécialisée dans un domaine. Un framework peut néanmoins être spécialisé, sur un langage particulier, une plateforme spécifique, un domaine particulier : reporting, mapping, etc. ;

Le cadre de travail (traduction littérale de l’anglais : framework) qu’il impose de par sa construction même, guidant l’architecture logicielle voire conduisant le développeur à respecter certains patterns (modèle de conception) ; les bibliothèques le constituant sont alors organisées selon le même paradigme.

Framework Orienté Objet

Un framework dit orienté objet 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.

Le développeur qui utilise le framework pourra personnaliser les éléments principaux du framework 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 développeur en fonction des besoins spécifiques à son application.

Avantages
Inconvénients

Article comparatifs des 10 frameworks PHP les plus populaires de 2019:

Symfony

  • Framework MVC en PHP 5 (V2), PHP 7 (V3, V4 et V5 ), PHP 8.1 (V6) libre

  • Développé en 2005 par la société Sensio pour répondre à ses besoins

  • Division de la société Sensio en deux entités l’agence Web et l’entreprise qui soutient et maintient Symfony : SensioLabs, dirigée par Fabien Potencier, l’auteur de Symfony

SYMFONY : AVANTAGES

  • Connectable à presque tous les SGBD

  • De nombreux Bundles, contributeurs, utilisateurs

  • Moteur de template puissant et simple

  • Depuis la V4, Symfony est très léger et très rapide

Toutes les informations sur l'évolution du framework :

Symfony V4 : Un retour aux bases

Avec sa version 4 (et suivante), Symfony à pris un virage important par rapport aux précédentes versions, et se rapproche des "standards" de la majorité des framework, mais à aussi grandement optimisé son poids et sa vitesse d'exécution. Dans une grande logique de simplification Symfony à également automatisé de nombreux mécanismes qui auparavant auraient impliqués de nombreuses lignes de configuration.

SKELETON ET FLEX

La version Skeleton de Symfony : Apporte un framework Symfony très léger, avec le minimum pour faire fonctionner un controller.

La version 4 de Symfony introduit Flex qui est un gestionnaire de "recipes" (recettes), qui permet l'ajout de fonctionnalité à Symfony (gestionnaire de vue, de base de données, d'email, ...) avec un mécanisme d'auto-configuration de ces "bundles". Cela permet donc de fournir par défaut un framework très léger, avec une grande facilité pour lui ajouter tous les composants nécessaires, sans en mettre plus que nécessaire.

Par défaut Symfony version Skeleton ne sais rien faire ! Par contre, il n'embarque pas des dizaines de Bundles dont vous n'aurez peut être jamais besoin (fonctionnement des versions 2 et 3 avec plus de 46 bundles par défaut, contre 10 aujourd'hui).

Grâce à Flex vous installez rapidement le nécessaire pour répondre à votre projet.

Symfony V5 : Continuer vers la simplification et la standardisation

Avec sa version 5 (et suivante), Symfony continue sa simplification en facilitant l'usage de nombreux composants redéfinis et devenus génériques.

Une lecture intéressante sur la logique d'évolution du framework Symfony :

Installations (SF6)

Configuration requise pour votre serveur

  • Un serveur Web

  • PHP 8.1 ou supérieur

  • Le gestionnaire de dépendance

Vous pouvez suivre aussi les éléments de la documentation officielle :

Symfony - Eco-système

Symfony nécessite tout un environnement pour fonctionner. Symfony implique aussi différents "langages" et utilise un vocabulaire spécifique (souvent repris dans d'autres frameworks).

Composer

est un logiciel gestionnaire de dépendances libre écrit en PHP. Il permet à ses utilisateurs de déclarer et d'installer les bibliothèques dont le projet principal a besoin. Développé, depuis 2011, par Nils Adermann et Jordi Boggiano (qui continuent encore aujourd'hui à le maintenir), il est aujourd'hui en version 2.

Le logiciel Composer trouve son équivalent pour le front avec npm ou Yarn

ENTITÉ (EQ. DU MODÈLE)

Une entité est une classe PHP ! Elle peut faire le lien avec une base de données, on y déclare les différentes propriétés accessibles; Symfony utilise par défaut un outil de persistence de données : pour lier une entité à une table de base de données.

ORM : OBJECT RELATIONNAL MAPPING

Système permettant de se libérer des requêtes pour la base de données. Il se charge de générer les requêtes à effectuer sur les Entités spécifiées. Il existe plusieurs ORM, dans Symfony, il s'agit de .

Repository

Classe PHP qui fait le pont entre une entité et l'ORM, il permet notamment de structurer des requêtes complexes.

YAML

Format de structuration de données très utilisé dans Symfony, mais on peut utiliser du XML, du PHP ou encore les annotations (PHP 7) ou les attributs (php 8), les fichiers de configurations par défaut sont en YAML.

Annotations

Commentaire PHP directement dans les classes utiles (controller, entité, ...) interprété par Symfony pour générer des fichiers de configuration ;

Attributes

Les Attributes sont la nouvelle version des annotations, intégrés nativement dans les versions 8 et supérieures de PHP. Les Attributes sont une forme de données structurées, qui permet d'ajouter des métadonnées à nos déclarations de classes, propriétés, méthodes, fonctions, paramètres et constantes. Ils vont nous permettre de définir de la configuration au plus près du code, directement sur nos déclarations.

Routes

Les routes permettent de faire un lien entre une URL et un contrôleur.

Bundles

Sorte de modules Symfony qui peuvent contenir tout et n'importe quoi ; C'est la force de Symfony les modules peuvent fonctionner indépendamment et même sur d'autres structures PHP, autre framework etc.

ENVIRONNEMENTS

Symfony propose par défaut 2 environnements : dev et prod qui permettent de donner des configs différentes en fonction de l'environnement de travail ;

  • dev permet une utilisation sans cache avec des outils de dev comme le profiler ;

  • prod lui permet d'utiliser le site avec le cache et sans aucun message d'erreurs.

De plus on peut configurer les différents environnements pour par exemple rediriger tous les mails vers [email protected] en dev et laisser le fonctionnement normal pour prod ; pratique pour les debugs. Le changement d'un environnement à un autre se faire en modifiant la ligne suivante dans le fichier ".env" (ou .env.local) :

Profiler

Le profiler est un outil puissant (et indispensable) pour débuger une application. Par défaut le profiler n'est pas installé. Pour l'ajouter il faut exécuter la commande suivante :

Le profiler est toujours visible en bas de la page en mode développement.

Maker

Le maker est un outil puissant pour générer du code, et des éléments dans notre projet. Par défaut le maker n'est pas installé. Pour l'ajouter il faut exécuter la commande suivante :

Il est assez facile de modifier le maker pour que le code généré corresponde exactement à notre projet et nos attentes.

Moteur de template

Un moteur de template est un "langage", un "outil" permettant d'écrire les vues (partie visible) de manière efficace et rapide. Symfony utilise par défaut.

Séance 2 : Filtres twig

Twig propose de nombreux filtres permettant d'interagir avec vos variables. Ces filtres sont très utiles pour formater vos données, les transformer, les comparer, etc. Une longue liste est disponible par défaut , mais vous pouvez également créer vos propres filtres .

Création de votre premier filtre

Contexte du S4

Dans la continuité du S3

Tout ce qui a été vu et fait dans Symfony en S3 est considéré comme acquis (donc ne pas hésiter à réviser : le support est ici : ).

<?php

namespace App\Controller;

use Symfony\Component\HttpFoundation\Response;

class LuckyController
{
    public function number(): Response
    {
        $number = random_int(0, 100);

        return new Response(
            'Lucky number: '.$number
        );
    }
}
<?php

namespace App\Controller;

use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class LuckyController
{
    #[Route("/lucky/number", name:"app_lucky_number")]
    public function number(): Response
    {
        $number = random_int(0, 100);

        return new Response(
            'Lucky number: '.$number
        );
    }
}
composer require twig
# Uniquement si vous n'avez pas installé le projet avec toutes les dépendances --webapp
<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class LuckyController extends AbstractController
{
    #[Route("/lucky/number", name:"app_lucky_number")]
    public function number(): Response
    {
        $number = random_int(0, 100);

        return $this->render(
            'lucky/number.html.twig',
            [
                'number' => $number
            ]
        );
    }
}
{# templates/lucky/number.html.twig #}
<h1>Your lucky number is {{ number }}</h1>

Anciennes ressources

Séance 10 : Sécurité et voters

Séance 12 - 13 : TP Noté (2 heures 45)

26 Mars, 8h00 / 11h00, 2 heures 45 - Rattrapage

Vous pouvez préparer une installation d'un projet Symfony vide (avec webapp), avec un docker configuré sur le domaine wr407d.mmi-troyes.fr. Le projet sera nommé avec votre login MMI.

Le rendu doit être effectué à 11h00 au plus tard.

198KB
TPNoteRattrapage2025.pdf
PDF
Ouvrir
Sujet

Séance 14 : Evaluation écrite

25 mars, 14h00

Installation

Sous l'image docker MMI

Connexion sur le container Symfony de docker

docker exec -ti symfony /bin/bash

Installation de symfony dans /var/www

cd /var/www
symony new nomduprojet
ou
composer create-project symfony/skeleton nomduprojet

Installation des dépendances (toutes les dépendances de webapp)

cd /var/www/nomduprojet
composer require webapp

Dupliquer le .env (qui ne doit jamais être modifié avec vos données)

cp .env .env.local

Configurer apache, en créant une configuration qui pointe vers le dossier nomduprojet

Relancer apache

Première version

Nous souhaitons formater les prix de notre application dans le format monétaire en euros. Nous pourrions le faire avec le filtre number_format de twig, mais nous allons créer notre propre filtre, car nous aurons toujours la même configuration. Cela nous permettra également d'ajouter le symfony euro.

Pour créer un filtre nous devons créer une classe qui va étendre Twig\Extension\AbstractExtension. Cette classe peut se mettre n'importe où dans le dossier src, mais nous allons la mettre dans le dossier src/Twig.

Dans cette classe, nous avons créé une méthode getFilters qui va nous permettre de définir les filtres que nous souhaitons créer. Cette méthode doit retourner un tableau d'objets TwigFilter. Pour chaque filtre, nous devons définir un nom (qui est utilisé dans Twig) et une méthode qui va être appelée lorsque le filtre sera utilisé.

Dans notre exemple, nous avons créé un filtre price qui va formater un nombre en ajoutant le symbole euro.

Pour l'utiliser dans Twig, il suffit d'utiliser le nom du filtre :

Deuxième version

Nous pouvons améliorer notre filtre afin de pouvoir recevoir des paramètres si nous ne souhaitons pas avoir un format en euro, mais par exemple en dollar. Dans ce cas, le symbole est différent, le séparateur de décimales et de milliers également. Cependant notre cas le plus classique restant l'euro, nous allons mettre ces paramètres en option.

Et l'usage dans twig pourrait être

Un filtre qui retourne du HTML

Nous allons créer un filtre qui va retourner du HTML. Nous allons créer un filtre qui va permettre d'afficher un nombre d'étoiles en fonction d'une note. Par exemple, si la note est 3, nous allons afficher 3 étoiles pleines, 2 étoiles vides.

Et l'usage dans twig pourrait être

Mais vous aurez une erreur, et un résultat affiché en HTML (en fait Symfony à empéché l'interpréation du HTML car il ne sait pas s'il peut le faire. Cela pourrait potentiellement être dangeureux). Pour cela, nous devons utiliser le filtre raw de twig pour forcer l'affichage du HTML.

Comme ce filtre va toujours renvoyer un contenu HTML que nous allons vouloir afficher, nous pouvons configurer notre filtre pour qu'il retourne directement du HTML "affiché".

Nous ajoutons sur la déclaration du filtre un tableau avec la clé is_safe et la valeur html. Cela va permettre à twig de savoir que le filtre retourne du HTML et qu'il ne doit pas l'encoder (autrement dit l'afficher en texte).

Exercices

Exercice 1

Mettre en place les filtres price et stars dans votre projet pour pouvoir les utiliser dans vos vues. La notion d'étoile n'existe pas dans Jeu, vous devrez donc ajouter une entrée dans la base de données, saisir des valeurs pour les jeux et afficher les étoiles en fonction de la note.

Exercice 2

Créer un filtre qui va permettre d'afficher la date du jour au format français, sur la page d'accueil. L'appel du filtre dans twig pourrait être :

Ou dateObjet est un objet DateTime envoyé par le contrôleur

Modifier votre filtre pour que la syntaxe suivante puisse fonctionne en utilisant la date du jour

Exercice 3

Créer un filtre qui va formatter un numéro de téléphone de l'éditeur en ajoutant des espaces entre les groupes de chiffres. L'appel du filtre dans twig pourrait être :

Notez de nouveau que la donnée n'existe pas, il faut donc ajouter ce champs dans l'entité Editeur, saisir des données et les afficher dans la vue.

https://twig.symfony.com/doc/3.x/
https://symfony.com/doc/current/templates.html#writing-a-twig-extension
<?php

namespace App\Twig;

use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;

class AppExtension extends AbstractExtension
{
    public function getFilters()
    {
        return [
            new TwigFilter('price', [$this, 'formatPrice']),
        ];
    }

    public function formatPrice($number)
    {
        $price = number_format($number, 2);
        $price = $price.' €';

        return $price;
    }
}
{{ 10|price }}
<?php

namespace App\Twig;

use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;

class AppExtension extends AbstractExtension
{
    public function getFilters()
    {
        return [
            new TwigFilter('price', [$this, 'formatPrice']),
        ];
    }

        public function formatPrice($number, $symbol = '€', $decimals = 0, $decPoint = '.', $thousandsSep = ',')
    {
        $price = number_format($number, $decimals, $decPoint, $thousandsSep);
        $price = $price. ' €'; //il faudrait améliorer encore ce code car le symbole ne se met pas à la fin si le nombre est en dollar par exemple.

        return $price;
    }
}
{{ 10|price }}
{{ 10|price('€', 2, ',', ' ') }}
{{ 10|price('$', 2, '.', ',') }}
<?php

namespace App\Twig;

use Twig\Extension\AbstractExtension;

class AppExtension extends AbstractExtension
{
    public function getFilters()
    {
        return [
            new TwigFilter('stars', [$this, 'stars']),
        ];
    }

    public function stars($note)
    {
        // pour le moment nous mettons un simple tiret pour les étoiles vides, nous verrons plus tard comment ajouter des icônes avec une libraire CSS
        $html = '';
        for ($i = 0; $i < $note; $i++) {
            $html .= '<strong>*</strong>';
        }
        for ($i = 0; $i < 5 - $note; $i++) {
            $html .= '-';
        }

        return $html;
    }
}
{{ 3|stars }}
{{ 3|stars|raw }}
<?php

namespace App\Twig;

use Twig\Extension\AbstractExtension;

class AppExtension extends AbstractExtension
{
    public function getFilters()
    {
        return [
            new TwigFilter('stars', [$this, 'stars'], ['is_safe' => ['html']]),
        ];
    }

    public function stars($note)
    {
        $html = '';
        for ($i = 0; $i < $note; $i++) {
            $html .= '*';
        }
        for ($i = 0; $i < 5 - $note; $i++) {
            $html .= '-';
        }

        return $html;
    }
}
{{ dateObjet|dateFr }}
{{ 'now'|dateFr }}
{{ '0606060606'|formatPhone }}
{{ '+33606060606'|formatPhone }}

La gestion des formulaire, des utilisateurs, ...

Framework français !, De renommée mondiale
  • Premier framework en France et en Europe

  • ou Le nouveau gestionnaire d'installation de Symfony : https://symfony.com/download

  • Une maîtrise de son système d'exploitation ! (fichiers cachés, variables PATH, php.ini, console...)

  • Pour éviter des erreurs dans l’organisation des appels

    Apprentissage d’une couche supplémentair

    Éviter les appels directs aux commandes PHP

    La majorité des fonctionnalités PHP sont redéfinies

    Préférer les versions des Frameworks qui apportent leur lot de contrôles.

    Généralement apprentissage d’un moteur de template

    Plus grand portabilité du code

    Apprentissage de l’utilisation du framework choisit : ses classes, ses objets, sa logique !

    Ne pas réinventer la roue

    https://coderseye.com/best-php-frameworks-for-web-developers/
    https://symfony.com/releases
    https://www.disko.fr/reflexions/technique/symfony-4-4-5-0-les-nouveautes-venir/
    Git (différent de GitHub)
    Composer
    https://symfony.com/doc/current/setup.html
    Composer
    Doctrine
    Doctrine
    Twig
    Schéma de principe du MVC
    Les 10 frameworks les plus populaires en PHP (2019).
    Roadmap des versions de Symfony
    Quelques rappels seront fait, mais on ne revient pas sur les bases de l'installation, de la manipulation contrôleurs/vue, des entités, de la sécurité ou encore la configuration de docker qui doit maintenant être acquise.

    Enjeux du S4

    Approfondir un ensemble de concepts (personnalisation des formulaires, événements, webpack, repository, ...), qui facilitent le développement, permettent de garder un code "propre" et "bien rangé".

    Cette ressource sera en lien avec les ressources WR406/WRA406 (développement front) et la SAE WS401.

    L'objectif est de produire un workflow complet d'une application web "moderne".

    Organisation des cours du S4

    Evaluation de ce module

    Séance de TP noté individuelle (3h) en fin de ressource et évaluation écrite (1h) en fin de semestre.

    Les chapitres de la ressource

    1. Notion de service : exemple du mail : Correction de la dernière séance de S3 et rappels sur les services

    2. Filtres twig : Personnalisation des filtres Twig

    3. AssetMapper : Gestion des assets (CSS, JS, images, ...)

    4. Mise en place d'un panier : Utilisation des services et des sessions

    5. : Gestion des événements

    6. : Gestion des langues

    7. : Utilisation des repositories

    8. : Personnalisation des formulaires

    9. : Validation des formulaires

    10. : Gestion des droits

    11. : TP de révision

    12. : TP noté

    13. : Evaluation écrite

    https://docs.mmi-troyes.fr/books/wr319d-wra319d

    Séance 4 : Exercices

    Exercice 1

    Créer un nouveau projet Symfony et installer les dépendances nécessaires.

    Pour rappel, en ligne de commande depuis Docker/serveur et dans le repertoire où vous souhaitez mettre votre projet :

    symfony new nomDuProjet --webapp

    Créer un environnement Symfony collaboratif

    FAQ

    Composer global sur MacOS

    That's a tough question but thankfully, our team is on it. Please bear with us while we're investigating.

    Have you had a chance to answer the previous question?

    ###> symfony/framework-bundle ###
    APP_ENV=dev
    composer require profiler --dev
    composer require maker --dev

    Comme vous utilisez docker, pensez à créer un nouvel alias dans votre fichier de conf (symfony.conf) et redémarrer apache2

    En utilisant la notion d'héritage des templates créez un menu avec 3 items :

    • Accueil (qui mènera vers /)

    • Articles (qui mènera vers /articles)

    • Recherche (qui mènera vers /recherche)

    Chaque page sera associée à une méthode dans un controller différent.

    En utilisant les assets installer Bootstrap.

    Sur la page accueil insérez une image de votre choix

    Sur la page articles, ajouter des articles dans votre méthode de controller (dans un tableau associatif, ajouter au moins 3 articles).

    Affichez ces 3 articles dans votre page articles.

    Un article est composé d'un titre, d'un texte (on peut utiliser du loreum ipsun), d'une date de publication (on utilisera un objet datetime), et d'un auteur.

    Dans la page recherche, ajoutez simplement un formulaire de recherche comme vous l'auriez fait en BUT1.

    Soignez un minimum la mise en page.

    Pour la page recherche, on va simuler la récupération de la recherche utilisateur. Pour le moment, on a pas de source de donner pour effectuer la recherche.

    Récupéré la recherche de l'utilisateur soumise avec un formulaire en method post ($request peut vous aider), et afficher dans une nouvelle page le mot recherché.

    Vous veillerez à ce que le bon item soit "actif" dans le menu.

    composer require maker debug-pack --dev
  • Yes, after a few months we finally found the answer. Sadly, Mike is on vacations right now so I'm afraid we are not able to provide the answer at this point.

    Event Dispatcher/Listener
    Localisation de l'application
    Requetes personnalisés (repository)
    Formulaires
    Formulaires et validations
    Sécurité et voters
    Séance révision
    TP Noté (3 heures)
    Evaluation écrite

    Séance 11 : Séance révision

    Dans cette séance nous allons reprendre l'ensemble des concepts vu cette année sur Symfony et développer un mini forum.

    • Installer un nouveau projet (réutiliser symfony.mmi-troyes.fr)

    • ⚠️ Configurer une base de données

      • Les entités sont :

        • Categorie

          • titre, string 255

          • ordre, integer

          • couleur, string 6 caractères

        • Message

          • titre, string 255

          • message, text

          • datePosted, datetime

        • Tags

          • libelle

          • couleur

        • User (utiliser make:user et ajouter)

          • Nom, string 50

          • Email, string 255

          • Ville, string 50

        • Un user peut déposer plusieurs messages,

        • Un message est dans une seule catégorie

        • Une catégorie peut avoir plusieurs messages

        • Seul un admin peut créer des catégories,

        • un message pourra avoir un ou plusieurs tags

    • ⚠️ Définir une page d'accueil

      • ⚠️Reprenant les tags des messages et les 3 dernières messages publiés

    • ⚠️Définir une page listant les catégories (avec la couleur)

      • Chaque catégorie permet d'accéder à une page listant les messages, triés par date

    • ⚠️Mettre en place la sécurité

    • Chaque inscription doit envoyer un mail à l'utilisateur

    • ⚠️Dans la page d'une catégorie il sera possible d'ajouter un message (formulaire personnalisé et mis en page)

    • Un admin pourra ajouter une catégorie depuis une page de gestion dédiée (CRUD possible, mais mis en page)

    • Une page de recherche permettra de rechercher un message dans toutes les catégories (texte libre)

    • On pourra filtrer par tag

    • Ajouter un événement déclenché à chaque ajout d'un message et diffuser aux utilisateurs par mail

    Séance 8 : Exercices

    Ajoutez une entité "Auteur" comprenant les champs suivant :

    • Nom

    • Prénom

    • Email

    • droit (auteur/administrateur)

    Un auteur peut publier des posts, modifier ses posts.

    Un auteur avec des droits administrateurs peut modifier tous les posts, et gérer les catégories.

    Modifiez l'entité post pour lui ajouter un auteur et sauvegardé l'auteur dans le post (dans un premier temps on fera une liste déroulante des auteurs)

    Créer un formulaire pour pouvoir ajouter des auteurs.

    Etapes

    • Créer l'entité auteur (make:entity)

      • Ajouter les 4 champs (nom, prénom, email, droit)

      • Mettre à jour la base de données (bin/console d:s:u -f depuis docker)

    http://symfony.mmi-troyes.fr:8313/symfony.mmi-troyes.fr
    Créer un contrôleur ou lancer la commande make:crud sur auteur
    • Créer ou améliorer le formulaire avec les labels des champs, le mettre en forme en utilisant un thème.

  • Modifier l'entité post pour faire une relation avec auteur

    • make:entity sur Post

    • La relation est de type ManyToOne

  • Modifier le formulaire (PostType) pour ajouter une liste déroulante avec les auteurs (entity Auteur)

  • https://docs.mmi-troyes.fr/books/docker-symfony/page/creation-dun-container-pour-le-s3-devdocs.mmi-troyes.fr

    Séance 8 : Formulaires

    Formulaire pour Jeu

    Nous allons mettre en place le formulaire pour les jeux. Dans cette entité nous avons une liaison vers éditeur. Cette relation est de type 1..n (un jeu à un éditeur, un éditeur peut être dans plusieurs jeux).

    • Générez le formulaire pour jeu et configurez les champs "simple".

    Pour la liaison entre les deux table, il faut un champ de type entity. Le code pourrait être celui ci-dessous :

    Vous pouvez définir d'autres options.

    N'oubliez pas d'ajouter les use qui vont bien ! (ou utilisez PhpStorm avec le plugin Symfony).

    • Faite le contrôleur pour afficher et sauvegarder un jeu.

    Passer des paramètres aux formulaires

    Il est parfois nécessaire de passer des paramètres au formulaire (donc au fichier xxxType.php), pour par exemple afficher un champ selon des droits, activer ou désactiver une zone, pré-remplir une information ou encore filtrer les résultats d'une liste.

    Comme vous pouvez le constater dans la ligne permettant de "créer" le formulaire, il n'est pas possible de passer des paramètres comme vous pourriez le faire avec une méthode ou un constructeur en POO.

    Cependant, la méthode createForm, autorise un troisième paramètre, qui est un tableau et qui va permettre de passer tout ce que l'on souhaite à note fichier de formulaire. Ce troisième paramètre, les options, est contrôler par Symfony (et ce que l'on nomme un OptionResolver()), qui va s'assurer que les paramètres obligatoires sont bien présent, et qu'ils ont les bonnes valeurs. Dans le cas du createForm, aucun paramètre n'est obligatoire.

    Un ensemble de paramètre sont pré-définis (comme action pour modifier le lien de l'action à la validation, attr, ...). En fait ce troisième paramètre fonctionne comme celui des add(...) pour ajouter des champs (et au passage comme beaucoup de fonctionnalités dans Symfony).

    Si on souhaite passer un paramètre qui n'est pas initialement prévu dans l'OptionResolver, il faut lui expliquer s'il est obligatoire, le type attendu, les valeurs possibles. Au dela de cette contraintes, vous pouvez passer autant de paramètres que nécessaires.

    Configurons notre formulaire ArticleType pour recevoir un paramètre correspondant à un code postal que nous pourrons ensuite utiliser pour filtrer la liste des fournisseurs.

    Tout d'abord, on modifie l'appel dans le contrôleur :

    Le troisième paramètre est un tableau, avec une clé (du texte), et une valeur. Ici la valeur est du texte, mais c'est bien souvent une variable dépendant de notre contexte.

    Si vous essayez d'afficher votre formulaire, vous aurez une erreur vous indiquant que 'codePostal' n'est pas autorisé parmi les options définies dans l'OptionResolver.

    Notez au passage que vous avez ici la liste de toutes les options connues pour le formulaire.

    Nous devons donc ajouter cette possibilité dans notre formulaire, pour cela, nous modifions le fichier JeuType.php, et plus particulièrement la méthode configureOptions, qui permet de gérer les paramètres autorisés.

    Par défaut il contient les éléments ci-dessous

    C'est le code minimal, obligatoire pour fonctionner et qui permet de lier une classe à notre formulaire (ligne 4).

    Nous allons ajouter entre la ligne 4 et 5, notre nouvelle option, et lui donner une valeur par défaut, le code devient :

    Par défaut, on considère qu'il n'y a pas de valeur (null), on pourrait aussi fixer autre chose, cela dépend de votre projet. On pourrait également préciser le type, une plage de valeur possible, ...

    Si vous actualisez votre page, vous n'avez plus d'erreurs.

    Mais à ce stade, votre nouveau paramètre n'est pas utilisé dans votre code, la valeur que vous avez passé même pas récupérée.

    Dans la méthode buildForm, qui construit le formulaire, nous allons donc récupérer ce paramètre. Pour cela, nous avons le second paramètre $options, qui contient tout ce qui a été passé au formulaire, avec notamment, la classe (et les éventuelles données pré-remplies), et nos options.

    Un "dump" de cette variable vous donnerai un affichage comme ci-dessous :

    Vous pouvez constatez sur l'avant dernière ligne que nous avons notre codePostal. Pour le récupérer nous pourrons écrire la ligne 4 ci-dessous :

    Maintenant nous pouvons utiliser notre variable $codePostal comme nous le souhaitons dans notre formulaire. Par exemple, le code ci-dessous permettrait de filtrer nos adresses de fournisseurs ayant uniquement code postal donné.

    Quelques explications :

    • Les lignes 2 et 3 permettent de faire le lien avec l'entité Editeur (voir S3)

    • La ligne 4 (jusque 10) permet d'exécuter une requête pour filtrer les résultats sur l'entité.

      • Ligne 4 nous passons le repository qui permet de manipuler nos données, et on indique avec le use que l'on souhaite utiliser notre variable codePostal.

      • Les lignes 5 à 8 sont la requête qui permet de filtrer. Vous reconnaissez vaguement du SQL, mais en notamment objet, on appelle cela le DQL (Doctrine Query Language). On y reviendra en détail sur la partie repository.

    N'oubliez pas d'ajouter les use qui vont bien ! (ou utilisez PhpStorm avec le plugin Symfony).

    Exercice

    • Modifiez vos fichier pour permettre de filtrer sur un code postal le éditeur. Ajoutez des adresses et des éditeurs (avec le formulaire que vous avez créé sur la séance précédente). Vérifiez que tout fonctionne et que vous filtrez bien les éditeurs selon leur code postal.

    • Mise en forme du formulaire.

      • En reprenant les éléments de la séance 7 du S3, et en installant Bootstrap, affichez le formulaire article sur 2 colonnes avec le thème de formulaire adapté . Vous devez obtenir le résultat ci-dessous :

    Séance 4 : Mise en place d'un panier

    Objectif

    L'objectif de cette séance est de mettre en place un panier pour notre application. Nous allons le gérer en JavaScript et en PHP.

    • Ajouter un contrôleur, un lien dans le menu et une page afin d'afficher le panier (PanierController)

    • Ajouter un bouton sur la liste des jeux pour ajouter un jeu au panier. On va traiter cette action en JavaScript.

    • Afficher le contenu du panier sur la page panier.

    Préparation

    Création du contrôleur

    Créez un contrôleur PanierController avec une méthode index qui affiche le contenu du panier.

    Création de la vue

    Créez une vue panier/index.html.twig qui affiche le contenu du panier.

    Ajout d'un lien dans le menu

    Ajoutez un lien dans le menu pour accéder à la page du panier.

    Ajout d'un bouton pour ajouter un jeu au panier

    Ajoutez un bouton sur la page de la liste des jeux pour ajouter un jeu au panier. Ce bouton doit être traité en JavaScript.

    Exemple de code HTML pour le bouton :

    Ajout d'un script JavaScript

    Ajoutez un fichier panier.js dans le dossier assets/ pour gérer les actions sur le panier. N'oubliez pas de l'importer dans app.js.

    Détection du clic sur le bouton

    Ajout du jeu au panier

    On utilise ici le Local Storage pour stocker les données du panier. Cela permet de garder les données même si l'utilisateur recharge la page. Ces données sont dans le navigateur de l'utilisateur et ne sont pas envoyées au serveur. Regardez l'outil de développement de votre navigateur pour voir le contenu du Local Storage (application).

    Affichage du panier

    Affichez le contenu du panier sur la page panier/index.html.twig. Le contenu du panier est stocké dans le Local Storage et n'est donc pas accèssible directement en PHP. Il faut donc passer par JavaScript pour récupérer les données du panier.

    La partie Twig

    La partie JavaScript

    Pour aller plus loin

    • Ajouter un bouton pour supprimer un jeu du panier.

    • Ajouter un bouton pour vider le panier.

    • Permettre de gérer la quantité d'un produit.

    Séance 5 : Modèles - Entités - ORM

    Introduction

    Dans Symfony la notion de modèle se retrouve sous la forme (entre autre) d'une Entité. Une entité est une classe PHP, qui peut être connectée à une table de votre base de données via l'ORM. Lorsqu'une entité est liée à une table, via l'ORM, il y a en général un fichier "repository" associé. Un repository permet la génération de requêtes simples ou complexes et dont le développeur peut modifier à volonté.

    Un ORM (Object Relation Mapper) permet de gérer manipuler et de récupérer des tables de données de la même façon qu'un objet quelconque, donc en gardant le langage PHP. Plus besoin de requête MySQL, PostgresSQL ou autre.

    Symfony utilise Doctrine

    Séance 9 : Formulaires et validations

    Validation des formulaires

    La documentation officielle se trouve ici :

    Depuis le S3, nous écrivons systèmatiqument le code suivant pour enregistrer un formulaire :

    Ce code fait deux choses. Il vérifie que le formulaire a été soumis, et qu'il est valide. Jusqu'a présent le coté "validation" n'est pas géré car nous n'avons pas mis de règles de validation sur nos formulaires. Nous allons donc voir comment ajouter des règles de validation sur nos formulaires.

    Mise en production

    Mettre en production un projet réalisé avec Symfony

    La mise en production d'un projet avec Symfony est simple, mais implique de suivre les différentes étapes.

    Mise en production avec un FTP

    Pour effectuer une mise en ligne de votre projet avec un logiciel FTP, il faut télécharger sur le serveur les fichiers ou répertoires suivants.

    ->add('editeur', EntityType::class, [
        'class' => Editeur::class,
        'choice_label' => 'nom',
    ])
    https://symfony.com/doc/current/components/options_resolver.html
    Erreur si une option n'existe pas
    dump($options)
    Validation des champs

    Pour ajouter des règles de validation sur un champ, nous allons utiliser les contraintes de validation. Pour cela nous devons modifier notre formulaire JeuType :

    Dans cet exemple nous avons ajouté une contrainte de type Length sur le champ nom. Nous avons également ajouté une contrainte de type NotBlank sur le champ duree. Nous pouvons ajouter autant de contraintes que nous le souhaitons sur un champ.

    La liste des contraintes par défaut est disponible sur la documentation de Symfony.

    Pour chaque contrainte on peut définir un message. Ce message sera affiché sous le champs qui pose problème. (form_errors dans le thème de votre formulaire pour le personnaliser).

    Validation des entités

    Cette syntaxe est intéressante, mais ne vérifie les données que si le formulaire est soumis. Dans les autres cas, les données ne sont pas forcément validées. Selon votre fonctionnement, il peut être nécessaire de les valider (exemple envoie depuis une API, import de données, ...).

    Pour valider les entités, nous allons utiliser les attributs de validation. Les possibilités sont les mêmes que pour les contraintes de validation sur les formulaires. La syntaxe utilise les attributs (depuis PHP 8.0).

    Pour cela nous pouvons modifier notre entité Editeur par exemple, pour le code postal :

    Exercice

    • Ajoutez les contraintes de validations sur vos formulaires. Testez les en soumettant des données invalides.

    • Personnalisés l'affiche des erreurs dans vos formulaires

    https://symfony.com/doc/current/validation.html
  • Pour une mise à jour d'un produit existant, l'upload de src/ et templates/ peut suffire, selon ce que vous avez modifié sur votre projet.

    Il vous faut ensuite exécuter les différentes opérations de mise en ligne de la partie Mise en production

    Mise en production avec Git

    Première mise en ligne

    Pour une première mise en ligne vous pouvez faire un clone de votre dépôt.

    Cette commande va créer un nouveau répertoire et copier les fichiers de votre dépôt sur votre serveur.

    Il vous faut ensuite exécuter les différentes opérations de mise en ligne de la partie Mise en production

    Mise à jour d'un projet existant

    Pour mettre à jour un projet déjà existant sur votre serveur, et géré avec Git, vous devez executer la commande suivante

    Où master est la branche qui contient la version de votre projet que vous souhaitez installer.

    Il vous faut ensuite exécuter les différentes opérations de mise en ligne de la partie Mise en production

    Opération à réaliser après la mise en ligne des fichiers

    1) Configurer vos variables d'environnement

    Symfony propose une gestion poussée des environnements, et vous pourriez avoir plusieurs fichiers en fonction du contexte : https://symfony.com/doc/current/configuration.html#configuration-environments

    Si vous n'avez qu'un fichier .env, vous devez le mettre à jour avec les informations de votre serveur de production (base de données, éventuellement serveur de mail, ...).

    C'est également dans ce fichier d'environnement que vous devez définir que votre projet est en production et plus en développement :

    En production Symfony utilise un système de cache, n'affiche plus le profiler ou les messages d'erreur, mais utilise les pages 404, 500 que vous avez définies.

    Les erreurs peuvent être sauvegardées dans des fichiers de log, selon votre configuration.

    Toutes les configuration se trouvant dans config/packages/dev sont ignorées et celles se trouvant dans config/packages/prod sont utilisées.

    2) Installer/Mettre à jour les Vendors¶

    Le répertoire vendor n'est jamais installé ou téléchargé, quelque soit la méthode que vous utilisez. Il faut donc soit le créer (une première installation), soit le mettre à jour (si vous avez ajouté des bundles ou que vous souhaitez obtenir la dernière version des bundles que vous utilisez)

    L'information "--no-dev" permet de ne pas installer les bundles qui ne servent qu'en développement (make, profiler, ...). Si vous souhaitez être en développement sur ce serveur, vous devez retirer cette instruction.

    3) Nettoyer le cache de Symfony¶

    En production Syfmony utilise un système de cache très performant. Mais cela peut empêcher de voir vos dernières modifications. Il faut donc s'assurer de nettoyer le cache de Symfony.

    La commande suivante permet de supprimer le cache :

    4) Mettre à jour votre base de données

    Si vous avez ajouté des éléments dans vos entités, vous devez mettre à jour votre base de données. Pour cela vous pouvez executer la commande suivante :

    $form = $this->createForm(AdresseType::class, $adresse);
    $form = $this->createForm(JeuType::class, $jeu,
    [
        'codePostal' => '10000'
    ]);
        public function configureOptions(OptionsResolver $resolver): void
        {
            $resolver->setDefaults([
                'data_class' => Jeu::class,
            ]);
        }
       public function configureOptions(OptionsResolver $resolver): void
        {
            $resolver->setDefaults([
                'data_class' => Jeu::class,
                'codePostal' => null
            ]);
        }
    public function buildForm(FormBuilderInterface $builder, array $options): void
        {
            //dump($options);
            $codePostal = $options['codePostal'];
            $builder
                ->add('...')
                ...
            ;
        }
    ->add('editeur', EntityType::class, [
                    'class' => Editeur::class,
                    'choice_label' => 'libelle',
                    'query_builder' => function (EditeurRepository $fr) use ($codePostal) {
                        return $fr->createQueryBuilder('e')
                            ->where('e.cp = :codePostal')
                            ->setParameter('codePostal', $codePostal)
                        ;
                    },
                ])
    <button class="btn btn-primary ajoutJeu" data-id="{{ jeu.id }}" data-nom="{{ jeu.nom }}" data-prix="{{ jeu.prix }}">Ajouter au panier</button>
    document.querySelectorAll('.ajoutJeu').forEach(function(button) {
        button.addEventListener('click', function() {
            // Récupération des données du jeu
            let id = button.getAttribute('data-id');
            let nom = button.getAttribute('data-nom');
            let prix = button.getAttribute('data-prix');
    
            // Ajout du jeu au panier
            ajouterJeu(id, nom, prix);
        });
    });
    function ajouterJeu(id, nom, prix) {
        // Récupération du panier
        let panier = JSON.parse(localStorage.getItem('panier')) || [];
    
        // Vérification si le jeu est déjà dans le panier
        let jeu = panier.find(j => j.id == id);
        if (jeu) {
            jeu.quantite++;
        } else {
            panier.push({ id: id, nom: nom, prix: prix, quantite: 1 });
        }
    
        // Enregistrement du panier
        localStorage.setItem('panier', JSON.stringify(panier));
    }
     <h1>Panier</h1>
    
        <table class="table">
            <thead>
                <tr>
                    <th>Nom</th>
                    <th>Prix</th>
                    <th>Quantité</th>
                    <th>Total</th>
                </tr>
            </thead>
            <tbody id="panier">
            </tbody>
        </table>
    // dans panier.js
    document.addEventListener('DOMContentLoaded', function() {
        // Récupération du panier
        // détecter si un élément avec l'id panier existe, si oui appeler la fonction afficherPanier
        if (document.getElementById('panier')) {
            afficherPanier();
        }
    });
    
    
    function afficherPanier() {
        // Récupération du panier
        let panier = JSON.parse(localStorage.getItem('panier')) || [];
    
        // Affichage du panier
        let panierElement = document.getElementById('panier');
        panierElement.innerHTML = '';
        panier.forEach(function(jeu) {
            let tr = document.createElement('tr');
            tr.innerHTML = `
                <td>${jeu.nom}</td>
                <td>${jeu.prix} €</td>
                <td>${jeu.quantite}</td>
                <td>${jeu.prix * jeu.quantite} €</td>
            `;
            panierElement.appendChild(tr);
        });
    }
    if ($form->isSubmitted() && $form->isValid()) {
       ...
    }
    <?php
    
    namespace App\Form;
    
    ...
    use Symfony\Component\Validator\Constraints\Length;
    use Symfony\Component\Validator\Constraints\NotBlank;
    use Symfony\Component\Validator\Constraints\Positive;
    use Symfony\Component\Validator\Constraints\Type;
    
    class JeuType extends AbstractType
    {
        public function buildForm(FormBuilderInterface $builder, array $options): void
        {
            $builder
                ->add('nom', TextType::class, [
                    'label' => 'Nom du jeu',
                    'help' => 'Le nom du jeu',
                    'constraints' => [new Length(['min' => 2, 'max' => 100, 'minMessage' => 'Le nom doit faire au moins {{ limit }} caractères', 'maxMessage' => 'Le nom doit faire au plus {{ limit }} caractères'])],
                ])
                ->add('duree', TextareaType::class, [
                    'label' => 'Durée d\'une partie',
                    'help' => 'La durée en moyenne d\'une partie',
                    'constraints' => [new NotBlank(['message' => 'La durée moyenne d\'une partie ne peut pas être vide'])
                ]])
                ...
            ;
        }
    
       ...
    }
    <?php
    
    namespace App\Entity;
    
    use App\Repository\AdresseRepository;
    use Doctrine\ORM\Mapping as ORM;
    use Symfony\Component\Validator\Constraints as Assert;
    
    #[ORM\Entity(repositoryClass: EditeurRepository::class)]
    class Editeur
    {
        #[ORM\Column(length: 5)]
        #[Assert\Length(min: 5, max: 5)]
        private ?string $codePostal = null;
    ...
    }
    git clone https://...
    git pull origin master
    APP_ENV=prod #au lieu de APP_ENV=dev
    composer install --no-dev --optimize-autoloader
     APP_ENV=prod APP_DEBUG=0 php bin/console cache:clear
    bin/console doctrine:schema:update -f
    comme ORM dans son système par défaut. Nous allons utiliser Doctrine mais vous pouvez utiliser d'autres systèmes si vous le souhaitez. Doctrine peut-être géré de plusieurs façon : XML, JSON, YAML, PHP et en Annotation nous allons utiliser ce dernier format de données.

    La documentation officielle de Symfony sur l'ORM Doctrine La document officielle de Doctrine

    Vous êtes libre d'écrire le code qui permet le traitement métiers en dehors des entités et d'avoir votre propre logique d'organisation.

    Mise en application

    Configuration

    Comme à chaque fois, il est d'abord nécessaire d'installer les bundles nécessaires pour manipuler la base de données avec un ORM. Il vous faut donc exécuter la commande ci_dessous :

    On va également installer, si vous ne l'avez pas encore fait, le bundle "maker" qui contient des outils pour générer du code sous Symfony grâce à la console.

    Une fois ces deux éléments installés, il faut configurer la connexion à la base de données. Pour ce faire, il faut éditer le fichier .env à la racine de votre projet, qui doit normalement contenir une ligne d'exemple.

    Création de la base de données

    Une fois le fichier à jour avec vos données, vous pouvez créer votre base de données depuis la console.

    Les modifications de structure de votre base de données devront être réalisées avec la console pour que Symfony puisse faire le lien entre les tables et l'ORM.

    Création d'une entité liée à une table

    Utilisez la commande make:entity (qui est dans le bundle maker) pour avoir une série de question vous permettant de créer votre entité avec l'utilisation de l'ORM Doctrine. Vous pouvez créer une nouvelle entité ou modifier (ajouter des champs) une entité déjà existante en saisissant son nom.

    Vous allez devoir répondre à une suite de question avec le nom de l'entité (par défaut cela donnera le nom de la table), et les champs à créer. Dans Symfony une entité possède toujours un champs id, qui est la clé primaire et qui est auto-incrémenté. Vous ne devez donc pas l'ajouter dans la console.

    Pour la création d'un champs, il vous faudra donner :

    • son type

    • sa taille le cas échéant

    • si ce champs peut être null

    • s'il doit être unique (index)

    Vous pouvez obtenir la liste des types supportés en tapant "?" à la question du type.

    Une fois terminé, le fichier d'Entité et le repository associé sont générés.

    Exemple dans la console :

    Et le code de l'entité généré dans src/Entity/Product.php :

    A ce stade l'entité est créé, mais n'existe pas dans la base de données. Il reste deux étapes à exécuter.

    Mettre à jour votre base de données : méthode 1

    Sans générer de fichier de migration (qui contient toutes les instructions SQL à exécuter sur la base de données, notamment pour le déploiement d'une mise à jour)

    Mettre à jour votre base de données : méthode 2

    La création d'un fichier de migration qui va contenir le code SQL a exécuter en fonction de votre SGBD.

    La mise à jour de votre base de données en fonction du fichier précédemment généré.

    Si vous consultez votre PHPMyAdmin vous verrez la table apparaître.

    Modifications de champs et lien base de données

    Pour modifier des champs vous pouvez éditer directement le code généré dans la partie annotation: nom (par défaut le nom de la variable), taille, type.

    Pour ajouter des champs il vous faut relancer la commande make:entity en remettant le nom de votre entité.

    Après chaque modification ou ajout il faut de nouveau générer le fichier de migration et mettre à jour la base de données. Vous pouvez bien sûr modifier ou créer plusieurs entités avant de faire une mise à jour de votre base de données.

    ORM

    Une fois la base de données mise en place on va pouvoir insérer, modifier, supprimer et récupérer des informations de la base de données sans saisir de requêtes via des méthodes en initialisant l'entité fraichement créée :

    Il existe à la place de $em->persist, $em->remove($post); qui permettra de faire une suppression.

    Exercice

    • Configurer votre base de données (dupliquer le .env en .env.local et modifier les informations dans le .env.local)

    Si vous utilisez limage docker du cours, la ligne devrait être

    • Créer la base de données (bin/console doctrine:database:create)

    • Créer une entité (bin/console make:entity), nommée Categorie et ajouter les champs suivants

      • titre string 150 caractères

      • ordre int

    • L'id sera automatiquement ajouté

    • Créer un nouveau contrôleur nommé "CategorieTestController"

    • Ajouter une route pour créer un nouvel enregistrement

      • Créer un objet Categorie et compléter les informations (titre, ordre)

      • Récupérer la connexion à doctrine ($em = $this->getDoctrine()->getManager())

    • Appeler la route et vérifier que cela s'enregistre dans votre base de données

    • Essayer d'appeler la route plusieurs fois.

    Recherche d'entité

    Symfony et Doctrine proposes des requêtes prédéfinies, qui répondent aux usages les plus courant.

    Si $em est le manager associé à une entité :

    • $em->find($id); on récupère qu'un seul élément de l'entité avec l'id $id;

    • $em->findAll(); on récupère toutes les entrées de l'entité concernée

    • $em->findBy($where, $order, $limit, $offset); on recherche avec le tableau $where on tri avec le tableau $order on récupère $limit éléments à partir de l'élément $offset.

    • $em->findOneBy($where, $order); on récupère le premier élément respectant le tableau $where et trié avec le tableau $order;

    • $em->findByX($search); requêtes magiques où X correspond à n'importe quel champs défini dans votre entité

    • $em->findOneByX($search) ; requêtes magiques où X correspond à n'importe quel champs défini dans votre entité

      Par exemple findBySlug('home'); ou findByTitle('Bonjour); génèrera des requêtes de recherche automatiquement. Pour les requêtes avec plusieurs éléments il faudra faire une itération (foreach) ou lister les différents éléments.

    Exemple

    Si aucune requête prédéfinie ne correspond à vos besoin, vous pouvez bien sûr en créer une en passant par le repository.

    Vous pouvez également générer vos requêtes manuellement pour avoir une requête complexe et précise directement dans le controller mais idéalement il faudrait le placer dans le repository dédié.

    Et l'utiliser dans votre controller

    Modification

    Ce dernier code effectue une création dans la base de données; pour une modification il suffit de modifier l'instanciation de l'entité de la sorte :

    ici on récupère le repository de Post et on récupère l'id 1 ; tout le restant du code reste inchangé.

    Exercice

    • Créer une deuxième entité "Article" avec :

      • titre string 255

      • texte text

      • datePublication datetime

      • auteur string 255

      • image string 255

    • Ajoutez des données depuis phpMyAdmin dans la table Article ( 3 articles) ou avec un nouveau contrôler de test

    • Modifiez votre contrôleur et la page "/articles" pour afficher tous les articles de votre table, par ordre décroissant (plus récent au plus ancien)

    • Modifiez votre page d'accueil pour afficher le dernier article publié.

    Séance 6 : Localisation de l'application

    Le terme "internationalisation" (souvent abrégé i18n ) fait référence au processus d'extraction de chaînes et d'autres éléments spécifiques aux paramètres régionaux de votre application dans une couche où ils peuvent être traduits et convertis en fonction des paramètres régionaux de l'utilisateur (c'est-à-dire la langue et le pays). Pour le texte, cela signifie envelopper chacun avec une fonction capable de traduire le texte (ou "message") dans la langue de l'utilisateur :

    Le processus de traduction dans Symfony comporte plusieurs étapes :

    1. Activer et configurer le service de traduction de Symfony ;

    2. définir les chaînes (c'est-à-dire "messages") à localiser/traduire en les enveloppant dans des appels au traducteur ("Translations");

    3. Créer des ressources/fichiers de traduction pour chaque langue prise en charge qui traduisent chaque message dans l'application ;

    4. Déterminez, définissez et gérez les paramètres régionaux de l'utilisateur sur l'ensemble de la session de l'utilisateur.

    Activer et configurer le service de traduction de Symfony

    Il faut installer le bundle symfony/translation :

    Après l'installation, le service de traduction de Symfony est activé par défaut dans une application Symfony. Pour le configurer, il faut modifier le fichier config/packages/translation.yaml :

    Le paramètre default_locale définit la langue par défaut de l'application. Le paramètre default_path définit le chemin vers lequel Symfony doit rechercher les fichiers de traduction.

    Définir les chaînes à localiser/traduire

    Dans le code PHP

    Pour définir les chaînes à localiser/traduire, il faut les envelopper dans des appels au traducteur :

    Pour ce premier exemple, la traduction est gérée dans une méthode d'un de nos contrôleurs qui utilise le service translator injecté dans la méthode. La chaîne de caractères à traduire est passée en paramètre de la méthode trans() du service translator.

    Notez que nous avons ici la chaîne parfaitement compréhensible par l'utilisateur, mais nous pourrions aussi avoir une clé de traduction, par exemple home.welcome_message qui serait plus facile à gérer pour les développeurs.

    L'avantage de cette solution est que si la traduction n'existe pas, la chaîne de caractères passée en paramètre est retournée, et le texte reste donc lisible.

    Dans Twig

    Extraire les chaînes à traduire

    Il est possible de copier/coller les chaînes à traduire manuellement, mais Symfony propose une commande pour extraires automatiquement les textes à traduire depuis vos vues ou vos contrôlleurs (pour les autres, il faudra les ajouter à la main).

    Cette commande est à exécuter pour chaque locale de votre site (ici en). Le format permet de définir le type de fichier de sortie.

    Le --force va écraser les données existantes pour ajouter les clés manquantes.

    Créer des ressources/fichiers de traduction

    Pour créer des ressources/fichiers de traduction, il faut créer un fichier de traduction pour chaque langue prise en charge. Par exemple, pour la langue française, nous pouvons créer un fichier messages.fr.yaml dans le dossier translations :

    Le nom de fichier des fichiers de traduction est important : chaque fichier de message doit être nommé selon le chemin suivant domain.locale.loader:

    • domain (ici message) : Le domaine de traduction ;

    • locale (ici fr) : La locale à laquelle les traductions sont destinées (par exemple en_GB, en, etc.) ;

    • loader (ici yaml) : Comment Symfony doit charger et analyser le fichier (par exemple xlf, php, yaml, etc).

    Par défaut, Symfony fournit de nombreux "loader"" qui sont sélectionnés en fonction des extensions de fichiers suivantes :

    • .yaml: fichier YAML (vous pouvez également utiliser l'extension de fichier .yml) ;

    • .xlf: fichier XLIFF (vous pouvez également utiliser l'extension de fichier .xliff) ;

    • .php: un fichier PHP qui renvoie un tableau avec les traductions ;

    • .csv: fichier CSV ;

    Déterminer, définir et gérer les paramètres régionaux de l'utilisateur

    La traduction se produit en fonction des paramètres régionaux de l'utilisateur. La locale de l'utilisateur courant est stockée dans l'objet Request :

    Pour définir la locale de l'utilisateur, il faut utiliser la méthode setLocale() de l'objet Request :

    Symfony récupère automatiquement la locale pour choisir le bon fichier de traduction.

    Il est aussi possible de gérer la locale en utilisant les URL :

    La variable _locale est automatiquement injectée dans le contrôleur et peut être utilisée pour définir la locale de l'utilisateur.

    Exercices

    1. Traduire le menu et la page jeu (qui liste les jeux) de mmiple

    2. Créer un lien qui permet de changer de langue.

    Localisation des heures, des dates, ...

    Twig permet de faciliter la localisation des dates, des heures, ...

    Vous pouvez suivre les documentations selon vos besoins :

    • Les dates :

    • Les dates et heures :

    • Les heures :

    • Les monnaies :

    Dans tous les cas deux bundles sont nécessaires

    Ces deux dépendances nécessites d'installé sur le serveur.

    Exercices

    • Installez les éléments et testés différents cas de figure

      • avec les dates (afficher la date du jour sur la page d'accueil).

      • avec les monnaies à la place du filtre que nous avions écrit dans les séances précédentes.

    Séance 5 : Event Dispatcher/Listener

    Symfony (comme beaucoup de framework) émét des événements à chaque étape de son cycle de vie (préparation de la requête, traitement de la requête, génération de la réponse, etc.). Ces événements peuvent être écoutés par des listeners qui peuvent modifier le comportement de Symfony.

    Les événements permettent aussi d'éviter de surcharger un contrôleur ou un service. Par exemple, un utilisateur s'inscrit sur votre site. Vous souhaitez lui envoyer un email, puis une notifaction push et notifier d'autres utilisateurs d'un nouvel inscrit par exemple.

    1. Vous pourriez ajouter ces fonctionnalités dans le contrôleur qui gère l'inscription. Mais si vous avez besoin de faire la même chose dans un autre contrôleur, vous allez devoir dupliquer le code.

    2. Si vous souhaitez retirer l'un des comportements, vous allez devoir le retirer dans tous les contrôleurs qui l'utilisent.

    3. Si vous souhaitez ajouter un comportement vous allez devoir modifier tous les contrôleurs qui l'utilisent.

    Les points 2 et 3 ne sont pas forcément problèmatique si vous êtes propriétaire du code. Mais si vous développez un bundle ou utilisez un bundle existant, vous ne pourrez peut etre pas modifier le code.

    Dans ces trois exemples il est donc possible de définir nos événements qui pourront ensuite être écoutés par un ou plusieurs listeners, qui chacun pourra mener une action.

    Nous allons donc voir dans cette partie comment écouter des événements, et comment définir nos propres événements.

    Ecouter des événements (EventSubscriber)

    Symfony (ou votre application) émet des événements à chaque étape de son cycle de vie. Ces événements sont écoutés par des listeners qui peuvent modifier le comportement de Symfony.

    Pour écouter un événement, il faut créer une classe qui implémente l'interface Symfony\Component\EventDispatcher\EventSubscriberInterface. Cette interface contient une méthode getSubscribedEvents() qui retourne un tableau associatif. La clé est le nom de l'événement et la valeur est la méthode à appeler.

    Un exemple pourrait être :

    Dans cet exemple, la méthode onKernelRequest() sera appelée à chaque fois que l'événement kernel.request sera émis.

    Il existe aussi la possibilité de définir des listener avec une syntaxe différente qui ne va écouter qun' seul événement précis :

    Définir des événements (Event)

    Pour définir nos propres événements il faut deux choses :

    1. Définir une classe qui hérite de Symfony\Component\EventDispatcher\Event qui va contenir la description et les données de notre événement.

    2. "Emettre" l'événement avec la méthode dispatch() de l'objet Symfony\Component\EventDispatcher\EventDispatcherInterface quelque part dans notre application en lui passant un objet du type de la classe de l'événnement précédemment créée.

    Classe décrivant notre événement

    Pour définir notre événement, il faut créer une classe qui hérite de Symfony\Component\EventDispatcher\Event. Cette classe doit contenir les données de l'événement. Par exemple, si nous voulons définir un événement qui sera émis à chaque fois qu'un utilisateur s'inscrit, nous pourrions avoir une classe comme celle-ci :

    Emettre l'événement

    Pour émettre l'événement, il faut récupérer l'objet Symfony\Component\EventDispatcher\EventDispatcherInterface et appeler la méthode dispatch() en lui passant en premier paramètre le nom de l'événement et en second paramètre l'objet de l'événement.

    Et bien sûr définir un listener pour écouter l'événement, comme décrit dans la partie précédente, et qui pourrait être pour notre exemple :

    Exercices

    Exercice 1

    Créer un événement UserRegisteredEvent qui sera émis à chaque fois qu'un utilisateur s'inscrit. Créer un listener qui enverra un email (en utilisant notre service) à l'utilisateur pour lui dire qu'il a bien été enregistré.

    Exercice 2

    Créer deux événements : ADDED et UPDATED (mais un seul fichier JeuEvent) qui seront émis à chaque fois qu'un utilisateur ajoute ou modifie un jeu. Créer un listener qui enverra un email (en utilisant notre service) à l'administrateur du site ([email protected]) pour lui indiquer qu'un jeu a été mis à jour ou ajouté avec les informations dans l'email.

    On utiliserai un email avec template pour afficher toutes les informations du jeu. Vous pouvez/devez modifier la méthode sendTemplatedMail pour qu'elle prenne en paramètre un nom de fichier Twig qui sera utilisé comme modèle pour chaque mail.

    Séance 9 : Mails

    Mise en place d'une page de contact

    Installation du composant de mail

    Cette commande n'est pas nécessaire si vous avez installé le projet Symfony avec "webapp"

    Il faut ensuite configurer le serveur de mail qui sera utilisé.

    DATABASE_URL="mysql://root:123456@mariadb:3306/nomDeLaBDD?charset=utf8mb4"
    composer require symfony/orm-pack
    composer require symfony/maker-bundle --dev
    # .env
    
    # customize this line!
    DATABASE_URL="mysql://db_user:[email protected]:3306/db_name"
    
    # to use sqlite:
    # DATABASE_URL="sqlite:///%kernel.project_dir%/var/app.db"
    php bin/console doctrine:database:create
    php bin/console make:entity
    php bin/console make:entity
    
    Class name of the entity to create or update:
    > Product
    
     to stop adding fields):
    > name
    
    Field type (enter ? to see all types) [string]:
    > string
    
    Field length [255]:
    > 255
    
    Can this field be null in the database (nullable) (yes/no) [no]:
    > no
    
     to stop adding fields):
    > price
    
    Field type (enter ? to see all types) [string]:
    > integer
    
    Can this field be null in the database (nullable) (yes/no) [no]:
    > no
    
     to stop adding fields):
    >
    (press enter again to finish)
    // src/Entity/Product.php
    namespace App\Entity;
    
    use Doctrine\ORM\Mapping as ORM;
    use Doctrine\DBAL\Types\Types;
    
    #[ORM\Entity(repositoryClass:"App\Repository\ProductRepository")]
    class Product
    {
        #[ORM\Id]
        #[ORM\GeneratedValue]
        #[ORM\Column]
        private $id;
    
        #[ORM\Column(type:Types::STRING, length:255)]
        private $name;
    
        #[ORM\Column(type:Types::INTEGER)]
        private $price;
    
        public function getId()
        {
            return $this->id;
        }
    
        // ...Les getters et les setters sont automatiquement générés également. Par défaut il n'y a pas de constructeur. Vous pouvez en ajouter un si besoin.
    }
    php bin/console doctrine:schema:update -f
    
    //ou
    php bin/console d:s:u -f
     php bin/console make:migration
    php bin/console doctrine:migrations:migrate
    use Doctrine\ORM\EntityManagerInterface;
    
    ...
    
    #[Route("/test", name:"test")]
    public function test(EntityManagerInterface $entityManager)
    {
        $post = new Post(); // initialise l'entité
        $post->setTitle('Mon titre'); // on set les différents champs
        $post->setEnable(true);
        $post->setDateCreated(new \Datetime);
    
        $entityManager->persist( $post ); // on déclare une modification de type persist et la génération des différents liens entre entité
        $entityManager->flush(); // on effectue les différentes modifications sur la base de données 
        // réelle
    
        return new Response('Sauvegarde OK sur : ' . $post->getId() );
    }
    // Modifications multiples : 
    #[Route("/est", name="test")]
    public function test(
            EntityManagerInterface $entityManager,
            PostRepository $postRepository)
    {
        // récupération de tous les posts
        $posts = $postRepository->findAll(); 
        //équivalent à SELECT * FROM post
    
        foreach($posts as $post)
        {
            $post->setTitle('Mon titre ' . $post->getId() ); // on set les différents champs
        }
    
        $entityManager->flush(); // on effectue les différentes modifications sur la base de données 
        // réelle
    
        return new Response('Sauvegarde OK ');
    }
    // src/AppBundle/Repository/Post.php
    
    public function maRequete( $where )
    {
        // avec querybuilder
        $queryBuilder = $this->createQueryBuilder("p")
            ->where(' p.title like :w')
            ->setParameter(':w', '%'.$where.'%')
            ->getQuery(); // on récupère la requêtes 
    
       return $query->getResult(); // on renvoie le résultat
    }
    //OU
     public function maRequeteSQL( $where )
        {
            // avec requête SQL
            $em = $this->getEntityManager();
            $query = $em->createQuery('SELECT p from AppBundle:Post p 
        WHERE p.title like :w');
    
            $query->setParameter(':w', '%'.$where.'%');
    
    
            return $query->getResult(); // on renvoie le résultat
         }
    }
    // src/AppBundle/Controller/DefautController
    $postRepository->maRequete('test');
    use App\Repository\PostRepositoy;
    use Doctrine\ORM\EntityManagerInterface;
    
    ...
    
    #[Route("/test/modification", name:"test")]
    public function testModification(
            EntityManagerInterface $entityManager,
            PostRepository $postRepository)
    {
        // récupération du post avec id 1 
        $post = $postRepository->find(1); 
        //equivalent à SELECT * FROM post WHERE id=1
        
        $post->setTitle('Mon titre'); // on set les différents champs
        $post->setEnable(true);
        $post->setDateCreated(new \Datetime);
    
        $entityManager->flush(); // on effectue les différentes modifications sur la base de données 
        // réelle
    
        return new Response('Sauvegarde OK sur : ' . $post->getId() );
    }
    // text will *always* print out in English
    echo 'Hello World';
    
    // text can be translated into the end-user's language or
    // default to English
    echo $translator->trans('Hello World');
    Associer l'instance de Categorie avec l'ORM ($em->persist($categorie))
  • Enregistrer dans la base de données ($em->flush())

  • https://symfony.com/doc/current/event_dispatcher.html#creating-an-event-listener
    https://symfony.com/doc/current/components/event_dispatcher.html
    <?php
    
    namespace App\EventSubscriber;
    
    use Symfony\Component\EventDispatcher\EventSubscriberInterface;
    
    class MyEventSubscriber implements EventSubscriberInterface
    {
        public static function getSubscribedEvents()
        {
            return [
                'kernel.request' => 'onKernelRequest',
            ];
        }
    
        public function onKernelRequest()
        {
            // ...
        }
    }
    
    <?php
    
    namespace App\Event;
    
    use Symfony\Contracts\EventDispatcher\Event;
    
    class UserRegisteredEvent extends Event
    {
        const NAME = 'user.registered';
    
        private $user;
    
        public function __construct(User $user)
        {
            $this->user = $user;
        }
    
        public function getUser()
        {
            return $this->user;
        }
    }
    
    <?php
    
    namespace App\Controller;
    
    use App\Event\UserRegisteredEvent;
    use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
    use Symfony\Component\EventDispatcher\EventDispatcherInterface;
    use Symfony\Component\HttpFoundation\Response;
    use Symfony\Component\Routing\Annotation\Route;
    
    class UserController extends AbstractController
    {
        /**
         * @Route("/register", name="user_register")
         */
        public function register(EventDispatcherInterface $eventDispatcher): Response
        {
            // ...
    
            $event = new UserRegisteredEvent($user);
            $eventDispatcher->dispatch($event, UserRegisteredEvent::NAME);
    
            // ...
        }
    }
    
    <?php
    
    namespace App\EventSubscriber;
    
    use App\Event\UserRegisteredEvent;
    
    class UserRegisteredSubscriber implements EventSubscriberInterface
    {
        public static function getSubscribedEvents()
        {
            return [
                UserRegisteredEvent::NAME => 'onUserRegistered',
            ];
        }
    
        public function onUserRegistered(UserRegisteredEvent $event)
        {
            // ...
        }
    }
    

    .json: fichier JSON ;

  • .ini: fichier INI ;

  • ... https://symfony.com/doc/current/translation.html#translation-resource-file-names-and-locations

  • https://twig.symfony.com/doc/3.x/filters/format_date.html
    https://twig.symfony.com/doc/3.x/filters/format_datetime.html
    https://twig.symfony.com/doc/3.x/filters/format_time.html
    https://twig.symfony.com/doc/3.x/filters/format_currency.html
    php-intl
    Pour cela modifier le fichier .env.local (rappel: on ne modifie jamais le .env avec vos données personnelles).

    Modifier la ligne :

    Symfony propose par défaut l'usage du SMTP, mais vous pouvez ajouter d'autres gestionnaires de mail (gmail, mailChimp...). La liste ici : https://symfony.com/doc/current/mailer.html#using-a-3rd-party-transport

    Installation d'un outil SMTP et Webmail

    Si vous ne disposez pas d'un serveur SMTP, vous pouvez en installer un fictif, via docker, qui permettra de gérer les mails en environnement de développement (notamment ne pas les envoyer réellement aux destinataires, les consulter...).

    Dans votre fichier docker-compose.yml ajoutez le service suivant

    Ce service ajoute deux choses :

    • Le serveur smtp sur le port 1025 => Modifier le Mailer_DSN :

    • un webmail

      Ouvrir dans votre navigateur : http://127.0.0.1:8081/#/

    Envoyer un mail simple

    Prenons l'exemple de Symfony

    Cet exemple ajoute une route (email), qui est associé à une méthode (sendEmail) dans le contrôleur (MailerController).

    Modifiez les adresses ligne 16 (l'expéditeur) et ligne 17 (le destinataire).

    Vous pourriez ajouter un ensemble d'option (cc, bcc, reply...)

    • Ligne 22 : C'es le sujet de votre mail

    • Ligne 23 : C'est votre email au format texte brut (sans HTML donc)

    • Ligne 24 : C'est le contenu du "body" de votre mail au format HTML

    Dans cette construction, le mail est envoyé au format HTML et au format texte, c'est votre outil de mail qui choisira le plus approprié à afficher. Dans l'exemple les contenus sont différents selon le format, ca n'est qu'un exemple bien sûr.

    A faire

    Saisir le code, configurer le serveur de mail, modifier les emails et le texte.

    Profiler

    Profiler

    En mode développement, on interdit la distribution des mails réellement (on définit MAILER_DSN=null). Pour voir les mails, on peut donc utiliser le profiler (le dernier icône de la barre).

    On peut aussi utiliser un outil type MailTrap, mailDev, ... qui va simuler un SMTP et récupérer l'ensemble des mails envoyés par Symfony.

    On peut voir l'ensemble des éléments du mail, le format HTML, Text, brut, l'en-tête...

    Envoyer un mail au format HTML

    La solution précédente est fonctionnelle, mais pas très pratique si vous souhaitez faire un long mail, ou un mail complexe, ou encore gérer de nombreuses données.

    Il est donc possible de construire un email en utilisant la puissance de Twig et des templates.

    Dans ce deuxième exemple on créé un objet TemplatedEmail qui permet de manipuler des templates Twig.

    La structure de base ne change pas (destinataires,... sujet).

    • Ligne 21 : on associe un template (dans le répertoire templates, puis emails/signup.html.twig)

    • Ligne 24-27 : on passe les paramètres nécessaires à notre vue (comme on le ferait depuis un contrôleur).

    Le template de mail pourrait ressembler à :

    Dans cet exemple seul le format HTML est proposé. SI l'outil de mail ne supporte pas le format HTML il va afficher un format texte brut que Symfony aussi essayé de construire automatiquement à partir de votre format HTML

    Pour ajouter le format texte du mail on peut passer un template spécifique pour le format texte, qui va utiliser les mêmes données fournies par context.

    Ce deuxième template (txt) ne devra pas contenir de balises HTML.

    Tester

    Tester ce second envoi d'email en format HTML et texte

    Gestion des adresses emails

    Dans Symfony il existe plusieurs façon de gérer les adresses emails. Vous trouverez ci-après quelques exemples.

    Faire un formulaire de contact

    Exercice :

    Créer un formulaire de contact.

    • Créer un nouveau contrôleur nommé ContactController

    • Ajouter une méthode index qui va se charger d'afficher un formulaire (adresse de l'expéditeur, sujet, message), vous serez le destinataire par défaut..

      • Vous pouvez créer le formulaire directement dans le template en HTML ou avec l'objet FormBuilder à votre convenance.

    • Ajouter une méthode sendMail qui va se charger d'envoyer l'email en fonction des données du formulaire

      • On utilisera Request pour récupérer les données du formulaire

      • L'email sera envoyé à vous et en copie à la personne ayant complété le formulaire

      • Le champ reply sera configuré pour répondre à l'expéditeur

      • Le mail sera au format texte et HTML

    Séance 3 : AssetMapper

    Depuis la version 7 de Symfony, il est conseillé d'utiliser une version sans webpack : https://symfony.com/doc/current/frontend/asset_mapper.html

    Introduction

    Extrait de la documentation officielle de Symfony :

    Le composant AssetMapper vous permet d'écrire du JavaScript et du CSS modernes sans la complexité d'utiliser un bundler. Les navigateurs prennent déjà en charge de nombreuses fonctionnalités modernes de JavaScript comme l'instruction import et les classes ES6. Et le protocole HTTP/2 signifie que combiner vos assets pour réduire les connexions HTTP n'est plus urgent. Ce composant est une couche légère qui aide à servir vos fichiers directement au navigateur.

    Le composant a deux fonctionnalités principales :

    Mappage et versionnage des assets : Tous les fichiers à l'intérieur du répertoire assets/ sont rendus disponibles publiquement et versionnés. Vous pouvez référencer le fichier assets/images/product.jpg dans un template Twig avec {{ asset('images/product.jpg') }}. L'URL finale inclura un hash de version, comme /assets/images/product-3c16d92m.jpg.

    Importmaps : Une fonctionnalité native du navigateur qui facilite l'utilisation de l'instruction import de JavaScript (par exemple, import { Modal } from 'bootstrap') sans système de build. Elle est prise en charge par tous les navigateurs (grâce à un shim) et fait partie de la norme HTML.

    Installation

    Pour installer le composant AssetMapper, vous devez exécuter la commande suivante :

    Ces éléments sont déjà installés si vous avez installé le bundle webapp.

    Plusieurs fichiers ont été ajoutés à votre projet :

    • assets/app.js Your main JavaScript file;

    • assets/styles/app.css Your main CSS file;

    • config/packages/asset_mapper.yaml Where you define your asset "paths";

    Et dans base.html.twig, vous avez maintenant :

    Réorganiser vos images

    Il est recommandé de déplacer vos images (celles de votre site : logo, bannière, décoration, ...) dans le répertoire assets/** pour les rendre accessibles publiquement. Vous pouvez ensuite les référencer dans vos templates Twig avec {{ asset('images/product.jpg') }} si vous avez un répertoire images dans assets.

    Les images uploadées par les utilisateurs (avatars, photos de profil, ...) ne doivent pas être déplacées dans assets/. Elles doivent être stockées dans un répertoire public (comme public/uploads/) et référencées directement dans vos templates Twig.

    Si vous regardez la source de la page, vous verrez que les images sont maintenant servies avec un hash de version dans l'URL. Cela signifie que les navigateurs peuvent mettre en cache les images indéfiniment, mais que si vous modifiez une image, le navigateur téléchargera la nouvelle version.

    En production il faudra "compiler" les assets avec la commande :

    Il est possible de lister tous les assets à disposition avec la commande :

    Utiliser du JavaScript moderne

    Le composant AssetMapper utilise Importmaps pour permettre l'utilisation de l'instruction import de JavaScript sans système de build. Cela signifie que vous pouvez écrire du JavaScript moderne sans avoir à vous soucier de la compilation.

    Par exemple, en suivant la documentation de Symfony, nous allons créer un fichier javascript assets/duck.js :

    Ce code JavaScript crée une classe Duck avec une méthode quack() qui affiche un message dans la console.

    Pour utiliser ce code dans un fichier assets/app.js, vous pouvez importer le fichier duck.js :

    Comme nous avons déjà ajouté le fichier app.js dans le fichier base.html.twig avec l'instruction {{ importmap('app') }}, vous pouvez maintenant ouvrir la console de votre navigateur pour voir le message Waddles says: Quack!. Et voilà !

    Importer des dépendances externes

    Pour ajouter un package externe, vous pouvez utiliser la commande suivante :

    Pour ajouter une dépendance à Bootrstrap. Vous pouvez ajouter tous les packages disponibles sur Npm (https://www.npmjs.com/) avec cette commande.

    Cela ajoutera une entrée dans le fichier importmap.php :

    Cette commande peut installer plusieurs lignes si la librairie a des dépendances. Automatiquement les fichiers sont télécargés et ajoutés dans le répertoire assets/vendor/. Ce dossier ne doit pas être modifié ni ajouté dans Git

    Vous pouvez maintenant utiliser Bootstrap dans votre fichier app.js :

    Importer des fichiers CSS

    Pour des fichiers CSS internes

    Il faut ajouter dans app.js votre fichier CSS :

    Si styles est un dossier dans assets.

    Pour des fichiers CSS externes

    Pour importer des fichiers CSS, vous pouvez utiliser la commande suivante :

    Puis l'importer dans votre fichier app.js :

    Exercices

    Exercice 1

    Tester l'exemple de duck.js et app.js. Modifier le message de la console.

    Exercice 2

    Ajouter Bootstrap à votre projet. Créer un fichier assets/styles/app.css et ajouter une couleur de fond à la page. Ajouter une couleur de texte.

    Exercice 3

    Ajoutez une dépendance à une librairie d'icône (par exemple Font Awesome) et ajoutez un icône à votre page dans le menu.

    Modifiez le filtre Twig créé sur la séance d'avant pour ajouter des étoiles à la place des * ou -.

    Précédemment dans Symfony, Webpack Encore et CSS

    Dans les versions 4 à 6 la recommandation était d'utiliser Webpack Encore pour gérer les assets de votre projet Symfony.

    Ci-dessous le cours sur Webpack Encore, si besoin.

    Webpack () est un outil logiciel open-source de type « module bundler » (littéralement, « groupeur de modules »), conçu pour faciliter le développement et la gestion de sites et d'applications web modernes. (source Wikipedia).

    Webpack va permettre de gérer vos fichiers CSS/SCSS, JS, images, etc. Il va permettre de les regrouper, de les minifier, de les compiler, etc. Il permet aussi d'utiliser des langages comme VueJS, React, etc.

    Utiliser Webpack dans un projet Symfony va vous permettre de gérer vos fichiers CSS/SCSS, JS, images, etc. comme dans un projet classique, puis de les regrouper, de les minifier, de les compiler, etc. et de les mettre dans le répertoire public pour qu'ils soient accessibles de vos vues.

    Installation

    Pour installer et utiliser Webpack, vous devez installer NodeJS (), pour disposer de npm () ou de yarn ().

    Dans le contexte de Symfony, nous n'allons pas utiliser directement Webpack, mais une version adaptée de Webpack appelée Encore (). Ce package permet de simplifier l'utilisation de Webpack dans un projet Symfony, avec une gestion dans le projet et dans twig.

    Pour installer Encore, il faut utiliser la commande suivante :

    puis lancer la commande suivante :

    Pour installer les dépendances "front".

    Ces installations ont eu plusieurs effets :

    • Le fichier package.json a été créé. Ce fichier contient les dépendances "front" de votre projet. Il est important de ne pas le supprimer.

    • Le fichier webpack.config.js a été créé. Ce fichier contient la configuration de Webpack, et la liste de toutes les tâches qu'il doit réaliser. Il est important de ne pas le supprimer.

    • Le répertoire assets a été créé. Ce répertoire contient tous les fichiers "front" de votre projet (SCSS, CSS, JS, ...), dans leur version non compilée.

    Regardons en détail ces fichiers.

    package.json

    webpack.config.js

    Le répertoire assets/

    base.html.twig

    Utilisation

    Nous avons quelques données d'exemples dans les fichiers du repertoire assets. Mais si vous essayez de les utiliser, vous allez avoir une erreur. Cela est lié au fait que Webpack n'a pas encore été lancé, et n'a donc pas généré les fichiers compilés dans le répertoire public/build.

    Donc, pour utiliser les fichiers "front" de votre projet, vous devez lancer la commande suivante :

    Cette commande va lancer Webpack, et va générer les fichiers compilés dans le répertoire public/build. Un fichier entrypoints.json a été créé dans le répertoire public/build. Ce fichier contient la liste des fichiers compilés, et les chemins vers ces fichiers. C'est grâce à ce fichier que Twig va pouvoir charger les fichiers compilés.

    Retournez dans votre navigateur pour réessayer d'afficher votre page, elle devrait fonctionner.

    Essayez de modifier le fichier css dans le repertoire assets pour voir les changements. N'oublier pas de relancer la commande yarn encore dev pour voir les changements.

    Comme il peut être fastidieux de relancer la commande à chaque fois que vous modifiez un fichier, vous pouvez utiliser la commande suivante :

    Cette commande va lancer Webpack, et va générer les fichiers compilés dans le répertoire public/build. En plus, elle va surveiller les fichiers du répertoire assets, et va relancer Webpack à chaque fois qu'un fichier est modifié, tant que vous ne fermez pas la fenêtre de votre terminal.

    Les fichiers SCSS

    Si vous souhaitez utiliser des fichiers SCSS, vous devez créer ces fichiers, et les importer dans le fichier app.scss (le fichier app.css doit être renommé en app.scss) du répertoire assets. Par exemple, si vous souhaitez créer un fichier style.scss dans le répertoire assets/scss, vous devez ajouter la ligne suivante dans le fichier app.scss :

    Vous devez ensuite expliquer à webpack comment compiler ce fichier SCSS. Pour cela, vous devez ajouter (en fait décommenter) la ligne suivante dans le fichier webpack.config.js :

    Comme vous avez modifié le fichier webpack.config.js, vous devez relancer la commande yarn watch ou npm run watch pour que les changements soient pris en compte.

    Vous allez obtenir une erreur, car il manque quelques dépendances. Suivez les instructions du terminal et installez les dépendances manquantes.

    Relancez la commande yarn watch ou npm run watch pour voir les changements.

    Vous pouvez maintenant mettre en pratique ce que vous savez faire en SCSS/SASS dans un projet Symfony, sans avoir besoin de compiler vos fichiers SCSS/SASS à la main.

    Exercices

    Exercice 1

    Créer un fichier style.scss dans le répertoire assets/scss, et ajouter une couleur de fond à la page. Ajoutez aussi une couleur de texte.

    Créer et modifier des contrôleurs et des vues pour tester votre style.

    Exercice 2

    En vous appuyant sur la documentation de Boostrap et de Symfony, ajouter Bootstrap à votre projet (ni en CDN, ni en téléchargement, mais en l'installant avec npm ou yarn).

    Séance 3 et 4 : Controller, Routes et vues : les bases de notre fil rouge

    Objectifs

    Tout au long des prochaines séances nous allons mettre en place un système de blog simple permettant de poster des articles, d'obtenir des commentaires et de gérer les accès avec des droits différenciés.

    Ce projet va évoluer au fur et à mesure des connaissances de Symfony.

    composer require symfony/translation
    # config/packages/translation.yaml
    framework:
        default_locale: 'en'
        translator:
            default_path: '%kernel.project_dir%/translations'
    // src/Controller/DefaultController.php
    namespace App\Controller;
    
    use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
    use Symfony\Contracts\Translation\TranslatorInterface;
    //...
    
    class DefaultController extends AbstractController
    {
        public function index(TranslatorInterface $translator)
        {
            $translated = $translator->trans('Symfony is great');
            //....
            return $this->render('default/index.html.twig', [
                'translated' => $translated,
            ]);
        }
    }
    // La forme la plus simple
    {{ 'symfony is great'|trans }}
    
    // avec un domaine
    {{ 'symfony is great'|trans({},'message') }}
    
    // avec des paramètres
    {{ 'Symfony %version% is great'|trans({'%version%': 7}) }}
    bin/console translation:extract en --format yaml --force
    # translations/messages.fr.yaml
    Symfony is great: Symfony est génial
    use Symfony\Component\HttpFoundation\Request;
    
    public function index(Request $request)
    {
        $locale = $request->getLocale();
    }
    use Symfony\Component\HttpFoundation\Request;
    
    public function index(Request $request)
    {
        $request->setLocale('fr'); //on pourrait associer une route pour changer de langue et sauvegarder la langue de l'utilisateur dans la session
    }
    // src/Controller/ContactController.php
    namespace App\Controller;
    
    // ...
    class ContactController extends AbstractController
    {
        #[Route(
            path: '/{_locale}/contact',
            name: 'contact',
            requirements: [
                '_locale' => 'en|fr|de',
            ],
        )]
        public function contact()
        {
        }
    }
    composer require twig/intl-extra
    composer require twig/extra-bundle
    MAILER_DSN=smtp://maildev_docker_symfony:1025
    composer require symfony/mailer
    MAILER_DSN=smtp://user:[email protected]:port
        maildev:
            image: maildev/maildev
            container_name: maildev_docker_symfony
            command: bin/maildev --web 80 --smtp 1025 --hide-extensions STARTTLS
            restart: always
            ports:
                - 8081:80
    // src/Controller/MailerController.php
    namespace App\Controller;
    
    use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
    use Symfony\Component\HttpFoundation\Response;
    use Symfony\Component\Mailer\MailerInterface;
    use Symfony\Component\Mime\Email;
    use Symfony\Component\Routing\Annotation\Route;
    
    class MailerController extends AbstractController
    {
        #[Route('/email')]
        public function sendEmail(MailerInterface $mailer): Response
        {
            $email = (new Email())
                ->from('[email protected]')
                ->to('[email protected]')
                //->cc('[email protected]')
                //->addCc('[email protected]')
                //->addTo('[email protected]')
                //->bcc('[email protected]')
                //->replyTo('[email protected]')
                //->priority(Email::PRIORITY_HIGH)
                ->subject('Time for Symfony Mailer!')
                ->text('Sending emails is fun again!')
                ->html('<p>See Twig integration for better HTML integration!</p>');
    
            $mailer->send($email);
    
            // ...
        }
    }
    // src/Controller/MailerController.php
    namespace App\Controller;
    
    use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
    use Symfony\Component\HttpFoundation\Response;
    use Symfony\Component\Mailer\MailerInterface;
    use Symfony\Bridge\Twig\Mime\TemplatedEmail;
    use Symfony\Component\Routing\Annotation\Route;
    
    class MailerController extends AbstractController
    {
        #[Route('/email2')]
        public function sendEmail2(MailerInterface $mailer): Response
        {
          $email = (new TemplatedEmail())
                  ->from('[email protected]')
                  ->to(new Address('[email protected]'))
                  ->subject('Thanks for signing up!')
              
                  // path of the Twig template to render
                  ->htmlTemplate('emails/signup.html.twig')
              
                  // pass variables (name => value) to the template
                  ->context([
                      'expiration_date' => new \DateTime('+7 days'),
                      'username' => 'foo',
                  ])
              ;
    
            // ...
            $mailer->send($email);
        }
    }
    
    {# templates/emails/signup.html.twig #}
    <h1>Welcome {{ email.toName }}!</h1>
    
    <p>
        You signed up as {{ username }} the following email:
    </p>
    <p><code>{{ email.to[0].address }}</code></p>
    
    <p>
        <a href="#">Click here to activate your account</a>
        (this link is valid until {{ expiration_date|date('F jS') }})
    </p>
    ->textTemplate('emails/signup.txt.twig')
    // ...
    use Symfony\Component\Mime\Address;
    
    $email = (new Email())
        // email address as a simple string
        ->from('[email protected]')
    
        // email address as an object
        ->from(new Address('[email protected]'))
    
        // defining the email address and name as an object
        // (email clients will display the name)
        ->from(new Address('[email protected]', 'Fabien'))
    
        // defining the email address and name as a string
        // (the format must match: 'Name <[email protected]>')
        ->from(Address::create('Fabien Potencier <[email protected]>'))
    
        // ...
    ;
    importmap.php Your importmap config file.
    https://webpack.js.org/
    https://nodejs.org/en/
    https://www.npmjs.com/
    https://yarnpkg.com/
    https://symfony.com/doc/current/frontend.html
    composer require symfony/asset-mapper symfony/asset 
    
    {% block javascripts %}
        {% block importmap %}{{ importmap('app') }}{% endblock %}
    {% endblock %}
    php bin/console asset-map:compile
    php bin/console debug:asset-map
    export default class {
        constructor(name) {
            this.name = name;
        }
        quack() {
            console.log(`${this.name} says: Quack!`);
        }
    }
    import Duck from './duck.js';
    
    const duck = new Duck('Waddles');
    duck.quack();
    php bin/console importmap:require bootstrap
    return [
        'app' => [
            'path' => './assets/app.js',
            'entrypoint' => true,
        ],
        'bootstrap' => [
            'version' => '5.3.0',
        ],
    ];
    import 'bootstrap';
    // ou
    import { Alert } from 'bootstrap'; //pour n'importer que les Alertes par exemple
    import '../styles/app.css';
    bin/console importmap:require bootstrap/dist/css/bootstrap.min.css
    import 'bootstrap/dist/css/bootstrap.min.css';
    composer require webpack-encore
    yarn install ou npm install
    yarn dev ou npm run dev
    yarn watch (qui est une version courte de yarn dev --watch) ou npm run watch
    @import 'scss/style.scss';
    .enableSassLoader()
    Les concepts vont être vus progressivement selon nos besoins pour le projet. Ils ne seront donc jamais décris de manière exhaustive dans ce cours. Vous pouvez avoir l'ensemble des détails dans la documentation officielle de Symfony : https://symfony.com/doc/current/index.html

    Par ailleurs vous pouvez aussi trouver un exemple complètement décrit de manière progressive dans le livre de Fabien Pontetier : https://symfony.com/book

    Présentation des routes et des contrôleurs

    Une route permet de diriger une url (ou un pattern d'url) vers une méthode de controller appelée Action.

    La documentation officielle de Symfony sur les routes et La documentation officielle de Symfony sur les controllers

    Il est possible de décrire des routes selon les formats de fichiers : XML, Classe PHP, en annotation ou avec les Attributes (php8 et +). Pour plus de commodité nous utiliserons les Attributes.

    Structures d'une route

    Une route peut être constante : /blog ou dynamique : /blog/{slug}. Ici slug englobé de { } devient une variable dynamique qui prend tous les caractères alphanumériques par exemple : /blog/42, /blog/lorem-ipsum, /blog/titi-32\_tata Ces 3 urls correspondent à la méthode ciblée par la route avec une variable slug différente. Cette variable peut être récupérée par le controller.

    • Une route est au minimum un chemin (path) et un nom;

    • Ces variables peuvent être mises par défaut grâce à "defaults"

    • Ces variables peuvent être soumises à une validation de format via "requirements"

    • On peut définir un path différent en fonction de la locale

    • Vous pouvez cumuler plusieurs routes pour une méthode Action

    • On peut spécifier le moyen d'accès à une route (GET, POST, PUT, ...), avec le paramètre "methods"

    Attributes

    Pour pouvoir utiliser les Attributes il faut :

    à ajouter après le namespace dans votre Controller.

    Routes et paramètres

    Un paramêtre

    On définit une variable d'url via des accolades {ma_variable} :

    Pour récupérer la variable dans le controller, il suffit de déclarer une variable dans la méthode avec le même nom que la variable d'url.

    Valeur par défaut

    Pour définir une valeur par défaut, on peut préciser la valeur sur le paramêtre de la méthode :

    Si l'utilisateur utilise l'URL /page/, la variable $page sera égale à 1.

    plusieurs paramètres

    On peut cumuler plusieurs variables :

    On pourrait utiliser un autre séparation que le slash, comme par exemple -, . ou _. La seule condition étant que Symfony puisse être en mesure de différencier les paramètres.

    Génération d'url

    Pour générer une url en PHP on utilise :

    Ou sous TWIG on a deux fonctions :

    Controller

    Les routes redirigent vers une méthode d'un contrôleur (une action); un controller Symfony se nomme de la sorte : NomDuController où le suffixe Controller est obligatoire et le nom du fichier et de la classe est en CamelCase.

    Les différentes méthodes se nomment de la sorte :

    nomDeLaMethode est lui en miniCamelCase

    Dans le cas idéal, le controller doit contenir le minimum de code possible (20 lignes maximum selon les standards de Symfony). Il ne doit que faire le lien entre les différents éléments de l'application et une réponse.

    Response

    Une méthode d'un contrôleur renvoie toujours un type Response ; il existe plusieurs type de Response : JsonResponse, RedirectResponse, HttpResponse, BinaryFileResponse etc ...

    La plus utilisée est Response pour l'utiliser on va ajouter dans le "use" suivant :

    et dans la méthode action on :

    Affiche à l'écran Ma response. Dans l'état cette réponse n'est pas du HTML, car rien n'est précisé dans le retour de la méthode.

    Une méthode render() (définie quand la classe AbstractController dont votre controller doit hériter) permet aux méthodes de récupérer une vue et d'afficher le contenu de la vue compilée avec les différentes variables envoyées.

    Ici on va récupérer le template présent dans templates/default/index.html.twig pour affecter la variable variable.

    Exercice 1

    Les premières étapes consistent à mettre en place les bases de notre projet :

    • Une nouvelle installation de Symfony, pour un projet nommé blog

      • On installera toutes les dépendances en une seule fois pour plus de confort : composer require webappdans le dossier blog/

    • Un contrôleur et la méthode pour la page d'accueil et la route "/"

    • La vue associée à la page d'accueil

    • Une page de contact avec une route "/contact" et une vue associée

    • Testez vos routes et vos vues, ajoutez un contenu fictif simple pour le moment avec uniquement du HTML comme vous savez le faire depuis le S1.

    Les vues/templates

    Un template ou une vue est le meilleur moyen d'organiser et de restituer le code HTML à partir de votre application, que vous deviez rendre le code HTML à partir d'un contrôleur ou générer le contenu d'un courrier électronique . Les templates dans Symfony sont créés avec Twig: un moteur de modèle flexible, rapide et sécurisé.

    Twig est un moteur de rendu de template comme Smarty (prestashop) ou Blade (laravel). Twig a cependant été développé pour Symfony à l'origine et peut être utilisé dans d'autres contextes.

    Les vues sont des fichiers HTML qui sont compilés par Symfony pour être envoyés au navigateur. Elles sont stockées dans le dossier templates et sont généralement organisées par contrôleur.

    Un moteur de template permet de limiter les logiques complexes pour réaliser des templates simples à coder. Un moteur de template intègre généralement des fonctionnalités qui sont récurrentes dans le développement "front" et qui permettent de simplifier le code à écrire.

    Twig

    Twig est le moteur de template par défaut de Symfony. Il permet de faire des boucles, des conditions, des inclusions, des héritages, des filtres, des fonctions, etc. Il est très complet et permet de faire des choses très puissantes. Twig est un langage à part entière, mais il est très simple à apprendre et à utiliser. Twig s'intègre dans le code HTML, il sera ensuite interprété par le moteur Symfony pour générer la page HTML finale.

    Twig est un outil très puissant, mais il implique une courte phase d'apprentissage, car contrairement à d'autres moteurs de template il n'utilise pas une syntaxe PHP. Vous pouvez vous référer à la documentation officielle : https://twig.symfony.com/doc/3.x/

    Exemple d'une vue avec twig

    Exemple d'un code que vous pourriez écrire en PHP

    Ce même code, écrit avec TWIG serait :

    La syntaxe est plus "legére" et moins encombrées des balises PHP. Le code semble donc plus lisible et par conséquent plus facile à maintenir.

    Affichage

    La syntaxe Twig est basée sur uniquement trois constructions:

    • {{ ... }}, utilisé pour afficher le contenu d'une variable ou le résultat de l'évaluation d'une expression;

    • {% ... %}, utilisé pour exécuter une logique, telle qu’une condition ou une boucle;

    • {# ... #}, utilisé pour ajouter des commentaires au modèle (contrairement aux commentaires HTML, ces commentaires ne sont pas inclus dans la page rendue).

    Vous ne pouvez pas exécuter de code PHP dans les modèles Twig, mais Twig fournit des utilitaires permettant d'exécuter une certaine logique dans les modèles. Par exemple, les filtres modifient le contenu avant le rendu, comme le upperfiltre pour mettre le contenu en majuscule :

    {{ title|upper }}

    Twig intègre une liste de filtre permettant de répondre aux usages courant. Mais il est très simple d'ajouter nos propres filtres afin de répondre parfaitement à nos besoins.

    Variables

    TWIG est très puissant lorsqu'il s'agit de manipuler des variables. Pour lui, si c'est un tableau associatif ou un objet avec des propriétés cela est identique dans la syntaxe.

    Par exemple si vous avez le tableau $tab['param1'] vous écrirez en TWIG

    Si vous avez un objet $tab, qui est une instance d'une classe ayant comme propriété $param1, la syntaxe en TWIG sera :

    Si vous avez un objet $tab, qui est une instance d'une classe ayant comme propriété $param1, qui est un tableau associatif avec une clé key1, la syntaxe pourrait être :

    La manière dont TWIG inspecte votre code pour trouver la meilleure façon d'interpréter une variable est la suivante :

    For convenience's sake foo.bar does the following things on the PHP layer:

    • check if foo is an array and bar a valid element;

    • if not, and if foo is an object, check that bar is a valid property;

    • if not, and if foo is an object, check that bar is a valid method (even if bar is the constructor - use __construct() instead);

    • if not, and if foo is an object, check that getBar is a valid method;

    • if not, and if foo is an object, check that isBar is a valid method;

    • if not, and if foo is an object, check that hasBar is a valid method;

    • if not, return a null value.

    foo['bar'] on the other hand only works with PHP arrays:

    • check if foo is an array and bar a valid element;

    • if not, return a null value.

    Héritage

    TWIG permet l'héritage de template via un extends dans les templates enfants :

    Dans les templates mère on définit des "block" que l'on vient surcharger dans les templates enfants :

    On peut également reprendre le block parent via

    On crée donc des templates mère assez flexibles pour pouvoir en hériter et surcharger les différents blocks

    Exercice 2

    En se basant sur les concept d'héritage, définir un menu avec nos deux pages (accueil et contact) dans le fichier base.html.twig et l'hériter dans les deux autres templates.

    Images et CSS/JavaScript

    Pour inclure des images, des fichiers CSS ou JavaScript, il faut utiliser la fonction asset() :

    Cette fonction va chercher le fichier dans le dossier public/images/logo.png

    Asset n'est pas installé par défaut, il faut donc l'installer :

    Vous pouvez utiliser cette fonction pour inclure des fichiers CSS ou JavaScript :

    Exercice 3

    • Créer un dossier css dans le dossier public

    • Créer un fichier style.css dans le dossier css, faites un peu de CSS pour mettre en forme votre page d'accueil

    • Ajouter une balise link dans le fichier base.html.twig pour inclure le fichier css

    • Ajouter une balise img dans le fichier index.html.twig de votre template de la page d'accueil pour inclure une image de votre choix

    Logique

    • {% %} permet d'utiliser des logiques tels que :

      • {% if %} {% else %} {%endif %} : condition

      • {% for item in items %}{%endfor} : foreach

      • {% set foo='foo' %} : set des variables

    Tests

    Il est possible d'écrire des tests, la syntaxe est très proche de celle de PHP. Attention toutefois, les opérateurs de comparaison ne s'écrive pas de manière identique.

    Le && de PHP s'écrit "and" dans TWIG, et le || de PHP s'écrit "or" dans TWIG.

    Il est possible d'avoir des elseif (autant que nécessaire) et un bloc else (1 au maximum)

    Boucles

    Il n'existe que la boucle for dans TWIG (elle est un peu l'équivalent d'un foreach).

    Le code ci-dessous est une boucle qui varie de 1 à 10.

    La boucle ci-dessous est une boucle qui parcours une "collection" users (l'équivalent du foreach). Attention la syntaxe est inversée par rapport au PHP

    Dans le cadre d'une boucle TWIG propose une variable nommée loop qui permet d'avoir des informations sur la boucle :

    Variable

    Description

    loop.index

    The current iteration of the loop. (1 indexed)

    loop.index0

    The current iteration of the loop. (0 indexed)

    loop.revindex

    The number of iterations from the end of the loop (1 indexed)

    loop.revindex0

    The number of iterations from the end of the loop (0 indexed)

    loop.first

    True if first iteration

    La boucle ci-dessous intègre un test. Ce qui simplifie l'écriture. Elle intègre également un else dans le cas ou la boucle ne ferait aucune itération. Il est possible d'utiliser le else sans le if et réciproquement.

    Le code ci-dessus serait équivalent en PHP au code suivant:

    Exercice 4

    • Ajouter une page /articles

    • Construire un tableau PHP contenant 3 articles, contenant chacun un titre, un texte et une date

    • Passer le tableau à la vue de la page article.

    • Afficher le tableau en twig

    Exemple de tableau PHP

    Exercice 5

    • Ajoutez une nouvelle page sur l'url /recherche

    • Ajouter une vue avec un formulaire avec une zone de saisie et un bouton submit. Le traitement se fera en poste sur l'url /recherche/resultat

    • La vue résultat affichera le contenu de la zone de texte saisie dans la page précédente.

    Séance 7 : Requetes personnalisés (repository)

    Dans cette partie nous allons revenir sur les notions de repository et de requêtes, afin de pouvoir écrire nos propres requêtes.

    Repository

    Un repository est une classe qui permet de faire des requêtes sur une table (par l'intérmédiaire de l'eentité associée) de la base de données. Dans Symfony, lorsque vous ajoutez une entité, un repository est automatiquement créé.

    Ce fichier va contenir nos requêtes spécifiques, en utilisant au choix du SQL ou du DQL (Doctrine Query Language), qui permet de construire des requêtes en notation objet sans utiliser la syntaxe SQL qui nous rendrait dépendant de notre SGBD.

    Prenons par exemple une entité Adresse. Nous allons analyser le repository pour l'entité Adresse.

    AdresseRepository.php

    Tout d'abord ce fichier contient une classe qui étend la classe ServiceEntityRepository, qui est une classe fournie par Doctrine. Cette classe permet de faire des requêtes sur une table de la base de données.

    La classe contient une méthode constructeur qui prend en paramètre un objet de type ManagerRegistry, qui est une classe fournie par Doctrine. Cette classe permet de récupérer des informations sur la base de données, notamment le nom de la table associée à l'entité.

    Enfin, notez les lignes de commentaires avant la classe. Elles indiquent les 4 méthodes nativement proposées par les repository avec Doctrine, à savoir (voir S3 pour plus de détails) :

    • find : permet de récupérer une entité à partir de son identifiant

    • findOneBy : permet de récupérer une entité à partir d'un tableau de critères

    • findAll : permet de récupérer toutes les données d'une table et de les retourner sous forme de tableau

    • findBy : permet de récupérer toutes les données d'une table et les retourner sous forme de tableau à partir d'un tableau de critères.

    Vous avez ensuite deux exemples de requêtes, qui sont commentés. Ces requêtes sont écrites en DQL, et permettent de récupérer des données à partir de critères. Vous pouvez les utiliser comme modèle pour écrire vos propres requêtes.

    Exemple 1

    La première requête permet de récupérer toutes les adresses dont le champ exampleField est égal à la valeur passée en paramètre. La seconde requête permet de récupérer une seule adresse dont le champ exampleField est égal à la valeur ($value) passée en paramètre. Pour que cet exemple fonctionne exampleField doit être un champ de la table Adresse.

    Ce qui est à noter c'est que pour construire une requête on utilise la méthode createQueryBuilder, qui prend en paramètre le nom de l'alias de l'entité. Dans notre exemple, l'alias est a. Cet alias est utilisé pour construire la requête. Par exemple, si on veut récupérer les adresses dont le champ exampleField est égal à la valeur passée en paramètre, on écrit a.exampleField = :val. L'alias est donc utilisé pour indiquer le nom du champ de la table. Les alias doivent être unique si vous souhaitez faire des jointures.

    Ensuite, on utilise la méthode andWhere pour ajouter une condition à la requête. Dans notre exemple, on ajoute une condition sur le champ exampleField qui doit être égal à :val. On utilise ce que l'on nomme des requêtes parametrées. Cela permet de sécuriser les requêtes, et d'éviter les injections SQL.

    Il faut ensuite définir une valeur pour notre paramètre :val. Pour cela on utilise la méthode setParameter, qui prend en paramètre le nom du paramètre (ici :val) et la valeur à lui attribuer (ici $value). (il existe une méthode setParameters qui permet de définir plusieurs paramètres en même temps dans un tableau associatif).

    On peut donc avoir plusieurs conditions et autant de paramètres que nécessaires.

    Ensuite, on peut ajouter des ordres de tri sur les résultats de la requête. Pour cela on utilise la méthode orderBy, qui prend en paramètre le nom du champ sur lequel on souhaite trier, et le sens du tri (ASC ou DESC). Si on souhaite ajouter d'autres tris, on peut ajouter des méthodes addOrderBy qui prennent les mêmes paramètres. Le "add" permet de dire que l'on souhaite ajouter un autre ordre de tri.

    Ensuite, on peut limiter le nombre de résultats retournés par la requête. Pour cela on utilise la méthode setMaxResults, qui prend en paramètre le nombre de résultats maximum. Dans notre exemple, on limite à 10 résultats.

    A ce stade la requête est prête mais non fonctionnelle. Pour l'exécuter, on utilise la méthode getQuery, qui permet de récupérer l'objet Query qui représente la requête. Enfin, on utilise la méthode getResult pour récupérer les résultats de la requête. La réponse est un tableau dans ce cas.

    Pour tester cette requête, vous pouvez l'appeler depuis un contrôleur, un service, une méthode... par l'intermédiaire du repository. Par exemple, si vous avez un contrôleur qui s'appelle AdresseController, vous pouvez écrire :

    Exemple 2

    Cette requête est très similaire à la précédente, sauf qu'elle ne doit retourner qu'un seul résultat ou rien (getOneOrNullResult). Dans ce cas, la réponse est un objet de type Adresse, ou null si aucun résultat n'est trouvé. Si la requête retour plus d'un résultat, alors une erreur est levée.

    Exercice

    • Proposer une requête qui permet de récupérer tous les éditeurs dont le champ code_postal est égal à 33000.

    • Proposer une requête qui permet de récupérer tous les éditeurs et trier les réponses par nom.

    • Testez ces deux méthodes dans un contrôleur pour voir les résultats.

    Ajoutez des données dans votre base de données pour effectuer vos tests.

    Quelques instructions DQL

    Les jointures

    Pour faire des jointures, il faut utiliser la méthode join de la requête. Par exemple, si on souhaite récupérer les adresses et les villes associées (qui serait dans une autre entité), on peut écrire :

    Dans cet exemple, on utilise la méthode join pour faire une jointure sur la table ville. On utilise l'alias v pour la table ville. On peut ensuite utiliser cet alias pour faire des requêtes sur la table ville. Par exemple, si on souhaite récupérer les adresses dont la ville est Paris, on peut écrire :

    Les autres jointures sont :

    • leftJoin : jointure gauche

    • rightJoin : jointure droite

    • innerJoin : jointure interne

    Exemple de jointure gauche :

    Les opérateurs

    Il existe plusieurs opérateurs pour faire des requêtes. Par exemple, pour récupérer les adresses dont le champ code_postal est supérieur à 10000, on peut écrire :

    Il existe plusieurs opérateurs :

    • = : égal

    • != : différent

    • > : supérieur

    Les clauses

    Il existe plusieurs clauses pour faire des requêtes. Par exemple, pour récupérer les adresses dont le champ code_postal est supérieur à 10000 et dont le champ rue contient rue, on peut écrire :

    Il existe plusieurs clauses :

    • andWhere : et

    • orWhere : ou

    • andHaving : et

    L'ordre et les priorités sont les mês que pour SQL.

    Les fonctions

    Comme en SQL il existe des fonctions qui peuvent s'appliquer sur la requête pour faire des sommes, moyennes, etc. Par exemple, pour récupérer la somme des codes postaux (ce qui ne sert à rien bien sûr !), on peut écrire :

    Il existe plusieurs fonctions :

    • COUNT : nombre d'éléments

    • SUM : somme

    • AVG : moyenne

    Exercice 2

    • Proposer une requête qui permet de récupérer tous les jeux dont le prix est supérieur à une valeur passée en paramètre.

    • Proposer une requête qui permet de récupérer tous les jeux dont le prix est supérieur à une valeur passée en paramètre et dont le nom contient une chaîne de caractères passée en paramètre.

    • Proposer une requête qui permet de récupérer tous les jeux dont le prix est supérieur à une valeur passée en paramètre et dont le nom contient une chaîne de caractères passée en paramètre, et qui sont triés par prix décroissant.

    • Proposer une requête qui permet de récupérer tous les jeux dont le prix est supérieur à une valeur passée en paramètre et dont le nom contient une chaîne de caractères passée en paramètre, et qui sont triés par prix décroissant, et qui est proposé par un éditeur dont le code postal est 33000.

    Exercice 3

    • Dans une page "recherche" (contrôleur + vue), mettre en place un formulaire avec les champs prix, texte (nom) et code postal. Ajouter un lien dans le menu

    • Récupérez ces données et les passer dans une requête qui filtre les produits inférieurs au prix saisie et dont le code postal de l'éditeur est celui saisi ou pour lesquels le nom contient le "texte" du champs associé.. Triez par prix décroissant.

    Séance 6 : Relations entre entités

    Introduction

    Il est fréquent que les entités soient liées entre-elles. Et nous avons la notion de clé étrangère qui peut apparaître dans nos tables. Il est possible (voire nécessaire) de gérer cela avec Doctrine.

    Pour ce faire, il va falloir expliquer à Doctrine, les liens qui existent entre nos entités. Et Doctrine, se chargera de créer les clés étrangères où cela est nécessaire.

    La documentation officielle de Symfony sur les relations/associations

    Relations

    Il existe les relations suivantes dans doctrine :

    • OneToMany (et son inverse ManyToOne)

    • ManyToMany

    • OneToOne

    Il existe également une notion très importante dans ces relations : propriétaire et inverse.

    Propriétaire et inverse

    Dans une relation entre deux entités, il y a toujours une entité dite propriétaire, et une dite inverse. L'entité propriétaire est celle qui contient la référence à l'autre entité.

    Prenons un exemple simple, les commentaires de nos annonces. En SQL pur, vous disposez de la tablecomment et de la tableadvert. Pour créer une relation entre ces deux tables, vous allez mettre naturellement une colonne advert_id dans la tablecomment. La tablecomment est donc propriétaire de la relation, car c'est elle qui contient la colonne de liaison advert_id.

    Unidirectionnelle ou bidirectionnelle

    Enfin, une relation peut être unidirectionnelle ou bidirectionnelle. Les relations bidirectionnelles peuvent être gérées automatiquement par Symfony modifiant un peu les entités inverses avec inversedBy et mappedBy.

    Dans le cas d'une relation bidirectionnelle, il faut aussi explicité la relation dans l'entité inverse. La relation bidirectionnelle permet de faciliter la recherche d'élement en partant de l'inverse (Article vers Commentaires).

    RELATION 1..N (ONETOMANY) ET N..1 (MANYTOONE)

    La relation 1..n définit une dépendance multiple entre 2 entités de sorte que la première peut être liée à plusieurs entités

    Prenons l'exemple des étudiants et des absences. Un étudiant peut avoir plusieurs (many) absences, mais une absence n'est associée qu'a un (one) seul étudiant.

    Cette relation peut donc se résumer à : plusieurs (many) absences pour un (one) étudiant, ou de manière équivalent un (one) étudiant pour plusieurs (many) absences.

    On se place prioritairement du coté du Many, et on doit écrire la relation ManyToOne. Elle est obligatoire pour définir la relation précédente.

    Le code précédent est le minimum pour définir une relation.

    On dit dans ce cas que Absence est propriétaire de la relation (toujours du coté du Many).

    La relation décrite précédemment est unidirectionnelle. Pour la rendre bidirectionnelle il faut décrire la relation dans l'entité etudiant (avec la relation inverse OneToMany).

    Le code de Absence devient

    Le code de Etudiant sera :

    La relation OneToMany n'est pas obligatoire. Elle permet juste d'inverser la relation, et de rendre la manipulation plus simple.

    Dans ce cas, on fait apparaître un tableau ($this->absences), de type ArrayCollection contenant tous les objets associés à cette relation (many).

    Relation 1..1 (OneToOne)

    La relation 1..1 est peu utilisée mais permet une flexibilité en terme relationnelle très importante. Elle définit une dépendance unique entre 2 entités :

    Utilisateur -> Adresse Produit -> Image Commande -> Facture

    Relation N..N (ManyToMany)

    Le fonctionnement est assez similaire à une relation ManyToOne/OneToMany, sauf que cette relation est forcément bidirectionnelle. Il faut décrire le comportement dans les deux entités et choisir, selon la logique désirée, qui sera la relation propriétaire (inversedBy) de la relation inverse (mappedBy).

    Cette relation va créer une nouvelle table, contenant les deux clés étrangères.

    Manipulation de la console

    La console (make:entity), nous facilite la création des relations. En créant ou en modifiant l'entité, il est possible d'ajouter le champs contenant la relation. Pour cela le type sera "relation". La console vous demandera de préciser l'entité liée, ainsi que le type de relation. Vous pourrez ensuite selon la relation choisie, préciser la relation inverse, de manière optionnelle ou obligatoire.

    Attention, il est d'usage de lancer la console dans l'entité qui porte la relation (propriétaire), ou l'entité qui recevra la clé étrangère.

    Comme après chaque modification, il faudra générer le fichier de migration, et appliquer les modifications sur la base de données.

    Exercice

    • Créer la liaison entre Article et Categorie.

    • Ajoutez deux catégories dans votre base de données et liées vos articles à l'une des catégories.

    • Modifiez la page "/articles", pour afficher la catégorie de chacun des articles.

    #[Route('/blog/{slug}', name:'article_blog')]
    public function article($slug)
    {
        ...
    }
    #[Route('/{id}', name:'index', requirements:['id'=>'\d+'], defaults:['id'=>1])]
    public function index($id)
    {
        ...
    }
    use Symfony\Component\Routing\Annotation\Route;
    #[Route('/page/{page}', name:'blog_index')]
    public function indexAction($page)
    {
        echo $page;
    }
    #[Route('/page/{page}', name:'blog_index')]
    public function indexAction($page = 1)
    {
        echo $page;
    }
    #[Route('/page/{page}/{subpage}', name:'blog_index')]
    </strong>public function indexAction($page, $subpage)
    {
        echo $page.' '.$subpage;
    }
    $this->generateUrl('nom_de_la_route', [$variables]);
    {{ path('nom_route', {'page': 'toto', 'vars2': 'titi'} ) }}
    
    {{ url('nom_route', {'page': 'toto', 'vars2': 'titi'} ) }}
    use Symfony\Component\HttpFoundation\Response;
    public function index(){
        return new Response('Ma response');
    }
    #[Route("/", name:"page")]
    public function index() 
    {
        // votre code
    
        return $this->render('default/index.html.twig', 
            ['variable' => $variable]
        );
    }
    <body>
        <h1><?php echo $page_title ?></h1>
        <ul id="navigation">
            <?php foreach ($navigation as $item): ?>
            <li>
                    <a href="<?php echo $item->getHref() ?>">
                        <?php echo $item->getCaption() ?>
                    </a>
                </li>
            <?php endforeach ?>
        </ul>
    </body>
    <body>
        <h1>{{ page_title }}</h1>
    
        <ul id="navigation">
            
    {% for item in navigation %}
                <li><a href="{{ item.href }}">{{ item.caption }}</a></li>
            {% endfor %}
    
        </ul>
    </body>
    {{ tab.param1 }}
    {{ tab.param1 }}
    {{ tab.param1.key1 }}
    {% extends 'base.html.twig' %}
    
    {% block body %}
    toto
    {% endblock %}
    {{ parent() }}
    <img src="{{ asset('images/logo.png') }}" alt="Symfony!" />
    composer require symfony/asset
    <link rel="stylesheet" href="{{ asset('css/blog.css') }}" />
    {% if condition %}
    
    {% else if condition %}
    
    {% else %}
    
    {% endif %}
    {% for i in 1..10 %}
    
    {% endfor %}
    <ul>
        {% for user in users %}
            <li>{{ user.username }}</li>
        {% endfor %}
    </ul>
    <ul>
        {% for user in users|filter(user => (user.active == true)) %}
            <li>{{ user.username }}</li>
        {% else %}
            <li>No users found</li>
        {% endfor %}
    </ul>
    <?php
    <ul>
    if (count($users) > 0) {
        foreach ($users as $user) {
            if ($user->isActive() == true) {//isActive() est une méthode de mon objet $user
                echo '<li>'.$user->getUsername().'</li>';
            }
        }
    }
    else {
        echo '<li>No users found</li>';
    }
    </ul>
    <?php
    $articles = [
    1 => ['titre' => 'Mon premier article',
          'texte' => 'Un texte de mon article un peu long...',
          'date' => new DateTime('now')
          ],
     ...
     ];
     ?>      

    loop.last

    True if last iteration

    loop.length

    The number of items in the sequence

    loop.parent

    The parent context

    < : inférieur
  • >= : supérieur ou égal

  • <= : inférieur ou égal

  • LIKE : contient. Le paramètre doit être une chaîne de caractères, et on peut utiliser le caractère % pour remplacer un nombre quelconque de caractères.

  • NOT LIKE : ne contient pas. Le paramètre doit être une chaîne de caractères, et on peut utiliser le caractère % pour remplacer un nombre quelconque de caractères.

  • IN : dans une liste. Le paramètre doit être un tableau.

  • NOT IN : pas dans une liste. Le paramètre doit être un tableau.

  • BETWEEN : entre deux valeurs. Le paramètre doit être un tableau de deux valeurs.

  • IS NULL : est null. Dans ce cas il n'y a pas de paramètre attendu.

  • IS NOT NULL : n'est pas null. Dans ce cas il n'y a pas de paramètre attendu.

  • EXISTS : existe

  • NOT EXISTS : n'existe pas

  • IS EMPTY : est vide

  • IS NOT EMPTY : n'est pas vide

  • orHaving : ou
  • groupBy : groupe par

  • orderBy : tri par

  • setMaxResults : nombre maximum de résultats

  • setFirstResult : nombre de résultats à sauter

  • addSelect : ajouter un champ à sélectionner

  • addOrderBy : ajouter un champ à trier

  • addGroupBy : ajouter un champ à grouper

  • MIN : minimum
  • MAX : maximum

  • ...

  • Ecrire les méthodes correspondantes dans le repository JeuRepository.

  • Utiliser les méthodes dans le contrôleur JeuxRequetesController pour afficher les résultats dans une vue. Chaque methode utilisera une requête (donc 4 méthodes/routes). Mais la même vue pourra être utilisée.

  • Modifier la page "accueil" pour afficher les catégories (et toujours le dernier article publié).

  • Créez une page "/categorie/..." pour afficher uniquement les articles de la catégorie.

    • On pourrait judicieusement utiliser un "include" pour avoir la présentation d'un article commune sur toutes les pages

  • Faire fonctionner la recherche en utilisant les données du formulaire pour trouver et afficher les articles associés.

    • Modifier le repository de Article pour créer une méthode search($word)

      qui recherchera dans le titre et le contenu le mot $word

      Tips : https://symfony.com/doc/current/doctrine.html#querying-for-objects-the-repository

  • <?php
    
    namespace App\Repository;
    
    use App\Entity\Adresse;
    use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
    use Doctrine\Persistence\ManagerRegistry;
    
    /**
     * @extends ServiceEntityRepository<Adresse>
     *
     * @method Adresse|null find($id, $lockMode = null, $lockVersion = null)
     * @method Adresse|null findOneBy(array $criteria, array $orderBy = null)
     * @method Adresse[]    findAll()
     * @method Adresse[]    findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
     */
    class AdresseRepository extends ServiceEntityRepository
    {
        public function __construct(ManagerRegistry $registry)
        {
            parent::__construct($registry, Adresse::class);
        }
    
    //    /**
    //     * @return Adresse[] Returns an array of Adresse objects
    //     */
    //    public function findByExampleField($value): array
    //    {
    //        return $this->createQueryBuilder('a')
    //            ->andWhere('a.exampleField = :val')
    //            ->setParameter('val', $value)
    //            ->orderBy('a.id', 'ASC')
    //            ->setMaxResults(10)
    //            ->getQuery()
    //            ->getResult()
    //        ;
    //    }
    
    //    public function findOneBySomeField($value): ?Adresse
    //    {
    //        return $this->createQueryBuilder('a')
    //            ->andWhere('a.exampleField = :val')
    //            ->setParameter('val', $value)
    //            ->getQuery()
    //            ->getOneOrNullResult()
    //        ;
    //    }
    }
        public function findByExampleField($value): array
        {
            return $this->createQueryBuilder('a')
                ->andWhere('a.exampleField = :val')
                ->setParameter('val', $value)
                ->orderBy('a.id', 'ASC')
                ->setMaxResults(10)
                ->getQuery()
                ->getResult()
            ;
        }
        public function index(AdresseRepository $adresseRepository): Response
        {
            $adresses = $adresseRepository->findByExampleField('test');
            return $this->render('adresse/index.html.twig', [
                'adresses' => $adresses,
            ]);
        }
        public function findOneBySomeField($value): ?Adresse
        {
            return $this->createQueryBuilder('a')
                ->andWhere('a.exampleField = :val')
                ->setParameter('val', $value)
                ->getQuery()
                ->getOneOrNullResult()
            ;
        }
        public function findAllWithVille(): array
        {
            return $this->createQueryBuilder('a')
                ->join('a.ville', 'v')
                ->orderBy('a.id', 'ASC')
                ->getQuery()
                ->getResult()
            ;
        }
        public function findAllWithVilleParis(): array
        {
            return $this->createQueryBuilder('a')
                ->join('a.ville', 'v')
                ->andWhere('v.nom = :val')
                ->setParameter('val', 'Paris')
                ->orderBy('a.id', 'ASC')
                ->getQuery()
                ->getResult()
            ;
        }
        public function findAllWithVilleParis(): array
        {
            return $this->createQueryBuilder('a')
                ->leftJoin('a.ville', 'v')
                ->andWhere('v.nom = :val')
                ->setParameter('val', 'Paris')
                ->orderBy('a.id', 'ASC')
                ->getQuery()
                ->getResult()
            ;
        }
        public function findAllWithCodePostalSup10000(): array
        {
            return $this->createQueryBuilder('a')
                ->andWhere('a.code_postal > :val')
                ->setParameter('val', 10000)
                ->orderBy('a.id', 'ASC')
                ->getQuery()
                ->getResult()
            ;
        }
        public function findAllWithCodePostalSup10000AndRueContainsRue(): array
        {
            return $this->createQueryBuilder('a')
                ->andWhere('a.code_postal > :val')
                ->setParameter('val', 10000)
                ->andWhere('a.rue LIKE :val2')
                ->setParameter('val2', '%rue%')
                ->orderBy('a.id', 'ASC')
                ->getQuery()
                ->getResult()
            ;
        }
        public function findAllWithSumCodePostal(): array
        {
            return $this->createQueryBuilder('a')
                ->select('SUM(a.code_postal) as somme')
                ->getQuery()
                ->getResult()
            ;
        }
    class Absence
    {
    // ...
    
    #[ORM\ManyToOne(targetEntity:Etudiant::class)]
    #[ORM\JoinColumn(nullable:true)]
    private $etudiant;
    ...
    }
    class Absence
    {
    // ...
    
    #[ORM\ManyToOne(targetEntity:Etudiant::class, inversedBy:"absences")]
    #[ORM\JoinColumn(nullable:true)]
    private Etudiant $etudiant;
    ...
    }
    class Etudiant
    {
        // ...
    
        #[ORM\OneToMany(targetEntity:Absence::class, mappedBy:"etudiant")]
        private Collection $absences;
        ...
    
        public function __construct()
        {
            $this->absences = new ArrayCollection();
        }
    
        public function getAbsences(): array
        {
            return $this->absences;
        }
    }
    #[ORM\OneToOne(targetEntity:Address::class)]
    private Address $address;
    php bin/console make:entity
    
    Class name of the entity to create or update (e.g. BraveChef):
    > Product
    
     to stop adding fields):
    > category
    
    Field type (enter ? to see all types) [string]:
    > relation
    
    What class should this entity be related to?:
    > Category
    
    Relation type? [ManyToOne, OneToMany, ManyToMany, OneToOne]:
    > ManyToOne
    
    Is the Product.category property allowed to be null (nullable)? (yes/no) [yes]:
    > no
    
    Do you want to add a new property to Category so that you can access/update
    getProducts()? (yes/no) [yes]:
    > yes
    
    New field name inside Category [products]:
    > products
    
    Do you want to automatically delete orphaned App\Entity\Product objects
    (orphanRemoval)? (yes/no) [no]:
    > no
    
     to stop adding fields):
    >
    (press enter again to finish)

    Séance 1 : Notion de service, exemple du mail

    Le concept de service a déjà été évoqué en S3, il s'agit ici d'un rappel et d'une mise en pratique.

    Cours

    Dans Symfony, tous les objets (au sens d'un ensemble de code qui apporte une fonctionnalité : mail, base de données, entitée, ...) sont des services. Dès l'instanciation d'une application symfony, de nombreux services sont lancés et accessible de partout dans l'application. Il est aussi possible de créer ses propres services.

    La documentation officielle de Symfony sur les services : https://symfony.com/doc/current/service_container.html

    Utilisation d'un service existant (rappel sur l'usage des mails)

    Dans un controller, on peut utiliser un service existant en l'injectant dans la méthode de notre classe "controller". Par exemple, pour envoyer un mail, on peut utiliser le service mailer :

    Dans cet exemple, on injecte le service mailer (par l'intermédiaire de son interface MailerInterface) dans la méthode index de notre controller. On peut ensuite utiliser ce service pour envoyer un mail.

    Grâce au mécanisme d'injection de dépendance de Symfony, le service mailer est automatiquement instancié et injecté dans notre controller. On peut donc l'utiliser dans notre méthode index. Il n'est pas nécessaire de l'instancier nous même et de le passer à la méthode. Cela fonctionne parce que tout est considéré comme un service dans Symfony.

    Il est possible de cumuler des services et des paramètres issus de nos routes. D'ailleurs, vous le faites déjà dans vos controllers. Par exemple, dans le controller EditeurController, on pourrait avoir :

    La méthode index utilise le service EditeurRepository pour récupérer tous les éditeurs. La méthode edit utilise les services Requestet EntityManagerInterface pour récupérer les données du formulaire et sauvegarder les modifications d'un article, qui est passé en paramètre de la méthode et dans la route.

    Notez au passage que la récupération de l'éditeur déclenche aussi un mécanisme particulier qui vient executer une requete SQL pour récupérer l'éditeur à partir de l'id passé en paramètre de la route. C'est le mécanisme de "ParamConverter".

    Création d'un service

    Tout comme nous disposons de nombreux services déjà créés, il est possible de créer ses propres services. Pour cela, il faut créer une classe qui va contenir le code de notre service. Vous pouvez mettre ce fichier dans un repertoire dédié, par exemple src/Service. Par définition et sauf indication contraire cette classe sera considérée comme un service par Symfony.

    Exemple, créons un service qui va nous permettre de récupérer le nombre d'éditeurs dans la base de données :

    Dans cet exemple, on a créé une classe EditeurService qui contient une méthode countEditeurs qui va nous permettre de récupérer le nombre d'éditeur dans la base de données.

    Notez que dans ce service, on utilise le service EditeurRepository qui est injecté dans le constructeur de notre classe.

    Pour utiliser ce service, on peut l'injecter dans un controller :

    A quoi ca sert ?

    Les services sont très utiles pour plusieurs raisons :

    • Ils permettent de découper notre code en plusieurs parties. Par exemple, on peut avoir un service qui va nous permettre de récupérer les articles, un autre qui va nous permettre de récupérer les utilisateurs, un autre qui va nous permettre de récupérer les commentaires, etc.

    • On peut ainsi avoir un service par entité. Cela permet de découper notre code en plusieurs parties et de rendre notre code plus lisible et plus maintenable.

    Il n'y a pas de règle pour savoir quand créer un service. Cela dépend de votre code et de vos besoins. Par exemple, si vous avez un controller qui fait trop de choses, vous pouvez créer un service qui va vous permettre de déplacer une partie du code dans ce service.

    L'objectif est de garder vos contrôleurs et les méthodes qu'ils contiennent relativement légers. Les services permettent aussi de réduire le code qui serait dupliqué dans plusieurs contrôleurs en mettant à un seul endroit les méthodes qui sont communes à plusieurs contrôleurs.

    Exercice de (re)découverte

    Reprenons quelques bases

    Pour faciliter le point de départ, vous avez une correction du S3 à votre disposition dans le dossier ci-dessous. Cette correction peut être incomplète par rapport au S3 et peu contenir des différences de nomages ou d'organisation.

    Ou via GitHub :

    Un fois le dossier récupéré il faut lancer les commandes suivantes :

    • Installer les dépendances :

    • Créer et mettre à jour le fichier .env.local avec les informations de connexion à votre base de données.

    • puis créer la base de données :

    • puis créer les tables :

    On pourrait utiliser une migration comme vu en S3, mais pour simplifier nous allons utiliser cette commande qu va ici reconstruire toutes les tables de la base de données

    • Charger les données du fichier sql qui se trouve dans le dossier.

    • N'oubliez pas de faire le nécessaire du côté de docker pour rendre ce projet accessible depuis votre navigateur sur l'adresse http://mmiple.mmi-troyes:8319/. (Url à adapter en fonction de votre configuration).

    Utilisation du mailer

    Dans un premier temps vous allez créer un contrôleur et une méthode, associée à une route permettant d'envoyer un mail avec avec des informations pré-remplies (n'hésitez pas à reprendre les séances du S3 sur le mailer).

    Création d'un service

    Dans un deuxième temps nous allons utiliser notre propre service de mailer pour gérer l'envoi des mails.

    L'intérêt de faire cela est que nous pourrons ainsi définir un ensemble de paramètre par défaut pour nos mails (expéditeur, destinataire, sujet, etc.) et ainsi simplifier l'utilisation de notre service, sans repeter à chaque fois que nécessaire ces paramètres.

    L'autre intéret est que nous pourrons ainsi facilement changer les paramètres de notre service de mailer, voire changer de bibliothèque, sans avoir à modifier le code de nos contrôleurs.

    Cela évite donc que nos contrôleurs et notre code ne soit trop dépendant des bibliothèques que nous utilisons.

    1. Créer une classe MailerService dans le dossier src/Service et ajouter les méthodes suivantes :

      • le constructeur qui prend en paramètre l'objet MailerInterface et qui le stocke dans une propriété

      • sendMail : qui prend en paramètre le destinataire, le sujet et le corps du mail et qui envoie le mail

    Exercice de mise en pratique

    • Créer un formulaire de contact.

    • Créer un nouveau contrôleur nommé ContactController

    • Ajouter une méthode index qui va se charger d'afficher le formulaire de contact (adresse de l'expéditeur, sujet, message), vous serez le destinataire par défaut..

    • Ajouter une méthode send qui va se charger de récupérer les informations du formulaire et de les envoyer par mail en utilisant le service de mailer.

    Séance 7 : Formulaires

    FORM

    Introduction

    La gestion des formulaire se fait via plusieurs classes PHP qui permettent entre autre :

    Séance 3 : Vues - TWIG

    Présentation

    Un template ou une vue est le meilleur moyen d'organiser et de restituer le code HTML à partir de votre application, que vous deviez rendre le code HTML à partir d'un contrôleur ou générer le contenu d'un courrier électronique . Les templates dans Symfony sont créés avec Twig: un moteur de modèle flexible, rapide et sécurisé.

    est un moteur de rendu de template comme (prestashop) ou (laravel). Twig a cependant été développé pour Symfony à l'origine et peut être utilisé dans d'autres contextes.

    Twig Extensions Defined by Symfony (Symfony Docs)symfony
  • Modifier le contrôleur pour utiliser le service de mailer

  • Pour aller plus loin

    1. Ajoutez une méthode qui permet d'envoyer un email en utilisant "twig"

      1. Quelles sont les propriétés nécessaires ?

      2. Comment modifier le code ?

  • 10MB
    mmiple-base.zip
    archive
    Ouvrir
    Projet de départ
    https://github.com/Dannebicque/mmiples4.git

    La structure et les propriétés du formulaire se gèrent via FormBuilder et peuvent être réutilisées;

  • On peut créer des classes spécifiques pour chacun de nos formulaires

  • Permet une gestion des validations simplifiée et une sécurité renforcée

  • Permet d'hydrater une entité ou un objet rapidement

  • Gestion de template simple

  • La documentation officielle de Symfony sur les formulaires se trouve ici

    Pour pouvoir utiliser les formulaires, selon la version d'installation de Symfony, il peut être nécessaire d'installer les packages :

    Pour les exemples ci-dessous, on considère l'entité suivante (exemple issue de la documentation Symfony) :

    Création

    On peut créer un Form de 2 façons différentes :

    Directement dans un contrôleur

    Cette première solution est rapide à mettre en place, et ne nécessite pas de fichier supplémentaire. Cependant, le formulaire ainsi créé n'est pas réutilisation dans d'autres méthodes.

    Ou via des classes dédiées de type FormType en général dans le répertoire Form

    Cette seconde solution, qui implique des fichiers complémentaires, permet une plus grande souplesse et une meilleure ré-utilisation dans d'autres contextes.

    Le fichier ci-dessous permet de créer le même formulaire.

    On utilise la classe FormBuilder accessible avec la méthode

    dans un contrôleur ; l'argument $task est l'entité que vous souhaitez hydrater; l'argument n'est pas obligatoire (pour un formulaire de recherche par exemple)

    On peut mettre des champs de formulaire par défaut en modifiant l'entité avant de créer le formulaire et de lier le formulaire à l'entité :

    Par exemple

    Pré-remplira le formulaire avec Ma tâche pré-définie.

    Ensuite nous aurons des suites de ->add('nom_du_champs', TypeDeChamps::class, $options);

    Par défaut si vous ne mettez que le nom du champs Symfony se chargera de récupérer un type de champs en fonction du type de champs (string, text, boolean, date, ...)

    Liste des champs possibles : https://symfony.com/doc/current/reference/forms/types.html

    Dans une classe dédiée le createFormBuilder est déjà instancié il ne vous reste qu'à rajouter les différents add.

    TWIG

    Une fois le formulaire créé et initié il faut renvoyer le tout à TWIG via la méthode :

    Soit par exemple

    Ensuite nous aurons plusieurs fonctions twig utiles:

    • {{ form }} permet d'afficher tout le formulaire

    • {{ form_start }} permet de générer la balise <form> avec les différents attributs

    • {{ form_end }} permet de générer la fermeture de <form> avec les différents champs restants non affichés

    • {{ form_errors }} affiche les erreurs éventuelles du formulaire

    • {{ form_widget(mon formulaire.nomduchamps) }} affiche le type de champs

    • {{ form_label(mon formulaire.nomduchamps) }} affiche le label du champs

    • {{ form_row(monformulaire.nomduchamps) }} affiche le form_widget et form_label

    • {{ form_rest }} affiche les champs restants non récupéré précédemment (token de vérification par exemple)

    Ces fonctions permettent une grande maîtrise de la mise en forme d'un formulaire. Cependant, elle implique de détailler les éléments.

    Il est donc possible d'afficher un formulaire en une seule ligne, et le rendu dépendra du paramètrage (ou du template modèle) existant.

    la variable_form correspond à la variable contenant le formulaire envoyée par le contrôleur. Il est enfin possible de préciser un "thème" pour votre formulaire avec la syntaxe :

    Le template est un fichier twig qui vient préciser et définir pour chaque élément du formulaire (de manière globale), le rendu en HTML.

    Des thèmes par défaut sont proposées : https://symfony.com/doc/current/form/bootstrap4.html Et la documentation pour créer votre template : https://symfony.com/doc/current/form/form_customization.html

    Action / Request

    Une fois le formulaire créé et affiche via TWIG il faut rajouter un comportement qui va gérer la soumission du formulaire grâce à ces méthodes :

    • handleRequest($request) permet d'associer les valeurs input à la classe Form précédemment créé

    • isSubmitted() permet de savoir si le formulaire a été envoyé

    • isValid() permet de savoir si les données saisies sont valides

    Dans la majorité des cas on va tester si :

    Validation

    Les validations permettent de gérer des contraintes au niveau du formulaire ; Par exemple pour pourra forcer en PHP que le champs email soit bien un email ou que tel champs ne peut pas dépasser tel nombre de caractères, vous trouverez la liste des contraintes basiques sur site site de symfony : http://symfony.com/doc/4.1/validation.html

    Ces contraintes ou assert peuvent être gérée de plusieurs façon XML, JSON, YAML, PHP ou en annotation dans notre cas; il faudra utiliser cette ligne tout en haut du contrôleur :

    Pour ensuite pouvoir utiliser l'annotation :

    Ici on vérifiera que le champs name doit être rempli.

    Exercice

    • Créer un formulaire directement dans CategorieController qui gèrera la création des Categories

    • Modifier la page de listing des catégories pour rajouter un lien édition et suppression

    • Mettre en place les différentes routes pour le "backoffice" de Categorie (ajout / modification / suppression / visualisation)

    • Déporter le formulaire de gestion de Categorie vers une classe dédiée Form/CategorieType.php

    • Modifier CategorieController pour utiliser CategorieType

    • Ajouter une validation au formulaire sur le titre qui ne doit pas dépasser 150 caractères

    • Rechercher pour mettre en place le template bootstrap pour les Form

    • Gérer les autres routes : modification et suppression

    Génération de CRUD

    Ce que nous venons de faire manuellement peut être généré en ligne de commande par Symfony via la commande :

    On vous demandera le nom de l'entité précédé du nom de bundle, le chemin pour ce crud et si vous souhaitez avoir les fonction d'édition (ajout/ modification) mettez oui.

    On peut également générer seulement les FormType :

    Attention si vous modifier une entité les FormType ne sont pas générés automatiquement il faudra rajouter manuellement le champs fraichement créé.

    Exercice

    • Générer le CRUD de Article

    • Tester le fonctionnement, comprendre pourquoi ca ne fonctionne pas et corriger

    • Mettre les liens dans le menu pour aller sur le listing post et listing postcategory; mettre en actif si url courante

    <?php
    
    namespace App\Controller;
    
    use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
    use Symfony\Component\HttpFoundation\Response;
    use Symfony\Component\Routing\Annotation\Route;
    use Symfony\Component\Mime\Email;
    use Symfony\Component\Mailer\MailerInterface;
    
    class MailController extends AbstractController
    {
        #[Route("/mail", name:"mail")]
        public function index(MailerInterface $mailer): Response
        {
            $email = (new Email())
                ->from('[email protected]')
                ->to('[email protected]')
                ->subject('Hello Email')
                ->text('Sending emails is fun again!')
                ->html('<p>See Twig integration for better HTML integration!</p>');
    
            $mailer->send($email);
    
            return $this->render('mail/index.html.twig', []);
    
        }
    }
    <?php
    
    namespace App\Controller;
    
    use App\Entity\Editeur;
    use App\Form\EditeurType;
    use App\Repository\EditeurRepository;
    use Doctrine\ORM\EntityManagerInterface;
    use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
    use Symfony\Component\HttpFoundation\Request;
    use Symfony\Component\HttpFoundation\Response;
    use Symfony\Component\Routing\Annotation\Route;
    
    class EditeurController extends AbstractController
    {
        #[Route('/editeur', name: 'editeur')]
        public function index(EditeurRepository $editeurRepository): Response
        {
            $editeurs = $editeurRepository->findAll();
    
            return $this->render('editeur/index.html.twig', [
                'editeurs' => $editeurs,
            ]);
        }
    
        #[Route('/editeur/{id}/edit', name: 'editeur_edit', methods: ['GET', 'POST'])]
        public function edit(Request $request, Editeur $editeur, EntityManagerInterface $entityManager): Response
        {
            $form = $this->createForm(EditeurType::class, $editeur);
            $form->handleRequest($request);
    
            if ($form->isSubmitted() && $form->isValid()) {
                $entityManager->flush();
    
                return $this->redirectToRoute('editeur');
            }
    
            return $this->render('editeur/edit.html.twig', [
                'editeur' => $editeur,
                'form' => $form->createView(),
            ]);
        }
    }
    <?php
    
    namespace App\Service;
    
    use App\Repository\EditeurRepository;
    
    class EditeurService
    {
        public function __construct(private EditeurRepository $editeurRepository)
        {
        }
    
        public function countEditeurs(): int
        {
            return $this->editeurRepository->count([]);
        }
    }
    <?php
    
    namespace App\Controller;
    
    use App\Service\EditeurService;
    use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
    use Symfony\Component\HttpFoundation\Response;
    use Symfony\Component\Routing\Annotation\Route;
    
    class EditeurController extends AbstractController
    {
        #[Route('/editeur', name: 'editeur')]
        public function index(EditeurService $editeurService): Response
        {
            $count = $editeurService->countEditeurs();
    
            return $this->render('editeur/index.html.twig', [
                'count' => $count,
            ]);
        }
    }
    composer install
    php bin/console doctrine:database:create
    php bin/console doctrine:schema:update --force
    composer require symfony/form
    // src/Entity/Task.php
    namespace App\Entity;
    
    class Task
    {
        protected $task;
        protected $dueDate;
    
        public function getTask()
        {
            return $this->task;
        }
    
        public function setTask($task)
        {
            $this->task = $task;
        }
    
        public function getDueDate()
        {
            return $this->dueDate;
        }
    
        public function setDueDate(\DateTime $dueDate = null)
        {
            $this->dueDate = $dueDate;
        }
    }
    // src/Controller/DefaultController.php
    namespace App\Controller;
    
    use App\Entity\Task;
    use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
    use Symfony\Component\HttpFoundation\Request;
    use Symfony\Component\Form\Extension\Core\Type\TextType;
    use Symfony\Component\Form\Extension\Core\Type\DateType;
    use Symfony\Component\Form\Extension\Core\Type\SubmitType;
    
    class DefaultController extends AbstractController
    {
        public function new(Request $request)
        {
            // creates a task and gives it some dummy data for this example
            $task = new Task();
            $task->setTask('Write a blog post');
            $task->setDueDate(new \DateTime('tomorrow'));
    
            $form = $this->createFormBuilder($task)
                ->add('task', TextType::class)
                ->add('dueDate', DateType::class)
                ->getForm();
    
            return $this->render('default/new.html.twig', [
                'form' => $form->createView(),
            ]);
        }
    }
    // src/Form/TaskType.php
    namespace App\Form;
    
    use Symfony\Component\Form\AbstractType;
    use Symfony\Component\Form\FormBuilderInterface;
    use Symfony\Component\Form\Extension\Core\Type\SubmitType;
    
    class TaskType extends AbstractType
    {
        public function buildForm(FormBuilderInterface $builder, array $options)
        {
            $builder
                ->add('task')
                ->add('dueDate', null, ['widget' => 'single_text'])
            ;
        }
    }
    $task = new Task();
    $form = $this->createForm(TaskType::class, $task);
    $task = new Task();
    
    $task->setTask('Ma tâche pré-définie');
    $form = $this->createForm(TaskType::class, $task);
    $form->createView();
    return $this->render('template.html.twig', ['variable_form' => $form->createView()]);
    //ou
    return $this->renderForm('template.html.twig', ['variable_form' => $form ];
    {{ form(variable_form) }}
    {% form_theme form 'nom_du_template.html.twig' %}
    {{ form(variable_form) }}
    public function newAction(Request $request, EntityManagerInterface $entityManager){
      //... génération ou récupération du formulaire
    
      $form->handleRequest($request); // hydratation du form 
      if($form->isSubmitted() && $form->isValid()){ // test si le formulaire a été soumis et s'il est valide
        $entityManager->persist($task); // on effectue les mise à jours internes
        $entityManager->flush(); // on effectue la mise à jour vers la base de données
        return $this->redirectToRoute('show_task', ['id' => $task->getId()]); // on redirige vers la route show_task avec l'id du post créé ou modifié 
      }
    }
    use Symfony\Component\Validator\Constraints as Assert;
    class Author
    {
        /**
         * @Assert\NotBlank()
         */
        public $name;
    }
    bin/console make:crud
    php bin/console make:form

    Un moteur de template permet de limiter les logiques complexes pour réaliser des templates simples à coder. Un moteur de template intègre généralement des fonctionnalités qui sont récurrentes dans le développement "front" et qui permettent de simplifier le code à écrire.

    La documentation officielle de Symfony sur les vues et TWIG et La documentation officielle de TWIG

    Exemple d'une vue avec twig

    Exemple d'un code que vous pourriez écrire en PHP

    Ce même code, écrit avec TWIG serait :

    La syntaxe est plus "legére" et moins encombrées des balises PHP. Le code semble donc plus lisible et par conséquent plus facile à maintenir.

    Affichage

    La syntaxe Twig est basée sur uniquement trois constructions:

    • {{ ... }}, utilisé pour afficher le contenu d'une variable ou le résultat de l'évaluation d'une expression;

    • {% ... %}, utilisé pour exécuter une logique, telle qu’une condition ou une boucle;

    • {# ... #}, utilisé pour ajouter des commentaires au modèle (contrairement aux commentaires HTML, ces commentaires ne sont pas inclus dans la page rendue).

    Vous ne pouvez pas exécuter de code PHP dans les modèles Twig, mais Twig fournit des utilitaires permettant d'exécuter une certaine logique dans les modèles. Par exemple, les filtres modifient le contenu avant le rendu, comme le upperfiltre pour mettre le contenu en majuscule :

    {{ title|upper }}

    Twig intègre une liste de filtre permettant de répondre aux usages courant. Mais il est très simple d'ajouter nos propres filtres afin de répondre parfaitement à nos besoins.

    Variables

    TWIG est très puissant lorsqu'il s'agit de manipuler des variables. Pour lui, si c'est un tableau associatif ou un objet avec des propriétés cela est identique dans la syntaxe.

    Par exemple si vous avez le tableau $tab['param1'] vous écrirez en TWIG

    Si vous avez un objet $tab, qui est une instance d'une classe ayant comme propriété $param1, la syntaxe en TWIG sera :

    Si vous avez un objet $tab, qui est une instance d'une classe ayant comme propriété $param1, qui est un tableau associatif avec une clé key1, la syntaxe pourrait être :

    La manière dont TWIG inspecte votre code pour trouver la meilleure façon d'interpréter une variable est la suivante :

    For convenience's sake foo.bar does the following things on the PHP layer:

    • check if foo is an array and bar a valid element;

    • if not, and if foo is an object, check that bar is a valid property;

    • if not, and if foo is an object, check that bar is a valid method (even if bar is the constructor - use __construct() instead);

    • if not, and if foo is an object, check that getBar is a valid method;

    • if not, and if foo is an object, check that isBar is a valid method;

    • if not, and if foo is an object, check that hasBar is a valid method;

    • if not, return a null value.

    foo['bar'] on the other hand only works with PHP arrays:

    • check if foo is an array and bar a valid element;

    • if not, return a null value.

    Logique

    • {% %} permet d'utiliser des logiques tels que :

      • {% if %} {% else %} {%endif %} : condition

      • {% for item in items %}{%endfor} : foreach

      • {% set foo='foo' %} : set des variables

    Tests

    Il est possible d'écrire des tests, la syntaxe est très proche de celle de PHP. Attention toutefois, les opérateurs de comparaison ne s'écrive pas de manière identique.

    Le && de PHP s'écrit "and" dans TWIG, et le || de PHP s'écrit "or" dans TWIG.

    Il est possible d'avoir des elseif (autant que nécessaire) et un bloc else (1 au maximum)

    Boucles

    Il n'existe que la boucle for dans TWIG (elle est un peu l'équivalent d'un foreach).

    Le code ci-dessous est une boucle qui varie de 1 à 10.

    La boucle ci-dessous est une boucle qui parcours une "collection" users (l'équivalent du foreach). Attention la syntaxe est inversée par rapport au PHP

    Dans le cadre d'une boucle TWIG propose une variable nommée loop qui permet d'avoir des informations sur la boucle :

    Variable

    Description

    loop.index

    The current iteration of the loop. (1 indexed)

    loop.index0

    The current iteration of the loop. (0 indexed)

    loop.revindex

    The number of iterations from the end of the loop (1 indexed)

    loop.revindex0

    The number of iterations from the end of the loop (0 indexed)

    loop.first

    True if first iteration

    La boucle ci-dessous intègre un test. Ce qui simplifie l'écriture. Elle intègre également un else dans le cas ou la boucle ne ferait aucune itération. Il est possible d'utiliser le else sans le if et réciproquement.

    Le code ci-dessus serait équivalent en PHP au code suivant:

    Héritage

    TWIG permet l'héritage de template via un extends dans les templates enfants :

    Dans les templates mère on définit des "block" que l'on vient surcharger dans les templates enfants :

    On peut également reprendre le block parent via

    On crée donc des templates mère assez flexibles pour pouvoir en hériter et surcharger les différents blocks

    Exercice 1

    • Utiliser l'héritage pour mettre le menu dans un seul fichier mais visible sur les 2 pages.

    • Intégrer bootstrap (CDN)

    • Pour la page /color afficher le mot de la même couleur dynamiquement ("bleu" en bleu) (en CSS)

    • Pour la couleur rouge afficher en plus le Message : "Attention risque de virus" en rouge

    Inclusions

    De la même manière que l'on peut hériter d'un template de base, on peut aussi inclure des "morceaux" de template dans une vue. Toujours dans la perspective de ne jamais multiplier un morceau de code identique dans plusieurs fichiers.

    Filtres

    Il est possible d'appliquer des filtres sur les variables pour effectuer des transformations : Majuscules, minuscules, dates, ...

    Vous trouverez la liste des filtres sur l'adresse : Liste des filtres de TWIG

    Il est possible de cumuler les filtres. Mais il faut faire attention à l'ordre dans lequel on les appliques. Il est aussi possible de créer ses propres filtres.

    Assets

    TWIG propose de gérer facilement les URL de vos images, fichiers CSS ou encore javascript.

    Pour cela vous aurez besoin d'ajouter le bundle suivant :

    Ensuite, vous pouvez écrire dans TWIG

    Par défaut, vos "assets" doivent se trouver dans le répertoire public de Symfony.

    Depuis la version 3.4 et 4, Symfony propose de gérer les assets avec WebPack-Encore, qui est une adaptation de Webpack pour Symfony Vous trouverez les éléments sur WebPack-Encore ici : https://symfony.com/doc/current/frontend.html

    Exercice 2

    • Modifiez le code précédemment écrit pour manipuler des assets directement dans votre projet. Vous intégrer une image de votre choix et Bootstrap en version locale (supprimer toutes les dépendances aux CDN).

    Twig
    Smarty
    Blade
    <body>
        <h1><?php echo $page_title ?></h1>
        <ul id="navigation">
            <?php foreach ($navigation as $item): ?>
            <li>
                    <a href="<?php echo $item->getHref() ?>">
                        <?php echo $item->getCaption() ?>
                    </a>
                </li>
            <?php endforeach ?>
        </ul>
    </body>
    <body>
        <h1>{{ page_title }}</h1>
    
        <ul id="navigation">
            {% for item in navigation %}
                <li><a href="{{ item.href }}">{{ item.caption }}</a></li>
            {% endfor %}
        </ul>
    </body>
    {{ tab.param1 }}
    {{ tab.param1 }}
    {{ tab.param1.key1 }}
    {% if condition %}
    
    {% else if condition %}
    
    {% else %}
    
    {% endif %}
    {% for i in 1..10 %}
    
    {% endfor %}
    <ul>
        {% for user in users %}
            <li>{{ user.username }}</li>
        {% endfor %}
    </ul>
    <ul>
        {% for user in users|filter(user => (user.active == true)) %}
            <li>{{ user.username }}</li>
        {% else %}
            <li>No users found</li>
        {% endfor %}
    </ul>
    <?php
    <ul>
    if (count($users) > 0) {
        foreach ($users as $user) {
            if ($user->isActive() == true) {//isActive() est une méthode de mon objet $user
                echo '<li>'.$user->getUsername().'</li>';
            }
        }
    }
    else {
        echo '<li>No users found</li>';
    }
    </ul>
    {% extends 'base.html.twig' %}
    {% block body %}
    toto
    {% endblock %}
    {{ parent() }}
    {{ variable|upper }}
    composer require symfony/asset
    <img src="{{ asset('images/logo.png') }}" alt="Symfony!" />
    
    <link href="{{ asset('css/blog.css') }}" rel="stylesheet" />

    loop.last

    True if last iteration

    loop.length

    The number of items in the sequence

    loop.parent

    The parent context

    Présentation

    Le cours de 2025-2026 est mené par Patrice Gommery et se trouve ici : https://docs.mmi-troyes.fr/books/wr319d-wra319d

    L'ensemble de ce cours se base sur la version 6 de Symfony

    Le cours des versions précédentes se trouve : ou

    Objectifs du S3

    • Comprendre le concept de MVC (Modèle-Vue-Contrôleur)

    • Comprendre le fonctionnement d'un framework

    • Installer et débuter avec le framework Symfony

    Durant ce semestre nous aurons pour fil rouge la construction d'un mini forum de discussion. Vous pouvez en voir une demonstration ici :

    https://cours.davidannebicque.fr/symfony/v/version-4.1/
    https://cours.davidannebicque.fr/symfony/v/version-5/
    https://forum.davidannebicque.ovh

    Séance 10 : Sécurité

    La documentation officielle : https://symfony.com/doc/current/security.html

    Introduction

    Deux notions majeures interviennent dans la conception de sécurité de Symfony :

    • Authentification : Qui êtes vous ? ; vous pouvez vous authentifier de plusieurs manières (HTTP authentification, certificat, formulaire de login, API, OAuth etc)

    • Authorization : Avez vous accès à ? ; permet d'autoriser de faire telle ou telle action ou accéder à telle page sans forcément savoir qui vous êtes, utilisateur anonyme par exemple.

    Pour fonctionner, il est nécessaire d'ajouter le composant security à votre symfony.

    Si vous avez installé le projet avec la version complète (webapp), cette ligne n'est pas nécessaire.

    La sécurité dans symfony implique plusieurs éléments :

    • Le firewall: qui est la porte d'entrée pour le système d'authentification, on définit différents firewall (au minimum 1 seul) qui va permettre de mettre en place le bon système de connexion pour l'url spécifiée via un pattern.

    • Le provider : qui permet au firewall d'interroger une collection d'utilisateurs/mot de passe ; C'est une sorte de base de tous les utilisateurs avec les mots de passe. Il existe deux type par défaut :

      • in memory : directement dans le fichier security.yml mais du coup les hash des mots de passes sont disponible dans un fichier

    Configuration

    A partir de la version 4, et avec le composant "maker", la gestion de la sécurité a été grandement facilitée. Là où sur les précédentes versions (la 2 notamment), il était d'usage de passer par un bundle tierce (FOSUserBundle par exemple), aujourd'hui cela n'est plus nécessaire.

    Créer sa classe User

    Si vous ne disposez pas encore d'une classe permettant la gestion des utilisateurs, il est possible d'en créer une avec la console. Si vous disposez déjà d'une classe utilisateur (ou que vous souhaitez utiliser plusieurs entités, il faudra modifier votre code en implémentant les interfaces UserInterface, PasswordAuthenticatedUserInterface et en implémentant les méthodes imposées par ces interfaces).

    L'instruction ci-dessous permet de lancer la console pour créer la table User.

    Symfony va vous poser plusieurs questions afin de configurer les éléments (le nom de l'entité, si vous utilisez doctrine, le champ correspondant au login, et l'encodage du password.

    Une fois cette commande exécutée vous avez un fichier d'entité de créé, un repository associé, et le fichier security.yaml (dans config) qui a été mis à jour.

    Le fichier entité

    Le fichier security.yaml

    Ce fichier fait le lien avec l'entité User (le provider), le login retenu (ici un email), et l'encodage du mot de passe, par défaut "auto"

    Mise à jour de la BDD

    Il faut ensuite mettre à jour votre base de données, avec les commandes suivantes:

    Créer la partie connexion

    Une nouvelle fois la console va nous permettre de dégrossir le travail et produire le contrôleur, le fichier de configuration et le formulaire de connexion.

    Pour le résultat ci-dessous.

    Cette commande, comme indiqué génère plusieurs fichiers :

    • LoginAuthenticator.php : qui explique comment on authentifie un utilisateur (au travers de passeport)

    • SecurityController.php : Car ici, nous avons choisi une connexion avec formulaire, ce contrôleur permettra d'afficher la page de login, de récupérer les informations et de gérer la déconnexion

    • login.html.twig : qui contient le formulaire

    Le fichier security.yaml est mis à jour pour faire le lien avec cet authenticator.

    LoginAuthenticator

    Attention !! Il faut modifier la ligne 51 avec une route qui existe dans votre projet

    SecurityController

    login.html.twig

    Security.yaml mis à jour

    La partie firewall est modifiée pour indiqué quel authenticator utiliser. On pourrait en avoir plusieurs.

    Comme indiqué cette commande va créer plusieurs fichiers :

    • src/Security/LoginAuthenticator.php : qui va contenir la logique de votre authentification. Que faire une fois l'authetification réussie, ou en cas d'échec. Comment récupérer les informations de l'utilisateur.

    • src/Controller/SecurityController.php : qui va être le contrôleur gérant la partie sécurité et authentification. Par défaut la méthode login pour afficher le formulaire et traiter (avec l'aide de l'authenticator précédent), valider les données. C'est dans ce contraôleur que vous pouvez ajouter la déconnexion, l'enregistrement et le mot de passe perdu par exemple.

    • templates/security/login.html.twig : la vue contenant le formulaire de connexion, que vous pouvez librement adapter.

    Les étapes suivantes expliques ce qui a été créé.

    Analyse du fichier security.yaml

    Quasiment toute la sécurité se joue dans le fichier security.yaml qui fait le lien entre les différents éléments et gère les accès.

    L'encodage du mot de passe (encoders) :

    Tout est pré-confiuré, vous pouvez bien sûr adapter. L'encodage permet de définir le "format" de cryptage du mot de passe. Par défaut c'est "auto", c'est à dire que selon votre configuration Symfony choisira le niveau le plus élevé possible (bcrypt ou Argon2i)

    Partie "User Provider" (providers) :

    Le Provider permet de faire le lien avec une source de données contenant les couples login/mot de passe ou des clés d'API... Les providers peuvent être des entités, des données "in_memory", ... Cette partie est configurée a été configurée suite à la création de l'entité User, avec la méthode de connection (login), ici l'email sera utilisé. Il est possible de coupler plusieurs provider tant que l'email est unique sur l'ensemble des sources.

    L'authentification et le firewall

    C'est la partie essentielle du process de sécurisation. C'est lui qui permet de dire quand il faut vérifier et authentifier un utilisateur. Le firewall permet de déterminer pour un pattern d'url (une requête (request)), la méthode d'authentification à utiliser (une page de connexion, une clé d'API, une dépendance à un fournisseur OAuth, ...).

    L'exemple ci-dessus permet de définir que pour les routes particulières (les assets, le profiler), il n'y a pas de vérification. Pour toutes les autres routes (main), il faudra utiliser le provider contenant nos User et l'authenticator gérant le formulaire de Login. C'est ici que l'on pourrait proposer plusieurs méthodes de connexion en ajoutant les authenticator adaptés.

    Symfony propose des exemples pour de nombreuses méthodes d'authentification (login, ldap, json, ...) que vous

    La gestion des rôles et les autorisations

    La gestion des roles se fait dans la partie "access_control" du fichier security. Il permet de définir pour chaque pattern d'URL quel rôle peut y accèder. C'est là que l'on sécurise nos différentes parties. Il est donc important de construire et structurer nos URL correctement pour être efficace sur le filtrage. Il faut évidemment veiller à ce que les pages de connexion ne soient pas derrière une page sécurisée...

    Exemple:

    De cette manière les URL seront automatiquement bloquées si l'utilisateur ne dispose pas du bon rôle. Il est aussi possible de tester ce rôle directement dans un contrôleur ou dans une vue selon les besoins.

    Récupérer l'utilisateur connecté

    Enfin, il est souvent nécessaire de récupérer les informations sur l'utilisateur connecté. Pour cela, dans un contrô leur il est possible d'utiliser directement l'instruction :

    Gérer la déconnexion

    Sur la même idée que pour la connexion, il est possible de gérer la déconnexion. Pour cela, dans le fichier security.yaml, il faut définir le path (la route) pour la méthode qui gére la déconnexion, et la cible (une route, optionnelle), une fois la déconnexion réussie.

    La méthode dans le contrôleur peut se résumer à :

    Cette méthode qui ne retourne rien, permet la déconnexion, et la redirection se fait via le target définit dans security.yaml.

    Générer un mot de passe avec le bon encodage

    Grâce à la console, il est possible de générer un mot de passe selon l'encodage utilisé par Symfony.

    Création d'un utilisateur

    Il est possible d'ajouter des utilisateurs directement dans la base de données (par exemple avec PhpMyAdmin ou en mode console Mysql) avec le mot de passe encodé correctement ou alors en créant un formulaire d'inscription.

    Dans la base de données

    On va ajouter PhpMyAdmin à Docker, pour cela dans votre fichier docker-composer.yaml et ajouter les lignes ci-dessous :

    Pour vous connecter , puis vos identifiants.

    Dans la table User ajouter une entrée, avec un mot de passe crypté, et un rôle, qui doit être un tableau, exemple : ["ROLE_ADMIN"]

    Attention ! On modifie le docker-compose.yaml MMI pas celui de Symfony !!!

    Avec un formulaire d'inscription

    La aussi le maker peut nous aider grandement...

    bin/console make:registration

    Répondez aux questions, il est nécessaire d'installer un complément si vous voulez vérifier le mail de vos utilisateurs : composer require symfonycasts/verify-email-bundle

    Exercice

    Mettre en place une classe User, et créer le formulaire de connexion en suivant la documentation (une commande dans le maker existe).

    Ajouter une page qui ne sera accessible qu'a des utilisateurs Admin.

    Vous pouvez ajouter une fonction pour récupérer le mot de passe perdu (une commande dans le maker existe...)

    Entity : N'importe quelle entité qui implémente à minima les deux interfaces

  • Enfin, plusieurs providers peuvent fonctionner en même temps par exemple in_memory et entity voire plusieurs entités simultanément. http://symfony.com/doc/current/security/entity_provider.html

  • Un encoder : qui permet de générer des hashs/d'encoder des mots de passe ; le plus connu étant MD5 mais vous pouvez utiliser d'autres encoders tels que : sha1, bcrypt ou plaintext (qui n'encode rien c'est le mot de passe en clair) http://symfony.com/doc/current/security/named_encoders.html

  • Les rôles : qui permettent de définir le niveau d'accès des utilisateurs connectés (authentifiés) et de configurer le firewall en fonction de ces rôles. Les rôles peuvent être hierarchisées afin d'expliquer par exemple qu'un administrateur (ROLE_ADMIN par exemple) et avant tout un utilisateur (ROLE_USER).

  • Le "guard" ou "authenticator" qui va gérer l'authentification, au travers de "passport". Il va par exemple vérifier que le couple login/mot de passe existe dans l'un des provider.

  • Mettre à jour le fichier security.yaml afin qu'il fasse le lien avec les différents éléments de sécurité.

    trouverez sur la documentation officielle
    Voir la documentation pour plus d'éléments sur ce point
    http://localhost:8082
    use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
    use Symfony\Component\Security\Core\User\UserInterface;
    composer require symfony/security-bundle
    bin/console make:user
    bin/console make:user                                                                                                    davidannebicque@MacBook-Pro-de-David-2
    
     The name of the security user class (e.g. User) [User]:
     > 
    
     Do you want to store user data in the database (via Doctrine)? (yes/no) [yes]:
     > 
    
     Enter a property name that will be the unique "display" name for the user (e.g. email, username, uuid) [email]:
     > 
    
     Will this app need to hash/check user passwords? Choose No if passwords are not needed or will be checked/hashed by some other system (e.g. a single sign-on server).
    
     Does this app need to hash/check user passwords? (yes/no) [yes]:
     > 
    
     created: src/Entity/User.php
     created: src/Repository/UserRepository.php
     updated: src/Entity/User.php
     updated: config/packages/security.yaml
    
               
      Success! 
               
    
     Next Steps:
       - Review your new App\Entity\User class.
       - Use make:entity to add more fields to your User entity and then run make:migration.
       - Create a way to authenticate! See https://symfony.com/doc/current/security.html
    <?php
    
    namespace App\Entity;
    
    use App\Repository\UserRepository;
    use Doctrine\ORM\Mapping as ORM;
    use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
    use Symfony\Component\Security\Core\User\UserInterface;
    
    #[ORM\Entity(repositoryClass: UserRepository::class)]
    class User implements UserInterface, PasswordAuthenticatedUserInterface
    {
        #[ORM\Id]
        #[ORM\GeneratedValue]
        #[ORM\Column]
        private ?int $id = null;
    
        #[ORM\Column(length: 180, unique: true)]
        private ?string $email = null;
    
        #[ORM\Column]
        private array $roles = [];
    
        /**
         * @var string The hashed password
         */
        #[ORM\Column]
        private ?string $password = null;
    
        public function getId(): ?int
        {
            return $this->id;
        }
    
        public function getEmail(): ?string
        {
            return $this->email;
        }
    
        public function setEmail(string $email): self
        {
            $this->email = $email;
    
            return $this;
        }
    
        /**
         * A visual identifier that represents this user.
         *
         * @see UserInterface
         */
        public function getUserIdentifier(): string
        {
            return (string) $this->email;
        }
    
        /**
         * @see UserInterface
         */
        public function getRoles(): array
        {
            $roles = $this->roles;
            // guarantee every user at least has ROLE_USER
            $roles[] = 'ROLE_USER';
    
            return array_unique($roles);
        }
    
        public function setRoles(array $roles): self
        {
            $this->roles = $roles;
    
            return $this;
        }
    
        /**
         * @see PasswordAuthenticatedUserInterface
         */
        public function getPassword(): string
        {
            return $this->password;
        }
    
        public function setPassword(string $password): self
        {
            $this->password = $password;
    
            return $this;
        }
    
        /**
         * @see UserInterface
         */
        public function eraseCredentials()
        {
            // If you store any temporary, sensitive data on the user, clear it here
            // $this->plainPassword = null;
        }
    }
    
    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:
            # used to reload user from session & other features (e.g. switch_user)
            app_user_provider:
                entity:
                    class: App\Entity\User
                    property: email
        firewalls:
            dev:
                pattern: ^/(_(profiler|wdt)|css|images|js)/
                security: false
            main:
                lazy: true
                provider: app_user_provider
    
                # 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
    
    bin/console d:s:u -f
    
    #ou
    
    bin/console make:migration
    bin/console doctrine:migrations:migrate
    bin/console make:auth
    bin/console make:auth                                                                                                    davidannebicque@MacBook-Pro-de-David-2
    
     What style of authentication do you want? [Empty authenticator]:
      [0] Empty authenticator
      [1] Login form authenticator
     > 1
    
     The class name of the authenticator to create (e.g. AppCustomAuthenticator):
     > LoginAuthenticator
    
     Choose a name for the controller class (e.g. SecurityController) [SecurityController]:
     > 
    
     Do you want to generate a '/logout' URL? (yes/no) [yes]:
     > 
    
     created: src/Security/LoginAuthenticator.php
     updated: config/packages/security.yaml
     created: src/Controller/SecurityController.php
     created: templates/security/login.html.twig
    
               
      Success! 
               
    
     Next:
     - Customize your new authenticator.
     - Finish the redirect "TODO" in the App\Security\LoginAuthenticator::onAuthenticationSuccess() method.
     - Review & adapt the login template: templates/security/login.html.twig.
    <?php
    
    namespace App\Security;
    
    use Symfony\Component\HttpFoundation\RedirectResponse;
    use Symfony\Component\HttpFoundation\Request;
    use Symfony\Component\HttpFoundation\Response;
    use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
    use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
    use Symfony\Component\Security\Core\Security;
    use Symfony\Component\Security\Http\Authenticator\AbstractLoginFormAuthenticator;
    use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge;
    use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
    use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;
    use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
    use Symfony\Component\Security\Http\Util\TargetPathTrait;
    
    class LoginAuthenticator extends AbstractLoginFormAuthenticator
    {
        use TargetPathTrait;
    
        public const LOGIN_ROUTE = 'app_login';
    
        public function __construct(private UrlGeneratorInterface $urlGenerator)
        {
        }
    
        public function authenticate(Request $request): Passport
        {
            $email = $request->request->get('email', '');
    
            $request->getSession()->set(Security::LAST_USERNAME, $email);
    
            return new Passport(
                new UserBadge($email),
                new PasswordCredentials($request->request->get('password', '')),
                [
                    new CsrfTokenBadge('authenticate', $request->request->get('_csrf_token')),
                ]
            );
        }
    
        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'));
            throw new \Exception('TODO: provide a valid redirect inside '.__FILE__);
        }
    
        protected function getLoginUrl(Request $request): string
        {
            return $this->urlGenerator->generate(self::LOGIN_ROUTE);
        }
    }
    
    <?php
    
    namespace App\Controller;
    
    use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
    use Symfony\Component\HttpFoundation\Response;
    use Symfony\Component\Routing\Annotation\Route;
    use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
    
    class SecurityController extends AbstractController
    {
        #[Route(path: '/login', name: 'app_login')]
        public function login(AuthenticationUtils $authenticationUtils): Response
        {
            // if ($this->getUser()) {
            //     return $this->redirectToRoute('target_path');
            // }
    
            // get the login error if there is one
            $error = $authenticationUtils->getLastAuthenticationError();
            // last username entered by the user
            $lastUsername = $authenticationUtils->getLastUsername();
    
            return $this->render('security/login.html.twig', ['last_username' => $lastUsername, 'error' => $error]);
        }
    
        #[Route(path: '/logout', name: 'app_logout')]
        public function logout(): void
        {
            throw new \LogicException('This method can be blank - it will be intercepted by the logout key on your firewall.');
        }
    }
    
    {% extends 'base.html.twig' %}
    
    {% block title %}Log in!{% endblock %}
    
    {% block body %}
    <form method="post">
        {% if error %}
            <div class="alert alert-danger">{{ error.messageKey|trans(error.messageData, 'security') }}</div>
        {% endif %}
    
        {% if app.user %}
            <div class="mb-3">
                You are logged in as {{ app.user.userIdentifier }}, <a href="{{ path('app_logout') }}">Logout</a>
            </div>
        {% endif %}
    
        <h1 class="h3 mb-3 font-weight-normal">Please sign in</h1>
        <label for="inputEmail">Email</label>
        <input type="email" value="{{ last_username }}" name="email" id="inputEmail" class="form-control" autocomplete="email" required autofocus>
        <label for="inputPassword">Password</label>
        <input type="password" name="password" id="inputPassword" class="form-control" autocomplete="current-password" required>
    
        <input type="hidden" name="_csrf_token"
               value="{{ csrf_token('authenticate') }}"
        >
    
        {#
            Uncomment this section and add a remember_me option below your firewall to activate remember me functionality.
            See https://symfony.com/doc/current/security/remember_me.html
    
            <div class="checkbox mb-3">
                <label>
                    <input type="checkbox" name="_remember_me"> Remember me
                </label>
            </div>
        #}
    
        <button class="btn btn-lg btn-primary" type="submit">
            Sign in
        </button>
    </form>
    {% endblock %}
    
    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:
            # used to reload user from session & other features (e.g. switch_user)
            app_user_provider:
                entity:
                    class: App\Entity\User
                    property: email
        firewalls:
            dev:
                pattern: ^/(_(profiler|wdt)|css|images|js)/
                security: false
            main:
                lazy: true
                provider: app_user_provider
                custom_authenticator: App\Security\LoginAuthenticator
                logout:
                    path: app_logout
                    # where to redirect after logout
                    # target: app_any_route
    
                # 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
    
    password_hashers:
        Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
    providers:
            # used to reload user from session & other features (e.g. switch_user)
            app_user_provider:
                entity:
                    class: App\Entity\User
                    property: email
        firewalls:
            dev:
                pattern: ^/(_(profiler|wdt)|css|images|js)/
                security: false
            main:
                lazy: true
                provider: app_user_provider
                custom_authenticator: App\Security\LoginAuthenticator
                logout:
                    path: app_logout
        access_control:
            # - { path: ^/admin, roles: ROLE_ADMIN }
            # - { path: ^/profile, roles: ROLE_USER }
    $user = $this->getUser();
    firewalls:
         main:
             # ...
             logout:
                 path:   app_logout
                 # where to redirect after logout
                 # target: app_any_route
        #[Route(path: '/logout', name: 'app_logout')]
        public function logout(): void
        {
            throw new \LogicException('This method can be blank - it will be intercepted by the logout key on your firewall.');
        }
    bin/console security:hash-password
        phpmyadmin:
            image: phpmyadmin/phpmyadmin
            container_name: phpmyadmin_docker_symfony
            restart: always
            ports:
                - 8082:80
    Logo