Symfony 4 : faire une formulaire avec soumission (sans BDD)

Temps de lecture: 9 minutes

Maintenant que vous avez réussi à faire votre première page web avec Symfony 4, nous allons faire ce que toute application web utilise : les formulaires. Nous allons faire un formulaire avec la page de formulaire, et la page après la soumission de formulaire. Nous allons aussi mettre en place la vérification des données, côté client avec HTML5 mais aussi côté serveur, ce qui constitue une double vérification.

Installer les Forms

Tapez la ligne de commande suivante pour installer les packages pour gérer les formulaire

composer require symfony/form

Restricting packages listed in "symfony/symfony" to "4.3.*"
./composer.json has been updated
Restricting packages listed in "symfony/symfony" to "4.3.*"
Loading composer repositories with package information
Updating dependencies (including require-dev)

Prefetching 5 packages 🎵 💨
  - Downloading (100%)

Package operations: 6 installs, 0 updates, 0 removals
  - Installing symfony/inflector (v4.3.1): Loading from cache
  - Installing symfony/property-access (v4.3.1): Loading from cache
  - Installing symfony/options-resolver (v4.3.1): Loading from cache
  - Installing symfony/intl (v4.3.1): Loading from cache
  - Installing symfony/polyfill-intl-icu (v1.11.0): Loading from cache
  - Installing symfony/form (v4.3.1): Loading from cache
Writing lock file
Generating autoload files
ocramius/package-versions:  Generating version class...
ocramius/package-versions: ...done generating version class
Executing script cache:clear [OK]
Executing script assets:install public [OK]

Dans le répertoire vendor/ il y aura six créations de répertoire dont un pour le bundle Form. Dans le fichier composer.json cependant il n’y a qu’une seule ligne ajoutée dans l’attribut "require" .

Création des fichiers nécessaires à une page web de type formulaire

Nous devons créer une classe PHP qui décrit l’entité à persister, dans notre cas nous allons créer une page pour entrer des tâches à faire. Donc on va créer une classe Task, qu’on va placer dans le répertoire Entity, ensuite on doit créer un controller qui sera chargé de montrer le formulaire, on va voir aussi qu’il faut créer une classe pour le formulaire !

# src/Entity/Task.php
namespace App\Entity;

class Task
{
    protected $task;
    protected $dueDate;

    public function getTask()
    {
        return $this->task;
    }

    public function setTask($task)
    {
        $this->task = $task;
    }

    public function getDueDate()
    {
        return $this->dueDate;
    }

    public function setDueDate(\DateTime $dueDate = null)
    {
        $this->dueDate = $dueDate;
    }
}

Remarquez le namespace, il vous indique où se trouve le fichier, dans le répertorie Entity/, cette classe est un objet PHP, il sert simplement à contenir des informations. Maintenant faisons le controller qui va être responsable du traitement de la requête et de l’affichage du formulaire dans un template Twig.

# src/Controller/TaskController.php
namespace App\Controller;

use App\Entity\Task;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\HttpFoundation\Request;

class TaskController extends AbstractController
{
    public function new(Request $request)
    {
        // creates a task and gives it some dummy data for this example
        $task = new Task();
        $task->setTask('Write a blog post');
        $task->setDueDate(new \DateTime('tomorrow'));

        $form = $this->createFormBuilder($task)
            ->add('task', TextType::class)
            ->add('dueDate', DateType::class)
            ->add('save', SubmitType::class, ['label' => 'Create Task'])
            ->getForm();

        return $this->render('task/new.html.twig', [
            'form' => $form->createView(),
        ]);
    }
}

On instancie l’objet $form avec la méthode createFormBuilder en y passant en paramètre l’entité Task, et on y ajoute tous les champs, dans le return , on appelle la fonction render avec en paramètre un nom de template qui se trouve dans le répertoire task. On n’a pas encore installé Twig, committons les fichiers et faisons l’installation avec la commande suivante :

composer require twig
Package operations: 3 installs, 0 updates, 0 removals
  - Installing twig/twig (v2.11.2): Loading from cache
  - Installing symfony/twig-bridge (v4.3.1): Loading from cache
  - Installing symfony/twig-bundle (v4.3.1): Loading from cache
