Saluuut, bienvenue sur cette série de tutoriels sur comment créer un blog avec Symfony 4. Dans les précédentes partie, nous avons créer nos vues, la base de données, maintenant nous allons apprendre à ajouter un article dans la base de données, donc à interagir avec la base de données avec un formulaire. Aller c'est parti.

Ce tutoriel était vraiment long, j'ai donc dȗ le mettre en deux parties. La deuxième partie sera publier dans exactement une semaine.

Les formulaires: des classes

En Symfony, pour définir un formulaire, nous allons utiliser une classe. Il est bien possible de construire un formulaire sans passer par une classe, directement dans le contrôleur, mais nous n'allons pas voir cette méthode ici. Pour des raisons de modularité, nous allons définir nos formulaires dans des classes, cela nous permettra de réutiliser la classe (le formulaire) où l'on veut, c'est à dire dans n'importe quel contrôleur juste en définissant une instance de la classe du formulaire. C'est d'ailleurs l'un des intérêts de définir ces formulaires comme étant des classes.

Quand on utilise les formulaires en Symfony, nous passons par trois étapes: d'abord on construit le formulaire (dans le contrôleur ou en lui définissant une classe), on affiche le formulaire dans une vue, puis on traite le formulaire pour valider les données et exécuter les actions demandées par l'utilisateur (ajouter un article par exemple).

Créer un formulaire

Nous allons créer notre premier formulaire, à savoir le formulaire pour l'entité Article. La première chose à retenir c'est que chaque formulaire que nous allons créer sera rattacher à une entité.

Pour rappel, voici à quoi ressemble l’entité Article

<?php

namespace App\Entity;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity(repositoryClass="App\Repository\ArticleRepository")
 */
class Article
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\Column(type="string", length=255, nullable=true)
     */
    private $picture;

    /**
     * @ORM\Column(type="string", length=255)
     */
    private $title;

    /**
     * @ORM\Column(type="text", nullable=true)
     */
    private $content;

    /**
     * @ORM\Column(type="datetime", nullable=true)
     */
    private $publicationDate;

    /**
     * @ORM\Column(type="datetime")
     */
    private $lastUpdateDate;

    /**
     * @ORM\Column(type="boolean")
     */
    private $isPublished;

    /**
     * @ORM\ManyToMany(targetEntity="App\Entity\Category", inversedBy="articles")
     */
    private $categories;

    public function __construct()
    {
        $this->categories = new ArrayCollection();
    }

    public function getId(): ?int
    {
        return $this->id;
    }

    // ...
}

Maintenant nous allons créer le formulaire, pour cela, vous vous rappelez de ces commandes de Symfony qui nous font autant de bien? Eh ben nous allons utiliser une commande, ouvrez donc un terminal et rentrer la commande:

$ php bin/console make:form

puis appuyer sur Entrer, et là vous allez devoir rentrer le nom du formulaire (de la classe). Pour nominer les classes de formulaire, nous allons juste prendre le nom de l’entité: Article. puis ajouter le suffixe Type ce qui va donner ArticleType. Si l’entité s'appelle Category, le nom du formulaire va donc être CategoryTypePicture va donner PictureType et ainsi de suite, compris? Super.

Vous saisissez donc ArticleType puis Entrer. Maintenant on vous demande le nom de l’entité à la quelle sera rattacher ce formulaire, on en a parler, chaque formulaire doit être rattacher à une entité. Notre entité c'est donc Article puis Entrer et c'est fini.

Créer la classe du formulaire avec make:form
Créer la classe du formulaire avec make:form

Nous pouvons maintenant ouvrir notre classe dans src/Form/ArticleType.php

<?php

namespace App\Form;

use App\Entity\Article;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class ArticleType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('picture')
            ->add('title')
            ->add('content')
            ->add('publicationDate')
            ->add('lastUpdateDate')
            ->add('isPublished')
            ->add('categories')
        ;
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'data_class' => Article::class,
        ]);
    }
}

Un peu d'explications s'impose là.

