Le framework Symfony



Symfony est un ensemble de composants PHP ainsi qu'un framework MVC libre écrit en PHP. Il fournit des fonctionnalités modulables et adaptables qui permettent de faciliter et d’accélérer le développement d'un site web.

L'initiateur du projet Symfony est le français Fabien Potencier.

Source : Wikipedia.fr

1.1. Environnement de travail

Afin d'utiliser Symfony, on peut opter pour deux solutions :

1.2. Fonctionnement de Symfony

Le framework Symfony utilise les espaces de nom (namespace) en PHP afin de gérer les différentes classes qu'il utilise.

Les espaces de nom en PHP sont en quelque sorte des dossiers virtuels qui servent à encapsuler certains objets afin de les différentier d'autres objets de même nom.

1.2.1. Component et Bundle

Symfony utilise deux types d'éléments (classes) :

Voici quelques exemples de components (composants) que l'on peut installer en utilisant composer (Composer is a tool for dependency management in PHP) :

Le framework Laravel utilise notamment ces trois classes.

En ce qui concerne les bundles, on trouve par exemple :

1.3. Création d'une application Symfony

richer@zentopia:~\$ symfony new mon_projet

* Creating a new Symfony project with Composer
  WARNING: Unable to find Composer, downloading one. It is recommended to install Composer yourself at https://getcomposer.org/download/
  (running /home/richer/.symfony5/composer/composer.phar create-project symfony/skeleton /home/richer/dev/symfony/mon_projet  --no-interaction)

* Setting up the project under Git version control
  (running git init /home/richer/dev/symfony/mon_projet)


 [OK] Your project is now ready in /home/richer/dev/symfony/mon_projet                                   
 

Note : on peut également ajouter --webapp à la commande précédente pour indiquer qu'il s'agit d'un site web.

Un simple projet contient plus de 270 répertoires et plus de 1500 fichiers. Voici l'organisation du projet dans le répertoire qui a été créé :


richer@zentopia:~/dev/symfony/mon_projet\$ tree -L 1
.
├── bin
├── composer.json
├── composer.lock
├── config
├── public
├── src
├── symfony.lock
├── var
└── vendor

L'utilisation de la console peut être réalisé par :

php bin/console
# ou
symfony console

1.4. Editeur

On peut utiliser VSCode pour gérer le projet, en lançant par exemple la commande :


richer@zentopia:~/\$ code ./mon_projet

1.5. index.php

Comme mentionné précédemment, toutes les requêtes au site web passent par le fichier index.php. Ce script réalise les actions suivantes :

1.6. Fonctionnement du framework

Lors de l'envoi d'une requête au serveur web, ce dernier la transmet à php-fpm (FastCGI Process Manager) qui exécute le script public/index.php.

Ce fichier est appelé point d'entrée ou Front Controller.

  1. <?php
  2.  
  3. use App\Kernel;
  4.  
  5. require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
  6.  
  7. return function (array $context) {
  8.     return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
  9. };
  10.  
  11.  

On crée une instance de la classe Kernel (src/Kernel.php) qui se charge de l'initialisation des bundles et du traitement de la requête.

  1. <?php
  2.  
  3. namespace App;
  4.  
  5. use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
  6. use Symfony\Component\HttpKernel\Kernel as BaseKernel;
  7.  
  8. class Kernel extends BaseKernel
  9. {
  10.     use MicroKernelTrait;
  11. }
  12. ?>
  13.  

Cette classe appelle notamment une fonction handle() de la classe HttpKernel (fichier vendor/symfony/http-kernel/HttpKernel.php) qui prend en paramètre une requête et fournit une réponse.

  1. <?php
  2.  
  3. // ....
  4.  
  5. class HttpKernel implements HttpKernelInterface, TerminableInterface {
  6.  
  7.     public function handle(Request $request, int $type = HttpKernelInterface::MAIN_REQUEST, bool $catch = true): Response {
  8.    
  9.         // ...
  10.        
  11.     }
  12.  
  13.     // ...
  14. }
  15.  
  16. ?>
  17.  
  18.  

On détermine également dans cette fonction quel contrôleur appeler en fonction des routes existantes (redirections).

La Response contient des headers, du contenu et un code d'erreur.

1.7. Création d'une première page

1.7.1. Mode fonctionnement

Le mode de fonctionnement des frameworks pour le web s'appuie sur une modèle Route / Controller :