Writing lock file
Generating autoload files
ocramius/package-versions:  Generating version class...
ocramius/package-versions: ...done generating version class
Symfony operations: 1 recipe (a4e1f1d1971a33f26dd306c75c720e18)
  - Configuring symfony/twig-bundle (>=3.3): From github.com/symfony/recipes:master

Nous remarquons qu’une seule ligne s’est ajoutée dans le fichier composer.json , mais qu’il y a en fait 3 bundles téléchargés, et que le fichier config/bundles.php est mis à jour avec une ligne, enfin il y a un répertoire templates/ qui est créé, parfait pour continuer notre tuto.

Ajouter la route dans routes.yaml

app_form:
    path: /addtask
    controller: App\Controller\TaskController::new

Vérifiez qu’elle est bien présente avec cette commande : php bin/console debug:route . En essayant d’aller à la page /addtask nous avons le message d’erreur suivant :

An exception has been thrown during the rendering of a template ("You cannot use the "Symfony\Bridge\Twig\Extension\TranslationExtension" if the Translation Contracts are not available. Try running "composer require symfony/translation".").

Il faut installer le package symfony/translation . Personnellement je trouve un peu agaçant d’être confronté à ce genre de chose, surtout pour un débutant, ça peut rebuter.

composer require symfony/translation

Package operations: 2 installs, 0 updates, 0 removals
  - Installing symfony/translation-contracts (v1.1.2): Loading from cache
  - Installing symfony/translation (v4.3.1): Loading from cache
Writing lock file
Generating autoload files
ocramius/package-versions:  Generating version class...
ocramius/package-versions: ...done generating version class
Symfony operations: 1 recipe (b42ee68b7a5958896d5e0e4d90c3222a)
  - Configuring symfony/translation (>=3.3): From github.com/symfony/recipes:master

Notez la création du répertoire translations/ . Cette fois-ci nous avons un formulaire très sommaire qui s’affiche :

Utiliser le template de base Twig pour un design cohérent

Il existe un template de base duquel doit hériter les autres templates, il s’appelle base.html.twig, dans notre nouveau template nous allons faire hériter de ce template de base. Voici le contenu de base.html.twig suivi de index.html.twig .

{% extends "base.html.twig" %}
{% block body %}
    <h1>Add a task</h1>
    <p class="important">
        Please add a task via this form.
    </p>
{{ form(form) }}
{% endblock %}

Bien que base.html.twig se trouve un niveau plus haut dans l’arbre des répertoires, Symfony arrive à le retrouver.

Les formulaires Twig en plus détaillés

Il y a plusieurs degrés de détail lorsqu’on programme un form dans une vue TWIG. Nous aurions pu écrire d’une façon plus détaillées donc plus personnalisable de cette façon :

{{ form_start(form) }}
    <div class="my-custom-class-for-errors">
        {{ form_errors(form) }}
    </div>

    <div class="row">
        <div class="col">
            {{ form_row(form.task) }}
        </div>
        <div class="col" id="some-custom-id">
            {{ form_row(form.dueDate) }}
        </div>
    </div>
{{ form_end(form) }}

Nous pouvons aller encore plus dans le détail, voici une image extraite de la documentation officielle de Symfony:

Les fonctions ci-dessus sont des fonctions Twig, vous permettant de faire des réglages très fins pour l’affichage.

<div class="form-control">
    <i class="fa fa-calendar"></i> {{ form_label(form.dueDate) }}
    {{ form_widget(form.dueDate) }}

    <small>{{ form_help(form.dueDate) }}</small>

    <div class="form-error">
        {{ form_errors(form.dueDate) }}
    </div>
</div>

Il est nécessaire de travailler de cette façon pour avoir un design de site consistant sur toutes les pages. A ce stade nous n’avons pas encore la prise en charge de la soumission du formulaire (pas de page qui s’occupe de lire les données passées en POST) mais surtout on n’a pas encore la validation du formulaire. Nous allons installer un package pour prendre en charge ceci :

composer require symfony/validator

Restricting packages listed in "symfony/symfony" to "4.3.*"
./composer.json has been updated
Restricting packages listed in "symfony/symfony" to "4.3.*"
Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 1 install, 0 updates, 0 removals
  - Installing symfony/validator (v4.3.1): Downloading (100%)         
