- Code
- Utilisation des tags de github
- Historique de Symfony
- Prérequis
- Création d'un projet de démonstration
- Création d'un nouveau projet Symfony
- Structure d'un projet Symfony
- Lancement du serveur web de Symfony
- Création du premier contrôleur
- Manipulation des routes
- Création du fichier .env.local
- Mise à jour de version mineure de Symfony
- Création d'un utilisateur
- Création des fixtures
- Modification de la page d'accueil
- Twig : Création d'un template de base
- Choix du template
- Modification de base.html.twig
- Modification de template.html.twig
- Modification de la page d'accueil
- Création de la section Catégories
- Affichage des articles par catégorie
- Création de la section Article
- Création de la route dans le controller
- Création des liens vers les articles dans la section
categorie
etindex
- Affichage d'un résumé de l'article avec slice
- Installation de la bibliothèque Twig Extra String
- Utilisation de la fonction
truncate
de la bibliothèque Twig Extra String - Modification de la méthode
article
du controller - Création de la vue
article.html.twig
- Création de la vue
commentaire.html.twig
- Authentification et autorisation
- Création de la connexion utilisateur
- Modification du formulaire de connexion
- Mise en place de la création de commentaires
- Inscription des utilisateurs
- Création du formulaire d'inscription
- Lancement de la migration de la DB après make:registration-form
- Sauvegarde de la DB dans le dossier
datas
après make:registration-form - Mise à jour du .env.local pour le mailer
- Ajout du champ
name
dans le formulaire d'inscription - Traduction du formulaire d'inscription et des mails
- Création du lien d'enregistrement et design de celui-ci
- Création du formulaire d'inscription
- Installation d'EasyAdmin
Différents tag
de git
sont utilisés pour marquer les différentes étapes de ce tutoriel sur Symfony.
Vous les trouverez à cette URL :
https://github.com/mikhawa/symfony-2023-05-10/tags
Ainsi, vous pourrez retourner dans le code source et pourrez voir le code source correspondant à l'étape du tutoriel.
Une partie du code sera supprimée et une autre partie sera ajoutée, le système de tag
de git
permettra de retrouver le code source correspondant à celle-ci, car il est lié à un commit
de git
.
Par exemple le tag v0.0.1 correspond au commit qui met la route par défaut de Symfony sur la page d'accueil :
https://github.com/mikhawa/symfony-2023-05-10/commit/f5d6f13df83f64551cfc8250a65eda8ed964ed29
Retour au Menu de navigation
Symfony
est un framework web open-source
écrit en PHP
, qui a été créé par le développeur français Fabien Potencier
en 2005. Le but principal de Symfony était de faciliter le développement d'applications web en fournissant un ensemble d'outils et de bibliothèques réutilisables, ainsi qu'une architecture claire et cohérente.
La première version de Symfony, la version 1.0, a été publiée en octobre 2005. Elle a rapidement gagné en popularité auprès de la communauté des développeurs PHP en raison de sa simplicité et de sa flexibilité.
Aujourd'hui, Symfony est l'un des frameworks web les plus populaires pour le développement d'applications PHP. Il est utilisé par des entreprises de toutes tailles, des petites start-ups aux grandes entreprises internationales. La communauté Symfony est également très active et contribue régulièrement à l'amélioration du framework en fournissant des mises à jour, des correctifs de bugs et de nouveaux composants.
Il y a plusieurs raisons pour lesquelles Symfony est un choix populaire pour le développement d'applications web
en PHP :
-
Structuration et modularité : Symfony offre une
structure et une organisation claire
de typeMVC
(Model-View-Controller) pour les projets, ce qui facilite leurmaintenance
et leurévolution
. Le framework est égalementmodulaire
, ce qui signifie que les développeurs peuvent utiliser uniquement les composants dont ils ont besoin, sans avoir à intégrer des fonctionnalités superflues. -
Grande flexibilité : Symfony est conçu pour être flexible et s'adapter à différents types de projets, qu'il s'agisse de petites applications ou de grandes plateformes web. Il est également facilement extensible grâce à sa capacité à intégrer des bibliothèques tierces.
-
Sécurité : La
sécurité
est unepréoccupation majeure
dans le développement web, et Symfony offre des fonctionnalités de sécurité avancées telles que la protection contre lesinjections SQL
et les attaquesXSS
. Il fournit également des fonctionnalités d'authentification
et d'autorisation
pour garantir que les utilisateurs ne peuvent accéder qu'aux parties de l'application qui leur sont autorisées. -
Documentation et communauté active : Symfony est livré avec une
documentation complète
, (à présent uniquement en anglais, choisie comme langue internationale) qui est constammentmise à jour
pour refléter les dernières fonctionnalités. La communauté Symfony est également très active et fournit un support et des ressources utiles pour les développeurs qui travaillent avec le framework. -
Performance : Symfony est conçu pour être rapide et efficace, et offre des fonctionnalités telles que le
cache
de requête pour améliorer les performances des applications web.
Dans l'ensemble, Symfony est un choix solide pour les développeurs PHP qui cherchent à construire des applications web évolutives, modulaires et sécurisées.
Retour au Menu de navigation
- Windows 10 ou 11
- WampServer 3.2.5 ou supérieur
- PHP 8.1 ou supérieur (depuis WampServer en le rajoutant dans le
PATH
, les variables d'environnement Windows) - Composer
- Symfony CLI
- Git
- MySQL - MariaDB (depuis WampServer)
- Node.js (pour Webpack Encore)
- Yarn (pour Webpack Encore)
Retour au Menu de navigation
Lien de téléchargement de composer :
https://getcomposer.org/download/
ou le mettre à jour avec la commande :
composer self-update
Lien de téléchargement de WampServer :
https://wampserver.aviatechno.net/?lang=french
Lien de téléchargement de Symfony CLI :
Lien de téléchargement de Git :
https://git-scm.com/download/win
Lien de téléchargement de Node.js :
https://nodejs.org/fr/download
ou le mettre à jour avec la commande :
npm install -g npm@latest
Lien de téléchargement de Yarn :
https://classic.yarnpkg.com/lang/en/docs/install/#windows-stable
ou l'installer avec la commande :
npm install --global yarn
Retour au Menu de navigation
- Vérifier que PHP est bien installé
php -v
- Vérifier que Composer est bien installé
composer -V
- Vérifier que Symfony CLI est bien installé
symfony -V
- Vérifier que Git est bien installé
git --version
- Créer un nouveau projet Symfony, ici, nous ne choisirons pas la version
LTS
(Long Term Support), mais la dernière version stable
Documentation de versioning de Symfony :
Vérifions si notre poste de travail est bien configuré pour Symfony
symfony check:requirements
Retour au Menu de navigation
Nous allons créer un projet de démonstration, la documentation se trouve à cette adresse :
https://symfony.com/doc/current/setup.html#the-symfony-demo-application
Nous utilisons la commande suivante :
symfony new symfonyDemo --demo
Nous pouvons entrer dans le dossier et tester ce projet de démonstration, la base de donnée étant déjà configurée en SQLite
, nous pouvons lancer le serveur web avec la commande suivante :
symfony serve
Nous pouvons le tester dans le navigateur avec l'adresse suivante :
Cette démo permet de tester les fonctionnalités de Symfony, et de voir comment est structuré un projet Symfony.
Retour au Menu de navigation
L'écriture de la commande suivante va créer un nouveau projet (dossier) Symfony, en utilisant la dernière version stable de Symfony, et en utilisant le template webapp
, qui est un template de base pour créer une application web.
symfony new nom_du_projet
--webapp
Lors de l'écriture de ces lignes la version stable de Symfony est la version 6.2.10 (La LTS est la version 5.4.27). Pour voir les versions LTS de nombreux projets, vous pouvez consulter le lien suivant :
https://endoflife.date/symfony
Dans la console, nous allons écrire la commande suivante :
symfony new symfony6 --webapp
Voici le lien de la documentation officielle de Symfony pour créer un nouveau projet :
https://symfony.com/doc/current/setup.html
- Vérifions que les dépendances sont bien installées et sécurisées
symfony check:security
- Mettons à jour les dépendances
composer update
Retour au Menu de navigation
- Le dossier
bin
contient les fichiers binaires, qui sont des fichiers exécutables qui peuvent être utilisés pour exécuter des tâches spécifiques. (par exemple, le fichierconsole
est un fichier binaire qui peut être utilisé pour exécuter des commandes Symfony) en ligne de commande (CLI).:
php bin/console
# ou
symfony console
-
Le dossier
config
contient les fichiers de configuration de l'application, tels que les fichiers de configuration de la base de données, les fichiers de configuration de l'environnement, etc. -
Le dossier
public
contient les fichiers publics de l'application, tels que les fichiers CSS, JavaScript, les images, etc. Le fichierindex.php
est le point d'entrée de l'application (contrôleur frontal
). -
Le dossier
src
contient le code source de l'application, y compris les contrôleurs, les entités, les formulaires, etc.- Le dossier
src/Controller
contient les contrôleurs de l'application. (MVC
) - Le dossier
src/Entity
contient les entités de l'application. (M
VC) - Le dossier
src/Form
contient les formulaires de l'application. (M
VC) - Le dossier
src/Repository
contient les dépôts de l'application. (M
VC) - Le dossier
src/Service
contient les services de l'application. (M
VC) - etc...
- Le dossier
-
Le dossier
templates
contient les fichiers de template de l'application, qui sont des fichiers HTML qui sont utilisés pour afficher les pages de l'application. (MV
C) -
Le dossier
tests
contient les tests de l'application. -
Le dossier
translations
contient les fichiers de traduction de l'application. -
Le dossier
var
contient les fichiers de cache, les fichiers de logs, etc. Il se trouvera dans le.gitignore
, et ne sera donc pas envoyé sur le dépôt distant. -
Le dossier
vendor
contient les dépendances de l'application, qui sont des bibliothèques tierces qui sont utilisées par l'application. Il se trouvera dans le.gitignore
. (M
VC) -
Le fichier
.env
contient les variables d'environnement de l'application. (par exemple, les informations de connexion à la base de données, etc.). Ce fichier est utilisé par défaut pour l'environnement de développement. Pour l'environnement de production, le fichier.env
est ignoré, et le fichier.env.local
est utilisé à la place, c'est ce fichier.env.local
qui se trouvera dans le.gitignore
, et ne sera donc pas envoyé sur le dépôt distant. Le.env
est l'équivalent du fichierconfig.php.bak
utilisé couramment dans les projets PHP. -
Le fichier
.env.local
contient les variables d'environnement de l'application pour l'environnement de production. Ce fichier est ignoré par Git, et ne sera donc pas envoyé sur le dépôt distant. C'est l'équivalent du fichierconfig.php
utilisé couramment dans les projets PHP. -
Le fichier
.env.test
contient les variables d'environnement de l'application pour l'environnement de test. Ce fichier est utilisé par défaut pour l'environnement de test. Pour l'environnement de production, le fichier.env.test
est ignoré, et le fichier.env.test.local
est utilisé à la place. Il se trouvera dans le.gitignore
, et ne sera donc pas envoyé sur le dépôt distant. -
Le fichier
.gitignore
contient la liste des fichiers et dossiers qui ne doivent pas être envoyés sur le dépôt distant. (par exemple, les fichiers de logs, les fichiers de cache, les fichiers de configuration, etc.) -
Le fichier
composer.json
contient les dépendances de l'application, qui sont des bibliothèques tierces qui sont utilisées par l'application. Il permet à un utilisateur de pouvoir installer les dépendances de l'application en exécutant la commandecomposer install
(création du dossiervendor
) ou de mettre à jour les dépendances en exécutant la commandecomposer update
.
Retour au Menu de navigation
symfony serve
Si le serveur est démarré en http://, quittez avec ctrl-c, et installez le certificat SSL
symfony server:ca:install
Une manière plus "propre" de lancer le serveur en mode deamon (invisible) est la suivante :
symfony server:start -d
Et pour le fermer proprement :
symfony server:stop
Vous pourrez retrouver le serveur web de Symfony à l'adresse suivante :
Retour au Menu de navigation
Dans le terminal, à la racine du projet, exécutez la commande suivante :
symfony console make:controller
- Nom du contrôleur à indiquer :
PublicController
2 fichiers ont été créés :
src/Controller/PublicController.php
templates/public/index.html.twig
Le premier fichier est le contrôleur en PHP, classe héritant de AbstractController
, le second est la vue en Twig
(moteur de template que nous verrons plus tard dans ce cours).
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class PublicController extends AbstractController
{
#[Route('/public', name: 'app_public')]
public function index(): Response
{
return $this->render('public/index.html.twig', [
'controller_name' => 'PublicController',
]);
}
}
Fichier Twig :
{% extends 'base.html.twig' %}
{% block title %}Hello PublicController!{% endblock %}
{% block body %}
...
Vous pouvez le tester en vous rendant à l'adresse suivante :
Retour au Menu de navigation
On peut créer des routes en utilisant 4 méthodes différentes :
annotation
: dans le contrôleuryaml
: dans le fichierconfig/routes.yaml
xml
: dans le fichierconfig/routes.xml
php
: dans le fichierconfig/routes.php
Symfony utilise par défaut la méthode annotation
et l'utilisation des attributs
(depuis PHP 8) #[Route()]
dans les contrôleurs.
Voir la documentation :
https://symfony.com/doc/current/routing.html
Pour l'utilisation des attributs dans Symfony ou de manière plus générale dans PHP 8, voir ces liens :
https://www.elao.com/blog/dev/les-attributs-php-8-dans-symfony
https://grafikart.fr/tutoriels/attribut-php8-1371
https://www.php.net/manual/fr/language.attributes.overview.php
Retour au Menu de navigation
Dans le fichier src/Controller/PublicController.php
, nous allons modifier la route de la méthode index()
.
Nous choisissons de mettre le nom de la route en annotation
pour éviter de devoir la mettre dans le fichier config/routes.yaml
(ce qui est possible également, comme dans Laravel, par exemple, mais ce n'est pas la méthode préconisée par Symfony).
Nous choisissons le chemin de la page d'accueil à la racine du site /
, et nous la nommons public_accueil
namespace App\Controller;
###
#[Route('/', name: 'public_accueil')]
public function index(): Response
{
// chemin du fichier twig à partir du dossier templates
return $this->render('public/index.html.twig', [
// variable envoyée au fichier twig
'controller_name' => 'PublicController',
]);
}
###
Nous pouvons maintenant tester la route à l'adresse suivante :
Nous pouvons également voir nos routes disponibles en tapant la commande suivante :
symfony console debug:router
Nous verrons notre route public_accueil
avec la méthode ANY
et le chemin /
. Les méthodes ANY
signifie que la route est disponible en GET
et en POST
.
Les routes en _...
sont des routes qui permettent de voir les requêtes SQL, les requêtes HTTP, les variables globales, etc... en mode développement.
Retour au Menu de navigation
Nous allons créer une nouvelle méthode dans le contrôleur PublicController.php
:
public function contact(): Response
{
// Nous allons envoyer une réponse de type texte en utilisant la classe Response (html basique)
return new Response('<body><h1>Page de contact</h1><a href="./">Retour à l\'accueil</a></body>');
}
Pour en savoir plus sur la classe Response
et les réponses HTTP, vous pouvez consulter la documentation officielle de Symfony :
https://symfony.com/doc/current/introduction/http_fundamentals.html
Nous allons utiliser le fichier de configuration config/routes.yaml
pour créer un chemin vers cette nouvelle méthode.
public_contact:
path: /contact
controller: App\Controller\PublicController::contact
Nous pouvons mettre un lien sur l'accueil vers la page de contact en utilisant la fonction path()
de Twig.
Dans le fichier templates/public/index.html.twig
:
{# chemin vers la page de contact en utilisant son nom
de route (public_contact). Ceci est une bonne pratique
dans Symfony #}
<li>Me <a href="{{ path('public_contact') }}">contacter</a></li>
Nous pouvons maintenant tester la route à l'adresse suivante :
https://127.0.0.1:8000/contact
Cette méthode est également valable pour les routes avec paramètres, mais ne fait pas partie de la méthode préconisée par Symfony : Les fichiers de configuration sont plutôt utilisés pour les routes de type API.
Voir le guide des bonnes pratiques :
Retour au Menu de navigation
Nous allons créer une nouvelle méthode dans le contrôleur PublicController.php
en utilisant un paramètre dans la route, la variable GET
{id}
qui sera récupérée dans la méthode sous le nom $id
:
#[Route('/article/{id}', name: 'public_article')]
public function article($id): Response
{
// Nous allons envoyer une réponse de type texte en utilisant
// la classe Response en utilisant la variable $id
return new Response("<body><h1>Page de l'article dont l'id est $id</h1>
<a href='./'>Retour à l'accueil</a></body>");
}
Nous pouvons mettre un lien sur l'accueil vers la page de l'article en utilisant la fonction path()
de Twig. Attention, il faut envoyer l'id en paramètre de la route.
Pour le moment aucune vérification n'est faite sur l'id, il peut être n'importe quoi, il faut donc faire attention à ce que l'on envoie dans la route.
Dans le fichier templates/public/index.html.twig
:
# chemin vers la page de l'article en utilisant son nom de route (public_article) et en envoyant l'id 1
en paramètre#}
<li>Un <a href="{{ path('public_article', {'id': 1}) }}">article dont l'id vaut 1</a></li>
Nous pouvons maintenant tester la route à l'adresse suivante :
https://127.0.0.1:8000/article/1
Sans protections, nous pouvons passer n'importe quoi dans l'id ! :
https://127.0.0.1:8000/article/Coucou-les-amis
Retour au Menu de navigation
Nous allons créer une nouvelle méthode dans le contrôleur PublicController.php
en utilisant un paramètre dans la route, la variable {id}
qui sera ensuite récupérée dans la méthode sous le nom $id
, on va vérifier que la méthode est bien en GET
et le type en Int
:
#[Route('/articleType/{id<\d+>}', name: 'public_article_type',methods: ['GET'])]
public function articleType(int $id): Response
{
// Nous allons envoyer une réponse de type texte en utilisant la classe Response en utilisant
// la variable $id
return new Response("<body><h1>Page de l'article Typée en int dont l'id est $id</h1>
<p>Ne fonctionne qu'avec une variable GET de type numérique !</p><a href='../../'>Retour à l'accueil</a></body>");
}
Puis dans le fichier templates/public/index.html.twig
:
{# chemin vers la page de l'article en utilisant son nom de route (public_article_type)
et en envoyant l'id 1 en paramètre protégé#}
<li>Un <a href="{{ path('public_article_type', {'id': 1}) }}">article dont l'id vaut 1</a></li>
On peut vérifier que la route ne fonctionne pas avec une variable de type string
:
https://127.0.0.1:8000/articleType/coucou
Pour debugger les routes, nous pouvons utiliser la commande suivante :
php bin/console debug:router
Retour au Menu de navigation
Nous allons créer une nouvelle méthode dans le contrôleur PublicController.php
en utilisant un paramètre dans la route, la variable {id}
qui sera ensuite récupérée dans la méthode sous le nom $id
, on va vérifier que la méthode est bien en GET
et le type en Int
et on va mettre une valeur par défaut à l'id :
#[Route('/articleTypeDefault/{id<\d+>}',
name: 'public_article_type_default',
defaults: ['id' => 1],
methods: ['GET'])]
public function articleTypeDefault(int $id): Response
{
// Nous allons envoyer une réponse de type texte en utilisant la classe Response en utilisant
// la variable $id
return new Response("<body><h1>Page de l'article Typée avec valeur par défaut en int dont l'id est $id</h1>
<p>Ne fonctionne qu'avec une variable GET de type numérique !<br>
La valeur par défaut est 1</p><a href='../../'>Retour à l'accueil</a></body>");
}
Puis dans le fichier templates/public/index.html.twig
:
{# chemin vers la page de l'article en utilisant son nom de route (public_article_type_default)
sans envoyer de paramètre#}
<li>Un <a href="{{ path('public_article_type_default') }}">articleTypeDefault
sans id</a></li>
<li>Un <a href="{{ path('public_article_type_default', {'id': 15}) }}">articleTypeDefault
avec un id de 15</a></li>
Retour au Menu de navigation
Nous allons créer un fichier .env.local
en copiant le fichier .env
et en le renommant .env.local
:
cp .env .env.local
Ou de manière plus simple en utilisant la commande suivante :
composer dump-env dev
Ce qui créera un fichier .env.local.php
, que nous ne garderons pas pour le moment. Cette commande est à utiliser pour la mise en production : composer dump-env prod
.
Nous allons modifier le fichier .env.local
pour y mettre les informations de connexion à la base de données MariaDB, ici pas de danger car nous travaillons en local, mais en production il faudra faire attention à ne pas mettre les informations de connexion à la base de données dans un fichier qui sera versionné.
# .env.local
###> doctrine/doctrine-bundle ###
DATABASE_URL="mysql://root:@127.0.0.1:3307/sym_62?serverVersion=10.10.2-MariaDB&charset=utf8mb4"
# DATABASE_URL="postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=15&charset=utf8"
###< doctrine/doctrine-bundle ###
! Depuis la version 6.3 de Symfony, il est possible qu'il faille installer une librairie supplémentaire pour les annotations de doctrine :
composer require doctrine/annotations
Sinon un bug est possible.
Retour au Menu de navigation
Nous allons créer la base de données en utilisant la commande suivante :
php bin/console doctrine:database:create
Retour au Menu de navigation
Si la base de données existe déjà et que des tables y sont présentes, nous pouvons importer toute la structure !
Les tables seront importées sous forme d'entités avec annotations dans les fichiers src/Entity
de notre projet.
Nous allons en faire la démonstration dans le tag V0.1.1
en utilisant la DB mvcprojets
que vous trouverez dans le dossier datas
de ce projet.
Pour le tester vous pouvez importer la base de données dans votre serveur MariaDB en utilisant le fichier contenu dans le dossier datas
de ce projet:
https://raw.githubusercontent.com/mikhawa/symfony-2023-05-10/main/datas/mvcprojets.sql
Remplacez les informations de connexion à la base de données dans le fichier .env.local
par les vôtres :
# .env.local
###> doctrine/doctrine-bundle ###
DATABASE_URL="mysql://root:@127.0.0.1:3307/mvcprojets?serverVersion=10.10.2-MariaDB&charset=utf8mb4"
###< doctrine/doctrine-bundle ###
Puis lancez la commande suivante :
php bin/console doctrine:mapping:import "App\Entity" annotation --path=src/Entity
Vous devriez voir apparaître les fichiers
src/Entity/Category.php
src/Entity/Post.php
src/Entity/User.php
Qui contiennent les annotations de nos entités. C'est-à-dire le mapping de nos tables avec les propriétés et les liens entre les tables.
Il faut une étape de plus pour que les entités soient prises en compte par le système de migration de Symfony, nous allons donc lancer la commande suivante :
php bin/console make:entity --regenerate
Il va ainsi créer les getters et les setters de nos entités, ainsi que des méthodes pour les liens entre les tables.
Il y aura des erreurs ! Nous devons comprendre le fonctionnement de Doctrine pour pouvoir les corriger !
Nous allons donc commencer par comprendre le fonctionnement de Doctrine en créant un premier CRUD.
Retour au Menu de navigation
Nous allons créer un CRUD pour la table post
en utilisant la commande suivante :
php bin/console make:crud Post
Le nom choisi pour le controller est PostController
et le nom de l'entité est Post
.
Voici le résultat de la commande :
created: src/Controller/PostController.php
created: src/Form/PostType.php
created: templates/post/_delete_form.html.twig
created: templates/post/_form.html.twig
created: templates/post/edit.html.twig
created: templates/post/index.html.twig
created: templates/post/new.html.twig
created: templates/post/show.html.twig
Nous allons rajouter un lien vers la route /post
dans le fichier templates/public/index.html.twig
:
#
<li>Un lien vers le CRUD de 'post'
<a href="{{ path('app_post_index')}}">CRUD Post</a></li>
#
Nous pouvons ensuite aller sur la route /post
de notre projet et voir le résultat :
Nous voici sur la page d'index du CRUD de la table post
!
Elle devrait être fonctionnelle, mais nous allons avoir des erreurs si nous essayons de créer un nouveau post, ou de modifier un post existant.
Retour au Menu de navigation
Nous allons donc commencer par corriger ces erreurs.
Nous allons commencer par la création d'un nouveau post.
Nous allons donc aller sur la route /post/new
de notre projet :
https://127.0.0.1:8000/post/new
Nous avons une erreur :
App\Entity\Post::getDatecreate(): Return value must be of type ?DateTimeInterface, string returned
Dans le constructeur du fichier src/Entity/Post.php
, nous rajoutons la ligne suivante
public function __construct()
{
$this->category = new
\Doctrine\Common\Collections\ArrayCollection();
// rajout d'une date de création par défaut
$this->setDatecreate(new \DateTime());
}
Nous avons ensuite cette erreur :
Object of class App\Entity\User could not be converted to string
Dans le fichier src/Entity/User.php
nous allons rajouter la méthode __toString()
:
// permet l'affichage du nom de l'utilisateur dans le formulaire du CRUD
// de la table Post
public function __toString()
{
return $this->getUsername();
}
Dans le fichier src/Entity/Category.php
nous allons rajouter la méthode __toString()
:
// permet l'affichage du nom de la catégorie dans le formulaire du CRUD
// de la table Post
public function __toString()
{
return $this->getTitle();
}
Nous pouvons maintenant créer un nouveau post !
Nous remarquons que nous avons une erreur, les catégories sélectionnées le sont en many to many, mais l'insertion ne fonctionne pas pour les catégories de notre post !
Nous allons modifier le fichier de formulaire pour changer l'affichage de celui-ci src/Form/PostType.php
:
// ...
// pour le many to many
use App\Entity\Category;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
// ...
->add('category', EntityType::class, [
'class' => Category::class,
'choice_label' => 'title',
'multiple' => true,
'expanded' => true,
])
// ...
Ensuite nous allons modifier le fichier src/Entity/Post.php
:
En changeant la ligne de jointure qui ne semble pas fonctionner en create et update depuis Post vers Category :
/**
* @var \Doctrine\Common\Collections\Collection
*
* @ORM\ManyToMany(targetEntity="Category", mappedBy="post")
*/
private $category = array();
Par
/**
* @var \Doctrine\Common\Collections\Collection
*
* @ORM\ManyToMany(targetEntity="Category", inversedBy="Post")
* @ORM\JoinTable(name="category_has_post",
* inverseJoinColumns={
* @ORM\JoinColumn(name="category_id", referencedColumnName="id")
* },
* joinColumns={
* @ORM\JoinColumn(name="post_id", referencedColumnName="id")
* }
* )
*/
private $category = array();
Et voici notre CRUD de la table post
fonctionnel !
Retour au Menu de navigation
Nous allons retourner dans le passé du projet, en enlevant le CRUD de la table post
que nous venons de créer, les contrôleurs, les formulaires, les templates, les entités, etc.
On va aussi modifier les fichiers .env
, .env.local
pour changer le nom de la base de données locale.
.env
# ...
DATABASE_URL="mysql://root:@127.0.0.1:3307/sym_62?serverVersion=10.10.2-MariaDB&charset=utf8mb4"
# ...
Nous passerons à la version 0.2.0
Retour au Menu de navigation
Nous allons tout d'abord créer à nouveau un contrôleur pour pouvoir tester nos entités.
php bin/console make:controller BlogController
puis modifier le fichier src/Controller/BlogController.php
:
#[Route('/', name: 'app_blog')]
public function index(): Response
{
return $this->render('blog/index.html.twig', [
'controller_name' => 'BlogController',
]);
}
Retour au Menu de navigation
Nous allons créer un mini blog, avec des articles, des catégories, des utilisateurs, des commentaires etc.
Pour créer une entité Article
, nous allons utiliser la commande suivante :
php bin/console make:entity
Nous allons ensuite répondre aux questions suivantes :
> Class name of the entity to create or update (e.g. BravePuppy):
> > Article
> ArticleTitle
> > string
> > 160
> > nullable => no
> ArticleSlug
> > string
> > 160
> > nullable => no
> ArticleContent
> > text
> > nullable => no
> ArticleDateCreate
> > date
> > nullable => yes
> ArticleDateUpdate
> > datetime
> > nullable => yes
> ArticleIsPublished
> > boolean
> > nullable => no
L'enregistrement de l'entité se fait automatiquement dans le fichier src/Entity/Article.php
. Un fichier src/Repository/ArticleRepository.php
contenant la gestion de la table article
est aussi créé.
On peut voir si les champs correspondent bien à ce que nous souhaitons dans la DB de notre projet.
dans le fichier src/Entity/Article.php
nous allons modifier la ligne suivante :
// ...
// pour que l'id soit unsigned
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(options: ["unsigned" => true])]
private ?int $id = null;
Il peut y avoir d'autres modifications à faire plus tard, pour le moment cela nous suffit.
Retour au Menu de navigation
Nous allons maintenant effectuer une migration vers la DB pour créer la table article
:
php bin/console make:migration
Ce qui nous a créé un fichier src/Migrations/Version***.php
contenant la migration de la table article
.
Nous allons maintenant effectuer la migration vers la DB :
php bin/console doctrine:migrations:migrate
Retour au Menu de navigation
Les commentaires seront liés à un article, nous allons donc créer une entité Commentaire
avec une relation ManyToOne
vers Article
.
php bin/console make:entity Commentaire
Nous allons ensuite répondre aux questions suivantes :
> Class name of the entity to create or update (e.g. BravePuppy):
> > Commentaire
> CommentaireTitle
> > string
> > 100
> > nullable => no
> CommentaireText
> > string
> > 800
> > nullable => no
> CommentaireDateCreate
> > datetime
> > nullable => yes
> > CommentaireManyToOneArticle
> > ManyToOne
> > Article
> > nullable => no
> > CommentaireIsPublished
> > boolean
> > nullable => no
Nous allons faire quelques modifications dans le fichier src/Entity/Commentaire.php
:
// ...
// pour que l'id soit unsigned
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(options: ["unsigned" => true])]
private ?int $id = null;
// ...
// pour que la date actuelle soit insérée automatiquement
#[ORM\Column(type: Types::DATETIME_MUTABLE, nullable: true,options: ["default" => "CURRENT_TIMESTAMP"])]
private ?\DateTimeInterface $CommentaireDateCreate = null;
// ...
// Pour que la relation soit bidirectionnelle,
// on peut ajouter une propriété targetEntity et inversedBy
#[ORM\ManyToOne(targetEntity: Article::class, inversedBy: 'Commentaires')]
private ?Article $CommentaireManyToOneArticle = null;
// pour que la valeur par défaut soit false
#[ORM\Column(type: Types::BOOLEAN, options: ["default" => false])]
private ?bool $CommentaireIsPublished = null;
Nous allons faire quelques modifications dans le fichier src/Entity/Article.php
:
// ...
// pour que la date actuelle soit insérée automatiquement lors de la création
#[ORM\Column(type: Types::DATETIME_MUTABLE, nullable: true,
options: ["default" => "CURRENT_TIMESTAMP"])]
private ?\DateTimeInterface $AritcleDateCreate = null;
// pour que la date actuelle soit insérée automatiquement lors de la mise à jour
#[ORM\Column(type: Types::DATETIME_MUTABLE, nullable: true, columnDefinition: "TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP",)]
private ?\DateTimeInterface $AgirticleDateUpdate = null;
Retour au Menu de navigation
php bin/console make:migration
Ce qui nous a créé un fichier src/Migrations/Version***.php
contenant la migration de la table commentaire
et les modifications de la table article
.
Nous allons maintenant effectuer la migration vers la DB :
php bin/console doctrine:migrations:migrate
Retour au Menu de navigation
Les articles seront liés à des catégories, nous allons donc créer une entité Categorie
avec une relation ManyToMany
vers Article
.
php bin/console make:entity Categorie
Nous allons ensuite répondre aux questions suivantes :
> CategorieTitle
> > string
> > 160
> > nullable => no
> > CategorieSlug
> > string
> > 160
> > nullable => no
> > CategorieDesc
> > string
> > 500
> > nullable => yes
> > Categorie_m2m_Article
> > ManyToMany
> > Article
> > acces into Article
> > yes
Nous allons faire quelques modifications dans le fichier src/Entity/Categorie.php
:
// ...
// pour que l'id soit unsigned
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(options: ["unsigned" => true])]
private ?int $id = null;
// ...
Retour au Menu de navigation
Ensuite on refait une migration :
php bin/console make:migration
Puis on effectue la migration :
php bin/console doctrine:migrations:migrate
Retour au Menu de navigation
Pour des raisons de sécurité, il est important de mettre à jour régulièrement les versions des packages utilisés dans notre projet:
Aujourd'hui nous sommes le 2023-06-13, et il ne reste qu'un mois de mises à jour de sécurité pour la version 6.2 de Symfony :
https://endoflife.date/symfony
Nous allons mettre à jour la version de Symfony de 6.2.11 à 6.3.0 :
Il vaut mieux commencer par créer une branche git pour pouvoir retourner sur la version précédente si besoin !
Dans le fichier composer.json
:
"require": {
"php": ">=8.1",
"ext-ctype": "*",
"ext-iconv": "*",
"doctrine/annotations": "^2.0",
"doctrine/doctrine-bundle": "^2.9",
"doctrine/doctrine-migrations-bundle": "^3.2",
"doctrine/orm": "^2.15",
"phpdocumentor/reflection-docblock": "^5.3",
"phpstan/phpdoc-parser": "^1.20",
"sensio/framework-extra-bundle": "^6.1",
"symfony/asset": "6.2.*",
"symfony/console": "6.2.*",
"symfony/doctrine-messenger": "6.2.*",
"symfony/dotenv": "6.2.*",
"symfony/expression-language": "6.2.*",
"symfony/flex": "^2",
"symfony/form": "6.2.*",
"symfony/framework-bundle": "6.2.*",
"symfony/http-client": "6.2.*",
"symfony/intl": "6.2.*",
"symfony/mailer": "6.2.*",
"symfony/mime": "6.2.*",
"symfony/monolog-bundle": "^3.0",
"symfony/notifier": "6.2.*",
"symfony/process": "6.2.*",
"symfony/property-access": "6.2.*",
"symfony/property-info": "6.2.*",
"symfony/runtime": "6.2.*",
"symfony/security-bundle": "6.2.*",
"symfony/serializer": "6.2.*",
"symfony/string": "6.2.*",
"symfony/translation": "6.2.*",
"symfony/twig-bundle": "6.2.*",
"symfony/validator": "6.2.*",
"symfony/web-link": "6.2.*",
"symfony/yaml": "6.2.*",
...
"extra": {
"symfony": {
"allow-contrib": false,
"require": "6.2.*"
}
},
"require-dev": {
"phpunit/phpunit": "^9.5",
"symfony/browser-kit": "6.2.*",
"symfony/css-selector": "6.2.*",
"symfony/debug-bundle": "6.2.*",
"symfony/maker-bundle": "^1.0",
"symfony/phpunit-bridge": "^6.2",
"symfony/stopwatch": "6.2.*",
"symfony/web-profiler-bundle": "6.2.*"
}
Par les lignes suivantes :
"require": {
"php": ">=8.1",
"ext-ctype": "*",
"ext-iconv": "*",
"doctrine/annotations": "^2.0",
"doctrine/doctrine-bundle": "^2.9",
"doctrine/doctrine-migrations-bundle": "^3.2",
"doctrine/orm": "^2.15",
"phpdocumentor/reflection-docblock": "^5.3",
"phpstan/phpdoc-parser": "^1.20",
"sensio/framework-extra-bundle": "^6.1",
"symfony/asset": "6.3.*",
"symfony/console": "6.3.*",
"symfony/doctrine-messenger": "6.3.*",
"symfony/dotenv": "6.3.*",
"symfony/expression-language": "6.3.*",
"symfony/flex": "^2",
"symfony/form": "6.3.*",
"symfony/framework-bundle": "6.3.*",
"symfony/http-client": "6.3.*",
"symfony/intl": "6.3.*",
"symfony/mailer": "6.3.*",
"symfony/mime": "6.3.*",
"symfony/monolog-bundle": "^3.0",
"symfony/notifier": "6.3.*",
"symfony/process": "6.3.*",
"symfony/property-access": "6.3.*",
"symfony/property-info": "6.3.*",
"symfony/runtime": "6.3.*",
"symfony/security-bundle": "6.3.*",
"symfony/serializer": "6.3.*",
"symfony/string": "6.3.*",
"symfony/translation": "6.3.*",
"symfony/twig-bundle": "6.3.*",
"symfony/validator": "6.3.*",
"symfony/web-link": "6.3.*",
"symfony/yaml": "6.3.*",
"twig/extra-bundle": "^2.12|^3.0",
"twig/twig": "^2.12|^3.0"
},
... et
"extra": {
"symfony": {
"allow-contrib": false,
"require": "6.3.*"
}
},
"require-dev": {
"phpunit/phpunit": "^9.5",
"symfony/browser-kit": "6.3.*",
"symfony/css-selector": "6.3.*",
"symfony/debug-bundle": "6.3.*",
"symfony/maker-bundle": "^1.0",
"symfony/phpunit-bridge": "^6.3",
"symfony/stopwatch": "6.3.*",
"symfony/web-profiler-bundle": "6.3.*"
}
Puis dans le terminal :
composer update "symfony/*"
En cas de soucis, voir cette page :
Pour être certain de la compatibilité des dépendances, il est possible de lancer la commande suivante :
composer recipes:install --force -v
Retour au Menu de navigation
Nous allons créer la table utilisateur et l'entité associée avec la commande suivante :
php bin/console make:user
Nous choisissons comme options pour cette entité sécurisée :
> The class name of the security user entity (e.g. User) [User]: Utilisateur
> 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). (yes/no) [yes]: yes
Fichiers créés :
created: src/Entity/Utilisateur.php
created: src/Repository/UtilisateurRepository.php
updated: src/Entity/Utilisateur.php
updated: config/packages/security.yaml
Retour au Menu de navigation
Nous allons modifier la table utilisateur pour ajouter les champs suivants :
#[ORM\Column(options: ["unsigned" => true])]
private ?int $id = null;
Nous allons ensuite mettre à jour la base de données :
php bin/console make:migration
php bin/console doctrine:migrations:migrate
Retour au Menu de navigation
Nous allons lier la table utilisateur
en OneToMany
avec la table Article
:
php bin/console make:entity Utilisateur
En utilisant les options suivantes :
Your entity already exists! So let's add some new fields!
New property name (press <return> to stop adding fields):
> name
Field type (enter ? to see all types) [string]:
>
Field length [255]:
> 100
Can this field be null in the database (nullable) (yes/no) [no]:
>
updated: src/Entity/Utilisateur.php
Add another property? Enter the property name (or press <return> to stop adding fields):
> articles
Field type (enter ? to see all types) [string]:
> OneToMany
OneToMany
What class should this entity be related to?:
> Article
Article
A new property will also be added to the Article class so that you can access and set the related Utilisateur object from it.
New field name inside Article [utilisateur]:
>
Is the Article.utilisateur property allowed to be null (nullable)? (yes/no) [yes]:
> yes
updated: src/Entity/Utilisateur.php
updated: src/Entity/Article.php
Ce qui liera la table utilisateur
en OneToMany
avec la table Article
.
src/Entity/Utilisateur.php
#[ORM\OneToMany(mappedBy: 'utilisateur', targetEntity: Article::class)]
private Collection $articles;
...
/**
* @return Collection<int, Article>
*/
public function getArticles(): Collection
{
return $this->articles;
}
public function addArticle(Article $article): static
{
if (!$this->articles->contains($article)) {
$this->articles->add($article);
$article->setUtilisateur($this);
}
return $this;
}
public function removeArticle(Article $article): static
{
if ($this->articles->removeElement($article)) {
// set the owning side to null (unless already changed)
if ($article->getUtilisateur() === $this) {
$article->setUtilisateur(null);
}
}
return $this;
}
Et la table Article
en ManyToOne
avec la table Utilisateur
:
src/Entity/Article.php
#[ORM\ManyToOne(inversedBy: 'articles')]
private ?Utilisateur $utilisateur = null;
...
public function getUtilisateur(): ?Utilisateur
{
return $this->utilisateur;
}
public function setUtilisateur(?Utilisateur $utilisateur): static
{
$this->utilisateur = $utilisateur;
return $this;
}
Nous allons ensuite mettre à jour la base de données :
php bin/console make:migration
php bin/console doctrine:migrations:migrate
Voici l'export de la table à ce moment :
https://github.com/mikhawa/symfony-2023-05-10/blob/main/datas/sym_64_2023-06-13.sql
Retour au Menu de navigation
Nous aurons une bonne base pour commencer le projet.
Nous allons lier la table utilisateur
en OneToMany
avec la table Commentaire
:
php bin/console make:entity Utilisateur
En utilisant les options suivantes :
New property name (press <return> to stop adding fields):
> commentaires
Field type (enter ? to see all types) [string]:
> OneToMany
OneToMany
What class should this entity be related to?:
> Commentaire
Commentaire
A new property will also be added to the Commentaire class so that you can access and set the related Utilisateur object from it.
New field name inside Commentaire [utilisateur]:
>
Is the Commentaire.utilisateur property allowed to be null (nullable)? (yes/no) [yes]:
>
updated: src/Entity/Utilisateur.php
updated: src/Entity/Commentaire.php
Ensuite nous allons mettre à jour la base de données :
php bin/console make:migration
php bin/console doctrine:migrations:migrate
Voici l'export de la table à ce moment :
https://raw.githubusercontent.com/mikhawa/symfony-2023-05-10/main/datas/sym_64_2023-06-14.sql
Et le schéma :
Retour au Menu de navigation
Nous allons créer des fixtures pour avoir des données de test dans notre base de donnée.
Nous allons charger le bundle doctrine/doctrine-fixtures-bundle
:
composer require orm-fixtures --dev
Retour au Menu de navigation
Nous allons commencer par créer une fixture pour la table Utilisateur
:
php bin/console make:fixtures AllFixtures
Ce qui créera le fichier src/DataFixtures/UtilisateurFixtures.php
.
Nous allons ensuite créer des données de test dans la méthode load()
:
src/DataFixtures/UtilisateurFixtures.php
<?php
namespace App\DataFixtures;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Persistence\ObjectManager;
# 1. Importer l'entité Utilisateur
use App\Entity\Utilisateur;
# 2. Importer le gestionnaire de mot de passe
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
class UtilisateurFixtures extends Fixture
{
private UserPasswordHasherInterface $passwordEncoder;
public function __construct(UserPasswordHasherInterface $passwordEncoder)
{
$this->passwordEncoder = $passwordEncoder;
}
public function load(ObjectManager $manager): void
{
$user = new Utilisateur();
$user->setName('Dupont');
$user->setEmail('dupont@dupont.com');
$password = $this->passwordEncoder->hashPassword($user,'123456');
$user->setPassword($password);
$manager->persist($user);
$manager->flush();
}
}
Nous allons ensuite charger les fixtures :
php bin/console doctrine:fixtures:load
Nous avons donc un utilisateur dans la base de données ! :)
Le fichier .sql :
https://raw.githubusercontent.com/mikhawa/symfony-2023-05-10/main/datas/sym_64_2023-06-14-1.sql
Retour au Menu de navigation
Pour éviter de devoir définir l'ordre de chargement des fixtures, nous allons créer une fixture pour toutes les tables :
src/DataFixtures/AllFixtures.php
Nous allons également installer une bibliothèque pour générer des données aléatoires en Lorem Ipsum
:
composer require joshtronic/php-loremipsum
Esuite une bibliothèque pour slugifier les données :
composer require cocur/slugify
Nous pouvons commencer par Utilisateur
puis Article
qui est lié :
<?php
namespace App\DataFixtures;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Persistence\ObjectManager;
# 1. Importer l'entité Utilisateur
use App\Entity\Utilisateur;
# 2. Importer le gestionnaire de mot de passe
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
# 3. Importer l'entité Article
use App\Entity\Article;
# 4. Importer le générateur de texte en Lorem Ipsum
use joshtronic\LoremIpsum;
# 5. Importer le slugger
use Cocur\Slugify\Slugify;
class AllFixtures extends Fixture
{
private UserPasswordHasherInterface $passwordEncoder;
public function __construct(UserPasswordHasherInterface $passwordEncoder)
{
$this->passwordEncoder = $passwordEncoder;
}
public function load(ObjectManager $manager): void
{
// création de 10 utilisateurs
for($i=0;$i<10;$i++) {
$user = new Utilisateur();
$user->setName('Dupont' . $i);
$user->setEmail("dupont$i@dupont.com");
// on encode le mot de passe
$password = $this->passwordEncoder->hashPassword($user, "123456$i");
$user->setPassword($password);
// on l'ajoute à la liste des utilisateurs pour les articles
$randUser[] = $user;
// on garde la requête de persistance pour la fin
$manager->persist($user);
}
// instanciation du générateur de Lorem Ipsum
$lipsum = new LoremIpsum();
// instanciation du slugger
$slugify = new Slugify();
// création de 30 articles
for($i=0;$i<30;$i++) {
$article = new Article();
$title = $lipsum->words(5);
$article->setArticleTitle($title);
$article->setArticleContent($lipsum->paragraphs(3));
$article->setArticleSlug($slugify->slugify($title));
$article->setArticleDateCreate(new \DateTime());
$article->setArticleIsPublished(true);
// on tire au sort la clef d'un utilisateur pour l'article
$keyUser=array_rand($randUser);
// on récupère l'utilisateur correspondant
$oneUser = $randUser[$keyUser];
// on ajoute l'article à la liste des articles de l'utilisateur
$article->setUtilisateur($oneUser);
$manager->persist($article);
}
// on exécute les requêtes de persistance
$manager->flush();
}
}
Ensuite nous vérifions si cela fonctionne :
php bin/console doctrine:fixtures:load
le fichier .sql :
https://raw.githubusercontent.com/mikhawa/symfony-2023-05-10/main/datas/sym_64_2023-06-15.sql
Retour au Menu de navigation
On va remplir toute notre base de données avec des données. On pourra ainsi tester notre application avec des données réalistes.
<?php
namespace App\DataFixtures;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Persistence\ObjectManager;
# 1. Importer l'entité Utilisateur
use App\Entity\Utilisateur;
# 2. Importer le gestionnaire de mot de passe
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
# 3. Importer l'entité Article
use App\Entity\Article;
# 4. Importer l'entité Commentaire
use App\Entity\Commentaire;
# 5. Importer l'entité Catégorie
use App\Entity\Categorie;
# 6. Importer le générateur de texte en Lorem Ipsum
use joshtronic\LoremIpsum;
# 7. Importer le slugger
use Cocur\Slugify\Slugify;
###
// création de 100 commentaires
for($i=0;$i<100;$i++) {
$comment = new Commentaire();
$comment->setCommentaireTitle($lipsum->words(mt_rand(2,5)));
$comment->setCommentaireText($lipsum->sentences(mt_rand(1,2)));
$comment->setCommentaireDateCreate(new \DateTime());
$comment->setCommentaireIsPublished(true);
// on tire au sort la clef d'un utilisateur pour le commentaire
$keyUser=array_rand($randUser);
// on récupère l'utilisateur correspondant
$oneUser = $randUser[$keyUser];
// on ajoute le commentaire à la liste des commentaires de l'utilisateur
$comment->setUtilisateur($oneUser);
// on tire au sort la clef d'un article pour le commentaire
$keyArticle=array_rand($randArticle);
// on récupère l'article correspondant
$oneArticle = $randArticle[$keyArticle];
// on ajoute le commentaire à la liste des commentaires de l'article
$comment->setCommentaireManyToOneArticle($oneArticle);
$manager->persist($comment);
}
// création de 5 catégories
for($i=0;$i<5;$i++) {
$category = new Categorie();
$nameCategory = $lipsum->words(mt_rand(1,2));
$category->setCategorieTitle($nameCategory);
$category->setCategorySlug($slugify->slugify($nameCategory));
$category->setCategorieDesc($lipsum->sentences(mt_rand(1,2)));
// on va donner la catégorie à 20 articles au hasard
for($j=0;$j<35;$j++) {
// on tire au sort la clef d'un article pour la catégorie
$keyArticle=array_rand($randArticle);
// on récupère l'article correspondant
$oneArticle = $randArticle[$keyArticle];
// on ajoute la catégorie à la liste des catégories de l'article
$category->addCategorieM2mArticle($oneArticle);
}
$manager->persist($category);
}
// on exécute les requêtes de persistance
$manager->flush();
###
le fichier .sql :
https://raw.githubusercontent.com/mikhawa/symfony-2023-05-10/main/datas/sym_64_2023-06-15-1.sql
Retour au Menu de navigation
Nous allons modifier BlogController.php pour créer une page d'accueil avec la récupération des catégories.
<?php
# Controller/BlogController.php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
# Importation de la classe Response
use Symfony\Component\HttpFoundation\Response;
# Importation de la classe Request
use Symfony\Component\HttpFoundation\Request;
# Importation de la classe Route
use Symfony\Component\Routing\Annotation\Route;
# Appel de l'ORM Doctrine
use Doctrine\ORM\EntityManagerInterface;
# Importation de l'entité Categorie
use App\Entity\Categorie;
class BlogController extends AbstractController
{
#[Route('/', name: 'homepage')]
public function index(EntityManagerInterface $entityManager): Response
{
// récupération de toutes les catégories
$categories = $entityManager->getRepository(Categorie::class)->findAll();
return $this->render('blog/index.html.twig', [
// on envoie les catégories à la vue
'categories' => $categories,
]);
}
}
Et dans le fichier de vue au format Twig
, on va afficher les catégories :
{# templates/blog/index.html.twig #}
{# .... #}
<div class="example-wrapper">
<h1>Page d'accueil ✅</h1>
{% for categ in categories %}
<h2>{{ categ.CategorieTitle }}</h2>
<p>{{ categ.CategorySlug }}</p>
<p>{{ categ.CategorieDesc }}</p>
{% endfor %}
</div>
Et le résultat :
Maintenant que nous voyons que c'est fonctionnel, nous allons créer un template de base pour notre application.
Retour au Menu de navigation
Nous allons utiliser un moteur de template pour générer les vies de notre blog : Twig
.
La documentation officielle de Twig
: https://twig.symfony.com
Ce moteur de template est déjà installé dans notre projet. Il est utilisé par défaut par Symfony
.
Il utilise une syntaxe orientée objet et a un système de cache pour optimiser les performances.
Ce fichier va contenir le code HTML de base de notre application. C'est là que nous créerons les blocks principaux qui seront utilisés par toutes les autres vues de notre projet.
{# templates/base.html.twig #}
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>{% block title %}Notre Blog {% endblock %}</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 128 128%22><text y=%221.2em%22 font-size=%2296%22>⚫️</text></svg>">
{# Run `composer require symfony/webpack-encore-bundle` to start using Symfony UX #}
{% block stylesheets %}
{{ encore_entry_link_tags('app') }}
{% endblock %}
{% block javascripts %}
{{ encore_entry_script_tags('app') }}
{% endblock %}
</head>
<body>
{% block body %}{% endblock %}
</body>
</html>
Nous voyons dans les commentaires que nous pouvons utiliser Webpack Encore
pour gérer les fichiers CSS
et JS
de notre projet.
Pour cela il faut installer le bundle Webpack Encore
:
composer require symfony/webpack-encore-bundle
curl --compressed -o- -L https://yarnpkg.com/install.sh | bash
Puis
yarn install
Pour cela vous devez avoir installé Yarn
sur votre machine ainsi que NodeJS
:
https://nodejs.org/fr/download/
Puis Yarn
:
https://classic.yarnpkg.com/lang/en/docs/install/#windows-stable
Et nous allons lancer la commande npm
suivante pour créer nos fichiers CSS
et JS
dans le dossier public/build
:
npm run build
Ces fichiers sont créés à partir des fichiers CSS
et JS
du dossier assets
et sont minifiés et optimisés pour les performances. Ils sont chargés automatiquement dans le fichier base.html.twig
.
Nous modifierons le fichier webpack.config.js
et le dossier assests
pour ajouter le fichier CSS
de Bootstrap
par la suite.
Notre site est de nouveau fonctionnel :
Retour au Menu de navigation
Nous allons installer Bootstrap
et son CSS
pour avoir un site responsive et un peu plus joli.
npm i bootstrap@5.3.0
Puis chargeons le javascript :
npm install jquery @popperjs/core --save-dev
Et ajoutons le fichier CSS
et le JS
de Bootstrap
dans le fichier webpack.config.js
:
// webpack.config.js
// ...
Encore
.addEntry('app', './assets/app.js')
.addEntry('bootstrapJS', './node_modules/bootstrap/dist/js/bootstrap.min.js')
.addStyleEntry('bootstrapCSS', './node_modules/bootstrap/dist/css/bootstrap.min.css')
Puis nous allons modifier le fichier base.html.twig
pour charger les fichiers CSS
et JS
de Bootstrap
:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>{% block title %}Welcome!{% endblock %}</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 128 128%22><text y=%221.2em%22 font-size=%2296%22>⚫️</text></svg>">
{# Run `composer require symfony/webpack-encore-bundle` to start using Symfony UX #}
{% block stylesheets %}
{# création du CSS Bootstrap #}
{{ encore_entry_link_tags('bootstrapCSS') }}
{# création de notre CSS personnalisé #}
{{ encore_entry_link_tags('app') }}
{% endblock %}
{% block javascripts %}
{# création du JS Bootstrap #}
{{ encore_entry_script_tags('bootstrapJS') }}
{# création de notre JS personnalisé #}
{{ encore_entry_script_tags('app') }}
{% endblock %}
</head>
<body>
{% block body %}{% endblock %}
</body>
</html>
Nous pouvons maintenant utiliser les classes de Bootstrap
dans notre projet.
Lançons les commandes npm
pour créer les fichiers CSS
et JS
:
npm run build
Notre site est de nouveau fonctionnel :
Retour au Menu de navigation
Nous allons utiliser le template bootstrap Business frontpage
pour notre projet :
https://startbootstrap.com/template/business-frontpage
Nous l'avons dézippé dans le dossier datas
et nous allons le modifier pour l'adapter à notre projet.
J'ai mis les JS
et CSS
du template pour les utiliser, ils sont donc pour le js
dans app.js
et pour le style dans app.css
.
npm run build
Retour au Menu de navigation
Nous allons modifier le fichier base.html.twig
pour utiliser le template :
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<meta name="description" content="" />
<meta name="author" content="Pitz Michaël" />
<title>{% block title %}Welcome!{% endblock %}</title>
<link rel="icon" href="build/favicon.ico">
{# Run `composer require symfony/webpack-encore-bundle` to start using Symfony UX #}
{% block stylesheets %}
{# création de notre CSS personnalisé #}
{{ encore_entry_link_tags('app') }}
{% endblock %}
{% block javascripts %}
{# création de notre JS personnalisé #}
{{ encore_entry_script_tags('app') }}
{% endblock %}
</head>
<body>
{% block body %}{% endblock %}
</body>
</html>
Retour au Menu de navigation
Puis, nous créons le template avec de nouveaux blocs enfants :
templates/template.html.twig
{% extends 'base.html.twig' %}
{% block title %}MonSite |{% endblock %}
{% block body %}
{% block navbar %}
<!-- Responsive navbar-->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container px-5">
<a class="navbar-brand" href="{{ path('homepage') }}">MonSite</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"><span class="navbar-toggler-icon"></span></button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav ms-auto mb-2 mb-lg-0">
{% block menuLinks %}
<li class="nav-item"><a class="nav-link active" aria-current="page" href="{{ path('homepage') }}">Home</a></li>
<li class="nav-item"><a class="nav-link" href="#!">About</a></li>
<li class="nav-item"><a class="nav-link" href="#!">Contact</a></li>
<li class="nav-item"><a class="nav-link" href="#!">Services</a></li>
{% endblock %}
</ul>
</div>
</div>
</nav>
<!-- End Responsive navbar-->
{% endblock %}
{% block header %}
<!-- Header-->
<header class="bg-dark py-5">
<div class="container px-5">
<div class="row gx-5 justify-content-center">
<div class="col-lg-6">
<div class="text-center my-5">
<h1 class="display-5 fw-bolder text-white mb-2">{%block htitle %}Present your business in a whole new way{% endblock %}</h1>
<p class="lead text-white-50 mb-4">{%block hdesc %}Quickly design and customize responsive mobile-first sites with Bootstrap, the world’s most popular front-end open source toolkit!{% endblock %}</p>
<div class="d-grid gap-3 d-sm-flex justify-content-sm-center">
<a class="btn btn-primary btn-lg px-4 me-sm-3" href="#features">Get Started</a>
<a class="btn btn-outline-light btn-lg px-4" href="#!">Learn More</a>
</div>
</div>
</div>
</div>
</div>
</header>
{% endblock %}
{% block articles %}<!-- Features section-->
<section class="py-5 border-bottom" id="features">
<div class="container px-5 my-5">
<div class="row gx-5">
<div class="col-lg-4 mb-5 mb-lg-0">
<div class="feature bg-primary bg-gradient text-white rounded-3 mb-3"><i class="bi bi-collection"></i></div>
<h2 class="h4 fw-bolder">Featured title</h2>
<p>Paragraph of text beneath the heading to explain the heading. We'll add onto it with another sentence and probably just keep going until we run out of words.</p>
<a class="text-decoration-none" href="#!">
Call to action
<i class="bi bi-arrow-right"></i>
</a>
</div>
<div class="col-lg-4 mb-5 mb-lg-0">
<div class="feature bg-primary bg-gradient text-white rounded-3 mb-3"><i class="bi bi-building"></i></div>
<h2 class="h4 fw-bolder">Featured title</h2>
<p>Paragraph of text beneath the heading to explain the heading. We'll add onto it with another sentence and probably just keep going until we run out of words.</p>
<a class="text-decoration-none" href="#!">
Call to action
<i class="bi bi-arrow-right"></i>
</a>
</div>
<div class="col-lg-4">
<div class="feature bg-primary bg-gradient text-white rounded-3 mb-3"><i class="bi bi-toggles2"></i></div>
<h2 class="h4 fw-bolder">Featured title</h2>
<p>Paragraph of text beneath the heading to explain the heading. We'll add onto it with another sentence and probably just keep going until we run out of words.</p>
<a class="text-decoration-none" href="./">
Call to action
<i class="bi bi-arrow-right"></i>
</a>
</div>
</div>
</div>
</section>
{% endblock %}
{% block footer %}
<!-- Footer-->
<footer class="py-5 bg-dark">
<div class="container px-5"><p class="m-0 text-center text-white">Copyright © MonSite {{ "now"|date('Y') }}</p></div>
</footer>
{% endblock %}
{% endblock %}
Retour au Menu de navigation
Nous pouvons maintenant utiliser le template dans notre projet:
templates/blog/index.html.twig
{% extends 'template.html.twig' %}
{% block title %}{{ parent() }}Page d'accueil{% endblock %}
{% block articles %}
<h1>Page d'accueil ✅</h1>
{% for categ in categories %}
<h2>{{ categ.CategorieTitle }}</h2>
<p>{{ categ.CategorySlug }}</p>
<p>{{ categ.CategorieDesc }}</p>
{% endfor %}
{% endblock %}
Nous pouvons par la suite afficher les 12 derniers articles sur la page d'accueil avec le findBy() de Doctrine:
<?php
###
use App\Entity\Article;
###
#[Route('/', name: 'homepage')]
public function index(EntityManagerInterface $entityManager): Response
{
// récupération de toutes les catégories pour le menu
$categories = $entityManager->getRepository(Categorie::class)->findAll();
// récupération des 9 derniers articles
$articles = $entityManager->getRepository(Article::class)->findBy([], ['ArticleDateCreate' => 'DESC'], 12);
return $this->render('blog/index.html.twig', [
// on envoie les catégories à la vue
'categories' => $categories,
// on envoie les articles à la vue
'articles' => $articles,
]);
}
###
Et dans la vue:
{% block articlePerOne %}
{% for article in articles %}
<div class="col-lg-4 mb-5 mb-lg-0">
<div class="feature bg-primary bg-gradient text-white rounded-3 mb-3"><i class="bi bi-collection"></i></div>
<h2 class="h4 fw-bolder">{{ article.ArticleTitle }}</h2>
<p>Paragraph of text beneath the heading to explain the heading. We'll add onto it with another sentence and probably just keep going until we run out of words.</p>
<a class="text-decoration-none" href="#!">
Call to action
<i class="bi bi-arrow-right"></i>
</a>
</div>
{% endfor %}
{% endblock %}
Retour au Menu de navigation
Nous allons remplir le menu avec les catégories de notre blog:
templates/blog/index.html.twig
{% for categ in categories %}
<li class="nav-item"><a class="nav-link" aria-current="page" href="{{ path("categorie", { 'slug' : categ.CategorySlug }) }}">{{ categ.CategorieTitle }}</a></li>
{% endfor %}
Nous allons créer la section categorie
de notre site:
src/Controller/BlogController.php
:
###
#[Route('/categorie/{slug}', name: 'categorie')]
public function categorie($slug, EntityManagerInterface $entityManager): Response
{
// récupération de toutes les catégories pour le menu
$categories = $entityManager->getRepository(Categorie::class)->findAll();
// récupération de la catégorie dont le slug est $category_slug
$categorie = $entityManager->getRepository(Categorie::class)->findOneBy(['CategorySlug' => $slug]);
return $this->render('blog/categorie.html.twig', [
// on envoie la catégorie à la vue
'categories' => $categories,
'categorie' => $categorie,
]);
}
###
Après avoir créé le template categorie.html.twig
nous pouvons voir le résultat :
...
{%block htitle %}Catégorie : {{ categorie.CategorieTitle }}{% endblock %}
{%block hdesc %}{{ categorie.CategorieDesc }}{% endblock %}
...
Retour au Menu de navigation
Nous allons maintenant afficher les articles par catégorie:
src/Controller/BlogController.php
:
###
#[Route('/categorie/{slug}', name: 'categorie')]
public function categorie($slug, EntityManagerInterface $entityManager): Response
{
// récupération de toutes les catégories pour le menu
$categories = $entityManager->getRepository(Categorie::class)->findAll();
// récupération de la catégorie dont le slug est $category_slug
$categorie = $entityManager->getRepository(Categorie::class)->findOneBy(['CategorySlug' => $slug]);
// récupération des articles de la catégorie grâce à la relation ManyToMany de categorie vers articlesn puis prises de valeurs
$articles = $categorie->getCategorieM2mArticle()->getValues();
return $this->render('blog/categorie.html.twig', [
// on envoie la catégorie à la vue
'categories' => $categories,
'categorie' => $categorie,
'articles' => $articles,
]);
}
###
Et dans le template categorie.html.twig
:
{% block articlePerOne %}
{% for article in articles %}
<div class="col-lg-4 mb-5 mb-lg-0">
<div class="feature bg-primary bg-gradient text-white rounded-3 mb-3"><i class="bi bi-collection"></i></div>
<h2 class="h4 fw-bolder">{{ article.ArticleTitle }}</h2>
<p>Paragraph of text beneath the heading to explain the heading. We'll add onto it with another sentence and probably just keep going until we run out of words.</p>
<a class="text-decoration-none" href="#!">
Call to action
<i class="bi bi-arrow-right"></i>
</a>
</div>
{% endfor %}
{% endblock %}
Retour au Menu de navigation
Nous allons créer la section article
de notre site et afficher le slug de l'article demandé dans l'URL :
src/Controller/BlogController.php
:
###
#[Route('/article/{slug}', name: 'article')]
public function article($slug, EntityManagerInterface $entityManager): Response
{
// récupération de toutes les catégories pour le menu
$categories = $entityManager->getRepository(Categorie::class)->findAll();
// récupération de l'article dont le slug est $slug
$article = $entityManager->getRepository(Article::class)->findOneBy(['ArticleSlug' => $slug]);
// on commence par afficher le slug
return new Response($article->getArticleSlug());
}
###
Nous allons d'abord créer les liens vers les articles dans la section categorie
et index
:
templates/blog/categorie.html.twig
:
templates/blog/index.html.twig
:
{% block articlePerOne %}
{% for article in articles %}
...
<a class="text-decoration-none" href="{{ path("article", { 'slug' : article.ArticleSlug }) }}">
Lire la suite
<i class="bi bi-arrow-right"></i>
</a>
...
{% endfor %}
{% endblock %}
Retour au Menu de navigation
Au passage, nous allons mettre un résumé du texte des articles dans la section index
et categorie
:
templates/blog/index.html.twig
:
templates/blog/categorie.html.twig
:
{% block articlePerOne %}
{% for article in articles %}
<div class="col-lg-4 mb-5 mb-lg-0">
<div class="feature bg-primary bg-gradient text-white rounded-3 mb-3"><i class="bi bi-collection"></i></div>
<h2 class="h4 fw-bolder">{{ article.ArticleTitle }}</h2>
<p>{{ article.ArticleContent|slice(0, 150) }}...</p>
<a class="text-decoration-none" href="{{ path("article", { 'slug' : article.ArticleSlug }) }}">
Lire la suite
<i class="bi bi-arrow-right"></i>
</a>
</div>
{% endfor %}
{% endblock %}
Nous constatons que les mots sont coupés en plein milieu avec la fonction slice
, nous allons donc charger une bibliothèque de fonctions Twig pour couper les mots à la fin de la phrase :
composer require twig/string-extra
Pour la documentation de la bibliothèque, c'est ici
templates/blog/index.html.twig
:
templates/blog/categorie.html.twig
:
<p>{{ article.ArticleContent|u.truncate(120, '...', false) }}</p>
Le false à la fin permet de couper les mots à la fin de la phrase.
Nous allons maintenant modifier la méthode article
du controller pour afficher le contenu de l'article, avec les rubriques dans lesquelles il se trouve (si il si trouve) :
src/Controller/BlogController.php
:
###
#[Route('/article/{slug}', name: 'article')]
public function article($slug, EntityManagerInterface $entityManager): Response
{
// récupération de toutes les catégories pour le menu
$categories = $entityManager->getRepository(Categorie::class)->findAll();
// récupération de l'article dont le slug est $slug
$article = $entityManager->getRepository(Article::class)->findOneBy(['ArticleSlug' => $slug]);
// récupération des catégories grâce à la relation ManyToMany de 'article' vers 'catégorie' puis prises de valeurs
$categoriesArticle = $article->getCategories()->getValues();
// Appel de la vue
return $this->render('blog/article.html.twig', [
// on envoie la catégorie à la vue
'categories' => $categories,
// on envoie l'article à la vue
'article' => $article,
// on envoie les catégories de l'article à la vue
'categoriesArticle' => $categoriesArticle,
]);
}
###
Retour au Menu de navigation
Nous allons maintenant créer la vue
templates/blog/article.html.twig
:
...
{% block title %}{{ parent() }} Article | {{ article.ArticleTitle }}{% endblock %}
...
{% block articlePerOne %}
<div class="col-lg-12 mb-5 mb-lg-0">
<h2 class="h4 fw-bolder mb-3">{{ article.ArticleTitle }}</h2>
<h4 class="h5">Par {{ article.utilisateur.name }} le {{ article.ArticleDateCreate|date("d/m/Y") }}</h4>
<hr>
{% for categ in categoriesArticle %}
<a href="{{ path("categorie", { 'slug' : categ.CategorySlug }) }}"
class="badge bg-secondary text-decoration-none link-light">{{ categ.CategorieTitle }}</a>
{% else %}
<h5>Présent dans aucune catégorie</h5>
{% endfor %}
<hr>
<p>{{ article.ArticleContent|nl2br }}</p>
<h4 class="h5">Par {{ article.utilisateur.name }} le
{{ article.ArticleDateCreate|date("d/m/Y") }}</h4>
</div>
{% endblock %}
...
Retour au Menu de navigation
Cette vue sera appelée depuis la page article, un block est créé dans le block articlePerOne
de la vue article.html.twig
:
templates/blog/article.html.twig
:
...
{% block articlePerOne %}
...
{% block commentaire %}
{# Ici seront affichés les commentaires + formulaires,
on importe toujours depuis la racine templates #}
{% include 'blog/inc/commentaire.html.twig' %}
{% endblock %}
...
{% endblock %}
Et sa vue vierge :
templates/blog/inc/commentaire.html.twig
:
<div>
<h3>Commentaires</h3>
</div>
Retour au Menu de navigation
Nous allons maintenant charger les commentaires dans la partie article
du controller :
src/Controller/BlogController.php
:
###
# Importation de l'entité Commentaire
use App\Entity\Commentaire;
use App\Repository\CommentaireRepository;
###
#[Route('/article/{slug}', name: 'article')]
public function article($slug, EntityManagerInterface $entityManager): Response
{
$categories = $entityManager->getRepository(Categorie::class)->findAll();
$article = $entityManager->getRepository(Article::class)->findOneBy(['ArticleSlug' => $slug]);
$categoriesArticle = $article->getCategories()->getValues();
// récupération des commentaires de l'article grâce à son id
$commentaires = $entityManager->getRepository(Commentaire::class)
->findBy(['CommentaireManyToOneArticle' => $article->getId()]);
return $this->render('blog/article.html.twig', [
'categories' => $categories,
'article' => $article,
'categoriesArticle' => $categoriesArticle,
// on envoie les commentaires à la vue
'commentaires' => $commentaires,
]);
}
###
Retour au Menu de navigation
Nous allons maintenant afficher les commentaires dans la vue :
templates/blog/inc/commentaire.html.twig
:
<div>
<hr>
<h3>Commentaires ({{ commentaires|length }})</h3>
{% for commentaire in commentaires %}
<h5>{{ commentaire.CommentaireTitle }} <small>Par
{{ commentaire.utilisateur.name}} le
{{ commentaire.CommentaireDateCreate|date("Y-m-d") }}</small></h5>
<p>{{ commentaire.CommentaireText }}</p>
{% else %}
<p>Aucun commentaire</p>
{% endfor %}
</div>
Retour au Menu de navigation
Nous obtenons une erreur de mapping de type App\Entity\Commentaire The association App\Entity\Commentaire#CommentaireManyToOneArticle refers to the inverse side field App\Entity\Article#Commentaires which does not exist.
sur la page de détail d'un article.
Dans le mapping de l'entité Commentaire
, nous avons bien une relation ManyToOne
vers l'entité Article
:
src/Entity/Commentaire.php
:
###
// Pour que la relation soit bidirectionnelle, il faut ajouter une propriété targetEntity et inversedBy
#[ORM\ManyToOne(targetEntity: Article::class, inversedBy: 'Commentaires')]
private ?Article $CommentaireManyToOneArticle = null;
###
Nous devons créer la relation inverse dans l'entité Article
, puis l'appeler dans son constructeur en tant que ArrayCollection
:
src/Entity/Article.php
:
###
#[ORM\OneToMany(mappedBy: 'CommentaireManyToOneArticle', targetEntity: Commentaire::class)]
private Collection $Commentaires;
###
public function __construct()
{
$this->categories = new ArrayCollection();
$this->Commentaires = new ArrayCollection();
}
Retour au Menu de navigation
Nous pouvons maintenant utiliser la relation inverse plutôt que le findBy dans le controller :
src/Controller/BlogController.php
:
###
// récupération des commentaires de l'article en cours
$commentaires = $article->getCommentaires()->getValues();
/* code remplacé
* $commentaires = $entityManager->getRepository(Commentaire::class)
->findBy(['CommentaireManyToOneArticle' => $article->getId()]);
* */
###
Pour que cela fonctionne, nous devons rajouter le getter et les autres méthodes dans l'entité Article
:
src/Entity/Article.php
:
###
/**
* @return Collection<int, Categorie>
*/
public function getCommentaires(): Collection {
return $this->Commentaires;
}
public function addCommentaire(Commentaire $commentaire): self
{
if (!$this->Commentaires->contains($commentaire)) {
$this->Commentaires->add($commentaire);
$commentaire->setCommentaireManyToOneArticle($this);
}
return $this;
}
public function removeCommentaire(Commentaire $commentaire): self
{
if ($this->Commentaires->removeElement($commentaire)) {
if ($commentaire->getCommentaireManyToOneArticle() === $this) {
$commentaire->setCommentaireManyToOneArticle(null);
}
}
return $this;
}
###
Nous pouvons dorénavant supprimer toutes les lignes de code concernant les commentaires et les catégories dans le controller en utilisant les relations inverses dans les vues :
src/Controller/BlogController.php
:
###
#[Route('/article/{slug}', name: 'article')]
public function article($slug, EntityManagerInterface $entityManager): Response
{
// récupération de toutes les catégories pour le menu
$categories = $entityManager->getRepository(Categorie::class)->findAll();
// récupération de l'article dont le slug est $slug
$article = $entityManager->getRepository(Article::class)->findOneBy(['ArticleSlug' => $slug]);
/* code devenu non nécessaire avec les relations ManyToMany,
ManyToOne et OneToMany avec inversedBy et mappedBy
*
$categoriesArticle = $article->getCategories()->getValues();
* $commentaires =
* $entityManager->getRepository(Commentaire::class)->findBy(['CommentaireManyToOneArticle' => $article->getId()]);
* */
return $this->render('blog/article.html.twig', [
'categories' => $categories,
'article' => $article,
// 'categoriesArticle' => $categoriesArticle,
// 'commentaires' => $commentaires,
]);
}
###
Ceci est possible grâce à la relation inverse que nous avons créée dans l'entité Article
, mais non obligatoire. Nous aurions pu continuer à utiliser les méthodes findBy
et findOneBy
dans le controller.
Ceci permet surtout de comprendre le fonctionnement de Doctrine et de Symfony.
Ensuite, nous pouvons modifier les vues article.html.twig
et commentaire.html.twig
pour utiliser les relations inverses :
templates/blog/article.html.twig
:
###
{% for categ in article.categories %}
<a href="{{ path("categorie", { 'slug' : categ.CategorySlug }) }}" class="badge bg-secondary text-decoration-none link-light">{{ categ.CategorieTitle }}</a>
{% else %}
<h5>Présent dans aucune catégorie</h5>
{% endfor %}
###
templates/blog/inc/commentaire.html.twig
:
<div>
<hr>
<h3>Commentaires ({{ article.Commentaires|length }})</h3>
{% for commentaire in article.Commentaires %}
<h5>{{ commentaire.CommentaireTitle }} <small>Par {{ commentaire.utilisateur.name}} le {{ commentaire.CommentaireDateCreate|date("Y-m-d") }}</small></h5>
<p>{{ commentaire.CommentaireText }}</p>
{% else %}
<p>Aucun commentaire</p>
{% endfor %}
</div>
Retour au Menu de navigation
L'email et le mot de passe crypté sont stockés dans la table utilisateur
de la base de données.
Nous allons créer tout le système de connexion avec un formulaire de connexion, les vérifications de sécurités pour l'authentification des utilisateurs en une seule commande !
Pour cela, nous allons utiliser la commande make:auth
:
php bin/console make:auth
Nous allons créer un formulaire de connexion pour l'authentification des utilisateurs.
Choose a number or alias to load:
[0] - app
[1] - security
> 1
The class name of the authenticator to create
(e.g. AppCustomAuthenticator):
> UtilisateurAuthenticator
Choose a name for the controller class (e.g. SecurityController)
[SecurityController]: SecurityController
Do you want to generate a '/logout' URL? (yes/no) [yes]: yes
Do you want to support remember me? (yes/no) [yes]: yes
How should remember me be activated?
[Activate when the user checks a box]: 0
Nous pouvons désormais voir le nouveau fichier src/Security/UtilisateurAuthenticator.php
.
Nous devons modifier le fichier src/Security/UtilisateurAuthenticator.php
pour la redirection en cas de succès de l'authentification, pour le moment sur notre accueil :
###
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
if ($targetPath = $this->getTargetPath($request->getSession(), $firewallName)) {
return new RedirectResponse($targetPath);
}
// For example:
return new RedirectResponse($this->urlGenerator->generate('homepage'));
// throw new \Exception('TODO: provide a valid redirect inside '.__FILE__);
}
###
Le fichier src/Controller/SecurityController.php
devrait déjà être fonctionnel.
Le fichier config/packages/security.yaml
devrait déjà être fonctionnel, mais vous pouvez vérifier que les lignes suivantes sont présentes :
security:
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
password_hashers:
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
providers:
# used to reload user from session & other features (e.g. switch_user)
app_user_provider:
entity:
class: App\Entity\Utilisateur
property: email
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
lazy: true
provider: app_user_provider
custom_authenticator: App\Security\UtilisateurAuthenticator
logout:
path: app_logout
# where to redirect after logout
# target: app_any_route
remember_me:
secret: '%kernel.secret%'
lifetime: 604800
path: /
always_remember_me: true
# activate different ways to authenticate
# https://symfony.com/doc/current/security.html#the-firewall
# https://symfony.com/doc/current/security/impersonating_user.html
# switch_user: true
# Easy way to control access for large sections of your site
# Note: Only the *first* access control that matches will be used
access_control:
# - { path: ^/admin, roles: ROLE_ADMIN }
# - { path: ^/profile, roles: ROLE_USER }
when@test:
security:
password_hashers:
# By default, password hashers are resource intensive and take time. This is
# important to generate secure password hashes. In tests however, secure hashes
# are not important, waste resources and increase test times. The following
# reduces the work factor to the lowest possible values.
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface:
algorithm: auto
cost: 4 # Lowest possible value for bcrypt
time_cost: 3 # Lowest possible value for argon
memory_cost: 10 # Lowest possible value for argon
Les pages de connexion et de déconnexion sont déjà créées.
Nous pouvons tester notre formulaire de connexion en allant sur la route /login
avec par exemple l'email dupont9@dupont.com
et le mot de passe 1234569
.
Nous devrions être connectés et redirigés vers la page d'accueil.
Retour au Menu de navigation
Nous allons modifier la route vers le formulaire de connexion en /connect
au lieu de /login
, et également activer la redirection si on retourne sur cette page en étant déjà connecté.
Le fichier src/Controller/SecurityController.php
:
###
#[Route(path: '/connect', name: 'app_login')]
public function login(AuthenticationUtils
$authenticationUtils): Response
{
# redirection si on retourne sur cette page en
# étant déjà connecté
if ($this->getUser()) {
return $this->redirectToRoute('homepage');
}
###
###
Nous allons ensuite passer le menu depuis notre src/Controller/SecurityController.php
:
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
###
# Appel de l'ORM Doctrine
use Doctrine\ORM\EntityManagerInterface;
# Importation de l'entité Categorie
use App\Entity\Categorie;
class SecurityController extends AbstractController
{
#[Route(path: '/connect', name: 'app_login')]
public function login(AuthenticationUtils $authenticationUtils,EntityManagerInterface $entityManager): Response
{
###
$categories = $entityManager->getRepository(Categorie::class)->findAll();
###
return $this->render('security/login.html.twig', [
'last_username' => $lastUsername,
'error' => $error,
// on envoie les catégories à la vue
'categories' => $categories,
]);
}
###
}
Retour au Menu de navigation
Nous allons mettre de l'ordre dans les templates, en créant un dossier public
dans templates
et en y déplaçant les fichiers destinés à être publics
Attention aux chemins à cette étape !
Nous séparerons également le menu public templates/public/inc/menu.html.twig
pour ne pas devoir le modifier dans chaque template.
On va également ajouter la possibilité de se déconnecter, si nous sommes connectés bien sûr.
Nous allons pour cela vérifier si l'utilisateur est connecté avec la fonction is_granted()
, avec le rôle par défaut de tous les utilisateurs : ROLE_USER
.
Le fichier templates/public/inc/menu.html.twig
:
###
{% if is_granted("ROLE_USER") %}
<a class="nav-link" aria-current="page"
href="{{ path('app_logout') }}">Déconnexion</a>
{% else %}
<a class="nav-link" aria-current="page"
href="{{ path('app_login') }}">Connexion</a>
{% endif %}
###
Retour au Menu de navigation
Nous allons modifier le formulaire de connexion pour mettre le design de notre site et ajouter la checkbox remember me
.
Le fichier templates/security/login.html.twig
:
<label>
<input type="checkbox" name="_remember_me" checked>
Rester connecté
</label>
Nous allons désactiver le remember me
par défaut dans config/packages/security.yaml
:
remember_me:
secret: '%kernel.secret%'
lifetime: 604800
path: /
# always_remember_me: true
Ainsi le cookie REMEMBERME
ne sera pas créé par défaut.
Retour au Menu de navigation
Nous allons installer rate-limiter pour protéger le formulaire de connexion contre les attaques par force brute.
composer require symfony/rate-limiter
Nous allons protéger le formulaire de connexion contre les attaques par force brute, en ne permettant que 5 tentatives par 15 minutes, dans config/packages/security.yaml
:
# config/packages/security.yaml
security:
firewalls:
# ...
main:
# ...
# configure the maximum login attempts
login_throttling:
max_attempts: 5 # per minute ...
interval: '15 minutes' # ... or in a custom period
L'adresse ip de l'utilisateur sera bloquée pendant 15 minutes si il dépasse les 5 tentatives de connexion, cette information sera stockée dans le cache du serveur par défaut.
Retour au Menu de navigation
Chaque utilisateur connecté pourra créer un commentaire sur un article.
Pour cela le rôle ROLE_USER
sera suffisant. Il est défini comme tel par défaut
Nous allons créer un CRUD pour les commentaires, avec la commande make:crud
:
php bin/console make:crud Commentaire
Nous allons accepter la création de tests pour ce CRUD. Nous verrons les tests plus tard. Vous pouvez trouver le fichier de test dans tests/Controller/CommentaireControllerTest.php
.
Un contrôleur CommentaireController.php
est créé dans src/Controller
.
Nous allons vérifier ce CRUD en allant sur la route /commentaire/
:
https://127.0.0.1:8000/commentaire/
Si nous essayons de modifier un commentaire, nous avons une erreur de type : Object of class App\Entity\Article could not be converted to string
.
Nous allons corriger cela en ajoutant une méthode __toString()
dans l'entité Article
:
<?php
namespace App\Entity;
###
class Article
{
###
// si demandé en tant que string, on renvoie le titre de l'article
public function __toString()
{
return $this->ArticleTitle;
}
}
Nous devons répéter cette opération pour chaque entité qui est liée à une autre entité dans le CRUD.
Pour la modification d'un commentaire, nous avons une erreur pour l'utilisateur, car nous n'avons pas de méthode __toString()
dans l'entité Utilisateur
.
Nous allons donc en ajouter une :
<?php
namespace App\Entity;
###
// si demandé en tant que string, on renvoie le nom de l'utilisateur
public function __toString()
{
return $this->name;
}
###
Le CRUD est maintenant fonctionnel.
Nous allons modifier le fichier src/Entity/Commentaire.php
et lui ajouter un constructeur :
<?php
###
// Pour que la date actuelle soit insérée automatiquement
// dans le formulaire
public function __construct()
{
$this->CommentaireDateCreate = new \DateTime();
}
###
Retour au Menu de navigation
Nous protégerons ensuite les routes de ce CRUD pour que seuls les utilisateurs connectés puissent y accéder. Nous changerons les permissions plus tard pour ne permettre qu'aux administrateurs de modifier les commentaires.
Ajoutons admin
dans l'URL du contrôleur de CRUD src/Controller/CommentaireController.php
:
<?php
###
// #[Route('/commentaire')]
#[Route('/admin/commentaire')]
###
Nous avons plusieurs solutions pour protéger les routes de ce CRUD, nous allons dans notre cas choisir la solution du fichier security.yaml
.
Nous allons donc ajouter les lignes suivantes dans config/packages/security.yaml
:
# config/packages/security.yaml
security:
# ...
access_control:
- { path: ^/admin, roles: ROLE_USER }
Nous ne pourrons désormais y accéder qu'en étant connecté (pour le moment en tant que simple utilisateur (ROLE_ADMIN
), par le suite par une autre permission, par exemple ROLE_ADMIN
).
A l'adresse :
https://127.0.0.1:8000/admin/commentaire/
Nous avons désormais une erreur Access Denied
et une redirection sur https://127.0.0.1:8000/connect si nous ne sommes pas connecté.
Retour au Menu de navigation
Nous allons créer un formulaire pour les commentaires sous les articles.
Nous allons créer un formulaire avec la commande make:form
:
php bin/console make:form
Nous allons nommer ce formulaire CommentaireArticleType
et le lier à l'entité Commentaire
.
Nous allons ensuite retirer les champs que nous voulons par défaut dans ce formulaire et ajouter des types de champs pour les champs CommentaireTitle
et CommentaireText
.
Nous allons donc modifier le fichier src/Form/CommentaireArticleType.php
:
<?php
namespace App\Form;
use App\Entity\Commentaire;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
# types de champs
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\OptionsResolver\OptionsResolver;
class CommentaireArticleType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('CommentaireTitle', textType::class, [
'label' => 'Titre : ',
'attr' => [
'maxlength' => 100,
]
])
->add('CommentaireText', textareaType::class, [
'attr' => [
'class' => 'form-control',
'rows' => 5,
'placeholder' => 'Votre commentaire',
'required' => 'required',
],
])
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Commentaire::class,
]);
}
}
Nous allons ensuite ajouter ce formulaire dans le contrôleur BlogController.php
dans la méthode article()
:
<?php
namespace App\Controller;
###
# Importation du formulaire CommentaireArticleType
use App\Form\CommentaireArticleType;
###
#[Route('/article/{slug}', name: 'article', methods: ['GET', 'POST'])]
public function article(Request $request, $slug,
EntityManagerInterface $entityManager): Response
{
// récupération de toutes les catégories pour le menu
$categories = $entityManager->getRepository(Categorie::class)->findAll();
// récupération de l'article dont le slug est $slug
$article = $entityManager->getRepository(Article::class)->findOneBy(['ArticleSlug' => $slug]);
// si l'utilisateur est connecté
if ($this->getUser()) {
// Récupérer l'utilisateur connecté
$user = $this->getUser();
// on crée une nouvelle instance de commentaire
$commentaire = new Commentaire();
// on lie le commentaire à l'article
$commentaire->setCommentaireManyToOneArticle($article);
// on ne publie pas le commentaire par défaut
$commentaire->setCommentaireIsPublished(false);
// on lie le commentaire à l'utilisateur
$commentaire->setUtilisateur($user);
// on crée le formulaire
$form = $this->createForm(CommentaireArticleType::class,
$commentaire);
$form->handleRequest($request);
// si le formulaire est soumis et valide
if ($form->isSubmitted() && $form->isValid()) {
$entityManager->persist($commentaire);
$entityManager->flush();
// redirection vers la page de l'article
return $this->redirectToRoute('article', ['slug'=>$slug],
Response::HTTP_SEE_OTHER);
}
} else {
// pas de formulaire si l'utilisateur n'est pas connecté
$form = null;
}
return $this->render('public/article.html.twig', [
'categories' => $categories,
'article' => $article,
'form' => $form,
]);
}
Retour au Menu de navigation
Nous allons ensuite ajouter le formulaire dans le template templates/public/inc/commentaire.html.twig
, que l'on verra que si nous sommes connecté :
Nous pourrions utiliser le form
qui est null
dans ce cas, mais nous allons vérifier si l'utilisateur est connecté dans le template avec la variable app.user
.
<div>
<h3>Commentaires ({{ article.Commentaires|length }})</h3>
<hr>
{# si connecté #}
{% if app.user %}
{# Ajout du formulaire #}
{{ form_start(form) }}
{{ form_widget(form) }}
<button class="btn">{{ button_label|default('Insérer') }}</button>
{{ form_end(form) }}
{% else %}
{# Ajout de la connexion #}
<p>Vous devez être connecté pour poster un commentaire
<a href='{{ path('app_login') }}'>Connexion</a></p>
{% endif %}
<hr>
###
Retour au Menu de navigation
Nous allons ajouter une redirection vers la page de l'article après connexion si on clique sur connexion dans le formulaire de commentaire en utilisant une session.
Nous allons modifier le contrôleur BlogController.php
pour sauvegarder l'article dans la session :
<?php
namespace App\Controller;
###
#[Route('/article/{slug}', name: 'article', methods: ['GET', 'POST'])]
public function article(Request $request, $slug,
EntityManagerInterface $entityManager): Response
{
###
} else {
$form = null;
// on garde le slug de l'article pour le retour
// à l'article après connexion
$request->getSession()->set('slug', $slug);
}
###
###
// on peut mettre slug à false dans les autres méthodes
#[Route('/', name: 'homepage')]
public function index(Request $request, EntityManagerInterface
$entityManager): Response
{
###
// on retire le slug de l'article
// pour éviter le retour à l'article après connexion
$request->getSession()->set('slug', false);
###
###
#[Route('/categorie/{slug}', name: 'categorie')]
public function categorie(Request $request,$slug,
EntityManagerInterface $entityManager): Response
{
###
// on retire le slug de l'article
// pour éviter le retour à l'article après connexion
$request->getSession()->set('slug', false);
###
Nous allons ensuite modifier le contrôleur UtilisateurAuthenticator.php
pour récupérer le slug de l'article dans la session et rediriger vers la page de l'article après connexion :
<?php
namespace App\Security;
###
public function onAuthenticationSuccess(Request $request,
TokenInterface $token, string $firewallName): ?Response
{
if ($targetPath = $this->getTargetPath($request->getSession(), $firewallName)) {
return new RedirectResponse($targetPath);
}
// Récupération du slug de l'article dans la session
$slug = $request->getSession()->get('slug');
// si le slug existe
if($slug){
// redirection vers la page de l'article
return new RedirectResponse($this->urlGenerator->generate('article', ['slug' => $slug]));
}
return new RedirectResponse($this->urlGenerator->generate('homepage'));
// throw new \Exception('TODO: provide a valid redirect inside '.__FILE__);
}
###
Retour au Menu de navigation
Nous allons modifier le contrôleur BlogController.php
pour changer l'ordre des commentaires du plus récent au plus ancien :
<?php
###
#[Route('/article/{slug}', name: 'article', methods: ['GET', 'POST'])]
public function article(Request $request, $slug,
EntityManagerInterface $entityManager): Response
{
###
// récupération des commentaires de l'article grâce à son id,
// triés par date de création décroissante
$commentaires = $entityManager->getRepository(Commentaire::class)
->findBy(
['CommentaireManyToOneArticle' => $article->getId()],
['CommentaireDateCreate' => 'DESC']);
###
return $this->render('public/article.html.twig', [
'categories' => $categories,
'article' => $article,
'form' => $form,
// on envoie les commentaires à la vue
'commentaires' => $commentaires,
]);
###
Nous allons ensuite modifier le template templates/public/inc/commentaire.html.twig
remettre la variable commentaires
dans la page à la place de article.Commentaires
Retour au Menu de navigation
Nous allons charger le composant verify-email-bundle pour vérifier l'adresse email des utilisateurs :
composer require symfonycasts/verify-email-bundle
Retour au Menu de navigation
Nous allons créer le formulaire d'inscription des utilisateurs avec la commande make:registration-form
:
php bin/console make:registration-form
Nous choisissons l'entité Utilisateur
et le nom RegistrationFormType
pour le formulaire :
php bin/console make:registration-form
Creating a registration form for App\Entity\Utilisateur
Do you want to add a #[UniqueEntity] validation attribute
# to your Utilisateur class to make sure duplicate
# accounts aren't created? (yes/no) [yes]:
>
yes
Do you want to send an email to verify the user's
email address after registration? (yes/no) [yes]:
>
yes
By default, users are required to be authenticated when they click
the verification link that is emailed to them.
This prevents the user from registering on their
laptop, then clicking the link on their phone, without
having to log in.
To allow multi device email verification,
we can embed a user id in the verification link.
Would you like to include the user id in
the verification link to allow anonymous email verification? (yes/no) [no]:
>
no
What email address will be used to send registration
confirmations? (e.g. mailer@your-domain.com):
> bot@cf2m.be
What "name" should be associated with that email
address? (e.g. Acme Mail Bot):
> Bot CF2m
Do you want to automatically authenticate the user after registration? (yes/no) [yes]:
>
yes
updated: src/Entity/Utilisateur.php
updated: src/Entity/Utilisateur.php
created: src/Security/EmailVerifier.php
created: templates/registration/confirmation_email.html.twig
created: src/Form/RegistrationFormType.php
created: src/Controller/RegistrationController.php
created: templates/registration/register.html.twig
Success!
Next:
1) In RegistrationController::verifyUserEmail():
* Customize the last redirectToRoute() after a successful email verification.
* Make sure you're rendering success flash messages or change the $this->addFlash() line.
2) Review and customize the form, controller, and templates as needed.
3) Run "php bin/console make:migration" to generate a migration for the newly added
Utilisateur::isVerified property.
Then open your browser, go to "/register" and enjoy your new form!
Retour au Menu de navigation
Nous allons lancer la migration de la DB après la création du formulaire d'inscription :
php bin/console make:migration
Le fichier de migration est créé dans le dossier migrations
:
migrations/Version20230831064226.php
Lancement de la migration :
php bin/console doctrine:migrations:migrate
A l'adresse : datas/sym_64_2023-08-31.sql
, n'oubliez pas d'importer ce fichier dans votre DB locale.
Retour au Menu de navigation
Si nous allons à l'adresse :
https://127.0.0.1:8000/register
Nous avons une erreur :
The controller for URI "/register" is not callable: Environment variable not found: "MAILER_DSN".
Nous devons mettre à jour le fichier .env.local
pour le mailer :
###> symfony/mailer ###
# MAILER_DSN=null://null
###< symfony/mailer ###
Nous allons installer gmail pour le mailer en dev (en utilisant un mot de passe d'application) :
composer require symfony/google-mailer
Documentation : https://packagist.org/packages/symfony/google-mailer Et pour obtenir une clef d'activation : symfony/symfony-docs#17115
Nous allons ensuite mettre à jour le fichier .env.local
, ici le .env.
pour permettre de le voir sur github :
###> symfony/google-mailer ###
# Gmail SHOULD NOT be used on production, use it in development only.
# MAILER_DSN=gmail://USERNAME:PASSWORD@default
###< symfony/google-mailer ###
Cette adresse devrait fonctionner :
https://127.0.0.1:8000/register
Retour au Menu de navigation
Nous allons ajouter le champ name
dans le formulaire d'inscription :
src/Form/RegistrationFormType.php
###
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('email')
// ajout du champ name
->add('name')
->add('agreeTerms', CheckboxType::class, [
'mapped' => false,
'constraints' => [
new IsTrue([
'message' => 'Vous devez accepter les termes.',
]),
],
])
###
Puis dans la vue :
templates/registration/register.html.twig
{# #}
{{ form_errors(registrationForm) }}
{{ form_start(registrationForm) }}
{{ form_row(registrationForm.email) }}
{# Ajout du champs name #}
{{ form_row(registrationForm.name) }}
{{ form_row(registrationForm.plainPassword, {
label: 'Password'
}) }}
{{ form_row(registrationForm.agreeTerms) }}
<button type="submit" class="btn">S'inscrire</button>
{{ form_end(registrationForm) }}
{# #}
Un mail a dû être envoyé depuis src/Controller/RegistrationController.php
à l'adresse mail indiquée avec le template templates/registration/confirmation_email.html.twig
:
{# templates/registration/confirmation_email.html.twig#}
<h1>Hi! Please confirm your email!</h1>
<p>
Please confirm your email address by clicking the following link: <br><br>
<a href="https://127.0.0.1:8000/verify/email?expires=1693924223&signature=ZGpiLD%2Bk8J0EAN61PH54TzWODKm0fO31xk6o8EA7S6c%3D&token=OcRVcAey%2B8YqgTkp941idS8uYL%2FzOuotvODH16FAm1U%3D">Confirm my Email</a>.
This link will expire in 1 hour.
</p>
<p>
Cheers!
</p>
On peut donc valider ce mail et se connecter à cette adresse reçue par mail dans l'heure :
Retour au Menu de navigation
Les fichiers concernés sont :
src/Form/RegistrationFormType.php
templates/registration/register.html.twig
templates/registration/confirmation_email.html.twig
Nous n'utilisons pas encore le système de traduction de Symfony, nous le mettrons en place plus tard.
Et au niveau du contrôleur src/Controller/RegistrationController.php
:
###
#[Route('/register', name: 'app_register')]
###
// redirection vers l'accueil
return $this->redirectToRoute('homepage');
// on n'autorise pas l'utilisateur à se
// connecter directement après son inscription
/*
return $userAuthenticator->authenticateUser(
$user,
$authenticator,
$request
);
*/
###
Retour au Menu de navigation
Nous allons créer un lien d'enregistrement dans le menu de navigation :
templates/public/inc/menu.html.twig
{# templates/public/inc/menu.html.twig #}
{# ... #}
{% if is_granted("ROLE_USER") %}
<li class="nav-item">
<a class="nav-link" aria-current="page"
href="{{ path('app_logout') }}"
>Déconnexion</a>
</li>
{% else %}
<li class="nav-item">
<a class="nav-link" aria-current="page"
href="{{ path('app_login') }}"
>Connexion</a>
</li>
<li class="nav-item">
<a class="nav-link" aria-current="page"
href="{{ path('app_register') }}"
>Inscription</a>
</li>
{% endif %}
{# ... #}
Puis un lien dans la page de connexion :
templates/public/security/login.html.twig
{# templates/public/security/login.html.twig #}
{# ... #}
<h3 class="h5 mb-3 mt-4 font-weight-normal">Vous n'avez pas de compte ?
<a href="{{ path('app_register') }}">Inscrivez-vous</a></h3>
{# ... #}
Ensuite nous allons mettre le design à jour :
templates/registration/register.html.twig
{# templates/registration/register.html.twig #}
{% extends 'public/public.template.html.twig' %}
{% block title %}{{ parent() }} Inscription{% endblock %}
{% block menuLinks %}
{% include 'public/inc/menu.html.twig' %}
{% endblock %}
{%block htitle %}Inscription{% endblock %}
{%block hdesc %}Veuillez vous Inscrire.
Vous devrez valider votre compte via un
lien dans votre mail.{% endblock %}
{% block boutonshauts %}{% endblock %}
{% block articlePerOne %}
{% for flash_error in app.flashes('verify_email_error') %}
<div class="alert alert-danger" role="alert">
{{ flash_error }}</div>
{% endfor %}
{{ form_errors(registrationForm) }}
{{ form_start(registrationForm) }}
{{ form_row(registrationForm.email) }}
{{ form_row(registrationForm.name) }}
{{ form_row(registrationForm.plainPassword, {
label: 'Password'
}) }}
{{ form_row(registrationForm.agreeTerms) }}
<button type="submit" class="btn">S'inscrire</button>
{{ form_end(registrationForm) }}
{% endblock %}
Puis le formulaire d'inscription :
src/Form/RegistrationFormType.php
###
$builder
->add('email', null, [
'label' => 'Email',
'attr' => [
'placeholder' => 'Votre email',
'class' => 'form-control',
]
])
->add('name', null, [
'label' => 'Nom',
'attr' => [
'placeholder' => 'Votre nom',
'class' => 'form-control',
]
])
->add('agreeTerms', CheckboxType::class, [
'mapped' => false,
'label' => 'Vous acceptez les termes du site',
'constraints' => [
new IsTrue([
'message' => 'Vous devez accepter les termes.',
]),
],
])
->add('plainPassword', PasswordType::class, [
// instead of being set onto the object directly,
// this is read and encoded in the controller
'mapped' => false,
'attr' => [
'autocomplete' => 'new-password',
'placeholder' => 'Votre mot de passe',
'class' => 'form-control',
],
'constraints' => [
new NotBlank([
'message' => 'Please enter a password',
]),
new Length([
'min' => 6,
'minMessage' => 'Your password should be at least
{{ limit }} characters',
// max length allowed by Symfony for
// security reasons
'max' => 4096,
]),
],
])
;
}
###
Ajout de l'appel du menu dans src/Controller/RegistrationController.php
:
###
# Importation de l'entité Categorie
use App\Entity\Categorie;
###
// on récupère toutes les catégories
$categories = $entityManager->
getRepository(Categorie::class)->findAll();
return $this->render('registration/register.html.twig', [
'registrationForm' => $form->createView(),
// on envoie les catégories à la vue
'categories' => $categories,
]);
###
Retour au Menu de navigation
Nous allons installer EasyAdmin pour gérer les utilisateurs (dont leurs droits), les articles, les commentaires et les catégories.
composer require easycorp/easyadmin-bundle
La documentation est ici :
https://symfony.com/bundles/EasyAdminBundle/current/index.html
Et le github de la démo est là :
https://github.com/EasyCorp/easyadmin-demo
Retour au Menu de navigation
Puis nous allons créer un tableau de bord pour l'administration :
php bin/console make:admin:dashboard
On y accède à cette URL :
On va créer un lien vers l'administration dans le menu, pour le moment pour les simples utilisateurs, puis plus tard seulement pour les administrateurs :
templates/public/inc/menu.html.twig
{# templates/public/inc/menu.html.twig #}
{# ... #}
{% if is_granted("ROLE_USER") %}
<li class="nav-item">
<a class="nav-link" aria-current="page" href="{{ path('app_logout') }}">Déconnexion</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ path('admin') }}">Administration</a>
</li>
{% else %}
{# ... #}
Retour au Menu de navigation
On peut ensuite configurer le tableau de bord dans le fichier :
src/Controller/Admin/DashboardController.php
###
use Symfony\Component\Routing\Annotation\Route;
# Importation des entités utiles
use App\Entity\Categorie;
use App\Entity\Article;
use App\Entity\Commentaire;
use App\Entity\Utilisateur;
###
// Option 3. You can render some custom template to
// display a proper dashboard with widgets, etc.
// (tip: it's easier if your template extends from
// @EasyAdmin/page/content.html.twig)
return $this->render('admin/admin.homepage.html.twig');
###
En le liant au template :
templates/admin/admin.homepage.html.twig
{# templates/admin/admin.homepage.html.twig #}
{% extends '@EasyAdmin/page/content.html.twig' %}
{# On commente les actions comme il n'y en a pas encore
{% for data in my_own_data %}
<tr>
<td>{{ data.someColumn }}</td>
<td>{{ data.anotherColumn }}</td>
</tr>
{% endfor %}
#}
La documentation du dashboard est ici :
https://symfony.com/bundles/EasyAdminBundle/current/dashboards.html#dashboard-configuration
Retour au Menu de navigation
Voici la commande générale pour la création des CRUD :
php bin/console make:admin:crud
On peut ensuite choisir l'entité à gérer.
Le fichier du CRUD est ici :
src/Controller/Admin/ArticleCrudController.php
On peut l'utiliser immédiatement en le mettant dans les liens du fichier :
src/Controller/Admin/DashboardController.php
###
public function configureMenuItems(): iterable
{
return [
MenuItem::linkToDashboard('Dashboard', 'fa fa-home'),
MenuItem::linkToCrud('Les articles', 'fas fa-list', Article::class),
// yield MenuItem::linkToCrud('The Label',
// 'fas fa-list', EntityClass::class);
];
}
###
Retour au Menu de navigation
On peut les utiliser immédiatement en le mettant dans les liens du fichier :
src/Controller/Admin/DashboardController.php
public function configureMenuItems(): iterable
{
return [
MenuItem::linkToDashboard('Dashboard', 'fa fa-home'),
MenuItem::subMenu('Gestion du Blog', 'fas fa-newspaper')
->setSubItems([
MenuItem::linkToCrud('Les catégories',
'fas fa-list', Categorie::class),
MenuItem::linkToCrud('Les articles',
'fas fa-list', Article::class),
MenuItem::linkToCrud('Les commentaires',
'fas fa-list', Commentaire::class),
]),
MenuItem::linkToRoute('Retour au site',
'fas fa-home', 'homepage'),
// yield MenuItem::linkToCrud('The Label',
// 'fas fa-list', EntityClass::class);
];
Retour au Menu de navigation
Documentation des CRUD dans EasyAdmin :
https://symfony.com/bundles/EasyAdminBundle/current/crud.html#crud-controller-pages
On peut modifier le CRUD pour l'entité Article dans le fichier :
src/Controller/Admin/ArticleCrudController.php
<?php
namespace App\Controller\Admin;
use App\Entity\Article;
# Pour gérer le CRUD
use EasyCorp\Bundle\EasyAdminBundle\Config\Crud;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
class ArticleCrudController extends AbstractCrudController
{
public static function getEntityFqcn(): string
{
return Article::class;
}
# Options de configuration du CRUD
public function configureCrud(Crud $crud): Crud
{
return $crud
// classés par id décroissant
->setDefaultSort(['id' => 'DESC'])
// Nombre d'articles par page
->setPaginatorPageSize(20)
// Titres des pages
->setPageTitle('index', 'Liste des articles')
->setPageTitle('new', 'Créer un article')
->setPageTitle('edit', 'Modifier un article');
}
}
Retour au Menu de navigation
Documentation :
https://symfony.com/bundles/EasyAdminBundle/current/fields.html#displaying-different-fields-per-page
On peut modifier les champs du CRUD pour l'entité Article dans le fichier :
src/Controller/Admin/ArticleCrudController.php
Pour faire fonctionner les modifications/ajout/suppressions de catégories et/ou commentaires, il faut savoir que Doctrine gère les relations ManyToMany ou ManyToOne en lecture seule, il faut donc ajouter les options by_reference
à false
dans le fichier :
###
AssociationField::new('categories')->setFormTypeOptions([
'by_reference' => false,
]),
src/Controller/Admin/ArticleCrudController.php
###
# Utilisation des champs de EasyAdmin
use EasyCorp\Bundle\EasyAdminBundle\Field\AssociationField;
use EasyCorp\Bundle\EasyAdminBundle\Field\DateTimeField;
use EasyCorp\Bundle\EasyAdminBundle\Field\IntegerField;
use EasyCorp\Bundle\EasyAdminBundle\Field\SlugField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextEditorField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextField;
use EasyCorp\Bundle\EasyAdminBundle\Field\BooleanField;
use EasyCorp\Bundle\EasyAdminBundle\Field\FormField;
###
# Champs à afficher dans le CRUD
# https://symfony.com/bundles/EasyAdminBundle/current/fields.html#field-types
public function configureFields(string $pageName): iterable
{
return [
# id seulement sur l'accueil
IntegerField::new('id')->onlyOnIndex(),
TextField::new('ArticleTitle'),
# slug seulement sur les formulaires,
# lié au titre avec création automatique
SlugField::new('ArticleSlug')->onlyOnForms()
->setTargetFieldName('ArticleTitle'),
TextEditorField::new('ArticleContent'),
DateTimeField::new('ArticleDateCreate'),
# date de l'update cachée sur l'accueil
DateTimeField::new('ArticleDateUpdate')->hideOnIndex(),
BooleanField::new('ArticleIsPublished'),
# Panel pour regrouper les champs
FormField::addPanel('Lien avec les autres tables'),
#
# Association avec les autres tables
#
# https://symfony.com/bundles/EasyAdminBundle/current/fields/AssociationField.html
# Lien avec la table utilisateur ManyToOne
AssociationField::new('utilisateur'),
# Lien avec la table commentaire OneToMany
AssociationField::new('Commentaires')->setFormTypeOptions([
'by_reference' => false,
]),
# Lien avec la table catégorie ManyToMany -
# Il faut ajouter le setFormTypeOptions pour éviter que les catégories
# ne soient pas ajoutées, modifiées ou supprimées !
# https://stackoverflow.com/questions/65900855/easyadmin-manytomany-relation-not-saving-data-in-base
AssociationField::new('categories')->setFormTypeOptions([
'by_reference' => false,
]),
];
}
###
En cas d'erreurs de ce type :
Object of class App\Entity\Commentaire could not be converted to string
On doit ajouter la méthode __toString()
dans l'entité Commentaire
:
###
public function __toString(): string
{
return $this->CommentaireTitle;
}
###
Et de même pour les catégories :
###
public function __toString(): string
{
return $this->CategorieTitle;
}
###
Retour au Menu de navigation
On peut modifier le CRUD pour l'entité Commentaire dans le fichier :
src/Controller/Admin/CommentaireCrudController.php
###
public function configureCrud(Crud $crud): Crud
{
return $crud
// classés par id décroissant
->setDefaultSort(['id' => 'DESC'])
// Nombre d'articles par page
->setPaginatorPageSize(20)
// Titres des pages
->setPageTitle('index', 'Liste des commentaires')
->setPageTitle('new', 'Créer un commentaire')
->setPageTitle('edit', 'Modifier un commentaire');
}
###
Retour au Menu de navigation
src/Controller/Admin/CommentaireCrudController.php
###
public function configureFields(string $pageName): iterable
{
return [
# id seulement sur l'accueil
IntegerField::new('id')->onlyOnIndex(),
TextField::new('CommentaireTitle'),
TextEditorField::new('CommentaireText'),
DateTimeField::new('CommentaireDateCreate'),
BooleanField::new('CommentaireIsPublished'),
# Panel pour regrouper les champs
FormField::addPanel('Lien avec les autres tables'),
# Lien avec l'utilisateur
AssociationField::new('utilisateur'),
# Lien avec l'article, le rendre non modifiable
AssociationField::new('CommentaireManyToOneArticle')
->setDisabled()
->setFormTypeOptions([
'label' => 'Article',
'help' => 'Article non modifiable',
]),
];
}
###
Retour au Menu de navigation
On peut modifier le CRUD pour l'entité Categorie dans le fichier :
src/Controller/Admin/CategorieCrudController.php
###
public function configureCrud(Crud $crud): Crud
{
return $crud
// classés par titre croissant
->setDefaultSort(['CategorieTitle' => 'ASC'])
// Nombre d'articles par page
->setPaginatorPageSize(20)
// Titres des pages
->setPageTitle('index', 'Liste des catégories')
->setPageTitle('new', 'Créer une catégorie')
->setPageTitle('edit', 'Modifier une catégorie');
}
###
Retour au Menu de navigation
src/Controller/Admin/CategorieCrudController.php
###
# Pour gérer le CRUD
use EasyCorp\Bundle\EasyAdminBundle\Config\Crud;
use EasyCorp\Bundle\EasyAdminBundle\Field\AssociationField;
use EasyCorp\Bundle\EasyAdminBundle\Field\BooleanField;
use EasyCorp\Bundle\EasyAdminBundle\Field\DateTimeField;
use EasyCorp\Bundle\EasyAdminBundle\Field\FormField;
use EasyCorp\Bundle\EasyAdminBundle\Field\IntegerField;
use EasyCorp\Bundle\EasyAdminBundle\Field\SlugField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextEditorField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextField;
###
public function configureFields(string $pageName): iterable
{
return [
# id seulement sur l'accueil
IntegerField::new('id')->onlyOnIndex(),
TextField::new('CategorieTitle')->setFormTypeOptions([
'label' => 'Titre',
'help' => 'Titre de la catégorie',]),
# slug seulement sur les formulaires,
# lié au titre avec création automatique
SlugField::new('CategorySlug')->onlyOnForms()
->setTargetFieldName('CategorieTitle')
->setFormTypeOptions([
'label' => 'Slug',
'help' => 'Créé à partir du titre,
modifiable en cliquant sur le cadenas',]),
# description
TextEditorField::new('CategorieDesc')->setFormTypeOptions([
'label' => 'Description',
'help' => 'Description de la catégorie',
]),
# Panel pour regrouper les champs
FormField::addPanel('Lien avec les autres tables'),
# Lien avec les articles
AssociationField::new('Categorie_m2m_Article')
->setFormTypeOptions([
'label' => 'Articles',
'help' => 'Articles liés à cette catégorie',
]),
];
}
###
Retour au Menu de navigation
Dans le fichier src/Controller/Admin/DashboardController.php
###
// on ajoute la variable { _locale } pour la langue à l'URL
#[Route('/admin/{_locale}', name: 'admin')]
public function index(): Response
###
Puis on change la locale dans le fichier config/packages/translation.yaml
framework:
default_locale: fr
Les boutons seront automatiquement traduits en français avec une URL de ce type :
https://127.0.0.1:8000/admin/fr
On peut aussi traduire le titre et contenu du tableau de bord de l'administration dans le fichier
templates/admin/admin.homepage.html.twig
{% block content_title %}Administration du site{% endblock %}
Retour au Menu de navigation