1.7.2. Le Contrôleur de base

Un contrôleur sera placé dans le répertoire src/Controller. Par exemple, on peut créer un contrôleur pour gérer une page sur la musique. Pour cela on va associer le contrôleur à la route http://localhost:8000/music.

Le contrôleur est une classe de même nom que le fichier auquel elle appartient.

Il dispose d'une ou plusieurs fonctions qui vont traiter une requête et vont retourner une réponse.

Voici un contrôleur basique qui retourne du code HTML :

  1. <?php
  2.  
  3. namespace App\Controller;
  4.  
  5. use Symfony\Component\HttpFoundation\Request;
  6. use Symfony\Component\HttpFoundation\Response;
  7. use Symfony\Component\Routing\Annotation\Route;
  8.  
  9. class MusicController {
  10.  
  11.     // name of function doesn't matter
  12.     // use Annotation to specify the route
  13.     #[Route('/music')]
  14.     public function handleRequest( Request $request) : Response {
  15.    
  16.         return new Response( "<h1>Music</h1>" );
  17.        
  18.     }
  19. }
  20.  
  21. ?>
  22.  

Dans l'exemple précédent, on a créé, par l'intermédiaire d'une annotation, l'assocation entre le contrôleur et la route. Le contrôleur doit retourner une réponse (Response).

1.7.3. Contrôleur avec un paramètre

Imaginons que l'on désire passer un paramètre sur le type de musique que l'on désire consulter. Il est possible de passer en paramètre de la fonction un paramètre comme suit. On va définir dans la même classe, une autre méthode qui permet de traiter une autre route : http://localhost:8000/browse/pop-rock

  1. <?php
  2.  
  3. namespace App\Controller;
  4.  
  5. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  6. use Symfony\Component\HttpFoundation\Request;
  7. use Symfony\Component\HttpFoundation\Response;
  8. use Symfony\Component\Routing\Annotation\Route;
  9. use function Symfony\Component\String\u;
  10.  
  11. class MusicController extends AbstractController
  12. {
  13.  
  14.     #[Route('/music')]
  15.    public function handleRequest(Request $request): Response
  16.     {
  17.  
  18.         // ...
  19.  
  20.     }
  21.  
  22.     // call this route with '/browse/pop-rock'
  23.     #[Route('/browse/{category}')]
  24.    public function browseByCategory(string $category = null): Response
  25.     {
  26.         if ($category == null) {
  27.             $title = "all categories";
  28.         } else {
  29.             $title = str_replace('-', ' ', $category);
  30.             $title = u($title)->title(true);
  31.         }
  32.         return new Response('<p>Browse the collection for ' . $title . '</p>');
  33.     }
  34.  
  35. }
  36.  

Si la paramètre category n'est pas fourni, on affichera 'all categories', sinon on transforme le paramètre grâce à une fonction de php str_replace() qui remplace les '-' par des espaces. On utilise ensuite une fonction u() de Symfony qui permet de passer les lettres des premiers mots en majuscule.

1.7.4. Le Contrôleur retournant du code HTML sous forme de template

On pourrait construire chaque page dans le contrôleur mais cela serait fastidieux. Il est en général préférable d'utiliser des templates qui sont introduits par Twig.

Twig est un moteur de templates pour le langage de programmation PHP, utilisé par défaut par le framework Symfony.

Pour cela on crée par exemple, dans le répertoire templates, un sous répertoire music et on y crée un fichier homepage.html.twig. On peut ici donner n'importe quel nom à ce fichier.

Au niveau de notre contrôleur, on ajoute une route http://localhost:8000/home qui affichera la home page de notre site de musique : il faut alors appeler la fonction render du contrôleur en lui passant comme paramètre le fichier de templates que l'on désire afficher ainsi que la catégorie de musique éventuelle.

  1. <?php
  2.  
  3. namespace App\Controller;
  4.  
  5. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  6. use Symfony\Component\HttpFoundation\Request;
  7. use Symfony\Component\HttpFoundation\Response;
  8. use Symfony\Component\Routing\Annotation\Route;
  9. use function Symfony\Component\String\u;
  10.  
  11. class MusicController extends AbstractController
  12. {
  13.  
  14.     #[Route('/music')]
  15.    public function handleRequest(Request $request): Response
  16.     {
  17.  
  18.          // ...
  19.     }
  20.  
  21.     #[Route('/browse/{category}')]
  22.    public function browseByCategory(string $category = null): Response
  23.     {
  24.         // ...
  25.     }
  26.  
  27.     #[Route('/home/{category}')]
  28.    public function browseAndTwig($category = null): Response
  29.     {
  30.  
  31.         if (empty($category)) $category = "all categories";
  32.  
  33.         return $this->render(
  34.             'music/homepage.html.twig',
  35.             [
  36.                 'category' => $category,
  37.             ]
  38.         );
  39.     }
  40. }
  41.  
  42.  