Bon voilà ce qui s'est passer, nous avons demander à créer un formulaire pour l'entité Article, une classe ArticleType a donc été créée, cette classe hérite de la classe AbstractType de Symfony et définit deux méthodes:

  • La premiere méthode buildForm() reçoit deux paramètres, dont le $builder qui va nous permettre de définir les champs de notre formulaire. Par défaut toutes les attributs de l’entité Article sont présents. Et dites vous que chaque attribut que nous avons défini comme étant une colonne est considéré comme champs dans le formulaire à moins que nous décidons de le retirer nous même.
  • La deuxième méthode va au fait nous permettre d'explicitement définir la classe à la qu'elle est rattachée ce formulaire. Ce n'est pas obligatoire, mais c'est bien de le faire.

Notre formulaire est presque prêt, nous allons d'abord commencer par retirer les champs dont on a pas besoin du builder, la date de publication et la date de dernière modification sont des champs qui sont générés automatiquement, pas besoin de les retrouver donc dans le formulaire:

<?php

namespace App\Form;

use App\Entity\Article;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class ArticleType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('picture')
            ->add('title')
            ->add('content')
            ->add('isPublished')
            ->add('categories')
        ;
    }

    // ...
}

Il faut ensuite définir pour chaque champ, son type. C'est simple le champ picture est de type fichier (FileType), le champ title est de type text (TextType). Vous allez retrouver tout les types disponibles sur la documentation de Symfony. Voici maintenant notre formulaire ajuster:

<?php

namespace App\Form;

use App\Entity\Article;
use App\Entity\Category;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class ArticleType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('picture', FileType::class)
            ->add('title', TextType::class)
            ->add('content', TextareaType::class)
            ->add('isPublished', CheckboxType::class)
            ->add('categories', EntityType::class, [
                'class' => Category::class,
                'choice_label' => 'label',
                'multiple' => true,
                'expanded' => false
            ])
        ;
    }

    // ...
}

La seule explication qu'il y a ici je pense c'est sur le champ categories. Que fait-on là? D'abord nous lui définissons comme étant un EntityType, au fait ce champ est une relation entre les entités Article et Category. Le champ categories est donc une EntityType parce qu'il va contenir un tableau d'entité Category, ensuite dans le tableau des options, on définit la classe dont il s'agit, l'attribut de la classe Category a afficher, si vous regardez bien l’entité Category, il n'a que deux attributs, id et label, nous choisissons donc l'attribut label parce qu'il est plus parlant que d'afficher juste l'id, ensuite vient deux paramètres un peu complexes, qui vont définir comment ce champ sera afficher, soit un champ select avec un attribut multiple, ou des checkbox (autant que le nombre de catégories). Moi j'ai choisi le select avec l'attribut multiple. D'abord, un article peut disposer de plusieurs categories, donc le multiple à true, ensuite je veux le champ select au lieu des checkbox (ça risque de devenir trop si on a beaucoup de catégories), je mets donc expanded à false, vous pouvez retrouver la documentation complète sur le site de Symfony.

Afficher le formulaire

Maintenant que nous avons le formulaire, il nous faut l'afficher dans la vue. Voici ce que nous allons faire, nous allons créer le formulaire dans le contrôleur, puis nous l'enverrons à la vue pour l'afficher.

Le contrôleur pour créer un article se trouve dans BlogController.php qui se trouve dans src/Controller/, c'est la méthode add(), nous allons modifier cette méthode comme ceci:

<?php

namespace App\Controller;

use App\Entity\Article;
use App\Form\ArticleType;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;

class BlogController extends AbstractController
{
    public function index()
    {
        return $this->render('blog/index.html.twig');
    }

