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

Symfony-6-LP

Loading...

LP

Loading...

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

Webpack

Formulaires

Planning

Mes interventions sont sur deux modules de LP. Ces modules seront traitées dans la continuité indifféremment du code affiché.

Nous verrons les aspects suivant :

  • Symfony

  • VueJs

  • Lien entre Symfony et VueJs au travers des API

    • ApiPlatform

  • Testing

Repository

Personnaliser TWIG

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?

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.

Présentation

L'ensemble de ce cours se base sur la version 6 de Symfony

Le cours des versions précédentes se trouve : https://cours.davidannebicque.fr/symfony/v/version-4.1/ ou https://cours.davidannebicque.fr/symfony/v/version-5/

Introduction

  • "Eco-système" Symfony

Services et Injection de dépendances

Fetching Services

Symfony comes packed with a lot of useful objects, called services. These are used for rendering templates, sending emails, querying the database and any other "work" you can think of.

If you need a service in a controller, type-hint an argument with its class (or interface) name. Symfony will automatically pass you the service you need:

1 2 3 4 5 6 7 8 9 10 11

Première Page
Controller & Routes
Vues (TWIG)
Modèles - ORM - Entités
Relations entre entités
FORM
Securité
Bundles
TP

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

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.

Services

Créer un environnement Symfony collaboratif

Awesome!

use Psr\Log\LoggerInterface 

//...

/**
* @Route("/lucky/number/{max}")
*/
public function number($max, LoggerInterface $logger)
{
 $logger->info('We are logging!');
 // ...
}

Séance 1 : Architecture de Symfony

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.

Les Bundles

Symfony dispose d'une large communauté très active qui contribue à développer des "bundles" afin de venir enrichir le framework avec des fonctionnalités récurrentes : Back office, gestion d'upload, ...

Il existe un site regroupant les bundles https://flex.symfony.com/ (ou pour les versions précédentes de Symfony http://knpbundles.com/).

Parmis quelques bundles intéressants on peut citer :

  • EasyAdminBundle (bundle reconnu par Symfony pour la génération automatique de backoffice)

  • SonataAdminBundle

  • StofDoctrineExtensionsBundle, qui permet d'enrichir Doctrine (gestion de date created/updated, de traduction, d'upload, ...)

  • KnpSnappyBundle, pour la génération de PDF à partir de WKHtmlToPdf

et beaucoup d'autres pour la sérialization, l'authentification, la génération de pagination, ...

Installation

Avec Symfony 4 l'installation d'un Bundle est devenue très simple (notamment si le bundle dispose d'une recette recipe pour l'utilsisation optimale de flex).

Liste des recipes officielles pour Flex et Symfony , et le dépôt des recipes "non-officielles"

Il suffit, en général, d'installer le bundle avec Composer pour que ce dernier installe et configure les éléments.

Cas de VichUploadBundle

Ce bundle permet la gestion de l'upload en lien avec Doctrine.

https://github.com/symfony/recipes
https://github.com/symfony/recipes-contrib
https://github.com/dustin10/VichUploaderBundle
Arborescence du répertoire de configuration
Configuration classique du répertoire src/

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.

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 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 à 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

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 :

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 :

Séance 1 : installation

Vous pouvez installer Symfony dans un contexte docker ou sur votre système à votre convenance.

Dans tous les cas il vous faut :

  • Git

  • Composer et/ou Symfony CLI

  • PHP 8.1

  • Une base de données (MariaDb, MySql, PosGreSql, ...)

  • Une maîtrise de votre OS

  • PHPStorm et le plugin :

Vérifier que tout fonctionne

Installation de Symfony

ou avec la Symfony CLI

Séance 1 : Eco-Système de Symfony

Séance 1 : Eco-Système de Symfony

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

Mise en production
Mise en production
Mise en production
https://symfony.com/doc/current/configuration.html#configuration-environments
¶
¶
php -v //doit afficher au moins 8.1.xx
git --version
composer -v //doit afficher au moins 2.xx
composer create-project symfony/skeleton nomDuProjet
symfony new nomDuProjet
https://plugins.jetbrains.com/plugin/7219-symfony-support

Composer

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 : Doctrine 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 Doctrine.

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.

ENVIRONNEMENTS

Symfony propose également de définir autant d'environnement que nécessaire afin d'avoir différentes configurations. 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 Twig par défaut.

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
###> symfony/framework-bundle ###
APP_ENV=dev
composer require profiler --dev
composer require maker --dev

Séance 1 : 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.

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.

