- Cours : Symfony 4.4 ~ 5
- 0. À propos du cours
- 1. Installation
- 1.3. Configurer l'application
- 2. Routes et controllers, introduction
- 3. Vues et Twig
- 4. Doctrine, Entities et Repositories
- 4.1. Création de la base de données
- 4.2. Création des entités
- 4.3. Migrations
- 4.4. Migrer les fichiers migrations
- 4.5. Constructeur et createdAt
- 4.6. Enregistrer une donnée : service Doctrine et EntityManager (CREATE)
- 4.7. Lire des données (READ)
- 4.8. Lire des données: requêtes complexes avec le Repository et le QueryBuilder de Doctrine
- 4.8. Mettre à jour (UPDATE)
- 4.9. Supprimer un object (DELETE)
- 5. Commandes make
- 6. Forms
- 7. Notions diverses
- 8. Security et Auth
- 9. Injection de services
Durée du cours : 4 à 5 jours
Requirements :
- PHP > 7.2
- Composer
- Git
Recommandations :
- Un IDE (Visual Studio Code, PHPStorm...)
- Des modules d'autocomplétion pour :
- PHP (VSCode : PHP Intelephense)
- Twig (VSCode : TWIG Pack)
- Yaml (VSCode: YAML)
- .env (VSCode : DOTENV)
- Configurez les "tabulations" de votre IDE en mode
Spaces: 4
(4 caractères "espace" plutôt qu'un caractère "tab")- Consultez la doc fournie à chaque chapitre !
Documentation : Installing & Setting up the Symfony Framework
Suivez les instructions selon votre OS : https://symfony.com/download. ATTENTION à bien suivre les instructions affichées dans le terminal s'il y en a !.
🚀 Exercice 1 🚀 Créez le projet nommé blog
dans le dossier de votre choix. Pas besoin qu'il soit danswww
: Symfony a son propre serveur web et n'est pas dépendant de XAMPP/MAMP/etc !
Vous pouvez créer un nouveau projet Symfony avec la commande suivante :
symfony new nom-du-projet --full
Nous pouvons également utiliser la commande sans --full
qui contient les éléments minimaux d'une application web (microservices, APIs...) et nous laisse le choix d'installer les outils dont nous aurions besoin, néanmoins la commande avec --full
contient tous les outils nécessaires pour bien commencer une application full-stack.
L'installation nous a donné tout une boîte à outils en CLI : php bin/console
.
Pour lancer le serveur depuis le dossier de l'app : symfony server:start
Vous pouvez lancer php bin/console about
pour consulter la configuration actuelle de l'application.
Pour la modifier, modifiez le fichier .env
.
Documentation Routing
🚀 Exercice 2 🚀 Créez la route /about
dansroutes.yaml
et son controller.
Il existe plusieurs façons de déclarer des routes dans Symfony :
# config/routes.yaml
about:
path: /a-propos
controller: App\Controller\PagesController::about
Dans ce cas, nous nommons (c'est simplement un nom interne à l'application qui nous sert de référence pour cette route/méthode) notre route about
, et nous indiquons à Symfony de se diriger vers le contrôleur PagesController
et la méthode about
lorsque l'utilisateur va sur l'URI /a-propos
(donc l'URL http://127.0.0.1:8000/a-propos
par exemple).
Il faut donc créer un PagesController
: d'après le fichier composer.json
, dans la key psr-4
, on sait que le namespace App/
pointe vers le dossier src/
.
Nous allons donc créer un contrôleur dans src/Controller
:
// src/Controller/PagesController.php
namespace App\Controller;
use Symfony\Component\HttpFoundation\Response;
class PagesController {
public function about() {
return new Response('Hello world!');
}
}
Important : Notez bien l'usage de l'alias de
Symfony\Component\HttpFoundation\Response
! Utilisez bien l'autocomplétion de votre IDE afin de bien importer les alias nécessaires (tip : commencez à taper le nom d'une classe et choisissez avec les flèches du clavier la classe souhaitée, aidez vous du namespace pour savoir quelle est la bonne classe à inclure !).
Et voilà, nous avons fait notre premier Hello world.
🚀 Exercice 3 🚀 Créez la route /home
en annotations dans PagesController.
Une autre manière de créer des routes dans Symfony sont les annotations. Toujours dans PagesController.php
:
use Symfony\Component\Routing\Annotation\Route;
// ...
/**
* @Route("/home", name="home")
*/
public function home() {
return new Response ('Bienvenue sur la page d\'accueil !');
}
Notez bien l'utilisation de Symfony\Component\Routing\Annotation\Route
!
Les annotations permettent de déclarer les routes juste au dessus de la méthode qui prendra en charge l'URI. C'est donc plus pratique car tout est au même endroit, mais plus dispersé que d'avoir toutes les routes dans un fichier *.yaml.
🚀 Exercice 4 🚀 Dans PagesController, réez la route /articles/{id}
qui affiche "Voici l'article numéro {id}".
Nous pouvons écouter des paramètres dans les routes en ajoutant des {variables}
dans l'URL :
/**
* @Route("/users/{userId}/books/{bookId}", name="user_book")
*/
public function users(int $userId, int $bookId) {
return new Response ('Vous consultez le livre #' . $bookId . ' de l\'utilisateur numéro '. $userId);
}
🚀 Exercice 5 🚀 Modifiez la route /articles/{id}
de sorte à n'accepter que des nombres.
🚀 Exercice 6 🚀 Créez la route /products/{productName}
. Elle ne doit accepter que des lettres et des tirets. Documentez-vous sur les REGEX si besoin.
Nous pouvons utiliser des wildcards dans les routes, c'est à dire une chaîne de caractères quelconque que l'on peut valider par des expressions régulières (regex) :
/**
* @Route("/blog/{page}", name="blog_list", requirements={"page"="\d+"})
*/
Dans ce cas là, la route n'acceptera que les cas où l'argument {page}
correspond à la regex \d+
(= valeurs numériques uniquement).
Si jamais je souhaite pouvoir accéder à l'URI /blog/
malgré tout, avec une valeur par défaut (par exemple je veux que par défaut, page = 1
), je peux le passer en paramètre de l'action :
/**
* @Route("/blog/{page}", name="blog_list", requirements={"page"="\d+"})
*/
public function list($page = 1)
{
// ...
}
🚀 Exercice 7 🚀 Créez une route /contact
qui récupère en autowiring la requête utilisateur dans $request et débuguez la grâce àdump($request)
oudd($request)
(ce sont desvar_dump()
améliorés à utiliser avec Symfony)
🚀 Exercice 8 🚀 Trouvez la différence entre dd()
etdump()
.
Nous utilisions les Request et Response du package HttpFoundation pour gérer les requêtes et réponses HTTP. Grâce à l'autowiring (autochargement des classes), nous pouvons directement appeler la requête dans les arguments de la méthode :
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
// ...
/**
* @Route("/post-user", name="create_user", methods={"POST"})
*/
public function create(Request $request) : Response
{
dump($request);
}
Plusieurs nouveaux concepts ici :
- Nous avons importé les classes
Request
etResponse
- Grâce à l'autowiring, nous pouvons appeler l'object
Request $request
dans notre action (la méthodecreate()
) - Nous avons précisé les méthodes autorisées pour cette route avec
methods={"POST"}
- Nous avons indiqué le type de retour de la fonction (
: Response
)
Et voilà, l'object Request $request
, qui par exemple peut être issu de l'envoi d'un formulaire, est disponible à l'utilisation ! Nous pouvons accéder aux valeurs POST par exemple avec $request->get('name');
.
🚀 Exercice 9 🚀 Faites une route nommée contact-us
, accessible par/contactez-nous
ou par/contact-us
.
/**
* @Route({ "fr": "/a-propos", "en": "/about-us"}, name="about")
*/
public function about()
{
// ...
}
Un outil de la console nous permet de lister toutes les routes déclarées (pratique notamment lorsque l'on utilise les annotations !) : php bin/console debug:router
Documentation : Creating and Using Templates
🚀 Exercice 10 🚀 Faites hériter votre controller de AbstractController
(attention auuse
)
Maintenant que nous avons vu le routeur et le controller, nous allons voir comment retourner une vue depuis un controller.
Symfony utilise Twig comme moteur de template : grâce au container de service de la classe parente AbstractController qu'il vous faut hériter, il peut être disponible directement auprès du contrôleur :
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
// ...
class PagesController extends AbstractController {
/**
* @Route("/home", name="home")
*/
public function home() {
return $this->render('home.html.twig');
}
}
En héritant de AbstractController
, nous pouvons dorénavant utiliser la méthode render()
qui prend en premier paramètre le fichier Twig à utiliser.
🚀 Exercice 11 🚀 Créez une vue dans le dossier /templates
nomméecontact.html.twig
et affichez-la quand on va sur la routecontact-us
.
Les vues se trouvent dans le dossier /templates
(défini par dans twig.default_path
le fichier de configuration twig.yaml
).
Nous allons donc créer le fichier templates/home.html.twig
:
{# templates/home.html.twig #}
{% extends 'base.html.twig' %}
{% block title %}Page Home !{% endblock %}
{% block body %}
<div class="example-wrapper">
<h1>Hello Page Home !</h1>
</div>
{% endblock %}
Détaillons ce code :
{% extends 'base.html.twig' %}
Cette ligne indique à Twig d'utiliser le fichier de template base.html.twig
, qui se trouve aussi dans /templates
.
Si on regarde le fichier base.html.twig
, on voit qu'il s'agit d'un fichier HTML classique avec des éléments {%block ... %}{% endblock %}
.
Les blocs du template de "extends", base.html.twig
, sont les éléments "extensibles" : ils peuvent contenir une valeur par défaut, comme {% block title %}Welcome!{% endblock %}
ou rien du tout.
En fait, nous allons remplir leur contenu par les fichiers de vues comme home.html.twig
, comme avec ce morceau de code :
{% block body %}
<div class="example-wrapper">
<h1>Hello Page Home !</h1>
</div>
{% endblock %}
Le code HTML généré pour le client sera donc base.html.twig
avec ce code ci-dessus dans son bloc body
!
🚀 Exercice 12 🚀 Créez une route /articles
et affichez tous les articles (titre et contenu) issus du tableau suivant :
$articles = [ [ 'title' => 'Titre 1', 'content' => 'Contenu du premier article', ], [ 'title' => 'Titre 2', 'content' => 'Contenu du second article', ], ];
Nous pouvons évidemment passer des variables à la vue depuis le contrôleur :
// PagesController.php
// ...
public function home() {
$pageTitle = "Mon super site";
$movies = [
[
"title" => "Inception",
"length" => 135,
],
[
"title" => "Rocky",
"length" => 126,
]
];
return $this->render('home.html.twig', [
'pageTitle' => $pageTitle,
'movies' => $movies
]);
}
Le deuxième argument de render()
prend un tableau : la key
est le nom de la variable passé à Twig, la value
est le contenu de la variable.
Si, comme pour l'exemple ci-dessus, le nom des variables pour Twig et pour PHP ont le même nom, on peut rendre le code plus conscis avec compact()
(doc PHP) :
return $this->render('home.html.twig', compact('title', 'movies'));
Nous pouvons maintenant utiliser les variables dans Twig :
{% block title %}Page : {{ pageTitle }} {% endblock %}
{% block body %}
<div class="example-wrapper">
<h1>Hello, vous êtes sur la page {{ pageTitle }} !</h1>
</div>
{% endblock %}
Pour afficher les données d'un array ou d'une collection d'objects, nous pouvons utiliser la boucle for
de Twig :
{% block body %}
<h1>Films</h1>
<ul>
{% for movie in movies %}
<li>{{ movie.title }} (Durée : {{ movie.length }} min)</li>
{% endfor %}
</ul>
{% endblock %}
Nous pouvons faire un affichage conditionnel en Twig :
{% if not user.subscribed %}
<p>Vous n'êtes pas encore inscrit à la mailing list.</p>
{% endif %}
{% if temperature > 18 and temperature < 27 %}
<p>It's a nice day for a walk in the park.</p>
{% endif %}
🚀 Exercice 13 🚀 Dans l'affichage de /articles
, affichez les titres inversés (par exemple: "Titre" devient "ertiT" grâce à un pipe Twig (voir la documentation de Twig)
Nous pouvons modifier la donnée à la volée grâce aux filters (pipes) :
{{ 'bienvenue'|upper }} {# retourne : 'BIENVENUE' #}
La documentation complète de Twig est disponible ici : documentation Twig.
🚀 Exercice 14 🚀 Créez une route GET /messagerie qui affichera avec Twig un formulaire avec "nom", "prénom", "message"
🚀 Exercice 15 🚀 Créez une route POST /messagerie qui récupèrera les données du formulaire via $request (indice: google "symfony request post params") et les affichera dans une page Twig
🚀 Exercice d'application : créer la structure d'un e-commerce 🚀 Créez les pages suivantes, toutes dans un ProductController
- Liste des produits (
GET /products
) : faites une liste de 5 produits fictifs dans le controller, envoyés à la vue (ils seront ensuite remplacés par des produits en BDD) |- Ajouter un produit (formulaire + traitement) (
GET /products/create
etPOST /products
pour le traitement) |- Voir un produit (page d'un produit fictif :
GET /products/{id}
) |Champs à respecter pour les produits :
PRODUCT(id, title, description, price, quantity)
|
Documentation : Databases and the Doctrine ORM OpenClassrooms : Gérez vos données avec Doctrine ORM`
🚀 Exercice 16 🚀 Cnfigurer le .env de sorte à se connecter au mysql de WAMP/XAMPP/MAMP et à la base de données symfoblog .
Doctrine est un ORM (Object-relationnal Mapping), qui implémente le pattern Data Mapper. Concrètement, le Data Mapper synchronise un object dans le PHP avec la base de données, ce qui nous donne une couche Model performante dans notre MVC.
Pour commencer, vous devrez configurer votre base de données dans le fichier .env
qui se trouve à la racine du projet :
DATABASE_URL=mysql://db_user:[email protected]:3306/db_name
Remplacez les valeurs de db_user
et db_password
par les valeurs qui correspondent à votre configuration.
Attention : si votre mot de passe est vide, laissez bien les deux-points avant le @, exemple :
DATABASE_URL=mysql://root:@127.0.0.1:3306/db_name
Pour le champ db_name
, vous pouvez en créer un nouveau : nous allons pouvoir le créer depuis l'outil console
de Symfony !
Une fois configuré, ouvrez une console dans le dossier du projet et saisissez :
php bin/console doctrine:database:create
Et voilà, la base de données a été créée !
🚀 Exercice 17 🚀 Créer l'entité Article comme définie dans le cours. En cas d'erreur, supprimez les fichiers entity/Article.php et repositories/ArticleRepository.php et recommencez la commande
Nous allons créer des Entity : ce sont l'équivalent des Model du MVC, il s'agit de la classe qui mappera la table correspondante en base de données.
Pour cela, ouvrez une console et saisissez :
php bin/console make:entity Article
Attention : les entités ont la première lettre en majuscules et sont au singulier.
Le CLI vous guidera pour créer les champs un par un : créez par exemple les champs suivants :
title (string, NOT NULL)
description (text, NOT NULL)
created_at (datetime, NOT NULL)
Une fois l'entité créée, nous pouvons aller la voir dans src/Entity/Article.php
.
🚀 Exercice 18 🚀 Effectuer une migration et vérifier qu'il y ait la table Article créée dans PHPMyAdmin
L'entité est un mapping de notre base de données : c'est à dire que le fichier Entity correspond, grâce aux annotations @ORM
notamment, à ce à quoi ressemble notre table en base de données.
Si nous souhaitons faire une modification dans les tables, nous avons deux manières de faire :
- Modifier le fichier
Entity/Article.php
- Ajouter un champ grâce à
php bin/console make:entity Article
: le fait de reprendre le nomArticle
ici va éditer l'entity existanteArticle
!
Attention : Nous n'avons donc plus besoin de modifier la base de données directement dans PHPMyAdmin !
IMPORTANT: Maintenant que vous avez créé et éventuellement modifié votre fichier Entity, vous créerez un fichier migration :
php bin/console make:migration
Consultez le fichier créé qui se trouve dans src/Migrations
: une migration est en fait une instruction de DB qui nous indique quoi faire par rapport à l'état de nos fichiers Entity : par exemple là, vous verrez dans la méthode up()
un CREATE TABLE Article ...
.
Ce qui se passe en fait : Doctrine, l'ORM de Symfony, va comparer l'état de la base de données actuellement et à quoi ressemblent les Entity ! Là en effet, on n'a pas de table Article
en base de données mais on a une Entity Article
... La migration nous propose donc un CREATE TABLE
.
Il existe aussi une migration down
(la méthode down()
du fichier migration) : il s'agit de l'opposé de la migration up
: en effet, si vous voulez revenir à l'état précédent de la base de données, plutôt que de vous souvenir de vos modifications, il sera possible de faire une migration dite down
pour l'annuler !
Maintenant que les fichiers migrations sont fait, c'est à dire les instructions à donner à la base de données, nous allons migrer ces fichiers afin que la base de données lance ces commandes SQL :
php bin/console doctrine:migrations:migrate
Les deux commandes ci-dessus sont équivalentes, la seconde, plus courte, est simplement un alias.
Comme vous l'avez remarqué lors de la création de la base de données (4.1. Création de la base de données), une table migrations
a été créée : elle va en simplement enregistrer la liste des fichiers de migration qui ont été exécutés afin de garder une trace de ce qu'il reste à faire !
En exécutant cette commande, les fichiers migrations restant à migrer vont donc être exécutés.
Il est très important de se souvenir du fonctionnement des migrations, rappel :
- Modification du fichier
Entity
(avecphp bin/console make:entity Article
ou en modifiant le fichier à la main) - L'entité est modifiée, il faut persister ces changements en base de données :
php bin/console make:migration
make:migration
va simplement créer un fichier migration en comparant à quoi ressemble la base de données et à quoi ressemble le fichier Entity- Immédiatement, je peux persister les changements
php bin/console doctrine:migrations:migrate
.
TRÈS IMPORTANT : Après une modification de l'entity, et surtout après un make:migration
, exécutez systématiquement un migrate
: en effet, cela vous évite de refaire un second make:migration
qui n'aurait pas été migré et donc générer des erreurs, exemple à ne pas faire :
- J'ajoute une nouvelle
Entity
,User
par exemple - Je fais un
make:migration
- Le fichier migration créé ressemble à :
CREATE TABLE User...
- Je refais un
make:migration
au lieu d'unmigrate
: un autre fichier de migration se créée, et refait unCREATE TABLE User...
(en effet, il n'y a toujours rien dans ma DB, Doctrine pense devoir refaire unCREATE TABLE User
!) - Je migre un peu trop tard:
migrate
et... j'ai une erreur (en effet, j'aurai deuxCREATE TABLE User
au lieu d'un seul, MySQL lèvera une erreur)
Résumé: Pour éviter ce problème facilement, il suffit juste de faire un
migrate
avant chaquemake:migration
afin d'exécuter les migrations précédentes s'il en restait à faire :
// Créer une migration :
php bin/console doctrine:migrations:migrate # On migre les précédentes migrations éventuelles
php bin/console make:migration # On créée la nouvelle migration
php bin/console doctrine:migrations:migrate # On migre la nouvelle migration
🚀 Exercice 19 🚀 Modifiez le constructeur comme proposé dans le cours.
Comme notre entité représente notre table en base de données, nous pouvons gérer les données comme tel : pour donner une valeur par défaut au champ created_at
, nous pouvons créer un constructeur dans Article.php
: les getters et setters sont déjà générés !
// Article.php
// ...
public function __construct() {
$this->setCreatedAt(new \DateTime());
}
🚀 Exercice 20 🚀 Faites une route dans laquelle vous pourrez tester la création d'un article en dur comme indiqué dans le cours.
Un service est une classe qui remplit une fonction bien précise, accessible partout dans notre code grâce au container de services.
Dans une méthode d'un contrôleur, nous allons créer un nouvel objet Article
et lui donner quelques données grâce aux setters.
$article = new Article();
$article->setTitle('Nouveau titre !');
$article->setContent('Lorem ipsum....');
Doctrine est le service qu va nous permettre de gérer la base de données et de persister les données en base de données, c'est à dire d'enregistrer l'objet créé en une ligne de la base de données. Il est accessible depuis le contrôleur comme n'importe quel autre service :
$doctrine = $this->getDoctrine();
Doctrine s'occupe de plusieurs choses : d'une part la connexion à la base de données ($doctrine->getConnection($name))
récupère une connection à une base de données par exemple), et d'autre part de la partie EntityManager
, c'est la partie ORM, qui va persister les données :
$entityManager = $doctrine->getManager();
Nous pouvons avoir plusieurs EntityManager: un par connexion à une base de données par exemple (dans le cas où vous gérez plusieurs BDD pour votre projet).
Nous allons donc persister les données (enregistrer l'object en tant que ligne de DB) grâce à l'EntityManager :
$entityManager->persist($article); // On prépare l'article à être enregistré en BDD
$entityManager->flush(); // On execute effectivement la requête !
En résumé :
// On créée un nouvel object Article
$article = new Article();
$article->setTitle('Nouveau titre !');
$article->setContent('Lorem ipsum....');
// On récupère l'EntityManager du service Doctrine :
// Notez que le code est plus court que dans l'expliation ci-dessus !
$em = $this->getDoctrine()->getManager();
// On donne l'object en gestion à Doctrine pour qu'il "persiste" l'object, c'est à dire qu'il prépare la requête
$em->persist($article);
// On execute effectivement la requête :
$em->flush();
Et voilà ! L'article est enregistré en base de données. On peut dorénavant (sur le même object que ci-dessus !), faire un $article->getId()
pour récupérer l'ID de l'objet nouvellement enregistré.
🚀 Exercice 21 🚀 Affichez dans un dd()
puis dans Twig la liste des articles.
Lors de la création de notre entité, un fichier Repository\ArticleRepository.php
a été créé : le repository est le fichier qui s'occupe de récupérer les données de la base de données.
Voici comment il s'utilise :
// On importe le repository de l'entity Article
$articleRepository = $this->getDoctrine()->getRepository(Article::class);
// Tous les articles
$articles = $articleRepository->findAll();
// Un article (par son ID)
$article = $articleRepository->find(43);
// Une collection d'articles (search par un champ)
$articles = $articleRepository->findBy(['title' => 'Hello title!']);
🚀 Exercice 22 🚀 Voir ci desous :
Créez un ProductsController avec les routes suivantes :
GET /products
GET /products/new
POST /products
- Créez l'entité suivante :
Product
----
name (string)
price (int)
quantity (int)
Dans GET /products : faites une page avec Twig qui affichera la liste des produits. Vous pouvez en créer à la main dans la base de données pour tester.
Dans GET /products/new : faites un formulaire de création d'un produit avec Twig qui ira vers la route POST /products . Conseil : pour faire le lien dans le "action" du formulaire, il vous faut renseigner le name de la route de destination, que vous aurez renseigné au préalable dans le controller, de cette façon :
<form action="{{ path('products_new'}}" method="post">
Dans POST /products, vous récupérerez les données du formulaire grâce à $request et vous créérez un nouveau Product. Conseil pour récupérer les données du formulaire dans $request :
$request->request->get('title');
À la fin de la méthode du controller qui traite l'insert POST /products, faites une redirection vers la page GET /products grâce à $this->redirectToRoute('nom_de_la_route');
🚀 Exercice 23 A 🚀 en page d'accueil, grâce à une méthode findByQuantityNotNull() dans leProductController, affichez la liste des produits dont la quantité n'est pas nulle
🚀 Exercice 23 B 🚀 faire un formulaire de recherche en page d'accueil qui ira vers une route /products/search du ProductController, qui retournera les éléments recherchés grâce à une méthode créée dans le ProductRepository (findBySearch($elementDeRecherche)) Par exemple, quand je saisis chaise dans le formulaire je dois tomber sur les produits dont le name est "Chaise longue", "Grande chaise en bois", etc. Il faudra faire une requête avec LIKE %chaise% . Inspirez vous d'exemples grâce à une recherche Google également car les % auront une petite particularité pour fonctionner dans votre requête si jamais elle ne marche pas tout de suite.
Documentation: Doctrine - Working with Query Builder
On peut bien sûr exécuter des requêtes plus complexes avec le repository, éditions par exemple le fichier src/Repository/ArticleRepository.php
.
Le fichier contient deux exemples commentés, décommentons le premier exemple :
/**
* @return Article[] Returns an array of Article objects
*/
public function findByExampleField($value)
{
return $this->createQueryBuilder('a')
->andWhere('a.exampleField = :val')
->setParameter('val', $value)
->orderBy('a.id', 'ASC')
->setMaxResults(10)
->getQuery()
->getResult()
;
}
On voit comment est composée une requête avec le QueryBuilder, avec par exemple :
-
l'ajout de paramètres : on a
$value
en paramètres de la méthode. On prépare la requête avec une clé:val
dans leandWhere()
, et on va ajouter le paramètre à la requête avecsetParameter(key, $var)
. -
setMaxResults(10)
: permet de limiter les résultats... à 10 !
Pour utiliser cette requête, on peut l'appeler dans le contrôleur. Disons que nous l'avons renommée findByName($name)
au lieu de findByExampleField($value)
:
$articles = $articleRepository->findByName('sciences');
🚀 Exercice 24 🚀 Faites une page GET /products/{product}
qui affichera un produit (page show).
🚀 Exercice 25 🚀 Faites une page GET /products/{product}/edit
qui sera un formulaire d'édition d'un produit.
Maintenant que nous savons lire une donnée et écrire une donnée, nous allons mixer les deux et faire une méthode d'update.
C'est aussi l'occasion de voir des notions nouvelles :
-
Nous passons en argument à la méthode la requête qui vient du client,
Request $request
, afin de récupérer les données issues d'un formulaire -
Nous passons un paramètre à la route,
id
, un nom interne à l'applicationarticles_edit
et une liste de méthodes HTTP autorisées sur cette routePOST
(ce qui veut dire qu'aller sur/articles/{id}/edit
depuis un navigateur en GET ne marchera pas !). Pour prendre en compte l'id, on doit le passer en argument à la méthode : on peut aussi forcer le type !Article $article
. Grâce à cela, Symfony s'occupera pour nous de récupérer l'article dont l'id est égal à{id}
.
// @Route("/articles/{id}/edit", name="articles_edit", methods={"POST"})
public function update(Request $request, Article $article) {
}
Sans appeler l'article $article
en paramètres avec {id}
nous aurions aussi pu faire :
// @Route("/articles/{id}/edit", name="articles_edit", methods={"POST"})
public function update(Request $request, int $id) {
$articleRepository = $this->getDoctrine()->getRepository(Article::class);
$article = $articleRepository->find($id);
}
Maintenant que nous avons notre Entity $article
, nous allons l'éditer et la flusher comme pour un insert :
// @Route("/articles/{id}/edit", name="articles_edit", methods={"POST"})
public function update(Request $request, Article $article) {
// On met à jour l'article
$article->setTitle('Nouveau titre mis à jour');
// On récupère l'EntityManager et on met à jour (sans persister, juste flush)
$entityManager = $this->getDoctrine()->getManager();
$entityManager->flush();
}
🚀 Exercice 26 🚀 Faites une page GET /products/{product}/delete
qui sera un formulaire de suppression d'un produit.
La suppression est très facile en utilisant tout ce que nous venons de voir :
$entityManager->remove($article);
$entityManager->flush();
🚀 Exercice 27 🚀 Sur une page GET /products
qui liste tous les produits, ajoutez également des liens vers les pages show, edit, delete de chaque produit.
Les liens avec paramètres se crééent ainsi :
{{ path('nom_de_la_route', {param1: value1, param2: value2} ) }}
Exemple :
Selon ce qu'attend la route (un id, l'objet lui même...)
{{ path('article_show', {id: article.id} ) }}
Ou alors :
{{ path('commande_edit', {commande: commande'} ) }}
Vous pouvez créer un nouveau contrôleur avec la commande make:controller PagesController
. Ce contrôleur contiendra une première page index()
par défaut avec un template dans templates/pages/index.html.twig
!
Documentation : Forms
🚀 Exercice 28 🚀 Voir ci-dessous :
1. Créez une entité Category (title: string, description: text null). Créez et exécutez une migration.
2. Créez un formulaire Symfony pour l'entity Category
3. Créez un CategoryController avec 2 routes : category_index (get) et category_new (get, post) .
4. Dans la navbar ou la page d'accueil, faites un lien vers category_index
5. Dans category_index, faites un lien vers category_new
6. Dans la méthode de category_new, gérez le formulaire Symfony et affichez-le. Vérifiez si les données s'enregistrent en bdd
Vous pouvez créer un formulaire auto-généré (Type
) pour une entité : Symfony lira l'Entity et crééra le formulaire correspondant : make:form Article
. Cela crééra un fichier dans src/Form/ArticleType.php
.
Pour intégrer le formulaire, il suffira ensuite de l'appeler dans le contrôleur de cette façon :
// /new est accessible en 2 méthodes:
// GET : pour AFFICHER le formulaire
// POST : pour TRAITER le formulaire
/**
* @Route("/new", name="product_new", methods={"GET","POST"})
*/
public function new(Request $request): Response
{
// CAS GET (affichage) :
// On prépare l'article à créer avec le formulaire
$article = new Article();
// On prépare le formulaire : on utilise le service createForm avec en arguments: le formulaire généré (ArticleType) et l'objet traité par le formulaire ($article)
$form = $this->createForm(ArticleType::class, $article);
// CAS POST (traitement) :
// On indique au formulaire de traiter la requête
$form->handleRequest($request);
// Si le formulaire a été envoyé et est valide, on le traite
if ($form->isSubmitted() && $form->isValid()) {
// On enregistre la donnée
$entityManager = $this->getDoctrine()->getManager();
$entityManager->persist($product);
$entityManager->flush();
// On redirige vers la page article_index
return $this->redirectToRoute('article_index');
}
// CAS GET ou CAS POST SI FORMULAIRE INVALIDE (if ci-dessus) :
// On affiche le formulaire
return $this->render('product/new.html.twig', [
'product' => $product,
'form' => $form->createView(),
]);
}
Si on regarde le return
de la méthode ci-dessus :
return $this->render('product/new.html.twig', [
'product' => $product,
'form' => $form->createView(),
]);
On voit qu'on envoie à Twig une variable form
. Cette variable contiendra un formulaire prêt à être affiché et généré automatiquement !
Dans new.html.twig
, à l'endroit où afficher le formulaire :
{{ form_start(form) }}
{{ form_widget(form) }}
<button>Créer</button>
{{ form_end(form) }}
form_start
va ouvrir la balise<form> pour le formulaire passé en paramètres à Twig depuis le controller, nommé
form`form_widget
va afficher tous les champs du formulaire à la suite avec un style par défautform_end
va fermer la balise<form>
du formulaireform
passé en paramètres à Twig depuis le controller.
Les formulaires autogénérés peuvent prendre le style Boostrap en modifiant config/packages/twig.yaml
et en ajoutant l'attribut suivant :
twig:
form_themes: ['bootstrap_4_layout.html.twig']
Attention, ce sont bien 4 espaces et non pas une tabulation !
Documentation Forms Documentation Form Types Reference
Les formulaires peuvent donc être créés de 3 façons :
- Par la commande
make:crud
qui génère entre autres le formulaire généré pour une Entity - Par la commande
make:form
qui ne génère que le formulaire généré pour une Entity - Directement à la main dans un fichier Type ou dans le controller
Voyons comment sont composés les formulaires générés dans Symfony, prenons par exemple un LocationType
(formulaire d'ajout d'adresses) :
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name', TextType::class)
->add('street_number', IntegerType::class)
->add('street_name', TextType::class)
->add('zip', IntegerType::class)
->add('city', TextType::class)
->add('country', CountryType::class)
->add('longitude')
->add('latitude')
;
}
On utilise une instance de FormBuilderInterface
pour générer les formulaires.
Chaque champ est ajouté avec add()
qui prend 3 arguments :
- le nom du champ
- la classe
Type
correspondante, qui va gérer le formulaire selon le type (DateTimeType, EmailType...)
🚀 Exercice 29 🚀 Voir ci-dessous :
1. Ajoutez un champ category à l'entité Product (grâce à make:entity Product). Voir le cours 7.2 pour apprendre à faire une relation. Pensez à faire et executer une migration.
2. Modifiez le ProductController pour utiliser un formulaire Symfony tout comme on fait pour Category. Il faudra gérer le champ category dans le formulaire, inspirez vous du cours 6.4 ! Assurez vous d'avoir des catégories en bdd pour pouvoir tester le formulaire qui inclura le choix d'une category.
3. Dans les pages GET /product, GET /product/{product} et GET /, affichez le nom de la catégorie du produit grâce à product.category.title
On peut ajouter une relation dans un formulaire, de sorte à ce que, par exemple, avec Article 1-N Category
, nous ayons la liste des catégories dans un select !
//...
->add('category', EntityType::class, [
'class' => Category::class, // Quelle classe est reliée au champ category
'choice_label' => 'name', // Quel champ de Category afficher dans le select
])
//...
Dans le cas d'une relation N-N (Tag N-N Article
), on aurait plutôt un select multiple :
//...
->add('tags', EntityType::class, [
'class' => Tag::class,
'choice_label' => 'name',
'multiple' => true
])
//...
Attention : Choisissez le bon cas d'usage selon votre relation (mettre un selct multiple ou non), sinon vous aurez un bug !
Documentation : Validation Documentation : Constraints
Les formulaires peuvent être validés de plusieurs façons :
Ces validations se font au niveau de l'entité, par exemple on rend ici unique le titre avec UniqueEntity
et on limite la taille du titre à entre 2 et 50 caractères.
// ...
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
/**
* @UniqueEntity("title")
*/
class Article {
/**
* @Assert\Length(
* min = 2,
* max = 50,
* minMessage = "Your first name must be at least {{ limit }} characters long",
* maxMessage = "Your first name cannot be longer than {{ limit }} characters"
* )
* @ORM\Column(type="string")
*/
private $title;
// ...
Cette fois, dans le fichier src/Form/UserType
:
->add('nickname', TextType::class,[
'constraints' => [
new Length([
'min' => 1,
'minMessage' => 'Your nickname should be at least {{ limit }} characters',
'max' => 20,
'maxMessage' => 'Your nickname should be maximum {{ limit }} characters',
]),
],
])
Maintenant que vous savez composer un CRUD complet en Symfony, le configurer et le customiser : On peut créer automatiquement un CRUD pour une entité (qui doit exister avant de faire la commande) : make:crud Article
.
La commande va créer un controller
, un ficher Type
(le formulaire généré) et des vues dans /template
.
Attention: Il est très important de bien comprendre les fonctionnements que nous voyons de voir jusqu'à présent ! Bien que la commande
make:crud
fait "tout ça d'un coup", c'est important de comprendre tout ce que nous avons vu plutôt que d'utiliser des générateurs afin de savoir comment les débugger !
Maintenant que nous avons vu comment créer un CRUD en Symfony, gérer les routes, le MVC..., il s'agit surtout d'apprendre des pratiques et techniques au cas par cas.
// Pour rediriger vers /articles/{id} (name="articles_show")
return $this->redirectToRoute('articles_show', [
'id' => $article->getId()
]);
On peut ajouter une relation entre deux entités de la façon suivante :
php bin/console make:entity Article # On édite l'entité Article
# ATTENTION: Au pluriel ou au singulier en fonction de la relation !!!
# ATTENTION: On ne met pas l'id mais le nom de la relation !!!
New property name (press <return> to stop adding fields):
> category
# Vous pouvez taper directement le type de relation ou taper "relation" pour avoir la liste des relations disponibles
Field type (enter ? to see all types) [string]:
> relation
# On parle bien de l'entité (singulier, première lettre majuscule)
What class should this entity be related to?:
> Category
What type of relationship is this? # ManyToOne, OneToMany, OneToOne, ManyToMany
Relation type? [ManyToOne, OneToMany, ManyToMany, OneToOne]:
> ManyToOne
# Accéder aux articles depuis une catégorie ?
Do you want to add a new property to Category so that you can access/update Article objects from it - e.g. $category->getArticles()? (yes/no) [yes]:
> yes
A new property will also be added to the Category class so that you can access the related Article objects from it.
New field name inside Category [articles]:
> articles
Pensez à migrer :
php bin/console doctrine:migrations:migrate
php bin/console make:migration
php bin/console doctrine:migrations:migrate
Dorénavant nous aurons accès depuis une entity à une autre. Par exemple, la catégorie depuis l'article :
// On prend le repository de Article
$articleRepository = $this->getDoctrine()->getRepository(Article::class);
// On récupère le premier article
$article = $articleRepository->find(1);
// On a accès à sa catégorie
$category = $article->getCategory(); // object Category
Les articles depuis la catégorie :
// On prend le repository de Category
$categoryRepository = $this->getDoctrine()->getRepository(Category::class);
// On récupère la catégorie Sciences
$article = $categoryRepository->findBy(['name' => 'Sciences']);
// On a accès à ses articles
$category = $article->getArticles(); // object Collection<Article>
On peut, dans la page d'une catégorie par exemple, afficher tous les éléments :
{% for article in category.articles %}
<li>
<a href="{{ path('article_show', { id: article.id }) }}">
{{ article.title }}
</a>
</li>
{% endfor %}
Note : Voyez comme nous avons passé un argument à la route
article_show
! En effet la route est quelque chose comme/article/{id}
et c'est ici la manière de passer l'argument{id}
avecpath()
dans Twig.
Utiliser des assets dans Symfony (CSS, JS, images) :
Mettre les fichiers dans le dossier public de symfony, par exemple :
public/
assets/
img/
logo.png
css/
styles.css
js/
app.js
2/ Appeler les éléments dans Twig grâce à {{ asset('/chemin/depuis/public') }} , par exemple pour les fichiers ci dessus :
<img src="{{ asset('/assets/img/logo.png') }}">
<script src="{{ asset('/assets/img/app.js') }}"></script>
<style href="{{ asset('/assets/css/styles.css') }}">
Documentation : Security
L'authentification peut être générée par Symfony en suivant une petite recette :
- On créée la classe User via le générateur
- On créée l'authenticateur
- On créée le formulaire d'enregistrement
Dans la console : php bin/console make:user
La console vous demandera quelques informations à propos de votre classe User (le nom, la clé unique...). Il faut noter qu'elle implémentera UserInterface
de sorte à pouvoir fonctionner avec l'authentification de Symfony.
Dans la console : php bin/console make:auth
Pour les questions du CLI :
- Style of authentication :
Login Form Authenticator
- Classname :
FormAuthenticator
- Controller class:
SecurityController
Et voilà, la route /login
a été créée ainsi que le système d'authentification !
Vous devez modifier le fichier src\Security\LoginAuthenticator
dans la méthode onAuthenticationSuccess()
(vers la ligne 89) de la façon suivante :
// Supprimer la ligne suivante :
throw new \Exception('TODO: provide a valid redirect inside '.__FILE__);
// Ajouter la ligne suivante :
return new RedirectResponse($this->urlGenerator->generate('some_route'));
Attention: Assurez vous de mettre une route existante à la place de
some_route
!!! Il s'agit du nom de la route vers laquel on est redirigé après s'être loggué. L'espace membres ou l'accueil par exemple !
Attention Depuis Symfony 4.3, ce chapitre n'est plus utile : la route logout est créée automatiquement.
Pour ajouter la route Logout, nous devons :
Ajoutez la partie firewalls/main/logout
de la façon suivante dans /src/config/security.yaml
(attention, ce n'est que le bloc logout
qu'il faut rajouter, le reste existe déjà !) :
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
anonymous: true
guard:
authenticators:
- App\Security\FormAuthenticator
logout:
path: /logout
target: /
Ajoutez la route suivante dans src/config/routes.yaml
:
logout:
path: /logout
Et voilà, la route /logout
sera accessible pour déconnecter l'utilisateur.
Dans la console : php bin/console make:registration-form
Nous allons générer le formulaire et le contrôleur de création de compte. Répondez les réponses par défaut au CLI.
Voilà, vous avez un formulaire généré dans la route /register
!
Par défaut, nos utilisateurs ont tous un rôle ROLE_USER
(défini dans User::getRoles()
). Nous pouvons utiliser l'annotation @IsGranted
pour bloquer l'accès à une route :
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
// ...
/**
* @IsGranted("ROLE_ADMIN")
* @Route("/", name="location_index", methods={"GET"})
*/
public function index(LocationRepository $locationRepository): Response
{
return $this->render('location/index.html.twig', [
'locations' => $locationRepository->findAll(),
]);
}
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
// ...
/**
* @IsGranted("ROLE_USER")
* @Route("/location")
*/
class LocationController extends AbstractController
{
/**
* @Route("/", name="location_index", methods={"GET"})
*/
public function index(LocationRepository $locationRepository): Response
{ /* ... */ }
/**
* @Route("/new", name="location_new", methods={"GET","POST"})
*/
public function new(Request $request): Response
{ /* ... */ }
Toutes les routes
/location
ne sont accessibles qu'aux utilisateurs logués (ROLE_USER
).
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
// ...
/**
* @IsGranted("ROLE_USER")
* @Route("/location")
*/
class LocationController extends AbstractController
{
/**
* @Route("/", name="location_index", methods={"GET"})
*/
public function index(LocationRepository $locationRepository): Response
{ /* ... */ }
/**
* @IsGranted("ROLE_ADMIN")
* @Route("/new", name="location_new", methods={"GET","POST"})
*/
public function new(Request $request): Response
{ /* ... */ }
Toutes les routes
/location
ne sont accessibles qu'aux utilisateurs logués (ROLE_USER
), de plus,/location/new
n'est accessible qu'aux administrateurs (ROLE_ADMIN
).
Attention: N'oubliez pas le
use
pour pouvoir utiliser l'annotation !
On peut bien sûr vérifier l'authentification d'un utilisateur dans twig, par exemple :
{% if app.user %}
<a href="{{ path('user_home') }}">Accédez à votre espace membre</a>
{% endif %}
{% if is_granted('ROLE_ADMIN') %}
<a href="{{ path('admin_dashboard') }}">Accédez à votre espace administrateur sécurisé !</a>
{% endif %}
{% if not is_granted('ROLE_AUTEUR') %}
<p>Désolé, cet espace n'est accessible qu'aux auteurs !</p>
{% endif %}
Il existe 3 façons dans une classe d'injecter un service. Voyons par exemple comment injecter un Repository (on peut bien sûr en injecter plusieurs de la même manière s'il y a besoin de plus de dépendances !).
class ArticleController extends AbstractController {
private $articleRepository;
public function __construct(ArticleRepository $articleRepository) {
$this->articleRepository = $articleRepository;
}
public function index() {
$articles = $this->articleRepository->findAll();
return $this->render('articles/index.html.twig, [
'articles' => $articles
]);
}
}
C'est ce que l'on fait quand on appelle
Request $request
dans une méthode !
class ArticleController extends AbstractController {
public function index(ArticleRepository $articleRepository) {
$articles = $articleRepository->findAll();
return $this->render('articles/index.html.twig, [
'articles' => $articles
]);
}
}
class ArticleController extends AbstractController {
public function index() {
$articleRepository = $this->getDoctrine()->getRepository(Article::class);
$articles = $articleRepository->findAll();
return $this->render('articles/index.html.twig, [
'articles' => $articles
]);
}
}