    public function add()
    {
        $article = new Article();
        $form = $this->createForm(ArticleType::class, $article);

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

    // ...
}

Nous commençons d'abord par créer un objet de type Article, normal. Ensuite nous utilisons la méthode createForm() de la classe AbstractController dont notre contrôleur hérite pour créer le formulaire. En premier paramètre, nous l'envoyons l'EntityType sur le quel le formulaire est basé, puis nous lui envoyons l'objet qui va contenir les données. Enfin nous envoyons le formulaire à la vue. Voyons tout de suite comment afficher le formulaire dans la vue.

Afficher un formulaire

Pour afficher le formulaire, rendez-vous dans le fichier add.html.twig

{% extends "base.html.twig" %}

{% block title %}{{ parent() }} | Ajouter un article{% endblock %}

{% block content %}
	<div class="container">
		<div class="row">
			<div class="col s12 m12 l12">
				<h4>Ajout d'un article</h4>
			</div>
		</div>
		{% include "includes/article_form.html.twig" %}
	</div>
{% endblock %}

Ici nous incluons le fichier article_form.html.twig, c'est ce fichier qui contient le formulaire au fait, nous allons donc l'ouvrir

<form class="row">
	<div class="input-field col s12 m12 l12">
		<input type="file" name="">
	</div>
	<div class="input-field col s12 m6 l6">
		<input type="text" name="" placeholder="Titre de l'article">
	</div>
	<div class="input-field col s12 m6 l6">
		<input type="text" name="" placeholder="Catégories (php, html, css)">
	</div>
	<div class="input-field col s12 m12 l12">
		<textarea class="materialize-textarea" placeholder="Contenu de l'article"></textarea>
	</div>
	<div class="input-field col s12 m12 l12">
		<p>
			<label>
		        <input type="checkbox" class="filled-in" />
		        <span>Publier</span>
	      	</label>
      	</p>
	</div>
	<div class="input-field col s12 m12 l12">
		<input type="submit" name="" value="Enregistrer" class="btn btn-primary btn-large">
	</div>
</form>

Nous allons retirer tout son contenu et remplacer par le code suivant:

{{ form(form) }}

Aller afficher la page d'ajout d'un article que l'on avait créer ensemble http://127.0.0.1:8000/add et là, BIIIIIIIMMM, le formulaire s'affiche, tout les champs que nous avons mentionner dans ArticleType sont présents.

Si comme moi vous avez utiliser Materialize CSS, il doit y avoir un petit problème avec le champ catégorie qui est en effet un champ de type select. Pour remédier à cela, il faut modifier le bloc scripts dans le template de base base.html.twig comme ceci:

{% block scripts %}
	<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.0/jquery.min.js"></script>
	<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script>

	<script type="text/javascript">
		$('select').formSelect();
	</script>
{% endblock %}

Le seul problème c'est qu'il n'est pas afficher comme nous le voulons. Nous allons donc essayer de l'afficher comme nous le souhaitons, lui apporter le design de notre choix quoi. Pour cela nous allons utiliser des fonctions twig:

  • form_start() pour commencer le formulaire. Cette fonction va générer la balise <form> de HTML avec tout les attributs nameaction mais aussi l'enctype si notre formulaire doit traiter des images comme dans notre cas
  • form_end() pour fermer le formulaire avec la balise </form>
  • form_errors() permet d'afficher les erreurs du formulaire ou du champ mentionner
  • form_row() cette méthode permet d'afficher un champ de notre formulaire, incluant le label, le champ, le message d'erreur s'il y a erreur et le message d'aide
  • form_label() cette méthode affiche le label d'un champ
  • form_widget() affiche l'élément HTML correspondant au champ qu'il reçoit en paramètre (input, select, textarea, ...)
  • form_help() affiche un message d'aide correspondant à un champ

Voyons cette image qui se trouve sur le site de Symfony et qui résume un peu ces différentes fonctions:

Différentes fonctions pour afficher un formulaire en Twig. Source: symfony.com

Nous pouvons donc personnaliser notre formulaire. Le code est le suivant:

{{ form_start(form, {'attr': {'class': 'row'}}) }}
	<div class="input-field col s12 m12 l12">
		{{ form_widget(form.picture) }}
	</div>

	<div class="input-field col s12 m6 l6">
		{{ form_widget(form.title, {'attr': {'placeholder': 'Titre de l\'article'}}) }}
		{{ form_label(form.title) }}
	</div>

