You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Dans ce cours, nous allons créer deux projets Symfony de zéro :
NotaResto, Une application de notation de restaurants
HotelManager, Un webservice REST qui proposera une API pour un système de gestion de d'hôtels
NotaResto, l'appli de notation de restaurants
Vous êtes chargés de la réalisation de NotaResto, le site de notation de restaurants. Les fonctionnalités attendues sont :
Consulter la liste de restaurants par code postal
Un compte "Restaurateur" permettant l'ajout et la gestion d'un ou plusieurs restaurants
Un compte "Client" permettant la notation et les commentaires sur un restaurant
Un compte "Modérateur" permettant la gestion de tous les éléments du site
Les pages attendues sont :
En page d'accueil : la liste des restaurants les mieux notés dans l'application
Sur la page d'un restaurant : la liste des avis clients et l'ajout d'avis par les clients et de réponses par le restaurateur
Un back-office Restaurateur permettant de gérer ses restaurants
Un back-office Client permettant de gérer ses avis
Étapes de démarrage
Dessiner les vues sur papier pour comprendre les besoins de l'application
Créer un MLD pour concevoir l'application
Faire une liste des routes utiles à l'application
Gestion des droits: faire la liste des rôles dans l'application et attribuer à chaque route les rôles qui y ont accès
Créer le projet Symfony et le configurer (.env.local)
Créer les entités
Créer les controllers et les routes
Faire la page d'accueil
Créer un système d'authentification
Gérer la création de restaurant (restaurateur)
Gérer la recherche de restaurant (client)
Gérer l'ajout d'un avis sur un restaurant (client) et la réponse par le restaurant (restaurateur)
Gérer la modération des contenus du site (moderateur)
Exercice 1 - Créez le MLD
Dessinez sur papier les vues demandées par le client et créez un MLD idéal pour le projet présenté.
Exercice 2 - Créez le projet Symfony
Dans le dossier où se situera le projet Symfony, créez un projet via : symfony new project --full
Exercice 3 - Faites la liste des routes utiles au projet et leurs rôles
Faites une liste de routes qui seront présentes dans le projet en vous inspirant du MLD et des vues dessinées sur papier.
N'oubliez pas d'ajouter les méthodes HTTP pour chacune des routes
Ajoutez les rôles qui auront accès aux routes
Exemple :
GET /login anonyme
POST /login anonyme
GET /logout all
GET /register anonyme
GET /users modérateur
DELETE /restaurant/{id} restaurateur, modérateur
...
Exercice 4 - Configurer le projet
Créez le projet Symfony
Ajoutez-le dans votre Github
Créez le fichier .env.local et configurez-le
Faites un commit et pushez
Exercice 5 - Créer les modèles
Créez les modèles sans les relations pour les tables du projet.
Ajoutez les relations aux modèles. Rappel :
MLD
Doctrine
N:1
ManyToOne
1:N
OneToMany
N:N
ManyToMany
1:1
OneToOne
Créez les modèles définis dans le MLD
ATTENTION 1 !!! SAUF LE MODÈLE USER !!!
ATTENTION 2 !!! Créer l'entité des photos mais ne pas remplir de champs !!!
ATTENTION 3 !!! Symfony vous propose des valeurs par défaut quand il y a des questions lorsque utilisez bin/console. Faites Entrée pour les utiliser si besoin !
Exercice 7 - Faire les migrations
Créez la base de données avec la ligne de commande Symfony
Créez les migrations
Exécutez les migrations
Exercice 8 - Créer les controllers et les routes
ATTENTION : À partir de maintenant, vous allez réaliser du code de plus en plus complexe. Vous allez devoir tester des choses, essayer, recommencer : c'est le job de développeur ! Il est vital pour la bonne gestion de votre temps et de votre projet de faire des commits régulièrement, et éventuellement des branches, pour ne pas avoir à revenir trop loin en arrière en cas d'erreur.
Créez les controllers et les routes nécessaires à votre application. Ne créez pas les routes relatives à l'authentification.
Exercice 9 - Faire la page d'accueil
Créez des "fixtures" pour créer des données basiques dans votre base de données. Quelques liens :
Affichez la liste de tous les restaurants en page d'accueil
Ensuite, affichez plutôt les 10 derniers restaurants créés. Il faudra chercher sur Google comment faire une requête personnalisée ("custom query") dans Symfony.
Créez une méthode getAverageRating dans la classe Restaurant qui retourne la moyenne des notes d'un restaurant
Grâce à getAverageRating, affichez la moyenne de chaque restaurant sur la page d'accueil
Exercice 10 - Améliorer la requête et ne retourner que les 10 meilleurs
Dans PHPMyAdmin, trouvez la requête SQL qui permet de retourner les 10 restaurants qui ont la meilleure moyenne de reviews. Il faudra utiliser des jointures pour joindre la table Review à Restaurant ainsi que des fonctions d'agrégation de MySQL pour faire le calcul de la moyenne. Quelques liens :
Première étape : créez l'entité User ! Pour cela, on va dire que notre entité User est en fait un... user. Qu'est-ce que ça veut dire ? Ça veut dire que c'est une entité qui implémente l'interface UserInterface, la forçant à avoir des méthodes obligatoires pour la gestion de users dans Symfony :
bin/console make:user User
Ensuite, pour pouvoir vous loguer, créez un formulaire de connexion. Par chance, Symfony a tout prévu !
bin/console make:registration-form
Pensez à faire une migration pour enregistrer la nouvelle entité User en base de données.
Oups, un bug apparaît ! Résolvez-le. Tout est décrit dans l'erreur et le bout de code fourni par l'erreur !
Enfin, gérez le login d'utilisateurs :
bin/console make:auth
Testez en allant sur /login et en saisissant les identifiants d'un utilisateur créé plus tôt.
Corrigez l'erreur qui apparaît ! Comme avant, tout est indiqué.
Testez de vous loguer puis déloguez-vous en allant sur /logout.
Exercice 12 : Faire une navbar qui indique l'adresse e-mail de l'utilisateur
Ajoutez Bootstrap à votre projet (indiqué dans la correction de l'exercice précédent)
Maintenant que vous êtes logués, ajoutez une barre de navigation à votre projet (ajoutez-la dans base.html.twig directement ou dans un partial par exemple)
Dans la barre de navigation :
Si l'utilisateur est logué, affichez son e-mail et un lien "Déconnexion"
Si l'utilisateur n'est pas logué, affichez les liens "Créer un compte" et "Connexion"
Pour tester avec Twig si l'utilisateur est logué et afficher ses informations :
Pour le moment, on part du principe que notre utilisateur a tous les rôles possibles donc toutes les pages devraient être visibles.
Ajoutez les éléments suivants. Attention, pour les éléments "NON fonctionnel", ne mettez pas de vrais liens/éléments/redirections car on les traitera plus tard. Pour tout le reste, créez les pages correspondantes.
Dans la barre de navigation :
Un lien vers "Mes restaurants" (NON fonctionnel)
Un lien vers "Ajouter un restaurant"
Créez un formulaire de création de restaurant (NON fonctionnel)
Un lien vers "Tous les restaurants"
Créez la page qui liste tous les restaurants
Un lien vers "Tous les utilisateurs"
Créez la page qui liste tous les utilisateurs
Un "Input" qui permet de saisir un code postal (NON fonctionnel)
Dans la page d'accueil :
Rendez cliquable les restaurants pour qu'ils redirigent vers la page de 1 restaurant et ses détails
Dans la page d'un détail de restaurant
Ajoutez un formulaire pour créer une review (NON fonctionnel)
Il s'agit donc de faire toute la structure du site ! Les éléments que vous pouvez faire déjà sont notamment d'afficher soit TOUS (pour les listes) ou UN (pour le show d'un restaurant) élément. Nous ferrons plus tard les formulaires.
Exercice 14 : Gérer les nouvelles relations
Nous avons maintenant une entité User ! On peut donc la rattacher à nos tables existantes.
Créez les relations nécessaires que nous n'avions pas encore fait (un User possède des Restaurants, un User possède des Reviews) et migrez.
Un problème se pose ! Nos restaurants ont maintenant besoin d'un User pour exister. Il va donc falloir créer un UserFixtures. N'oubliez pas de modifier RestaurantFixtures pour lui dire de charger UserFixtures en premier, et de rajouter un ->setUser() dans RestaurantFixtures pour attribuer un User au restaurant.
Voilà un exemple de UserFixtures (attention, d'un autre projet, à adapter évidemment pour s'inspirer !) :
Quand UserFixtures sera fait et RestaurantFixtures adapté, relancez les fixtures :
# Suppression du schéma de bdd pour Doctrine
bin/console doc:schema:drop --force
# Création du schéma de bdd pour Doctrine
bin/console doc:schema:create
# Création des fixtures (validation automatique avec --no-interaction)
bin/console doc:fixtures:load --no-interaction
Rappel: Nettoyer un projet et tout relancer proprement
# Avant tout: SUPPRIMEZ TOUS LES FICHIERS DANS src/Migrations !
bin/console doctrine:database:drop --force # On supprime la bdd
bin/console doctrine:database:create # On créée la bdd
bin/console make:migration # On créée les migrations
bin/console doctrine:migrations:migrate # On migre
bin/console doctrine:fixtures:load --no-interaction # On execute les fixtures
En une ligne (Linux, OSX, GitBash) : bin/console doctrine:database:drop --force && bin/console doctrine:database:create && bin/console make:migration && bin/console doctrine:migrations:migrate && bin/console doctrine:fixtures:load --no-interaction
Rappel: Faire une migration en cours de projet sans tout supprimer
D'après le brief du client, voici le MLD qui a été décidé :
USER
------
id
email
password
roles
firstname
lastname
city_id # Dans quelle ville est l'utilisateur ?
RESTAURANT
------
id
name
description
created_at
city_id # Quelle est la ville du restaurant ?
user_id # Qui a créé le restaurant ?
CITY
-----
id
name
zipcode
RESTAURANT_PICTURE
-----
id
restaurant_id # De quel restaurant est-ce la photo ?
name
file
REVIEW
-----
id
message
rating
created_at
user_id # Qui a posé l'avis
restaurant_id # Sur quel restaurant a t-il été posé
review_id # Si c'est une réponse à un avis: quel est l'avis parent ?
Les relations sont :
Table A
Relation
Table B
USER
N:1
CITY
RESTAURANT
N:1
CITY
RESTAURANT_PICTURE
N:1
RESTAURANT
REVIEW
N:1
USER
REVIEW
N:1
RESTAURANT
REVIEW
N:1
REVIEW
Exercice 2 - Créez le projet Symfony
symfony new notaresto --full --version=lts
Exercice 3 - Faites la liste des routes utiles au projet et leurs rôles
GET /login anonyme
POST /login anonyme
GET /logout all
GET /register anonyme
GET /restaurants all
GET /restaurant/new restaurateur
POST /restaurant restaurateur
GET /restaurant/{id} all
DELETE /restaurant/{id} restaurateur, modérateur
GET /restaurant/{id}/edit restaurateur, modérateur
POST /restaurant/{id}/edit restaurateur, modérateur
GET /reviews modérateur
POST /review client, restaurateur
GET /review/{id} all
DELETE /review/{id} connecté
GET /review/{id}/edit connecté
POST /review/{id}/edit connecté
GET /users modérateur
GET /user/{id} connecté
DELETE /user/{id} connecté
GET /user/{id}/edit connecté
POST /user/{id}/edit connecté
GET /cities modérateur
POST /city modérateur
GET /city/{id} anonyme
DELETE /city/{id} modérateur
GET /city/{id}/edit modérateur
POST /city/{id}/edit modérateur
GET /restaurant_pictures modérateur
POST /restaurant_picture restaurateur, modérateur
GET /restaurant_picture/{id} all
DELETE /restaurant_picture/{id} restaurateur, modérateur
GET /restaurant_picture/{id}/edit restaurateur, modérateur
POST /restaurant_picture/{id}/edit restaurateur, modérateur
Description : Description de votre projet (par ex: TP Symfony lors de ma formation DWWM chez Human Booster)
Public/Private: selon la visiblité que vous voulez donner au repository
Initialize this repository with a README: Ne PAS cocher
Add .gitignore: None
Add a license: None
Cliquer sur Create repository
b. Ajouter notre projet au repository Github
IMPORTANT : Vérifiez que sous la ligne Quick setup — if you’ve done this kind of thing before, le bouton HTTPS soit bien cliqué.
Copiez la ligne https://github.com/.../notaresto.git qui apparaît dans le champ input de Quick setup — if you’ve done this kind of thing before (le bloc qui a un fond bleu).
Ouvrez un terminal situé dans votre projet et saisissez les lignes suivantes :
Si vous avez fait une erreur dans l'URL lorsque vous avez tapé la commande (il aurait du s'agit de celle que vous avez copiée au point 1), vous pouvez la modifier en saisissant :
Si vous souhaitez ouvrir le projet par exemple sur un autre ordinateur :
Allez sur la page Github du projet à copier
Cliquez sur Clone or download
Vérifiez qu'il soit écrit Clone with HTTPS (et non Clone with SSH, si ce n'est pas bon, cliquez sur Use HTTPS)
Copiez l'URL qui est dans le champ input
Ouvrez un terminal dans le dossier dans lequel le projet se situera (ATTENTION ! Le dossier qui ACCUEILLERA le projet ! C'est à dire par exemple c:/users/tomsihap/projects, pas besoin de créer à la main le sous dossier du futur projet !!)
Saisissez la ligne suivante :
git clone https://github.com/..../notaresto.git
Le dossier notaresto devrait maintenant être cloné ! Vous pouvez faire des commit/push dedans comme avant.
Note : Attention, si vous clonez un projet de quelqu'un d'autre et que vous n'avez pas de droits en écriture, les push ne seront pas possibles. Dans le cas où vous voulez copier un projet existant et pouvoir faire vos propres commits dessus comme s'il s'agissait d'un nouveau projet, faites plutôt un fork : cela crééra le projet dans votre propre Github.
AVEC Github Desktop
CRÉER LE PROJET ET FAIRE DES COMMIT/PUSH
Dans Github Desktop, cliquez sur Add an Existing Repository from your Hard Drive...
Cherchez le dossier du projet notaresto
Si le message This directory does not appear to be a Git repository. Would you like to create a repositorry here instead? apparaît, cliquez sur create a repository :
Name: notaresto
Description : Description de votre projet (par ex: TP Symfony lors de ma formation DWWM chez Human Booster)
Local Path : Ne pas toucher, normalement il s'agit du chemin vers le dossier de projet
Initialize this repository with a README: Ne pas cocher
Git Ignore: None
License: None
Cliquer sur Create Repository
Cliquer sur Publish repository
Name et Description: idem que en haut
Keep this code private: À cocher uniquement si vous voulez le laisser en privé.
Organization: None
Cliquer sur Publish repository
Pour faire des commits: après avoir fait des modifications dans le projet, remplissez le champ Summary puis cliquez sur Commit to master. Enfin, cliquez sur Push pour bien pusher les modifications.
RÉCUPÉRER LE PROJET
Si vous souhaitez ouvrir le projet par exemple sur un autre ordinateur :
Cliquez sur Clone a Repository
Cherchez le repository pseudo/notaresto (par exemple: tomsihap/notaresto)
Dans Local Path : remplissez le chemin vers lequel le projet sera cloné
Le projet est maintenant cloné ! Vous pouvez faire des commit/push comme avant.
Note : Attention, si vous clonez un projet de quelqu'un d'autre et que vous n'avez pas de droits en écriture, les push ne seront pas possibles. Dans le cas où vous voulez copier un projet existant et pouvoir faire vos propres commits dessus comme s'il s'agissait d'un nouveau projet, faites plutôt un fork : cela crééra le projet dans votre propre Github.
Exercice 5 - Configurez le projet Symfony
Copiez-collez le fichier .env existant, pour créer le fichier .env.local
En ligne de commande c'est plus rapide : cp .env .env.local
Modifiez uniquement le fichier .env.local !
Adaptez la ligne DATABASE_URL selon votre configuration. La base de données n'a pas besoin d'exister encore pour être renseignée ici ! Par exemple :
À quoi sert le .env.local ? En fait, le .env est commité par défaut, tandis que .env.local est dans le .gitignore par défaut de Symfony. Deux avantages à l'utiliser :
C'est une énorme faute de sécurité que de mettre des mots de passe dans un fichier qui se retrouve commité !
Si vous travaillez sur votre projet depuis plusieurs endroits (au travail, chez vous, version en serveur de prod, de préprod, de dev...), chaque version pourra avoir sa propre configuration .env.local sans qu'il n'y ait de conflits.
Mais à quoi sert alors d'avoir un .env ? Imaginez que vous rajoutiez des lignes de configuration dans votre .env.local (nom du site, clé d'API de Google Maps...). Si vous le laissez dans .env.local, ces clés n'apparaîtront pas dans Github. Si vous les ajoutez dans le .env avec des valeus par défaut (par exemple: GOOGLE_API_KEY=**remplir_la_clé_d_api**), vous aurez un exemple de fichier .env à remplir dans un .env.local pour déployer votre projet.
Note en cas de CLONE du projet : Si vous avez cloné le projet, n'oubliez pas de faire un composer install ! En effet, le dossier vendor n'est pas commité, il faut installer les dépendances après avoir cloné le projet.
Si vous suivez le MLD de la correction, les modèles à créer sont :
User
Restaurant
City
RestaurantPicture
Review
IMPORTANT : N'OUBLIEZ PAS de faire un commit après CHAQUE make:entity !!!! De cette façon, si vous faites une erreur à l'entité suivante, vous pouvez supprimer les fichiers créés de deux façons :
Console :
git checkout -- .
git clean -i
# Répondre "1" à la question
Github Desktop : clic droit sur 2 changed files et Discard changes.
On ne remplit rien pour le moment ! On créée l'entité vide. On la remplira lorsqu'on gèrera l'upload d'images.
4. Review: bin/console make:entity Review
Property name
Field type
Field length
Nullable ?
message
text
yes
rating
integer
no
createdAt
datetime
no
Création des relations
Les relations à créer seront :
ATTENTION ! Prenez garde à la relation Review avec elle même (commentaire "parent")
Entité à modifier
Nouveau champ
Relation
Classe
Nullable ?
Accessors ?
Nouveau champ
Orphan Removal ?
Restaurant
city
ManyToOne
City
no
yes
restaurants
yes
RestaurantPicture
restaurant
ManyToOne
Restaurant
no
yes
restaurantPictures
yes
Review
restaurant
ManyToOne
Restaurant
no
yes
reviews
yes
Review
parent
ManyToOne
Review
yes
yes
childs
Description des questions d'une relation
New property name (press <return> to stop adding fields):
C'est le nouvel attribut que l'on ajoute à notre classe Restaurant. Ici : on ajoute un attribut city (EN CAMEL CASE !!!! C'EST UN ATTRIBUT !)
Field type (enter ? to see all types) [string]:
Quel est le type de champ. On peut dire relation pour que l'interface nous propose les relations possibles, ou directement la relation voulue (ManyToOne, OneToMany...)
What class should this entity be related to?:
C'est une relation : on veut rattacher notre entité à une autre entité, c'est à dire à une autre classe. On saisit le nom de la classe ici : City (EN PASCAL CASE !!!! C'EST UNE CLASSE !)
What type of relationship is this? Relation type? [ManyToOne, OneToMany, ManyToMany, OneToOne]:
Comme on a indiqué relation à la question du FieldType tout à l'heure, l'interface nous propose une liste de relations possibles. Un restaurant n'a qu'une ville mais une ville a plusieurs restaurants : ce sera ManyToOne. Bien sûr, si nous avions modifié City au lieu de Restaurant, la relation aurait été inverse: OneToMany.
Is the Restaurant.city property allowed to be null (nullable)? (yes/no) [yes]:
Peut-on rendre nullable le champ city ? Non, on répond no.
Do you want to add a new property to City so that you can access/update Restaurant objects from it - e.g. $city->getRestaurants()? (yes/no) [yes]:
Comme ajoute un champ city à Restaurant, Symfony nous propose d'ajouter automatiquement un accesseur (un getter) dans l'entité City pour avoir tous les restaurants d'une ville ! Gardez la valeur par défaut, yes.
New field name inside City [restaurants]:
Comme un a un nouveau champ dans City (les restaurants), Symfony nous propose de l'ajouter. Gardez la valeur par défaut (déjà au pluriel !), restaurants.
Do you want to automatically delete orphaned App\Entity\Restaurant objects (orphanRemoval)? (yes/no) [no]:
Le Orphan Removal n'existe que quand le champ n'est pas nullable: c'est la suppression des éléments orphelins en base de données, c'est à dire quand l'élément de la table parente n'existe plus. Par exemple, si je supprime une ville qui a des restaurants. Dans ce cas, dois-je supprimer les restaurants de la base de données ? Répondons ici yes.
Exemple
$ bin/console make:entity Restaurant
Your entity already exists! So lets add some new fields!
New property name (press <return> to stop adding fields):
> city
Field type (enter ? to see all types) [string]:
> relation
What class should this entity be related to?:
> City
What type of relationship is this?
------------ ----------------------------------------------------------------------
Type Description
------------ ----------------------------------------------------------------------
ManyToOne Each Restaurant relates to (has) one City.
Each City can relate to (can have) many Restaurant objects
OneToMany Each Restaurant can relate to (can have) many City objects.
Each City relates to (has) one Restaurant
ManyToMany Each Restaurant can relate to (can have) many City objects.
Each City can also relate to (can also have) many Restaurant objects
OneToOne Each Restaurant relates to (has) exactly one City.
Each City also relates to (has) exactly one Restaurant.
------------ ----------------------------------------------------------------------
Relation type? [ManyToOne, OneToMany, ManyToMany, OneToOne]:
> ManyToOne
Is the Restaurant.city property allowed to be null (nullable)? (yes/no) [yes]:
> no
Do you want to add a new property to City so that you can access/update Restaurant objects from it - e.g. $city->getRestaurants()? (yes/no) [yes]:
> yes
A new property will also be added to the City class so that you can access the related Restaurant objects from it.
New field name inside City [restaurants]:
>
Do you want to activate orphanRemoval on your relationship?
A Restaurant is "orphaned" when it is removed from its related City.
e.g. $city->removeRestaurant($restaurant)
NOTE: If a Restaurant may *change* from one City to another, answer "no".
Do you want to automatically delete orphaned App\Entity\Restaurant objects (orphanRemoval)? (yes/no) [no]:
> yes
updated: src/Entity/Restaurant.php
updated: src/Entity/City.php
Add another property? Enter the property name (or press <return> to stop adding fields):
>
Success!
Next: When you're ready, create a migration with make:migration
Gérer les champs CreatedAt
Comme nous avons des champs CreatedAt, il faut leur fournir une valeur par défaut : c'est à dire que quand je créée un objet de type Restaurant par exemple, je dois ajouter une valeur par défaut à createdAt.
Pour cela, dans Restaurant.php et Review.php, ajoutez dans la méthode __construct() la ligne suivante :
Attention : La base de données définie dans .env.local ne doit pas être encore crée (faite à la main par exemple) !
# Création de la base de données
php bin/console doctrine:database:create
# Création des premières migrations
php bin/console make:migration
# Exécution des premières migrations
php bin/console doctrine:migrations:migrate
# Par la suite, lorsque vous ferez d'autres migrations :# 1. On teste si des migrations existent en essayant de les exécuter# 2. On créée les nouvelles migrations# 3. On relance l'exécution de migrations
php bin/console doctrine:migrations:migrate
php bin/console make:migration
php bin/console doctrine:migrations:migrate
Note : ces commandes crééent des vues par défaut dans templates. Vous n'aurez peut être pas besoin de toutes, n'oubliez pas de supprimer les dossiers à l'avenir si vous ne vous en servirez pas pour ne pas avoir de fichiers inutiles !
Astuce : Pensez à taper dans la console bin/console debug:router quand vous créez des routes pour vérifier si Symfony les a bien pris en compte ! Si elles n'aparaissent pas, c'est qu'elles sont mal placées dans la liste ou mal déclarées ou que deux routes ont le même nom. S'il y a une erreur dans la console, c'est une faute de frappe sans doute dans l'annotation.
Exemple: RestaurantController
<?phpnamespaceApp\Controller;
useApp\Entity\Restaurant;
useSymfony\Bundle\FrameworkBundle\Controller\AbstractController;
useSymfony\Component\Routing\Annotation\Route;
class RestaurantController extends AbstractController
{
/** * Affiche la liste des restaurants * @Route("/restaurants", name="restaurant_index", methods={"GET"}) */publicfunctionindex()
{
return$this->render('restaurant/index.html.twig', [
'controller_name' => 'RestaurantController',
]);
}
/** * Affiche un restaurant * @Route("/restaurant/{restaurant}", name="restaurant_show", methods={"GET"}, requirements={"restaurant"="\d+"}) * @param Restaurant $restaurant */publicfunctionshow(Restaurant$restaurant)
{
}
/** * Affiche le formulaire de création de restaurant * @Route("/restaurant/new", name="restaurant_new", methods={"GET"}) */publicfunctionnew()
{
}
/** * Traite la requête d'un formulaire de création de restaurant * @Route("/restaurant", name="restaurant_create", methods={"POST"}) */publicfunctioncreate()
{
}
/** * Affiche le formulaire d'édition d'un restaurant (GET) * Traite le formulaire d'édition d'un restaurant (POST) * @Route("/restaurant/{restaurant}/edit", name="restaurant_edit", methods={"GET", "POST"}) * @param Restaurant $restaurant */publicfunctionedit(Restaurant$restaurant)
{
}
/** * Supprime un restaurant * @Route("/restaurant/{restaurant}", name="restaurant_delete", methods={"DELETE"}) * @param Restaurant $restaurant */publicfunctiondelete(Restaurant$restaurant)
{
}
}
Exercice 9 - Faire la page d'accueil
Les fixtures sont des fichiers qui vont générer des fausses données pour votre base de données.
Il faut tout d'abord installer les fixtures dans le projet:
composer require orm-fixtures --dev
Ensuite, créez les fichiers de fixtures pour chaque entité : nous n'allons pas forcément toutes les utiliser mais elles seront prêtes ! Par convention, les fichiers de fixtures sont nommés ainsi : NomDeLaClasseFixtures. Par exemple: RestaurantFixtures.
Pour créer un Restaurant, nous avons besoin que des City existent. Pour créer des Review, nous avons besoin qu'un Restaurant existe. Pour créer des RestaurantPicture, nous avont besoin qu'un restaurant existe.
Par défaut, Symfony va exécuter les fixtures dans l'ordre alphabétique : nous allons lui dire qu'il y a des dépendances entre elles (c'est à dire qu'il doit en exécuter certaines avant d'autres) - en effet, si les City ne sont pas créées, les restaurants ne pourront pas être créés !
Voici l'ordre d'exécution des fixtures :
Priorité
Fixture
1
CityFixtures
2
RestaurantFixtures
3
RestaurantPictureFixtures
3
ReviewFixtures
Nous allons indiquer tout d'abord à RestaurantPictureFixtures et à ReviewFixtures que RestaurantFixtures doit être créé avant eux.
Pour cela, on dit à la fixture d'implémenter l'interface DependantFixtureInterface, qui l'obligera à avoir la méthode getDependancies(). Modifiez les ainsi:
Cool, 1000 villes seront créées ! Le souci... C'est qu'elles s'apelleront toutes "Lyon" et auront pour code postal "69001".
Pour pallier à ce souci, nous allons installer Faker (https://github.com/fzaninotto/Faker) qui nous permettra d'avoir des chaînes de caractères aléatoires et cohérentes :
composer require fzaninotto/faker
Modifiez maintenant CityFixtures. Attention à bien importer la classe Factory (Faker/Factory) :
Validez yes lors de la question : le CLI vous indique que la base de données va être vidée puis re-remplie par les fixtures.
Une fois la commande exécutée, vérifiez dans PHPMyAdmin : ça y est, 1000 villes aux noms réalistes ont été créées !
Maintenant que nos villes existent (de l'ID 1 à 1000), modifions maintenant RestaurantFixtures et ReviewFixtures. Petite différence : pour les clés étrangères, nous importerons le Repository correspondant. Par exemple, pour le $restaurrant->setCity() nous avons besoin d'un objet City en base de données, nous utilisons donc le CityRepository auquel on lui donnera un ID aléatoire entre 1 et 1000 (car il y a 1000 villes).
On termine sur ReviewFixtures avant de rééxécuter toutes nos fixtures. Allez, on va en faire 10 000 ! On va faire deux boucles : 7000 reviews d'utilisateurs, et 3000 reviews qui seront des réponses à d'autres reviews.
<?phpnamespaceApp\DataFixtures;
useApp\Entity\Review;
useApp\Repository\RestaurantRepository;
useApp\Repository\ReviewRepository;
useDoctrine\Bundle\FixturesBundle\Fixture;
useDoctrine\Common\DataFixtures\DependentFixtureInterface;
useDoctrine\Common\Persistence\ObjectManager;
useFaker\Factory;
class ReviewFixtures extends Fixture implements DependentFixtureInterface
{
private$restaurantRepository;
private$reviewRepository;
publicfunction__construct(RestaurantRepository$restaurantRepository, ReviewRepository$reviewRepository) {
$this->restaurantRepository = $restaurantRepository;
$this->reviewRepository = $reviewRepository;
}
publicfunctionload(ObjectManager$manager)
{
$faker = Factory::create('fr_FR');
/** * On créée 7000 reviews initiales */for ($i=0; $i<7000; $i++) {
$review = newReview();
$review->setMessage( $faker->text(800) );
$review->setRating( rand(0,5) );
$review->setRestaurant( $this->restaurantRepository->find(rand(1, 1000)) );
$manager->persist($review);
}
/** * On les enregistre en DB */$manager->flush();
/** * On créée 3000 reviews enfants (dont le parent est une des review initiales) */for ($i=0; $i<3000; $i++) {
$review = newReview();
$review->setMessage( $faker->text(800) );
$review->setRating( rand(0,5) );
$review->setParent( $this->reviewRepository->find(rand(1, 7000)) ); // On cherche un ID entre 1 et 7000 (un commentaire initial)$review->setRestaurant( $review->getParent()->getRestaurant() ); // On récupère le restaurant de la review parente$manager->persist($review);
}
// $manager->persist($product);$manager->flush();
}
publicfunctiongetDependencies()
{
returnarray(
RestaurantFixtures::class,
);
}
}
Ouf ! Exécutons enfin toutes ces fixtures. Pour cela, nous avons besoin de vider la base de données et de la re-remplir afin d'avoir des données propres. Voici les commandes à exécuter à la suite à chaque fois que vous chargerez des fixtures dorénavant :
# Suppression du schéma de bdd pour Doctrine
bin/console doc:schema:drop --force
# Création du schéma de bdd pour Doctrine
bin/console doc:schema:create
# Création des fixtures (validation automatique avec --no-interaction)
bin/console doc:fixtures:load --no-interaction
Afficher les restaurants en page d'accueil
Nous n'avons pas de page d'accueil ! Créons la tout de suite dans un AppController par exemple :
bin/console make:controller AppController
Modifiez le AppController.php créé pour créer une route de page d'accueil, qui appelera un Twig à qui on enverra la liste des objets Restaurant :
Modifiez le Twig correspondant app/index.html.twig :
{% extends 'base.html.twig' %}
{% block title %}Liste des restaurants{% endblock %}
{% block body %}
<ul>
{% for restaurant in restaurants %}
<li>
{{ restaurant.name }}<br><small>{{ restaurant.description }}</small></li>
{% endfor %}
</ul>
{% endblock %}
Bien sûr, à vous d'adapter tout ce code avec du CSS ou Bootstrap !
Afficher les 10 derniers restaurants créés
Comme on a vu plus haut, pour afficher tous les élements, la méthode findAll() du repository existe déjà et fait le job pour nous. Dès que nous avons des requêtes un peu plus complexes (ici : trier par created_at descendant puis prendre les 10 premiers résultats), on va devoir ajouter une méthode au RestaurantRepository qui fera ce travail.
Ensuite, ajoutons une méthode dans RestaurantRepository.php. Dans ce fichier, on trouve en commentaires 2 méthodes d'exemple sur lesquelles s'inspirer pour créer notre méthode !
Observez bien les requêtes en commentaires et voyez comme il est facile de composer sa requête comme ça. Il s'agit du QueryBuilder de Doctrine.
Et voilà comment utiliser une requête SQL personnalisée dans Symfony !
Afficher la valeur moyenne de la note d'un restaurant
On souhaiterait, dans twig, accéder à quelque chose comme ça :
{{ restaurant.averageRating }}
En fait, dans Twig, quand on fait {{ restaurant.name }}, {{ restaurant.description }}, ce qu'il se passe, c'est que Twig va chercher respectivement $restaurant->getName() et $restaurant->getDescription().
Donc si je souhaite avoir quelque chose comme {{ restaurant.averageRating }} qui me retourne la note moyenne, je dois créer... $restaurant->getAverageRating() dans Restaurant.php !
Modifions Restaurant.php et ajoutons la méthode suivante qui calcule la moyenne des reviews d'un restaurant (accessibles grâce à $this->getReviews() dans la classe elle-même) :
Cette fonction ne fait que calculer la moyenne des reviews du restaurant. Et c'est tout ! On n'a plus qu'à modifier un peu notre Twig pour appeler cette méthode :
{% extends 'base.html.twig' %}
{% block title %}Liste des restaurants{% endblock %}
{% block body %}
<ul>
{% for restaurant in restaurants %}
<li>
{{ restaurant.name }} (Moyenne de {{ restaurant.averageRating | number_format(2, ',') }})<br><small>{{ restaurant.description }}</small></li>
{% endfor %}
</ul>
{% endblock %}
Enfin fini ! C'était un très gros chapitre. Prenez le temps de bien tout avoir compris et maîtrisé avant de poursuivre !
Exercice 10 - Améliorer la requête et ne retourner que les 10 meilleurs
Requête SQL pour afficher les 10 meilleurs restaurants
La requête que nous voulons est en fait les 10 meilleures notes de restaurants, groupées par restaurants pour en faire la moyenne. Testez dans PHPMyAdmin vos requêtes.
Nous avons donc maintenant toutes les reviews avec le restaurant rattaché. Ce que nous voulons, c'est plutôt de grouper ces données par restaurant (donc une ligne par restaurant). Et de toutes ces données groupées, nous voulons la moyenne de la note (review.rating):
SELECTAVG(review.rating) as average, restaurant.idas restaurantId
FROM review
INNER JOIN restaurant
ONreview.restaurant_id=restaurant.idGROUP BY restaurant_id
Et voilà ! Listons par ordre décroissant :
SELECTAVG(review.rating) as average, restaurant.idas restaurantId
FROM review
INNER JOIN restaurant
ONreview.restaurant_id=restaurant.idGROUP BY restaurant_id
ORDER BY moyenne DESC
Et ne gardons que les 10 meilleurs (les 10 premiers donc) :
SELECTAVG(review.rating) as averarge, restaurant.idas restaurantId
FROM review
INNER JOIN restaurant
ONreview.restaurant_id=restaurant.idGROUP BY restaurant_id
ORDER BY moyenne DESCLIMIT0, 10
Traduire dans le QueryBuilder
Le QueryBuilder de Doctrine nous permet de construire des requêtes à la façon de Doctrine, directement dans le Repository.
Ici, on part de la table Review, on va donc travailler dans le ReviewRepository. Encore une fois, inspirez vous des requêtes déjà préparées en commentaires dans le Repository :
On comprend par exemple que pour ajouter un WHERE, on utilise andWhere. Un ORDER BY, on utilise orderBy('champ', 'direction').
Pour le reste, cherchez dans la documentation comment traduire les éléments manquants.
Les éléments de notre requête sont :
SELECTAVG(review.rating) as average, restaurant.idas restaurantId
FROM review
INNER JOIN restaurant
ONreview.restaurant_id=restaurant.idGROUP BY restaurant_id
ORDER BY moyenne DESCLIMIT0, 10
Voici la liste des éléments nécessaires avec leurs documentations :
En compilant toutes ces informations, on réussit à construire la requête suivante dans ReviewRepository.php :
publicfunctionfindBestTenRatings() {
return$this->createQueryBuilder('r')
->select('AVG(r.rating) as average', 'restaurant.id as restaurantId')
->innerJoin('r.restaurant', 'restaurant')
->groupBy('restaurant')
->orderBy('AVG(r.rating)', 'DESC')
->setMaxResults(10)
->getQuery()
->getResult()
;
}
Si on teste ça, dans AppController.php, ajoutons un dd() avant le return pour tester notre nouvelle méthode. Attention, la méthode vient bien de ReviewRepository !
En allant sur la page d'accueil, ça marche... presque ! En fait, il y a un petit problème : là où $this->getDoctrine()->getRepository(Restaurant::class)->findLastTenElements() nous retournait un array d'objets Restaurant, cette fois notre méthode nous retourne un array d'arrays pas très pratique à utiliser.
En effet : Doctrine ne peut pas savoir qu'il doit nous retourner des restaurants avec en plus le champ "average", il nous retourne donc juste ce qu'on lui a demandé, c'est à dire les champs du SELECT de la requête.
Ce que nous allons faire maintenant, c'est donc de faire une boucle sur ce résultat pour récupérer les objets Restaurant correspondant. Un petit détail d'optimisation cependant dans notre méthode. Elle nous retourne la note moyenne. Mais nous allons créer des objets Restaurant qui eux, ont déjà accès à leur propre note moyenne. Retirons donc ce champ du select, nous n'avons besoin que des ID de restaurants ! Dans ReviewRepository.php :
Maintenant dans AppController.php, faisons une boucle sur les données de findBestTenRatings pour créer des objects Restaurant :
/** * @Route("/", name="app_index", methods={"GET"}) */publicfunctionindex()
{
/** * On récupère les données de notre nouvelle méthode */$tenBestRestaurantsId = $this->getDoctrine()->getRepository(Review::class)->findBestTenRatings();
$tenBestRestaurants = array_map(function($data) {
return$this->getDoctrine()->getRepository(Restaurant::class)->find($data['restaurantId']);
}, $tenBestRestaurantsId);
/** * On prépare le futur array d'objets Restaurant */$tenBestRestaurants = [];
/** * On boucle sur le tableau de données retourné par le ReviewRepository */foreach($tenBestRestaurantsIdas$data) {
// Pour chaque élément on prend le `restaurantId` et on cherche l'objet Restaurant grace au RestaurantRepository :$tenBestRestaurants[] = $this->getDoctrine()->getRepository(Restaurant::class)->find($data['restaurantId']);
}
return$this->render('app/index.html.twig', [
// Cette fois, on envoie à Twig notre nouveau tableau'restaurants' => $tenBestRestaurants,
]);
}
Et voilà ! Une autre manière d'écrire le foreach qui est plus élégante, c'est array_map. C'est une fonction qui prend en paramètres une fonction anonyme et un array. La fonction anonyme est en fait ce qu'on va faire pour transformer le tableau passé en 2ème paramètres.
bin/console make:user User
Do you want to store user data in the database (via Doctrine)? (yes/no) [yes]:
> yes
Enter a property name that will be the unique "display" name for the user (e.g. email, username, uuid) [email]:
> 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]:
> yes
Explications des lignes
Do you want to store user data in the database (via Doctrine)? (yes/no) [yes]
Est-ce que vous souhaitez enregistrer les utilisateurs en base de données avec Doctrine ? Oui bien sûr ! yes
Enter a property name that will be the unique "display" name for the user (e.g. email, username, uuid) [email]:
Quelle sera la propriété de votre entité User qui serviva de login ? On choisit 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]:
Est-ce que notre application a besoin de hasher les mots de passe (c'est à dire les chiffrer) ? Évidemment oui ! yes
Créer un formulaire de création d'utilisateurs
bin/console make:registration-form
Creating a registration form for App\Entity\User
Do you want to add a @UniqueEntity validation annotation on your User class to make sure duplicate accounts aren't created? (yes/no) [yes]:
>
Do you want to automatically authenticate the user after registration? (yes/no) [yes]:
>
Explication des lignes
Do you want to add a @UniqueEntity validation annotation on your User class to make sure duplicate accounts aren't created? (yes/no) [yes]:
Est-ce que l'on souhaite que nos utilisateurs soient uniques sur le champ de login, leur e-mail donc ? Oui ! yes
Do you want to automatically authenticate the user after registration? (yes/no) [yes]:
Est-ce que l'on veut que nos utilisateurs soient logués automatiquement après la création du compte ? Oui ! yes
Migration
Maintenant que l'entité est créée, faites une migration pour créer la table en base de données :
Créez un compte ! Allez sur /register pour tester ça.
Corriger l'erreur
Une fois le formulaire rempli, vous devriez avoir l'erreur suivante :
Unable to generate a URL for the named route "" as such route does not exist.
Avec quelques informations de contexte qui ressemblent à peu près à ça :
// AbstractController->redirectToRoute('') in src/Controller/RegistrationController.php (line 39)$entityManager->persist($user);
$entityManager->flush();
// do anything else you need here, like send an emailreturn$this->redirectToRoute('');
}
C'est en fait vraiment très clair : nous devons modifier RegistrationController.php (un des fichiers créés par make:registration-form) et lui indiquer, à la ligne qui nous est donnée, où nous devons rediriger l'utilisateur après qu'il se soit créé un compte. Idéalement vers une page compte utilisateur par exemple, mais comme nous n'en avons pas, redirigeons-le vers la page d'accueil. Dans la correction, le nom de la route (dans AppController pour rappel) est app_index.
Dans RegistrationController.php en ligne 39 :
return$this->redirectToRoute('app_index');
Et voilà ! Vous avez vu comment créer un compte utilisateur et rediriger ce nouvel utilisateur vers la page d'accueil. Prenez le temps d'étudier tous les nouveaux fichiers qui ont été créés par make:registration-form. La création de User est plutôt complexe car beaucoup de choses entrent en compte (des sessions, le hashage des mots de passe...). Beaucoup de choses sont à apprendre des fichiers créés par Symfony.
Pour tester : retournez sur /register et créez un nouveau compte (avec un e-mail différent puisque le précédent a quand même été créé).
Gérer le login d'utilisateurs
Maintenant que nos utilisateurs peuvent créer un compte, il va falloir les loguer :
bin/console make:auth
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):
> LoginFormAuthenticator
Choose a name for the controller class (e.g. SecurityController) [SecurityController]:
>
Do you want to generate a '/logout' URL? (yes/no) [yes]:
>
Explication des lignes
What style of authentication do you want? [Empty authenticator]:
Symfony nous demande si il faut gérer l'authentification par un formulaire (c'est ce que nous voulons) ou par autre chose (login avec Google, Facebook, via une API...). Ici, on remplit : 1
The class name of the authenticator to create (e.g. AppCustomAuthenticator):
Symfony va créer un service qui va gérer les méthodes d'authentification. Apellons-le : LoginFormAuthenticator
Choose a name for the controller class (e.g. SecurityController) [SecurityController]:
Symfony va créer un controller qui va gérer les routes d'authentification (/login par exemple). Apellons-le avec le nom par défaut: SecurityController
Do you want to generate a '/logout' URL? (yes/no) [yes]:
Est-ce que l'on veut une route /logout créée automatiquement ? Oui ! yes
Corrigeons maintenant l'erreur qui a été générée. Comme pour /register, l'erreur est explicite et n'est pas méchante ! On n'a pas indiqué de page vers laquelle rediriger un utilisateur qui vient d'être logué.
Dans LoginFormAuthenticator.php, remplacez en lignes 97-98 :
// For example : return new RedirectResponse($this->urlGenerator->generate('some_route'));thrownew \Exception('TODO: provide a valid redirect inside '.__FILE__);
par ce qui est indiqué en commentaire, avec une route valide... la page d'accueil par exemple :
Et voilà ! Actualisez. Pour vérifier si vous êtes connectés, vérifiez dans la barre de débug de Symfony : l'email de l'utilisateur devrait apparaître. Essayez de vous déloguer en allant sur /logout : ça devrait remplacer l'adresse e-mail par "anon." (comme anonyme) !
Exercice 12 : Faire une navbar qui indique l'adresse e-mail de l'utilisateur
Nous partons du principe que Bootstrap est installé dans le projet.
Mettre une navbar
Créez un fichier src/templates/_partials/navbar.html.twig
Trouvez une navbar Boostrap et collez-la dedans et adaptez-la. Par exemple, une navbar avec tous les éléments utiles sans logique encore :
<head>
<metacharset="UTF-8">
<title>{% blocktitle %}Notaresto, l'appli de notation de restos !{% endblock %}</title>
<linkrel="stylesheet"href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css"integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh"crossorigin="anonymous">
<linkrel="stylesheet"href="{{ asset('dist/css/styles.css') }}" />
{% blockstylesheets %}{% endblock %}
</head>
Modifions maintenant la page d'accueil src/templates/app/index.html.twig pour afficher plus joliment nos restaurants et mettre un faux lien sur chaque restaurant :
Ajoutez aussi à AppController notre méthode qui correspond au moteur de recherche (attention, on importe bien (Symfony\Component\HttpFoundation\Request) :
Maintenant que les routes sont créées, modifions nos navbar et page d'accueil pour créer des liens. Nous utilisons la fonction Twig path('nom_de_route').
navbar.html.twig : Remplacez les liens dans la navbar.
<ulclass="navbar-nav">
<liclass="nav-item dropdown">
<aclass="nav-link dropdown-toggle"href="#"id="navbarDropdown"role="button"data-toggle="dropdown"aria-haspopup="true"aria-expanded="false">
Restaurants
</a>
<divclass="dropdown-menu"aria-labelledby="navbarDropdown">
<aclass="dropdown-item"href="{{ path('restaurant_index') }}">Voir mes restaurants</a>
<aclass="dropdown-item"href="{{ path('restaurant_index') }}">Voir tous les restaurants</a>
<divclass="dropdown-divider"></div>
<aclass="dropdown-item"href="{{ path('restaurant_new') }}">Créer un restaurant</a>
</div>
</li>
<liclass="nav-item dropdown">
<aclass="nav-link dropdown-toggle"href="#"id="navbarDropdown"role="button"data-toggle="dropdown"aria-haspopup="true"aria-expanded="false">
Utilisateurs
</a>
<divclass="dropdown-menu"aria-labelledby="navbarDropdown">
<aclass="dropdown-item"href="{{ path('user_index') }}">Voir tous les utilisateurs</a>
</div>
</li>
</ul>
app/index.html.twig Remplacez les 2 occurrences de liens "Donner un avis" par le lien vers le restaurant.
Dans templates/users/index.html.twig (on utilise if not loop.last pour dire "si ce n'est pas le dernier de la boucle, alors met une virgule", pour séparer par une virgule les rôles) :
Oups, un bug apparaît probablement lorsque l'on clique sur Voir les restaurants! En effet, certains restaurants n'ont pas de note, ils ne peuvent donc pas avoir de note moyenne, sinon on aurait une division par zéro (total/nombreNotes).
Modifions rapidement Restaurant.php pour corriger ce bug :
Préparons la route de création d'un restaurant dans RestaurantController.php :
/*** Affiche le formulaire de création de restaurant* @Route("/restaurant/new", name="restaurant_new", methods={"GET"})*/publicfunctionnew()
{
return$this->render('restaurants/form.html.twig');
}
Et créons un formulaire rapidement dans templates/restaurants/form.html.twig pour voir à quoi il pourrait ressembler :
{% extends'base.html.twig' %}
{% blocktitle %}Liste des restaurants{% endblock %}
{% blockbody %}
<divclass="container mt-3">
<divclass="row">
<divclass="col">
<formaction="">
<divclass="form-group">
<labelfor="">Nom du restaurant</label>
<inputtype="text"class="form-control">
</div>
<divclass="form-group">
<labelfor="">Ville</label>
<selectclass="form-control"name=""id="">
<optionvalue="">69001 - Ville 1</option>
<optionvalue="">69002 - Ville 2</option>
</select>
</div>
<divclass="form-group">
<labelfor="">Description du restaurant</label>
<textareaclass="form-control"></textarea>
</div>
<divclass="form-group">
<labelfor="">Description du restaurant</label>
<inputtype="file"class="form-control">
</div>
<buttonclass="btn btn-success">Créer un restaurant</button>
</form>
</div>
</div>
</div>
{% endblock %}
Et voilà, normalement le lien Créer un restaurant devrait afficher le formulaire.
5. Affichage d'un restaurant
Modifions le controller RestaurantController.php. Le restaurant est injecté en paramètres à la méthode show(Restaurant $restaurant) via l'ID de l'URL, on a juste à l'envoyer à une vue show.html.twig :
Et voilà, les boutons "Créer un avis" devraient afficher le restaurant et ses avis !
Exercice 14 : Gérer les nouvelles relations
Avant de rendre fonctionnels nos formulaires et la gestion de rôles, nous allons mettre à jour nos entités pour prendre en compte nos nouvelles relations possibles :
User ManyToOne City # Un user a une ville
User OneToMany Review # Un user a plusieurs reviews
User OneToMany Restaurant # Un user a plusieurs restaurants
Faites la commande :
bin/console make:entity User
Et remplissez les relations suivantes (les questions sont dans un ordre différent selon qu'on soit en OneToMany ou ManyToOne) :
ManyToOne
Entité à modifier
Nouveau champ
Relation
Classe
Nullable ?
Accessors ?
Nouveau champ
User
city
ManyToOne
City
yes
yes
users
OneToMany
Entité à modifier| Nouveau champ | Relation | Classe | Nouveau champ | Nullable ? | OrphanRemoval ?
---------|----------|---------|---------|---------|---------|---------|---------
User | reviews | OneToMany | Review | user | no | yes
User | restaurants | OneToMany | Restaurant | user | no | yes
Fixtures
Comme nous avons de nouvelles relations, nos fixtures ne vont plus marcher (en effet un restaurant et une review doivent avoir un User).
Créons UserFixtures (bin/console make:fixtures UserFixtures) et modifions-le. Nous allons en profiter pour commencer à découvrir les rôles et attribuer des rôles à nos 3 nouveaux utilisateurs :
Dans cette fixture, on a injecté par le constructeur un UserPasswordEncoder : c'est un service qui nous permet de chiffrer les mots de passe avant de les entrer en base de données ! Obligatoire car sinon, le login ne marcherait pas (rappel: lors du login, ce sont des mots de passe chiffrés qui sont comparés).
Nous importons aussi le CityRepository et nous attribuons la même ville aléatoire aux 3 utilisateurs pour faciliter les tests.
Comme nous avons besoin de CityFixtures pour créer un user (lui attribuer une ville), nous l'ajoutons dans getDependancies().
Nous importons le UserRepository dans le constructeur pour pouvoir ajouter un User au restaurant
Nous ajoutons justement le setUser et on trouve le user "restaurateur"
Nous ajoutons aussi dans getDependancies les fixtures UserFixutres pour qu'elles soient lancées avant RestaurantFixtures
On peut retirer CityFixtures des dépendances car elles seront de toute façon lancées avant UserFixtures (c'est une dépendance de UserFixtures)
On fait de même pour ReviewFixtures.php :
<?phpnamespaceApp\DataFixtures;
useApp\Entity\Review;
useApp\Repository\RestaurantRepository;
useApp\Repository\ReviewRepository;
useApp\Repository\UserRepository;
useDoctrine\Bundle\FixturesBundle\Fixture;
useDoctrine\Common\DataFixtures\DependentFixtureInterface;
useDoctrine\Common\Persistence\ObjectManager;
useFaker\Factory;
class ReviewFixtures extends Fixture implements DependentFixtureInterface
{
private$restaurantRepository;
private$reviewRepository;
private$userRepository;
publicfunction__construct(RestaurantRepository$restaurantRepository,
ReviewRepository$reviewRepository,
UserRepository$userRepository) {
$this->restaurantRepository = $restaurantRepository;
$this->reviewRepository = $reviewRepository;
$this->userRepository = $userRepository;
}
publicfunctionload(ObjectManager$manager)
{
$faker = Factory::create('fr_FR');
/** * On créée 7000 reviews initiales */for ($i=0; $i<7000; $i++) {
$review = newReview();
$review->setMessage( $faker->text(800) );
$review->setRating( rand(0,5) );
$review->setRestaurant( $this->restaurantRepository->find(rand(1, 1000)) );
$review->setUser( $this->userRepository->findOneBy(["email" => "[email protected]"]) );
$manager->persist($review);
}
/** * On les enregistre en DB */$manager->flush();
/** * On créée 3000 reviews enfants (dont le parent est une des review initiales) */for ($i=0; $i<3000; $i++) {
$review = newReview();
$review->setMessage( $faker->text(800) );
$review->setParent( $this->reviewRepository->find(rand(1, 7000)) ); // On cherche un ID entre 1 et 7000 (un commentaire initial)$review->setRestaurant( $review->getParent()->getRestaurant() ); // On récupère le restaurant de la review parente$review->setUser( $this->userRepository->findOneBy(["email" => "[email protected]"]) );
$manager->persist($review);
}
$manager->flush();
}
publicfunctiongetDependencies()
{
returnarray(
RestaurantFixtures::class,
);
}
}
on importe le UserRepository
on ajoute les deux setUser() : client pour les premiers, restaurateur pour les réponses
on supprime setRating() dans la réponse : pas besoin de mettre une note pour répondre !
on laisse RestaurantFixtures dans les dépendances (lui meme étant dépendant de User, qui est dépendant de City, on est sûr que tout sera déjà chargé)
Comme nous venons de retirer setRating() dans la fixture car nous voulons parfois ne pas mettre de notes à un avis, modifions l'entité en fonction. Dans Review.php, rendez nullable $rating :
Comme nous faisons une grosse modification structurelle de la base de données (ajout de clés étrangères), nous pouvons par tranquilité d'esprit supprimer la base de données et la re-créer. C'est aussi à ça que servent les fixtures : s'assurer que nos données de base soient facilement là même si on supprime la base de données !
Rappel: Nettoyer un projet et tout relancer proprement
# Avant tout: SUPPRIMEZ TOUS LES FICHIERS DANS src/Migrations !
bin/console doctrine:database:drop --force # On supprime la bdd
bin/console doctrine:database:create # On créée la bdd
bin/console make:migration # On créée les migrations
bin/console doctrine:migrations:migrate # On migre
bin/console doctrine:fixtures:load --no-interaction # On execute les fixtures
En une ligne (Linux, OSX, GitBash) : bin/console doctrine:database:drop --force && bin/console doctrine:database:create && bin/console make:migration && bin/console doctrine:migrations:migrate && bin/console doctrine:fixtures:load --no-interaction
Rappel: Faire une migration en cours de projet sans tout supprimer
En une ligne (Linux, OSX, GitBash) : bin/console doctrine:schema:drop --force && bin/console doctrine:schema:create && bin/console doctrine:fixtures:load --no-interaction
Exercice 15 : Créez le formulaire de création de restaurant
Nous allons utiliser le système de formulaires de Symfony pour créer notre formulaire de Restaurants :
╭─tomsihap@MacBook-Pro-de-Thomas ~/projects/notaresto ‹master›
╰─$ bin/console make:form
The name of the form class (e.g. FiercePuppyType):
> RestaurantType
The name of Entity or fully qualified model class name that the new form will be bound to (empty for none):
> Restaurant
Modifions ce formulaire pour l'adapter à nos besoins (nous ne voulons que les champs name, description et city). Dans RestaurantType.php :
Importons ce formulaire dans la méthode RestaurantController::new(), celle qui affiche le formulaire. Dans RestaurantController.php :
/*** Affiche le formulaire de création de restaurant* @Route("/restaurant/new", name="restaurant_new", methods={"GET"})*/publicfunctionnew()
{
$restaurant = newRestaurant();
$form = $this->createForm(RestaurantType::class, $restaurant);
// On veut que le user du restaurant soit le user connecté (on l'a grâce à $this->getUser())$restaurant->setUser($this->getUser());
return$this->render('restaurant/form.html.twig', [
'form' => $form->createView()
]);
}
Modifions notre fichier restaurant/form.html.twig en supprimant notre formulaire fait à la main et en important celui généré par Symfony :
Et voilà, notre formulaire fonctionne et toutes nos villes sont affichées !
Optionnel : Idéalement, il faudrait utiliser du JavaScript pour le styliser avec de l'autocomplétion par exemple. La librairie Select2 fait ça très bien : https://select2.org/getting-started/basic-usage, quelques modifications sont à faire :
D'après la documentation de Symfony (https://symfony.com/doc/current/forms.html#processing-forms), il est recommandé d'utiliser la même méthode pour afficher le formulaire et pour le traiter. En effet, par défaut, le formulaire va s'envoyer vers la même méthode que pour l'affichage (ici, RestaurantController::new()).
Adaptons notre controller par rapport aux conseils de la documentation :
/** * Affiche et gère le formulaire de création de restaurant * @Route("/restaurant/new", name="restaurant_new", methods={"GET", "POST"})*/publicfunctionnew(Request$request)
{
$restaurant = newRestaurant();
$form = $this->createForm(RestaurantType::class, $restaurant);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$restaurant = $form->getData();
$restaurant->setUser($this->getUser());
$entityManager = $this->getDoctrine()->getManager();
$entityManager->persist($restaurant);
$entityManager->flush();
return$this->redirectToRoute('restaurant_index');
}
return$this->render('restaurant/form.html.twig', [
'form' => $form->createView()
]);
}
ATTENTION : Pour tester, il faut d'abord être logué ! Passez dans /login et loguez vous avec un user Restaurateur, sinon la ligne $this->getUser() ne marchera pas.
Exercice 16 : Ajout d'images dans le formulaire de Restaurant
Ajoutez un champ "filename" à RestaurantPicture (string, 255, non nullable) :
bin/console make:entity RestaurantPicture
New property name (press <return> to stop adding fields):
> filename
Field type (enter ? to see all types) [string]:
>
Field length [255]:
>
Can this field be null in the database (nullable) (yes/no) [no]:
> yes
N'oubliez pas de faire une migration pour enregistrer ce champ en base de données :
bin/console make:form
The name of the form class (e.g. GrumpyJellybeanType):
> RestaurantPictureType
The name of Entity or fully qualified model class name that the new form will be bound to (empty for none):
> RestaurantPicture
Modifiez ce nouveau fichier RestaurantPictureType.php (attention aux nombreux use) :
Maintenant que le formulaire est créé et s'affiche dans la page d'un restaurant, traitons les données d'upload.
En résumant les instructions du tutoriel, nous allons créer une classe Service (c'est une classe qui contient de la logique qui aurait pu se trouver dans un controller mais que l'on met ailleurs, dans un service donc, pour pouvoir l'utiliser de partout. Ici, notre service, c'est un uploader de fichier : on en aura en effet potentiellement besoin de partout !)
Nous allons aussi modifier des fichiers de configuration pour indiquer à notre service où enregistrer nos fichiers.
Créez le futur dossier d'arrivée des images: public/uploads/pictures.
Et voilà ! Vous pouvez tester. Normalement, une ligne devrait apparaître en base de données et un fichier devrait apparaître dans le dossier public/uploads/pictures.
Afficher les images
Modifiez restaurant/show.html.twig pour récupérer les images. Dans notre exemple, on utilise un modal qui s'ouvre quand on clique sur une image, on a donc deux boucles (une pour afficher les images en petit, une pour générer les modals) :
Attention : vous ne voulez pas mettre sur Git les images que vous uploadez en test ! N'oubliez pas de rajouter au fichier .gitignore à la racine du projet la ligne /public/uploads.
Exercice 17 : Créez le formulaire de création de reviews
Le principe est le même :
Créer le formulaire Symfony make:form
L'appeler dans la route qui gèrera l'affichage et le traitement du formulaire
L'envoyer à Twig
Création du formulaire
bin/console make:form
The name of the form class (e.g. GentleKangarooType):
> ReviewType
The name of Entity or fully qualified model class name that the new form will be bound to (empty for none):
> Review
Modifiez ReviewType.php pour n'avoir que les champs utiles :
Import et traitement du formulaire dans le contrôleur
Nous n'allons pas dans ReviewController mais dans RestaurantController : en effet, notre formulaire de review s'affiche dans la page d'affichage d'un restaurant, c'est à dire RestaurantController::show() !
Voici la méthode show() complète :
/** * Affiche un restaurant * @Route("/restaurant/{restaurant}", name="restaurant_show", methods={"GET", "POST"}, requirements={"restaurant"="\d+"}) * @param Request $request * @param Restaurant $restaurant * @return Response */publicfunctionshow(Request$request, Restaurant$restaurant, FileUploader$fileUploader)
{
/** * Gestion du formulaire Picture */$picture = newRestaurantPicture();
$formPicture = $this->createForm(RestaurantPictureType::class, $picture);
$formPicture->handleRequest($request);
if ($formPicture->isSubmitted() && $formPicture->isValid()) {
$file = $formPicture['filename']->getData();
if ($file) {
$filename = $fileUploader->upload($file);
$picture->setFilename($filename);
// Le restaurant de l'image est le restaurant qui est affiché sur la page$picture->setRestaurant($restaurant);
}
$entityManager = $this->getDoctrine()->getManager();
$entityManager->persist($picture);
$entityManager->flush();
// On redirige vers la page du restaurant une fois l'image postéereturn$this->redirectToRoute('restaurant_show', ['restaurant' => $restaurant->getId()]);
}
/** * // Fin de gestion du formulaire Picture *//** * Gestion du formulaire Review */$review = newReview();
$formReview = $this->createForm(ReviewType::class, $review);
$formReview->handleRequest($request);
if ($formReview->isSubmitted() && $formReview->isValid()) {
$review = $formReview->getData();
// Le User de la review est le User connecté$review->setUser($this->getUser());
// Le restaurant de la review est le Restaurant qu'on affiche$review->setRestaurant($restaurant);
$entityManager = $this->getDoctrine()->getManager();
$entityManager->persist($review);
$entityManager->flush();
// On redirige vers la page du restaurant une fois la review postéereturn$this->redirectToRoute('restaurant_show', ['restaurant' => $restaurant->getId()]);
}
/** * // Fin de gestion du formulaire Review *//** * Par défaut : on renvoie la vue restaurant/show.html.twig avec: * - le restaurant à afficher * - le formulaire d'images formPicture * - le formulaire de review formReview */return$this->render('restaurant/show.html.twig', [
'restaurant' => $restaurant,
'formPicture' => $formPicture->createView(),
'formReview' => $formReview->createView()
]);
}
Affichage du formulaire
On n'a plus qu'à afficher le formulaire dans restaurant/show.html.twig à la place du formulaire de review qu'on avait écrit à la main :
<divclass="card mb-2">
<divclass="card-header">
Rédigez un avis sur ce restaurant !
</div>
<divclass="card-body">
{{ form_start(formReview) }}
{{ form_widget(formReview) }}
<buttonclass="btn-sm btn-primary float-right">Envoyer</button>
{{ form_end(formReview) }}
</div>
</div>
Et voilà ! Créez une review, elle devrait s'afficher.
Note : Pour faire des formulaires avancés, faire des validations de formulaires, limiter les champs (ici pour review, un INT de 0 à 5 par exemple), la documentation de Symfony est très bien faite : https://symfony.com/doc/current/forms.html
Exercice 18 : Gestion des rôles (modérateur, restaurateur, client)
Exercice 19 : Ajout d'une réponse par un restaurateur à une review
Exercice 20 : Recherche de restaurants par code postal
Modifions l'input de navbar.html.twig pour en faire un formulaire digne de ce nom :
Et adaptons la méthode d'arrivée pour qu'elle retourne notre tableau de restaurants. Comme on a déjà un template de tableau de restaurants (templates/restaurants/index.html.twig), on va le réutiliser ! Dans AppController.php :
/*** @Route("/search", name="app_search", methods={"GET"})* @param Request $request*/publicfunctionsearch(Request$request) {
// On récupère l'input de recherche du formulaire, le name=zipcode$searchZipcode = $request->query->get('zipcode');
// On recherche une ville par son code postal$city = $this->getDoctrine()->getRepository(City::class)->findOneBy(["zipcode" => $searchZipcode]);
// Si une ville est trouvéeif ($city) {
$restaurants = $city->getRestaurants();
return$this->render('restaurant/index.html.twig', [
'restaurants' => $restaurants,
]);
}
// Sinon, on redirige en page d'accueilreturn$this->redirectToRoute("app_index");
}
Et voilà ! Testez le moteur de recherche avec un code postal existant qui possède un restaurant (prenez un code postal de restaurant de la liste dans /restaurants par exemple).
Exercice 22 : Ajouter les routes Edit et Delete pour Restaurant
Exercice 23 : Ajouter les routes Edit et Delete pour Review
Exercice 24 : Ajouter les routes Edit et Delete pour User