thomas-ayissi / Symfony_Tuto_Installation

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

symfony-2023-05-10

Version installée de Symfony 6.2.10

Installation de Symfony dans l'environnement de développement sous Windows

Lien vers le site de démo

https://sym6.cf2m.be/


Menu de navigation


Utilisation des tags de github

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


Historique de Symfony

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 :

  1. Structuration et modularité : Symfony offre une structure et une organisation claire de type MVC (Model-View-Controller) pour les projets, ce qui facilite leur maintenance et leur évolution. Le framework est également modulaire, ce qui signifie que les développeurs peuvent utiliser uniquement les composants dont ils ont besoin, sans avoir à intégrer des fonctionnalités superflues.

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

  3. Sécurité : La sécurité est une préoccupation majeure dans le développement web, et Symfony offre des fonctionnalités de sécurité avancées telles que la protection contre les injections SQL et les attaques XSS. 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.

  4. Documentation et communauté active : Symfony est livré avec une documentation complète, (à présent uniquement en anglais, choisie comme langue internationale) qui est constamment mise à 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.

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


Prérequis

Environnement de développement

  • 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


Liens de téléchargement des logiciels

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 :

https://symfony.com/download

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


Installation de Symfony dans l'environnement de développement

  • 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 :

https://symfony.com/releases

Vérifions si notre poste de travail est bien configuré pour Symfony

symfony check:requirements

Retour au Menu de navigation


Création d'un projet de démonstration

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 :

http://127.0.0.1:8000/

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


Création d'un nouveau projet Symfony

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


Structure d'un projet Symfony

  • 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 fichier console 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 fichier index.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. (MVC)
    • Le dossier src/Form contient les formulaires de l'application. (MVC)
    • Le dossier src/Repository contient les dépôts de l'application. (MVC)
    • Le dossier src/Service contient les services de l'application. (MVC)
    • etc...
  • 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. (MVC)

  • 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. (MVC)

  • 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 fichier config.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 fichier config.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 commande composer install (création du dossier vendor) ou de mettre à jour les dépendances en exécutant la commande composer update.


Retour au Menu de navigation


Lancement du serveur web de Symfony

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 :

https://127.0.0.1:8000/


Retour au Menu de navigation


Création du premier contrôleur

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 :

https://127.0.0.1:8000/public


Retour au Menu de navigation


Manipulation des routes

On peut créer des routes en utilisant 4 méthodes différentes :

  • annotation : dans le contrôleur
  • yaml : dans le fichier config/routes.yaml
  • xml : dans le fichier config/routes.xml
  • php : dans le fichier config/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


Création d'une route depuis le contrôleur

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 :

https://127.0.0.1:8000/

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.

V0.0.1


Retour au Menu de navigation


Création d'une route depuis le fichier de configuration

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 :

https://symfony.com/doc/current/best_practices.html#use-the-yaml-format-to-configure-your-own-services

V0.0.2


Retour au Menu de navigation


Création d'une route avec paramètre

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


Création d'une route avec paramètre typé

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


Création d'une route avec paramètre typé et valeur par défaut

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>

V0.0.3


Retour au Menu de navigation


Création du fichier .env.local

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


Création de la base de données

Nous allons créer la base de données en utilisant la commande suivante :

php bin/console doctrine:database:create

V0.0.4


Retour au Menu de navigation


Si la base de données existe déjà

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


Création d'un crud pour la table post

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 

v0.1.2

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 :

https://127.0.0.1:8000/post/

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


Correction des erreurs

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 !

v0.1.3

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 !

v0.1.4


Retour au Menu de navigation


0.2.0

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


0.2.1

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',
        ]);
    }

v0.2.1


Retour au Menu de navigation


Création d'une entité

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


Première migration vers la DB

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

V0.2.2


Retour au Menu de navigation


Création d'une entité avec une relation ManyToOne

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;

v0.2.3


Retour au Menu de navigation


Deuxième migration vers la DB

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


Création d'une entité avec une relation ManyToMany

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;

    // ...

v0.2.4


Retour au Menu de navigation


Troisième migration vers la DB

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


Mise à jour de version mineure de Symfony

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 :

https://symfony.com/doc/current/setup/upgrade_major.html#2-update-to-the-new-major-version-via-composer

Pour être certain de la compatibilité des dépendances, il est possible de lancer la commande suivante :

composer recipes:install --force -v

v0.3.0


Retour au Menu de navigation


Création d'un utilisateur

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

v0.3.1


Retour au Menu de navigation


Modification de la table utilisateur

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

v0.3.2


Retour au Menu de navigation


Lions la table utilisateur à la table article

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

v0.3.3


Retour au Menu de navigation


Lions la table utilisateur avec commentaire

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 :

sym_64

v0.3.4


Retour au Menu de navigation


Création des fixtures

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


Création des fixtures pour la table utilisateur

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

v0.3.5


Retour au Menu de navigation


Création des fixtures pour les utilisateurs ET les articles

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

v0.3.6


Retour au Menu de navigation


Création des fixtures pour les autres tables

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

v0.3.7


Retour au Menu de navigation


Modification de la page d'accueil

Modification du contrôleur pour la page d'accueil

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 :

https://127.0.0.1:8000/

Maintenant que nous voyons que c'est fonctionnel, nous allons créer un template de base pour notre application.

v0.3.8


Retour au Menu de navigation


Twig : Création d'un template de base

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.

Modification du fichier templates/base.html.twig

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.

Installation de Webpack Encore