	<div class="input-field col s12 m6 l6">
		{{ form_widget(form.categories) }}
		{{ form_label(form.categories) }}
	</div>

	<div class="input-field col s12 m12 l12">
		{{ form_widget(form.content, {'attr': {'class': 'materialize-textarea', 'placeholder': 'Le contenu de l\'article'}}) }}
	</div>

	<div class="input-field col s12 m12 l12">
		<p>
			<label>
		        {{ form_widget(form.isPublished, {'attr': {'class': 'filled-in'}}) }}
		        <span>Publier</span>
	      	</label>
      	</p>
	</div>

	<div class="input-field col s12 m12 l12">
		<input type="submit" name="" value="Enregistrer" class="btn btn-primary">
	</div>
{{ form_end(form) }}

Et si vous actualisez maintenant la page, notre formulaire s'affiche bien. Bon nous avons seulement utiliser les fonctions et form_label() et form_widget(), sur chaque form_widget(), nous rajoutons des attributs pour la class et le placeholder. Il faut noter que nous ne sommes obliger de forcément mentionner toutes les fonctions form_...() cela va en effet dépendre de ce que vous voulez afficher, si vous ne voulez pas afficher le label sur vos formulaires, vous n'allez pas utiliser form_label(), pour afficher les erreurs, si l'utilisateur essaie de soumettre un article sans titre par exemple, vous utilisez form_errors().

Si vous affichez votre formulaire dans le navigateur maintenant vous verrez que tout s'affiche bien. Seul problème, les label Title et Categories sont en anglais, pour remédier à cela, il faut donner au form_label() le texte que vous voulez afficher en tant que label comme ceci:

{{ form_label(form.title, 'Le titre de votre article') }}

Notre fichier devra donc ressembler à ça:

{{ form_start(form, {'attr': {'class': 'row'}}) }}
	<div class="input-field col s12 m12 l12">
		{{ form_widget(form.picture) }}
	</div>

	<div class="input-field col s12 m6 l6">
		{{ form_widget(form.title, {'attr': {'placeholder': 'Titre de l\'article'}}) }}
		{{ form_label(form.title, 'Le titre de votre article') }}
	</div>

	<div class="input-field col s12 m6 l6">
		{{ form_widget(form.categories) }}
		{{ form_label(form.categories, 'Choisissez un ou plusieurs catégories') }}
	</div>

	<div class="input-field col s12 m12 l12">
		{{ form_widget(form.content, {'attr': {'class': 'materialize-textarea', 'placeholder': 'Le contenu de l\'article'}}) }}
	</div>

	<div class="input-field col s12 m12 l12">
		<p>
			<label>
		        {{ form_widget(form.isPublished, {'attr': {'class': 'filled-in'}}) }}
		        <span>Publier</span>
	      	</label>
      	</p>
	</div>

	<div class="input-field col s12 m12 l12">
		<input type="submit" name="" value="Enregistrer" class="btn btn-primary">
	</div>
{{ form_end(form) }}

Et voilà!

Le champ catégorie est vide, ce champ est rattacher à l'entité Category, s'il n'y a aucune catégorie enregistrer dans la base de données, ça ne va pas le faire. Je vais donc rajouter manuellement des catégories dans la table category de notre base de données. Je vous laisse en faire de même.

Si vous actualisez maintenant la page, dans le dropdown pour catégorie vous avez les catégories que vous avez enregistrer et vous pouvez en choisir plusieurs.

Vous pouvez sélectionner plusieurs catégories

Voilà nous avons maintenant un formulaire qui s'affiche bien, reste plus qu'à le remplir et enregistrer notre premier article. Cet article est devenu un peu long pour parler du traitement des formulaires ici, dans le prochain tutoriel, nous verrons comment traiter et valider notre formulaire puis enregistrer notre premier article en base de données. N'hésitez pas à poser vos questions dans les commentaires ci-dessous. Merci, à bientôt.


Partager cet article

alioukahere

Mamadou Aliou Diallo

@alioukahere

Développeur web fullstack avec une passion pour l’entrepreneuriat et les nouvelles technologies. Fondateur de Kaherecode.