Le fichier de templates a le format suivant :

  1.  
  2. <h1>Hello !</h1>
  3.  
  4. <p>What king of music do you like ?</p>
  5.  
  6. <p>Maybe {{ category }} ?</p>
  7.  

1.7.5. Contrôleur avec plusieurs paramètres

On peut passer un autre paramètre au renderer, dans le cas présent, on va lui passer un tableau.

  1. <?php
  2.  
  3. namespace App\Controller;
  4.  
  5. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  6. use Symfony\Component\HttpFoundation\Request;
  7. use Symfony\Component\HttpFoundation\Response;
  8. use Symfony\Component\Routing\Annotation\Route;
  9. use function Symfony\Component\String\u;
  10.  
  11. class MusicController extends AbstractController
  12. {
  13.  
  14.     #[Route('/music')]
  15.    public function handleRequest(Request $request): Response
  16.     {
  17.  
  18.          // ...
  19.     }
  20.  
  21.     #[Route('/browse/{category}')]
  22.    public function browseByCategory(string $category = null): Response
  23.     {
  24.         // ...
  25.     }
  26.  
  27.     #[Route('/home/{category}')]
  28.    public function browseAndTwig($category = null): Response
  29.     {
  30.         $kinds = array(
  31.             "pop-rock", "rock", "k-pop",
  32.             "funk", "death-metal", "soft-rock"
  33.         );
  34.  
  35.         if (empty($category)) $category = "all categories";
  36.  
  37.         return $this->render(
  38.             'music/homepage.html.twig',
  39.             [
  40.                 'category' => $category,
  41.                 'kinds' => $kinds
  42.             ]
  43.         );
  44.     }
  45. }
  46.  
  47.  

On utilise alors la structure de contrôle {% for %} de Twig pour énumérer suivant les valeurs du tableau :

  1. <h1>Welcome to the Music Box</h1>
  2.  
  3. <p>What king of music do you like ?</p>
  4.  
  5. <p>Maybe {{ category }} ?</p>
  6.  
  7. <p>or maybe one of </p>
  8.  
  9. <ul>
  10.     {% for kind in kinds %}
  11.         <li>
  12.             <a href="{{ kind }}">{{ kind|replace( {'-': ' '}  )|title }}</a>
  13.         </li>    
  14.     {% endfor %}
  15. </ul>
  16.  
  17.  
  18.  

1.7.6. Les filtres Twig

Les filtres permettent de modifier l'affichage des variables ou la transformation des variables qui sont introduits par le symbole '|'.

1.7.6.a  Filtres sur les chaînes

Par exemple, upper permet de passer une chaîne en majuscule ou capitalize qui met la première lettre en majuscule.

{{ 'mon titre' | upper }}

Un autre filtre intéressant est raw qui permet de prendre une variable qui contient du code HTML et l'afficher car par défaut, une variable avec du code HTML est modifiée et on verra le code correspondant s'afficher. Essayer avec les deux syntaxes suivantes :

// introduisez une variable 'code' au niveau du contrôleur qui contient du code HTML
// puis affichez la avec l'une des deux syntaxes suivantes :
{{ code }}

{{ code | raw }}

1.7.6.b  Filtres sur les tableaux

Le filtre join permet de transformer les éléments d'un tableau en chaîne :

{{ [ 2, 3, 5, 7 ] | join( ',' ) }}

Le filtre map permet d'appliquer une fonction à un tableau :

{{ [ 2, 3, 5, 7 ] | map( x => x * x ) | join( ',' ) }}

On obtiendra donc la chaîne "4, 9, 25, 49".

Le filtre filter sélectionne des éléments en fonction d'un critère de choix :

{% for x in [1,2,3,4,5,6] | filter( x => (x % 2) == 0 ) %}
    {{ x }}
{% endfor %}

On obtient donc uniquement les nombres pairs.

1.7.6.c  Filtres sur les dates