Writing lock file
Generating autoload files
ocramius/package-versions:  Generating version class...
ocramius/package-versions: ...done generating version class
Symfony operations: 1 recipe (1718218f44507b3c4e9de7f6d446ef3a)
  - Configuring symfony/validator (>=4.3): From github.com/symfony/recipes:master

Comment fonctionne la validation dans Symfony 4?

Elle fonctionne en mettant dans la classe de l’entité (Task dans notre cas) des contraintes, sous forme d’annotations. Qu’est-ce qu’une annotation? C’est un commentaire un peu spécial, qui commence par un @ suivi d’une instruction et de paramètre. Comment est interprétée l’instruction dépend de qui processe l’annotation. Dans le fichier controller, les routes vers une action qui sont spécifiée dans le fichier routes.yaml peuvent être remplacées par une annotation. Par exemple la route vers le formulaire peut être indiqué par l’annotation :

use Symfony\Component\Routing\Annotation\Route;

class TaskController extends AbstractController
{
    /**
     * @Route("/addtask", name="addtask")
     */
    public function new(Request $request)
    {
        // creates a task and gives it some dummy data for this example
        $task = new Task();
        $task->s

Cette possibilité d’annotation n’est pas construite dans le core de Symfony 4 mais est activé par le composant Routing\Annotation\Route . Il est à noter que les annotations de Doctrine ne sont pas responsables des routes, mais pour l’ORM (le mapping des entités (classes PHP) avec les tables de la base de données). Auparavant les annotations de routes étaient faites par SensioFrameworkExtraBundle. Nous verrons les annotations de Doctrine quand nous aborderons la persistence de données dans la base de données avec Symfony 4.

D’ors et déjà nous pouvons vérifier que le composant est oui ou non installé en creusant le répertoire vendor/, on va pouvoir retrouver cette classe qui est responsable des annotations. Ainsi si dans le fichier TaskController on ajoute la clause use pour importer la classe :

use Symfony\Component\Routing\Annotation\Route;

Il suffit de suivre le long du nom vers Route dans l’arborescence des répertoires pour retrouver le fichier Route.php

vendor/
    └─ symfony/
        └─ routing/
            └─ Annotation/
                └─ Route.php

Soumission du formulaire

Mais nous n’avons encore rien fait pour traiter la soumission du formulaire, avant d’aller plus en avant dans la méthode de conception du formulaire, nous allons soumettre les donnée à une action dans le controller et les afficher sur une autre page.

Ajoutons quelques lignes dans le controller en cas de succès

class TaskController extends AbstractController
{
    public function new(Request $request)
    {
		...
    }

    /**
     * @Route("/success", name="success_path_name")
     */
    public function submissionSuccess(){
        return $this->render('task/success.html.twig', [
            'message' => 'Enregistrement fait avec succès !'
        ]);
    }
}

Pour vérifier que la route a été bien prise en compte, faites la commande suivante :

php bin/console debug:route

------------------- -------- -------- ------ -------------------------- 
  Name                Method   Scheme   Host   Path                      
 ------------------- -------- -------- ------ -------------------------- 
  _twig_error_test    ANY      ANY      ANY    /_error/{code}.{_format}  
  success_path_name   ANY      ANY      ANY    <href=>/success           
  app_contact         ANY      ANY      ANY    <href=>/contact           
  app_form            ANY      ANY      ANY    <href=>/addtask           
 ------------------- -------- -------- ------ --------------------------

Le nom de la route est bien là ! Remarquez les colonnes Method et Name, vous pouvez accéder à une route dans un controller avec le Name, et en ce qui concerne la colonne Method, c’est pour spécifier le verbe HTTP par exemple ( GET, POST, PUT…), chaque méthode peut accepter plusieurs verbe. Attention aux slash dans les routes, suivant comment est paramétré Apache, un slash manquant peut amener à un échec de résolution d’adresse url.

Dans la fonction new du controller, nous devons gérer les données envoyées :

    public function index(Request $request)
    {
       // initialise le formulaire avec des données
        $task = new Task();
        $task->setTask('Write a blog post');
        $task->setDueDate(new \DateTime('tomorrow'));
        // crée le formulaire
        $form = $this->createFormBuilder($task)
            ->add('task', TextType::class)
            ->add('dueDate', DateType::class)
            ->add('save', SubmitType::class, ['label' => 'Create Task'])
            ->getForm();

        // Ne pas oublier cette ligne sinon $form->isSubmitted() retourne false !
        $form->handleRequest($request);
        // Si le formulaire est soumis et qu'il est valide
        if ($form->isSubmitted() && $form->isValid()) {
            $task = $form->getData();
            return $this->render('task/success.html.twig', [
                'task' => $task
            ]);
        }

        return $this->render('task/index.html.twig', [
            'form' => $form->createView()
        ]);
    }

Créez un template success.html.twig , avec le code suivant :

{% extends "base.html.twig" %}
{% block body %}
    <h1>Formulaire ajouté avec succès</h1>
    <p class="important">
        Task :{{ task.task }} <br>
        Date execution : {{ task.dueDate|date('Y-m-d') }}
    </p>
    
{% endblock %}

On a appelé la fonction render() du controller, en passant la variable $task dans un tableau. Félicitation vous avez fait votre première soumission de formulaire ! Nous allons améliorer un petit peu ce formulaire en créant une validation pour le formulaire

Symfony 4 : validation de formulaire

Supposons maintenant que le nom de Task ne peut être vide, ainsi que la date, comment allons nous ajouter des contraintes ? Le composant Validator nous permet d’ajouter des annotations pour ajouter des contraintes. Par défaut le formulaire contient déjà une validation HTML5 :

Mais nous pouvons faire plus en mettant des annotations :

# src/Entity/Task.php
use Symfony\Component\Validator\Constraints as Assert;

class Task
{
    /**
     * @Assert\NotBlank
     */
    protected $task;

    /**
     * @Assert\NotBlank
     * @Assert\Type("\DateTime")
     */
    protected $dueDate;

    ...
}

Si vous soumettez un champ vide vous aurez une erreur, même chose pour la date si elle n’est pas au bon format, c’est à dire si vous entrez une chaine de caractère quelconque (ceci ne devrai pas arriver car le composant saisie de date est bien contraint). Vous remarquerez que la validation est de type HTML5. Vou avez la possibilité de ne pas opter pour la validation HTML5, dans ce cas il faut un attribut novalidate. Notez que le champs Date a changé d’aspect :

Symfony 4 : Ajouter une classe de formulaire

Afin de rendre le code plus modulaire nous créerons une classe pour le formulaire

namespace App\Form;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormBuilderInterface;

class TaskType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('task')
            ->add('dueDate', null, ['widget' => 'single_text'])
            ->add('save', SubmitType::class)
        ;
    }
}