Twig
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 Post et PostCatégorie.

  • Modifier la page de génération de Post pour créer un nouveau post avec la relation vers "Catégorie 1"

    • La catégorie de ce post sera Catégorie 1 (s'il existe, sinon le créer, cf. séance précédente)

  • Modifier la page avec tous les posts pour afficher la catégorie liée à l'article.

  • Afficher la catégorie 1 avec tous les posts qui lui sont associés

  • Créer une route qui supprime "Post 1"

  • Créer une route qui supprime "Catégorie 1"

En plus : Flash Messages https://symfony.com/doc/current/controller.html#flash-messages

  • Essayer d'utiliser les flash messages.

La documentation officielle de Symfony sur les relations/associations
<?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 totues les dépendances --webapp
<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
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>
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 2 : Controller et Routes

Présentation

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 de Controller (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

L'usage du suffixe Action n'est plus requis dans Symfony.

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 Action 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 Actions 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

  • Créer 2 nouvelles pages :

    • : afficher la date l'heure minute et seconde

    • : affiche "blue" à l'écran dynamiquement

Manipuler les requests

L'objet "request" contient toutes les données envoyées par l'utilisateur (formulaire, ...), mais aussi les données envoyées par le navigateur.

En passant en paramètre un objet de type Request, on peut le manipuler selon les méthodes ci-dessous.

Passer un objet en paramètre de cette manière est ce que l'on nomme de l'injection de dépendance. Le lien est fait automatiquement par Symfony grâce au mécanisme d'autowiring.

Séance 1 : Introduction

La présentation :

Présentation

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

http://localhost/color/red : affiche "red" à l'écran dynamiquement
  • Ajouter un menu avec des liens vers les 2 pages créées.

  • http://localhost/time/now
    http://localhost/color/blue
    #[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')]
    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]
        );
    }
    use Symfony\Component\HttpFoundation\Request;
    
    public function index(Request $request)
    {
        $request->isXmlHttpRequest(); // is it an Ajax request?
    
        $request->getPreferredLanguage(array('en', 'fr'));
    
        // retrieves GET and POST variables respectively
        $request->query->get('page');
        $request->request->get('page');
    
        // retrieves SERVER variables
        $request->server->get('HTTP_HOST');
    
        // retrieves an instance of UploadedFile identified by foo
        $request->files->get('foo');
    
        // retrieves a COOKIE value
        $request->cookies->get('PHPSESSID');
    
        // retrieves an HTTP request header, with normalized, lowercase keys
        $request->headers->get('host');
        $request->headers->get('content_type');
    }
    Pré-requis
    • PHP

    • Programmation Orientée Objet

    • Structure MVC

    • Base de données

    Rappels des concepts du MVC

    Schéma de principe du MVC

    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

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

    • Éviter les appels directs aux commandes PHP

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

    • Apprentissage d’une couche supplémentaire

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

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

    Les 10 frameworks les plus populaires en PHP (2019).

    Article comparatifs des 10 frameworks PHP les plus populaires de 2019: https://coderseye.com/best-php-frameworks-for-web-developers/

    Symfony

    • Framework MVC en PHP 5 (V2), PHP 7 (V3, V4 et V5 ), PHP8 (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

    • Framework français !, De renommée mondiale

    • Premier framework en France et en Europe

    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

    Roadmap des versions de Symfony

    Toutes les informations sur l'évolution du framework : https://symfony.com/releases

    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 : https://www.disko.fr/reflexions/technique/symfony-4-4-5-0-les-nouveautes-venir/

    Installations (SF6)

    Configuration requise pour votre serveur

    • Un serveur Web

    • PHP 8.1 ou supérieur

    • Git (différent de GitHub)

    • Le gestionnaire de dépendance Composer

    • ou Le nouveau gestionnaire d'installation de Symfony :

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

    Vous pouvez suivre aussi les éléments de la documentation officielle : https://symfony.com/doc/current/setup.html

    https://presentations.davidannebicque.fr/r319.html
    Plus grand portabilité du code
  • Ne pas réinventer la roue

  • La gestion des formulaire, des utilisateurs, ...

  • Apprentissage de l’utilisation du framework choisit : ses classes, ses objets, sa logique !
    https://symfony.com/download

    Séance 7 : Formulaires

    FORM

    Introduction

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

    • 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

    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 :

    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>

    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 : Et la documentation pour créer votre template :

    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 :

    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 DefaultController qui gèrera la création des Post

    • Créer un formulaire directement dans DefaultController qui gèrera la modification des Post

    • Modifier la page de listing des postes pour rajouter un lien edition et suppression

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

    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éé.

    Séance 2 : 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.

    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)

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

  • Modifier DefaultController pour utiliser PostType

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

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

  • Afficher le message d'erreur en rouge.

  • La documentation officielle de Symfony sur les formulaires se trouve ici
    https://symfony.com/doc/current/reference/forms/types.html
    https://symfony.com/doc/current/form/bootstrap4.html
    https://symfony.com/doc/current/form/form_customization.html
    http://symfony.com/doc/4.1/validation.html
    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

    Travail à rendre

    Pour ceux qui ont déjà fait du Symfony / qui en font en entreprise

    Présentation

    Réaliser un outil de dépôt de travaux étudiants, en utilisant Symfony. L'outil devra intégrer les fonctionnalités suivantes :

    <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

    • Inscription, connexion, déconnexion pour les étudiants

    • Création de rubriques (par un administrateur)

    • Possibilité de déposer un travail (vidéo, photo, ...), avec titre, description

    • Une recherche simple

    • Liste des catégories, et travaux associés

    • Administration : Création des catégories administration des utilisateurs et des dépôts

    • Il y aura 2 niveaux d'accès : Administrateur, étudiants (possibilité de déposer un travail ou de le modifier)

    • Les travaux comporteront des fichiers et/ou des liens

    • Possibilité de "noter" un travail (système de like par exemple). Affichage d'un top 3 par catégories.

    Vous êtes libre de la structure, de la mise en page et des données de votre base de données, mais vous devez répondre à la commande

    Dans le cadre de ce mini-projet vous utiliserez Webpack Encore pour la gestion de votre partie front (css/js) : https://symfony.com/doc/current/frontend.html

    Le travail pourra être réalisé en binôme. Le rendu sera évalué le 13/12 en fin de séance.

    Notation

    L'esthétique du forum n'est pas prise en compte. L'usage d'une librairie CSS ou d'un template est suffisant.

    Par contre, vous veillerez à l'ergonomie et à la lisibilité.

    Le respect des consignes peut vous apporter jusque 15 points.

    Les 5 points supplémentaires seront acquis en fonction des ajouts (pertinents) que vous ferez, soit pour proposer des fonctionnalités pertinentes, soit dans la qualité de la navigation et de l'accessibilité.

    Pour ceux qui découvrent Symfony cette année

    Présentation

    Réaliser un forum, en utilisant Symfony. Votre forum devra intégrer les fonctionnalités suivantes :

    • Inscription, connexion, déconnexion

    • Création de message dans les catégories du forum

    • Possibilité de répondre à un message

    • Une recherche simple

    • Liste des catégories, sous catégories, et messages associés

    • Administration : Création des catégories et sous-catégories, administration des messages

    • Il y aura 2 niveaux d'accès : Administrateur, membre (possibilité de créer un message ou de modifier ses messages)

    • Un message pourra intégrer des fichiers (images a minima)

    Vous êtes libre de la structure, de la mise en page et des données de votre base de données, mais vous devez répondre à la commande d'un forum

    Notation

    L'esthétique du forum n'est pas prise en compte. L'usage d'une librairie CSS ou d'un template est suffisant.

    Par contre, vous veillerez à l'ergonomie et à la lisibilité.

    Le respect des consignes peut vous apporter jusque 15 points.

    Les 5 points supplémentaires seront acquis en fonction des ajouts (pertinents) que vous ferez, soit pour proposer des fonctionnalités pertinentes, soit dans la qualité de la navigation et de l'accessibilité.

    Le travail est individuel, et sera évalué en direct lors de la séance prévue après le 4 janvier 2023.

    Etapes

    Le sujet pourra évoluer en fonction de l'avancement du cours

    Pour cette première séance vous devrez mettre en place les éléments suivants :

    Première étape

    • Une nouvelle installation de Symfony (6.1/6.2)

    • Réfléchir au MCD que vous aller mettre en place.

    • Mettre en place les entités et la base de données

    • Mettre en place les contrôleurs et les vues nécessaires à la navigation "publique" du site

    • Intégrer un template ou une librairie CSS et faire un minimum de mise en page

    Deuxième partie

    • Mettre en place la sécurité et les éléments de connexions

    • Intégrer les formulaires et la gestion des messages sur la partie publique.

    Troisième partie

    • Mettre en place l'administration et les accès sécurisés.

      • Vous pourriez utiliser EasyAdmin

    • Mettre en place l'upload

      • Tips : utiliser ou manuellement

    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

    VichUploaderBundle
    FileUpload
    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 l'image docker MMI, 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 Post et ajoute les champs suivants

      • titre, string de 150 caractères

      • message, text

      • datePublication, DateTime

    • L'id sera automatiquement ajouté

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

    • Ajouter une route pour créer un nouvel enregistrement

      • Créer un objet Post et compléter les informations (titre, message, datePublication)

      • Récupérer la connexion à doctrine (EntityManagerInterface $entityManager) (ne pas oublier le use associé)

    • 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 entité "PostCategory" avec :

      • title string 255

    • Créer une page qui va sauvegarder une catégorie avec le nom "Catégorie 1".

    • Créer une page qui va sauvegarder un post avec le nom Post 1 à la date courante avec comme message Lorem ipsum.

    • Créer une page qui va afficher le titre de la catégorie en id 1 et le post en id 1.

    • Créer un nouveau post identique au premier en changeant le titre.

    • Créer une page qui affiche la totalité des entités Post.

    • Créer une page qui récupère le Post avec le Titre "Post 1"

    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/PostRepository.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() );
    }
    Associer l'instance de Post avec l'ORM ($entityManager->persist($post))
  • Enregistrer dans la base de données ($entityManager->flush())

  • Séance 8 : Sécurité

    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