A partir d'un timestamp, on peut obtenir la date et l'afficher au format jour / mois /année :

{{ 23580000 | date('d/m/Y') }}

On peut également choisir d'afficher la date au format jour / mois en lettres / année mais dans ce cas on obtient un mois en anglais.

{{ 23580000 | date('d F Y') }}

Il faut alors utiliser le filtre format_date mais il est alors nécessaire d'installer deux packages : twig/intl-extra et twig/extra-bundle. Pour cela on utilise composer :

symfony composer require twig/intl-extra
symfony composer require twig/extra-bundle

Il existe également format_date qui permet de formater une date avec la locale adaptée :

{{ 23580000 | format_date(  pattern="dd MMMM yyyy", locale='fr' )  }}

Néanmoins, il faut aujouter le package Linux correspondant (par exemple php-8.2-intl) et modifier le fichier php.ini adéquat (par exemple dans /etc/php/8.2/apache2) et décommenter l'extension :

...
;extension=gettext
;extension=gmp
extension=intl
;extension=imap
;extension=mbstring
...

1.7.6.d  Réutilisation des templates

Il est possible de créer un template Twig et de l'inclure dans un autre template, en passant des paramètres :

{{ include( "template_a_inclure.twig",
	{ var : nom_var }  ) }}

1.7.6.e  Extension de page