Du coup nous pouvons remplacer la création du formulaire dans le controller qui prenait 4 lignes en une seule ligne, une autre différence qui est fondamentale, est que le code est plus typé, c’est à dire qu’il y a plus de classes introduite dans le projet, un formulaire qui était directement ajouté en « dur » dans le code est maintenant géré avec une classe.

$form = $this->createForm(TaskType::class, $task);

Voilà vous avez maintenant une soumission de formulaire qui marche ! Voyons dans la prochaine étape comment on peut persister des données dans la base de données.

Twig, des commandes à connaitre pour débugger

dump(form)
#accéder depuis Twig à des valeurs du formulaire (très utile)
form.vars.value.title
#vous pouvez faire un dump(form.vars) pour en voir le contenu
#si vous avez un sous-formulaire, ou un champ spécifique, vous pouvez y accéder directement sous la forme
form.NOMDUSOUSFORM.vars.value.CHAMP
#voir https://stackoverflow.com/questions/12497133/directly-access-a-form-fields-value-when-overriding-widget-in-a-twig-template

Les formulaires non liés à une entité

Quelquefois vous aurez besoin de faire un formulaire non lié à une entité. Cette manip est assez simple encore faudrait-il savoir comment dans le royaume de Symfony on le fait (soumission de formulaire validation ou non validation, comment lire les valuers soumise, tout ceci est expliqué dans le post consacré aux formulaire non liés.