Créez votre premier formulaire
Nous interagissons principalement avec nos utilisateurs à l'aide de formulaires : que ce soit pour la recherche, la demande d'informations, ou l'ajout d'une évaluation d'un produit sur un site e-commerce, par exemple. Le framework Symfony vous permet de créer, valider et rendre des formulaires HTML avec le moteur de gabarit Twig.
Imaginons un formulaire de création d'articles pour un blog. Au plus simple, pour un article, nous souhaitons un titre, un contenu principal, un nom d'auteur... que l'on représenterait sous forme d'objet :
<?php
// src/Entity/Article.php
namespace App\Entity;
class Article
{
private $author;
private $content;
private $title;
public function getAuthor()
{
return $this->author;
}
public function setAuthor($author)
{
$this->author = $author;
}
// ...
}
Comment créer un formulaire, alors ? Il va falloir créer un "type de formulaire" à partir de notre objet.
Création du formulaire
Un type de formulaire est une classe qui permet de construire un formulaire en définissant les différents types de champs. Voici un exemple valide de type de formulaire pour l'objet précédent :
<?php
// src/Form/ArticleType.php
namespace App\Form;
use App\Entity\Article;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
class ArticleType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('title')
->add('content', TextareaType::class)
->add('author', TextType::class)
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Article::class,
]);
}
}
La création d'un formulaire à partir d'un objet est plutôt simple pour deux raisons principales :
le composant Form dispose d'un constructeur de formulaires (le form builder) capable de générer tout type de formulaire à partir de champs qu'il connaît déjà (champs de type texte, de type date...) ;
en passant la classe au résolveur d'options du formulaire, le constructeur de formulaires est capable - à partir de la classe - de deviner quel type de champ il doit utiliser. Par exemple, si une classe a une propriété de type DateTime, alors le champ utilisé sera de type datetime en HTML.
Par défaut, le type de champ rendu sera un champ "texte" qui correspond à la classe TextType, mais on peut évidemment définir pour chaque propriété le type de rendu que l'on souhaite.
Les différents types de champs
Tous les types de champs disponibles dans la spécification HTML5 sont disponibles dans le composant ; ils couvriront l'essentiel des besoins de votre application.
Voici la liste des types de formulaires disponibles, assurez-vous de choisir un type de champ cohérent avec la propriété de l'objet correspondant :
Texte | Choix | Date et temps | Divers | Multiple | Caché |
TextType |
Chacun de ces types de formulaires possède de nombreuses options qui permettent encore de configurer l'affichage. Par exemple, chaque type de champ possède une option help qui permet d'afficher un message d'aide lié au champ de formulaire (ce qui est une bonne pratique).
<?php
$builder->add('title', null, [
'help' => "Le titre de l'article, utilisé dans l'url",
]);
Utilisation et rendu à l'aide de Twig
Pour afficher le formulaire dans un template Twig, il va falloir le créer dans un contrôleur et fournir à Twig une "représentation" de l'objet formulaire créé.
<?php
// src/Controller/FormController.php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use App\Form\ArticleType;
use App\Entity\Article;
class FormController extends AbstractController
{
/**
* @Route("/form/new")
*/
public function new(Request $request)
{
$article = new Article();
$article->setTitle('Hello World');
$article->setContent('Un très court article.');
$article->setAuthor('Zozor');
$form = $this->createForm(ArticleType::class, $article);
return $this->render('default/new.html.twig', array(
'form' => $form->createView(),
));
}
}
Et le template Twig correspondant :
{# templates/default/new.html.twig #}
{{ form(form) }}
Et en accédant à l’URL correspondante, voici un exemple de rendu :
![Rendu d'un formulaire avec Twig](https://user.oc-static.com/upload/2018/09/06/15361888137495_form.png)
Ce n'est pas très joli, mais c'est fonctionnel ! Et nous améliorerons cela très bientôt. :ange:
Validez vos formulaires simplement
Jusque là, nous avons affiché un nouveau formulaire, mais il n'est même pas possible de le soumettre (pas de bouton "Créer") et les données seront de toute façon ignorées par votre contrôleur.
Soumettez un formulaire et traitez les données reçues
Tout d'abord, le plus simple est l'ajout d'un bouton de type "submit".
Dans le gabarit Twig, nous allons adapter le rendu du formulaire et ajouter un bouton. Nous reviendrons sur les fonctions utilisées un peu plus tard :
{# templates/default/new.html.twig #}
<html>
<head></head>
<body>
{{ form_start(form) }}
{{ form_row(form.title) }}
{{ form_row(form.content) }}
{{ form_row(form.author) }}
<button type="submit" class="btn btn-primary">Créer</button>
{{ form_end(form) }}
</body>
</html>
Pour le rendu suivant:
![Capture d'écran qui correspond au rendu du fichier Twig précédemment présenté.](https://user.oc-static.com/upload/2018/09/12/15367036783067_submittable_form.png)
Et mettons à jour notre contrôleur Symfony afin de récupérer les données reçues de la requête, et spécifiquement à partir du formulaire soumis :
<?php
// src/Controller/FormController.php
/**
* @Route("/form/new")
*/
public function new(Request $request)
{
$article = new Article();
$article->setTitle('Hello World');
$article->setContent('Un très court article.');
$article->setAuthor('Zozor');
$form = $this->createForm(ArticleType::class, $article);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
dump($article);
}
return $this->render('default/new.html.twig', array(
'form' => $form->createView(),
));
}
Tout d'abord, nous mettons à jour le formulaire à l'aide des informations reçues de l'utilisateur : c'est le rôle de la fonction handleRequest().
Ensuite, nous vérifions que le formulaire a bien été soumis ($form->isSubmitted()) et qu'il est valide ($form->isValid()).
Enfin, nous pouvons manipuler notre article mis à jour avec les données du formulaire. En utilisant le WebProfiler, on accède aux données provenant du formulaire :
![Capture d'écran montrant un dump() de l'article qui a été modifié à l'aide d'un formulaire.](https://user.oc-static.com/upload/2018/09/12/15367048665305_dumped_form.png)
Une bonne chose de faite, n'est-ce pas ? :ange:
Nous nous préoccuperons de la sauvegarde en base de données dans un prochain chapitre.
Pour l'instant, concentrons-nous sur quelque chose de vraiment important concernant les formulaires : la validation des données et l'envoi d'erreurs à nos utilisateurs.
Validez un formulaire avec le composant Validator
La validation des formulaires est l'une des tâches les plus pénibles pour un développeur web. Le framework Symfony et son composant Validator simplifient beaucoup le travail.
Vous aurez remarqué, dans le contrôleur précédemment mis à jour, que nous avons utilisé la fonction isValid() associée au formulaire.
Eh bien, dès lors que des règles de validation ont été définies, les formulaires sont capables d'utiliser le validateur automatiquement ! Dans notre cas, nous n'avions pas encore défini de règles de validation, alors notre formulaire est forcément valide. ;)
La meilleure façon de définir un ensemble de règles à valider par le formulaire est d'utiliser les annotations fournies par le composant Validator au niveau de l'objet à valider :
<?php
// src/Entity/Article.php
namespace App\Entity;
use Symfony\Component\Validator\Constraints as Assert;
class Article
{
/**
* @Assert\Length(
* min = 10,
* max = 70,
* minMessage = "Ce titre est trop court",
* maxMessage = "Ce titre est trop long"
* )
*/
private $title;
/**
* @Assert\NotBlank(message = "Le contenu ne peut être vide.")
*/
private $content;
/**
* @Assert\NotBlank(message = "Un auteur doit être associé à l'article")
*/
private $author;
/* ... */
}
La validation - comme pour la gestion de nos URL - se déclare à l'aide d'annotations, en associant des contraintes de validation à des propriétés ou des méthodes publiques de nos objets.
Toutes les contraintes de validation ont une propriété message qui permet de redéfinir le message d'erreur qui sera retourné. Avant d'aller plus en détail dans la liste des contraintes de validation à votre disposition, regardons comment fonctionne le validateur de Symfony.
Le validateur en bref
Le composant Validator de Symfony permet de créer un validateur :
<?php
use Symfony\Component\Validator\Validation;
use Symfony\Component\Validator\Constraints\Length;
use Symfony\Component\Validator\Constraints\NotBlank;
use App\Entity\Article;
$article = new Article();
$validator = Validation::createValidator();
$violations = $validator->validate($article, [
new Length(['min' => 10]),
new NotBlank(),
]);
if (0 !== count($violations)) {
// Affiche les erreurs
foreach ($violations as $violation) {
echo $violation->getMessage().'<br>';
}
}
Tout d'abord, la classe Validation du composant permet de créer une instance du validateur. Ce validateur a une méthode validate qui prend 2 arguments :
la valeur à valider : ce peut être une chaîne de caractères, un tableau ou un objet ;
une collection de contraintes de validation.
Le résultat de cette fonction est une liste de violations qui permettent d'accéder aux messages d'erreurs correspondant aux contraintes qui n'ont pas été validées. Si la valeur est valide, alors la liste de violations est vide.
Lorsque nous soumettons notre formulaire et que nous appelons la fonction isValid du formulaire Symfony, les contraintes de validation déclarées dans la classe Article sont appliquées sur l'objet $article qui a été retrouvé à partir des données de la requête utilisateur. ;)
Les contraintes de validation
Il existe de nombreuses contraintes de validation et il est possible d'en créer de nouvelles assez facilement. Voici la liste exhaustive des contraintes de validation supportées par Symfony :
Basique | Texte | Comparaison | Date |
EqualTo |
Collection | Fichier | Nombres / Finances | Autres |
Retournez les erreurs à l'utilisateur
Maintenant que l'on a configuré le contrôleur et la classe Article pour valider les données envoyées par l'utilisateur, validons le formulaire :
![Capture d'écran montrant une validation HTML5 qui ne permet pas d'afficher les messages d'erreur configurées](https://user.oc-static.com/upload/2018/09/14/15368965916151_html5-validation.png)
Par défaut et en fonction des règles de validation configurées, le formulaire va appliquer certaines règles de validation HTML5 ; par exemple, voici le HTML généré pour le champ "Title" :
<input
type="text"
id="article_title"
name="article[title]"
maxlength="70"
pattern=".{10,}"
class="form-control"
value="Hello World"
>
La bonne nouvelle est que le serveur n'est pas appelé en cas d'erreur utilisateur ; la mauvaise est que, puisque le serveur n'est pas appelé, les messages que l'on a configurés ne sont pas accessibles...
Si la gestion des messages d'erreur est importante pour vous, une solution est de désactiver la validation HTML5 avec l'attribut "novalidate". Mettons à jour notre formulaire :
<html>
<head></head>
<body>
{{ form_start(form, {'attr': {'novalidate': 'novalidate'}}) }}
{{ form_row(form.title) }}
{{ form_row(form.content) }}
{{ form_row(form.author) }}
<button type="submit" class="btn btn-primary">Créer</button>
{{ form_end(form) }}
</body>
</html>
Et soumettons un formulaire invalide :
![Capture d'écran d'un exemple de soumission d'un formulaire invalide et affichage des erreurs configurées.](https://user.oc-static.com/upload/2018/09/14/1536897031205_form-errors.png)
Ce n'est vraiment pas joli, mais fonctionnel ! Maintenant que nous avons une gestion de formulaires complète, occupons-nous de rendre cela un minimum esthétique !
Customisez le rendu de vos formulaires
Revenons sur le gabarit utilisé pour générer notre formulaire : il utilise de nombreuses fonctions Twig.
Mais que font exactement ces fonctions ?
Utilisez Twig pour construire vos formulaires
Le framework Symfony fournit une intégration de Twig avec le composant Form, sous forme de fonctions dédiées au rendu de vos formulaires.
Les fonctions de Twig dédiées au composant Form
form_start rend la balise
<form>
: on peut définir l'action, l'attribut method et ajouter différents attributs à la balise ;form_row rend un champ de formulaire complet, label et messages d'erreurs compris ;
form_label rend le label ;
form_widget rend l'input ;
form_errors rend le bloc d'erreurs ;
form_rest rend tous les champs non encore rendus ;
csrf_token rend un input
hidden
avec une clé contre les attaques de type CSRF ;form_end rend la balise fermante
</form>
.
Avec l'ensemble de ces fonctions, nous pouvons construire n'importe type de formulaire qui sera rendu en fonction du thème actif.
Attends, là, c'est quoi un thème de formulaire ? :-°
Utilisez un thème de formulaire
Un thème de formulaire est un gabarit qui contient toutes des définitions, tous les gabarits pour chaque type de formulaire géré par votre application. En fait, sans le savoir, nous utilisons déjà un thème de formulaire, à savoir le fichier form_div_layout.html.twig fourni avec le framework Symfony 4 :
...
{%- block checkbox_widget -%}
<input type="checkbox" {{ block('widget_attributes') }}{% if value is defined %} value="{{ value }}"{% endif %}{% if checked %} checked="checked"{% endif %} />
{%- endblock checkbox_widget -%}
{%- block radio_widget -%}
<input type="radio" {{ block('widget_attributes') }}{% if value is defined %} value="{{ value }}"{% endif %}{% if checked %} checked="checked"{% endif %} />
{%- endblock radio_widget -%}
{%- block datetime_widget -%}
{% if widget == 'single_text' %}
{{- block('form_widget_simple') -}}
{%- else -%}
<div {{ block('widget_container_attributes') }}>
{{- form_errors(form.date) -}}
{{- form_errors(form.time) -}}
{{- form_widget(form.date) -}}
{{- form_widget(form.time) -}}
</div>
{%- endif -%}
{%- endblock datetime_widget -%}
...
Pour chaque type de formulaire livré avec Symfony, un thème de formulaire définit comment il doit être rendu en Twig. Ce n'est pas tout ! Par défaut, vous retrouverez dans Symfony 4 plusieurs thèmes :
Bootstrap 3 ;
et Foundation 5.
Commençons par activer le thème de formulaire pour Bootstrap 4 et par ajouter le fichier CSS correspondant.
D'abord, il va falloir mettre à jour la configuration du framework pour activer le thème Bootstrap 4 :
# config/packages/twig.yaml
twig:
debug: '%kernel.debug%'
strict_variables: '%kernel.debug%'
form_themes: ['bootstrap_4_layout.html.twig'] # accepte plusieurs thèmes
Maintenant que le thème est activé, mettons le gabarit du formulaire à jour en ajoutant Bootstrap 4 :
{# /templates/form/new_article.html.twig #}
<html>
<head>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css">
</head>
<body>
<div class="container">
<div class="row">
{{ form_start(articleForm, {
'attr': {
'novalidate': 'novalidate',
'class': 'col-lg-5'
}
})
}}
{{ form_row(articleForm.title) }}
{{ form_row(articleForm.content) }}
{{ form_row(articleForm.author) }}
{{ form_row(articleForm.publicationDate) }}
<button type="submit" class="btn btn-primary">Créer</button>
{{ form_end(articleForm) }}
</div>
</div>
</body>
</html>
Voici maintenant à quoi ressemble notre formulaire :
![capture d'écran du formulaire après activation du thème Bootstrap 4](https://user.oc-static.com/upload/2018/10/21/15400787821474_boostrap4.png)
C'est déjà beaucoup mieux, non ? :ange:
Vous l'aurez compris : une fois votre thème de formulaire parfaitement adapté à votre besoin activé, l'intégration de vos formulaires sera rapide et homogène dans toute votre application !
Customisation d'un champ spécifique dans un gabarit
Il arrivera que nous ayons besoin de customiser l'affichage d'un champ particulier. Dans ce cas, il va falloir redéfinir le gabarit utilisé pour ce champ précis dans le gabarit du formulaire concerné.
Imaginons le formulaire d'un produit qui a 2 champs : prix et nom. Nous voulons, pour le prix, afficher un message d'information complémentaire à côté du prix.
Il va falloir alors customiser uniquement ce "fragment" :
{% form_theme productForm _self %}
{% block _product_price_widget %}
<div class="price_widget">
{{ block('form_widget_money') }}
<i>Des frais de transaction seront ajoutés.</i>
</div>
{% endblock %}
{{ form_widget(productForm.price) }}
Pour définir l'exact nom du fragment, il faut prendre le nom du formulaire (par exemple, pour la classe "ProductType" , c'est "product" , qui peut être redéfini grâce à la méthode getName() du type de formulaire) suivi du nom du champ, ici le champ "price".
Encore plus loin, intégrer un formulaire qui génère une collection de formulaires
La customisation d'un formulaire qui permet de créer une liste de sous-formulaires peut être difficile à appréhender. Imaginons un produit de type "Chaussure" auquel on veut associer différentes pointures.
Nous aurions donc un objet "Chaussure" qui aurait une propriété "pointures" qui serait une collection d'objets de type "Pointure", eux-mêmes n'ayant qu'une propriété "valeur".
<?php
namespace App\Form\Type;
use App\Model\Shoe;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\Extension\Core\Type\MoneyType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class ShoeType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name')
->add('description')
->add('price', MoneyType::class)
->add('sizes', CollectionType::class, [
'entry_type' => SizeType::class,
'entry_options' => ['label' => false],
'allow_add' => true,
'allow_delete' => true,
])
->add('save', SubmitType::class)
;
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Shoe::class,
]);
}
}
Voici le gabarit correspondant :
{{ form_start(shoe) }}
{{ form_row(shoe.name) }}
<h3>Sizes</h3>
<ul class="sizes">
{% for size in shoe.sizes %}
<li>{{ form_row(size.value) }}</li>
{% endfor %}
</ul>
{{ form_end(form) }}
Si le formulaire a été configuré pour l'ajout de nouvelles pointures, alors le formulaire recevra une variable "prototype" avec le formulaire correspondant. Il faudra le manipuler en JavaScript pour construire de nouveaux formulaires.
Par exemple :
<ul class="sizes" data-prototype="{{ form_widget(shoe.sizes.vars.prototype)|e('html_attr') }}">
...
</ul>
Vous pourrez trouver un exemple de code fonctionnel sur jsfiddle.net ou encore dans la documentation officielle. D'ailleurs, le prochain chapitre de ce cours sera l'occasion pour vous d'expérimenter ce cas d'utilisation !
En résumé
Le composant Form est responsable de la gestion des formulaires. À partir de vos objets métiers, vous pouvez construire vos formulaires qui seront utilisables dans vos contrôleurs et dans vos vues.
Pour créer un formulaire, il vous faudra créer un type de formulaire qui sera automatiquement reconnu par le framework. Le composant fournit de très nombreux types de formulaires nativement, tous documentés et configurables selon vos besoins.
La validation des formulaires est automatique côté navigateur grâce aux attributs HTML5, la validation côté serveur se fait à l'aide du composant Validator, en appliquant des annotations sur les objets métiers directement.
Enfin, la customisation des formulaires se fait à l'aide de thèmes de formulaires. Le framework Symfony en fournit pour les principaux frameworks CSS, et il est possible de surcharger le gabarit de chaque type de formulaire, même pour un champ de formulaire spécifique.
Dans le prochain chapitre, nous introduirons une nouvelle brique importante pour nos applications : l'ORM Doctrine pour la persistance de données !