Le code de base d'une page est donné par le fichier templates/base.html.twig. Celui ci peut être réutilisé (hérité) et modifié pour certaines parties.

  1. <!DOCTYPE html>
  2. <html>
  3.     <head>
  4.         <meta charset="UTF-8">
  5.         <title>{% block title %}Welcome!{% endblock %}</title>
  6.         <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>">
  7.         {# Run `composer require symfony/webpack-encore-bundle` to start using Symfony UX #}
  8.         {% block stylesheets %}
  9.             {{ encore_entry_link_tags('app') }}
  10.         {% endblock %}
  11.  
  12.         {% block javascripts %}
  13.             {{ encore_entry_script_tags('app') }}
  14.         {% endblock %}
  15.     </head>
  16.     <body>
  17.         {% block body %}{% endblock %}
  18.     </body>
  19. </html>
  20.  

Cette page contient des blocks qui peuvent être modifiés, notamment le block body pour créer de nouvelles pages. On aura donc juste à redéfinir le block body pour toute nouvelle page ce qui permet à chaque fois d'inclure la même partie head avec les références aux fichiers CSS et Javascript.

  1. {{% extends( './base.html.twig") %}}
  2.  
  3. {% block body %}
  4.  
  5. mon code HTML spécifique à la page
  6.  
  7. {% endblock %}
  8.  
  9.  

1.7.6.f  La variable app

Cette variable permet d'avoir accès à différentes informations comme l'utilisateur connecté, les paramètres passés, environnement (développement, production), les variables de session.

On peut notamment afficher la requête en tapant :

{{ dump( app.request ) }}
{{ dump( app.request.method ) }}
{{ dump( app.request.environment ) }}
{{ dump( app.request.session ) }}

1.8. Formulaires

Il faut vérifier que le bundle form est bien installé, sinon il faudra l'installer :

symfony composer require form

Pour gérer un formulaire, il faut :

  1. construire le formulaire : on utilise la méthode createFormBuilder( classe )
  2. afficher le formulaire : Twig
  3. traiter le formulaire (valider les données) : FormInterface

Commencez par créer un nouveau controller en utilisant la console :

symfony console make:controller
PHP Warning:  Module "intl" is already loaded in Unknown on line 0

 Choose a name for your controller class (e.g. OrangePizzaController):
 > FormController

 created: src/Controller/FormController.php
 created: templates/form/index.html.twig

           
  Success! 

1.8.1. Premier formulaire avec données utilisateur

On peut créer un formulaire spécifique en chaînant les différents type de champs entre eux et en définissant leurs caractéristiques. Ici, on va créer un formulaire avec un prénom (first name) à saisir ainsi qu'un bouton pour soumettre les données saisies :

Voir la page des Types de champs définis par Symfony.

Il faut commencer par appeler la fonction createFormBuilder() puis ajouter des champs en utilisant add.

Un champ est défini par un nom (string), un type (XXX::class) et éventuellement une liste d'attributs [].

  1. <?php
  2.  
  3. namespace App\Controller;
  4.  
  5. use Twig\Environment;
  6. use Symfony\Component\HttpFoundation\Request;
  7. use Symfony\Component\HttpFoundation\Response;
  8. use Symfony\Component\Routing\Annotation\Route;
  9. use Symfony\Component\Form\Extension\Core\Type\TextType;
  10. use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
  11. use Symfony\Component\Form\Extension\Core\Type\SubmitType;
  12. use Symfony\Component\Form\Extension\Core\Type\CountryType;
  13. use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
  14. use Symfony\Component\Form\Extension\Core\Type\DateType;
  15. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  16.  
  17. class FormController extends AbstractController
  18. {
  19.     #[Route('/form1')]
  20.    public function index1(): Response
  21.     {
  22.         $myForm = $this->createFormBuilder()
  23.             ->add('first_name', TextType::class, [
  24.                 'data' => 'Jean', // valeur par défaut
  25.                 'label' => 'Prénom :'
  26.             ])
  27.             ->add('submit', SubmitType::class, ['label' => 'Soumettre'])
  28.             ->getForm();
  29.  
  30.         return $this->render('music/form.html.twig', [
  31.             'myForm' => $myForm->createView()
  32.         ]);
  33.     }
  34.  
  35. }
  36.  
  37. ?>
  38.  

On termine en appelant la fonction getForm().

Puis, il faut afficher le formulaire dans une page, en passant en paramère \$myForm->createView().

  1. {% extends "base.html.twig" %}
  2.  
  3. {% block stylesheets %}
  4.     <link href="{{ asset('css/main.css') }}" rel="stylesheet">
  5. {% endblock %}
  6.  
  7.  
  8. {% block body %}
  9.  
  10. <div id="contents">
  11.  
  12. <h1>Formulaire</h1>
  13.  
  14. {{ form( myForm ) }}
  15.  
  16. </div>
  17. {% endblock %}
  18.  
  19.  

1.8.2. Prise en compte des données saisies

On utilise \$myForm->handleRequest(\$request); pour traiter les données et remplir le formulaire

Si on veut envoyer les données vers la base de données, il faudra vérifier que les données ont été soumises et sont valides :

  1. <?php
  2.  
  3. namespace App\Controller;
  4.  
  5. use Twig\Environment;
  6. use Symfony\Component\HttpFoundation\Request;
  7. use Symfony\Component\HttpFoundation\Response;
  8. use Symfony\Component\Routing\Annotation\Route;
  9. use Symfony\Component\Form\Extension\Core\Type\TextType;
  10. use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
  11. use Symfony\Component\Form\Extension\Core\Type\SubmitType;
  12. use Symfony\Component\Form\Extension\Core\Type\CountryType;
  13. use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
  14. use Symfony\Component\Form\Extension\Core\Type\DateType;
  15. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  16.  
  17. class FormController extends AbstractController
  18. {
  19.     // ....
  20.    
  21.     #[Route('/form2')]
  22.    public function index2(Request $request): Response
  23.     {
  24.  
  25.         if ($request->getMethod() == "POST") {
  26.             dump($request);
  27.         }
  28.  
  29.         $myForm = $this->createFormBuilder()
  30.             ->add('first_name', TextType::class, [
  31.                 'data' => 'Jean', // valeur par défaut
  32.                 'label' => 'Prénom :'
  33.             ])
  34.             ->add('submit', SubmitType::class, ['label' => 'Soumettre'])
  35.             ->getForm();
  36.  
  37.         // gérer les données soumises
  38.         $myForm->handleRequest($request);
  39.  
  40.         // validation
  41.         if ($myForm->isSubmitted() && $myForm->isValid()) {
  42.             dd($myForm->getData());
  43.             // sauvegarder dans la base de données par exemple
  44.         }
  45.  
  46.         return $this->render('music/form.html.twig', [
  47.             'myForm' => $myForm->createView()
  48.         ]);
  49.     }
  50.    
  51. }
  52. ?>
  53.  

1.8.3. Utilisation d'une classe pour stocker et saisir les données

Soit une classe personne composée de deux champs : first_name et genre (sexe) :

  1. <?php
  2.  
  3. class Person
  4. {
  5.     public string $first_name;
  6.     // sexe : masculin => true, féminin => false
  7.     public bool $genre;
  8.  
  9.     public function __construct($first_name, $genre)
  10.     {
  11.         $this->first_name = $first_name;
  12.         $this->genre = $genre;
  13.     }
  14. }
  15.  
  16. ?>
  17.  

Pour gérer cette structure, il faut en créer une instance et faire en sorte que les champs à saisir aient le même nom que les champs de la classe.

Si on désire saisir un champ mais qu'il ne fasse pas partie de la classe il faut utiliser l'attribut 'mapped' et le positionner à false, c'est le cas du champ 'Commentaire'.

  1. <?php
  2.  
  3. namespace App\Controller;
  4.  
  5. // ...
  6.  
  7. class FormController extends AbstractController
  8. {
  9.     // ...
  10.  
  11.     #[Route('/form3')]
  12.    public function index3(Request $request): Response
  13.     {
  14.         $person = new Person( "Jean", true);
  15.  
  16.         $myForm = $this->createFormBuilder($person)
  17.             ->add('first_name', TextType::class, [
  18.                 'data' => 'Jean', // valeur par défaut
  19.                 'label' => 'Prénom :'
  20.             ])
  21.             ->add('genre', CheckboxType::class, [
  22.                 'label' => 'Masculin',
  23.                 'required' => false
  24.             ])
  25.             ->add('Commentaire', TextType::class, [
  26.                 'mapped' => false
  27.             ])
  28.             ->add('submit', SubmitType::class, ['label' => 'Soumettre'])
  29.             ->getForm();
  30.  
  31.         $myForm->handleRequest($request);
  32.  
  33.         if ($myForm->isSubmitted() && $myForm->isValid()) {
  34.             dump($request);
  35.             dd($person);
  36.         }
  37.  
  38.         return $this->render('music/form.html.twig', [
  39.             'myForm' => $myForm->createView()
  40.         ]);
  41.     }
  42.  
  43.  
  44. }
  45. ?>
  46.  

1.8.4. Autres types de champs

On peut manipuler une liste de Pays, des Date / DateTime, des checkbox, des radio button, etc. Voici un petit exemple :

  1. <?php
  2.  
  3. namespace App\Controller;
  4.  
  5. // ...
  6.  
  7. class FormController extends AbstractController
  8. {
  9.     // ...
  10.  
  11.    
  12.  
  13.     #[Route('/form4')]
  14.    public function index4(Request $request): Response
  15.     {
  16.         $myForm = $this->createFormBuilder()
  17.             ->add('Genre', ChoiceType::class, [
  18.                 'choices' => [
  19.                     'Masculin' => 0,
  20.                     'Féminin' => 1
  21.                 ]
  22.             ])
  23.             ->add('Choix', ChoiceType::class, [
  24.                 'choices' => [
  25.                     'choix 1' => 1,
  26.                     'choix 2' => 2,
  27.                     'choix 3' => 3
  28.                 ],
  29.                 'expanded' => true,
  30.                 'multiple' => true
  31.             ])
  32.             ->add('Pays', CountryType::class, [
  33.                 'mapped' => false
  34.             ])
  35.             ->add('date', DateType::class, [
  36.                 'widget' => 'single_text'
  37.             ])
  38.             ->getForm();
  39.  
  40.         $myForm->handleRequest($request);
  41.  
  42.         return $this->render('music/form.html.twig', [
  43.             'myForm' => $myForm->createView()
  44.         ]);
  45.     }
  46. }
  47. ?>
  48.  

Exercice 1.1

Créer un formulaire qui permet de saisir les informations suivantes :

  • Nom et prénom d'une personne
  • Date de naissance
  • email
  • mot de passe
  • Statut marital : célibataire, marié, divorcé, veuf (exclusif)
  • Fast-foods préférés (plusieurs choix possibles) : Mc Donald, KFC, Taco Bell, TGI Friday's, Wendy's

1.9. Doctrine pour l'ORM

On peut suivre la documentation Doctrine de Symfony pour cette partie ou suivre ce qui suit.

Pour installer MySQL 8.0, on peut suivre le tutoriel : MySQL how to install on Ubuntu. Personnellement, j'ai suivi la procédure suivante car Symfony refusait de créer une base de données en utilisant le compte root.

En utilisant le compte root, on peut créer une base de données ccp (Client Command Product) et un utilisateur qui pourra agir sur cette base de données :

mysql> create database ccp;
mysql> create user 'ccp_user'@'localhost' identified by 'ccp_user_pwd';
mysql> grant all privileges on ccp.* to 'ccp_user'@'localhost';
mysql> flush privileges;

Pour rappel l'ORM ou Object Relational Mapping consiste à transformer les tables du modèle relationnel à des classes dans un langage object (tels PHP, Java ou C++).

On peut réaliser cela avec PDO met on ne chargera que les enregistrements d'une table client par exemple. L'ORM permet de charger les clients et à partir d'un client, d'obtenir une ou des commandes, et à partir d'une commande, d'obtenir les produits commandés.

Doctrine définit deux classes :

Pour installer Doctrine, si celui-ci n'est pas installé, il suffit de saisir dans la console :

symfony composer require orm

1.9.1. Création de la base

Il faut commencer par configurer l'accès à la base de données en modifiant le fichier .env :

...
DATABASE_URL="mysql://root:tiger@127.0.0.1:3306/ccp?serverVersion=8&charset=utf8mb4"
...

Ici, on utilise MySQL sur le port 3306. Il s'agit d'une installation Docker pour laquelle le mot de passe du root est tiger. Le nom de la base de données est ccp.

On lance ensuite la création de la base :

symfony console doctrine:database:create

1.9.2. Création d'entité

Pour créer une nouvelle entité (class / table), on utilise la commande suivante :

symfony console make:entity

 Class name of the entity to create or update (e.g. OrangePuppy):
 > Client

 created: src/Entity/Client.php
 created: src/Repository/ClientRepository.php
 
 Entity generated! Now let's add some fields!
 You can always add more fields later manually or by re-running this command.

 New property name (press  to stop adding fields):
 > name

 Field type (enter ? to see all types) [string]:
 > 

 Field length [255]:
 > 30

 Can this field be null in the database (nullable) (yes/no) [no]:
 > no

 updated: src/Entity/Client.php

 Add another property? Enter the property name (or press  to stop adding fields):
 > 

  Success! 

 Next: When you're ready, create a migration with php bin/console make:migration

On a ainsi créé :

On peut modifier name, length, precision, nullable, ...

1.9.3. Migration

Il faut ensuite réaliser une migration, c'est à dire répercuter les modifications au niveau du serveur de la base de données :

symfony console make:migration

On peut ensuite vérifier que la modification a été effectuée :

richer@zentopia\$ mysql -h 127.0.0.1 -P 3306 -u root -p 
mysql> use ccp
Database changed
mysql> show tables;
+-----------------------------+
| Tables_in_ccp               |
+-----------------------------+
| client                      |
| doctrine_migration_versions |
| messenger_messages          |
+-----------------------------+
3 rows in set (0,00 sec)

mysql> describe client;
+-------+-------------+------+-----+---------+----------------+
| Field | Type        | Null | Key | Default | Extra          |
+-------+-------------+------+-----+---------+----------------+
| id    | int         | NO   | PRI | NULL    | auto_increment |
| name  | varchar(30) | NO   |     | NULL    |                |
+-------+-------------+------+-----+---------+----------------+
2 rows in set (0,01 sec)

1.9.4. Création d'une instance d'entité : persistance

On demande la création d'un formulaire pour un client :

richer@zentopia\$ symfony console make:form

 The name of the form class (e.g. OrangePopsicleType):
 > ClientForm

 The name of Entity or fully qualified model class name that the new form will be bound to (empty for none):
 > Client

 created: src/Form/ClientType.php

           
  Success! 
           

 Next: Add fields to your form and start using it.
 Find the documentation at https://symfony.com/doc/current/forms.html

Symfony crée un fichier src/Form/ClientFormType.php

On crée également un contrôleur pour gérer cette forme :

richer@zentopia\$ symfony console make:controller

 Choose a name for your controller class (e.g. GrumpyElephantController):
 > ClientFormController

 created: src/Controller/ClientFormController.php
 created: templates/client_form/index.html.twig

           
  Success! 
           

 Next: Open your new controller class and add some pages!

Le code du contrôleur est donc le suivant :

  1. <?php
  2.  
  3. namespace App\Controller;
  4.  
  5. use App\Entity\Client;
  6. use App\Form\ClientFormType;
  7. use Symfony\Component\HttpFoundation\Request;
  8. use Symfony\Component\HttpFoundation\Response;
  9. use Symfony\Component\Routing\Annotation\Route;
  10. use Symfony\Component\Form\Extension\Core\Type\SubmitType;
  11. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  12. use Doctrine\ORM\EntityManagerInterface;
  13.  
  14.  
  15. class ClientFormController extends AbstractController
  16. {
  17.     #[Route('/client/form', name: 'app_client_form')]
  18.    public function index(Request $request, EntityManagerInterface $entityManager): Response
  19.     {
  20.         // création d'un client
  21.         $client = new Client();
  22.  
  23.         // création du formulaire lié au client avec ajout d'un bouton
  24.         $clientForm = $this->createForm(ClientFormType::class, $client);
  25.         $clientForm->add('submit', SubmitType::class, ['label' => 'Soumettre']);
  26.  
  27.         // gestion des paramètres en entrée
  28.         $clientForm->handleRequest($request);
  29.  
  30.         // si on soumet le formulaire et qu'il est valide
  31.         // on enregistre le client
  32.         if ($clientForm->isSubmitted() && $clientForm->isValid()) {
  33.  
  34.             $entityManager->persist($client);
  35.             $entityManager->flush();
  36.         }
  37.  
  38.         return $this->render('client_form/index.html.twig', [
  39.             'clientForm' => $clientForm->createView()
  40.         ]);
  41.     }
  42. }
  43.  

A partir d'un EntityManager passé en paramètre, on insère les données dans la base de données grâce à la fonction persist(). Puis la fonction flush() réalise la série d'actions à répercuter sur la base de données. Ici il n'y en a qu'une seule.

1.9.5. Récupération des données

Pour obtenir des données depuis la base de données il faut utiliser un Repository, celui-ci a été créé lors de la création d'une entité.

A partir de la route client/list

  1. <?php
  2.  
  3. namespace App\Controller;
  4.  
  5. use App\Entity\Client;
  6. use App\Form\ClientFormType;
  7. use Symfony\Component\HttpFoundation\Request;
  8. use Symfony\Component\HttpFoundation\Response;
  9. use Symfony\Component\Routing\Annotation\Route;
  10. use Symfony\Component\Form\Extension\Core\Type\SubmitType;
  11. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  12. use Doctrine\ORM\EntityManagerInterface;
  13.  
  14. // !!!!!!!!!!!!!!!!!!!!!!!!!!!!
  15. // !!! Repository à inclure !!!
  16. // !!!!!!!!!!!!!!!!!!!!!!!!!!!!
  17. use App\Repository\ClientRepository;
  18.  
  19.  
  20. class ClientFormController extends AbstractController
  21. {
  22.     // ... code précédent ...
  23.    
  24.    
  25.     #[Route('/client/list')]
  26.    public function listClients(Request $request, ClientRepository $repo)
  27.     {
  28.         // on utilise la méthode findAll pour récupérer tous les clients
  29.         $clientsList = $repo->findAll();
  30.  
  31.         // on les affiche
  32.         return $this->render('client_form/list.html.twig', [
  33.             'clientsList' => $clientsList
  34.         ]);
  35.     }
  36.    
  37. }
  38.  

Le rendu par Twig peut être le suivant :

  1. {% extends "base.html.twig" %}
  2.  
  3. {% block stylesheets %}
  4.     <link href="{{ asset('css/main.css') }}" rel="stylesheet">
  5. {% endblock %}
  6.  
  7.  
  8. {% block body %}
  9.  
  10. <div id="contents">
  11.  
  12. <h1>Liste Clients</h1>
  13.  
  14. <ul>
  15. {%for client in clientsList %}
  16.     <li>{{ client.id }}, {{ client.name }}</li>
  17. {%endfor %}
  18. </ul>
  19.  
  20. </div>
  21. {% endblock %}
  22.  

Il existe bien d'autres fonctions comme :

Par exemple, on peut écrire les critères suivants pour obtenir tous les produits qui sont des claviers et les trier suivant leur prix en ordre croissant :

\$products = \$repository->findBy(
    ['name' => 'Keyboard'],
    ['price' => 'ASC']
);

1.9.6. Modification d'un objet

Voir la documentation de Doctrine : Updating an object.

1.9.7. Suppression d'un objet

Voir la documentation de Doctrine : Deleting an object.

Exercice 1.2

Mettre en place des routes pour ajouter / modifier / supprimer / donner la liste de clients dont les champs sont :

  • Nom et Prénom
  • Date de naissance
  • email
  • mot de passe

1.9.8. Relations

Il est possible de créer des relations entre les entités qui correspondent aux relations entre entités dans la base de données. Il suffit de définir un champ avec un type relation et de qualifier cette relation sous une des formes : OneToOne, OneToMany, ManyTiOne, ManyToMany.