Pour cela il faut installer le bundle Webpack Encore :

composer require symfony/webpack-encore-bundle

Installation de Yarn

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

Création des fichiers CSS et JS via Webpack Encore

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 :

https://127.0.0.1:8000/

v0.4.0


Retour au Menu de navigation


Installation de Bootstrap

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 :

https://127.0.0.1:8000/

v0.4.1


Retour au Menu de navigation


Choix du template

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


Modification de base.html.twig

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


Modification de template.html.twig

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 &copy; MonSite {{ "now"|date('Y') }}</p></div>
        </footer>
        {% endblock %}
{% endblock %}

Retour au Menu de navigation


Modification de la page d'accueil

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 %}

v0.4.2

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


Création de la section Catégories

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

v0.4.3


Retour au Menu de navigation


Affichage des articles par catégorie

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 %}

v0.4.4


Retour au Menu de navigation


Création de la section Article

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 :

Création de la route dans le controller

###
#[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());
    }
    
###

Création des liens vers les articles dans la section categorie et index

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


Affichage d'un résumé de l'article avec slice

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 :

Installation de la bibliothèque Twig Extra String

composer require twig/string-extra

Pour la documentation de la bibliothèque, c'est ici

Utilisation de la fonction truncate de la bibliothèque Twig Extra String

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.

Modification de la méthode article du controller

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


Création de la vue article.html.twig

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

v0.4.5


Retour au Menu de navigation


Création de la vue commentaire.html.twig

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


Chargement des commentaires dans BlogController

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


Affichage des commentaires dans la vue commentaire.html.twig

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


Erreur de mapping entre les entités Article et Commentaire

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();
    }

v0.4.6


Retour au Menu de navigation


Utilisation des relations inverses

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>

v0.4.7


Retour au Menu de navigation


Authentification et autorisation

L'email et le mot de passe crypté sont stockés dans la table utilisateur de la base de données.

Création de la connexion utilisateur

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.

https://127.0.0.1:8000/login

Nous devrions être connectés et redirigés vers la page d'accueil.

v0.5.0


Retour au Menu de navigation


Modification du formulaire de connexion

Route de connexion

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');
        }
        ###
###

Menu de navigation et formulaire de connexion

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


Réorganisation des templates

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 !

Possibilité de déconnexion

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


Remember me

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.

v0.5.1


Retour au Menu de navigation


Protection du formulaire de connexion

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


Mise en place de la création de commentaires

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

Création d'un CRUD pour les commentaires

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/

Correction des erreurs de type toString sur les commentaires

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.

Pour avoir une date par défaut lors de la création d'un commentaire

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();
    }
###

v0.5.2


Retour au Menu de navigation


Protection du CRUD des commentaires

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

v0.5.3


Retour au Menu de navigation


Création d'un formulaire pour les commentaires sous les articles

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


Ajout du formulaire dans le template commentaire.html.twig

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

v0.5.4


Retour au Menu de navigation


Redirection vers la page de l'article après connexion

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__);
    }
###

v0.5.5


Retour au Menu de navigation


Changement de l'ordre des commentaires

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

v0.5.6


Retour au Menu de navigation


Inscription des utilisateurs

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


Création du formulaire d'inscription

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


Lancement de la migration de la DB après make:registration-form

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
Sauvegarde de la DB dans le dossier datas après make:registration-form

A l'adresse : datas/sym_64_2023-08-31.sql, n'oubliez pas d'importer ce fichier dans votre DB locale.

v0.5.7


Retour au Menu de navigation


Mise à jour du .env.local pour le mailer

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


Ajout du champ name dans le formulaire d'inscription

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 :

https://127.0.0.1:8000/verify/email?expires=1693924223&signature=ZGpiLD%2Bk8J0EAN61PH54TzWODKm0fO31xk6o8EA7S6c%3D&token=OcRVcAey%2B8YqgTkp941idS8uYL%2FzOuotvODH16FAm1U%3D


Retour au Menu de navigation


Traduction du formulaire d'inscription et des mails

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


Création du lien d'enregistrement et design de celui-ci

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,
        ]);
        ###

v0.5.8


Retour au Menu de navigation


Installation d'EasyAdmin

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


Configuration d'EasyAdmin

Puis nous allons créer un tableau de bord pour l'administration :

php bin/console make:admin:dashboard

On y accède à cette URL :

https://127.0.0.1:8000/admin

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


Configuration du tableau de bord

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


Création des CRUD dans EasyAdmin

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.

Création du CRUD pour l'entité Article

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


Création du CRUD pour l'entité Commentaire et Catégorie

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);
        ];

v0.5.9


Retour au Menu de navigation


Modifications des CRUD

Documentation des CRUD dans EasyAdmin :

https://symfony.com/bundles/EasyAdminBundle/current/crud.html#crud-controller-pages

Modification du CRUD pour l'entité Article

ArticleCrudController : configureCrud

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


ArticleCrudController : configureFields

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;
    }
###

v0.5.10


Retour au Menu de navigation


Modification du CRUD pour l'entité Commentaire

CommentaireCrudController : configureCrud

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


CommentaireCrudController : configureFields

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


Modification du CRUD pour l'entité Categorie

CategorieCrudController : configureCrud

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


CategorieCrudController : configureFields

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


Mise en français de l'interface d'administration

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 %}

v0.5.11


Retour au Menu de navigation


About


Languages

Language:CSS 80.6%Language:PHP 13.0%Language:HTML 3.6%Language:Twig 2.3%Language:JavaScript 0.6%