Simple Formulaire de contact Symfony 4

Créons un formulaire de contact simple en utilisant Symfony 4.

Nous capturerons le nom des visiteurs, l'adresse email, la date de naissance, ainsi que leur message.

Nous allons prendre toutes les informations que le visiteur fournit, et nous vous enverrons une copie par e-mail en utilisant Gmail. Vous pouvez utiliser un fournisseur de messagerie différent, et nous verrons comment le faire en cours de route.

Enfin, nous allons montrer un bon message à l'utilisateur pour leur faire savoir que leur message a été envoyé.

Il y a beaucoup à couvrir, alors commençons.

Jeu de génération

Tout d'abord, nous allons générer une nouvelle classe de contrôleur.

Nous avons déjà couvert comment faire ceci, donc voici simplement la commande dont nous aurons besoin:

bin/console make:controller ContactController

 created: src/Controller/ContactController.php

  Success! 

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

Cela nous donne un cours de contrôleur.

Nous pourrions créer notre formulaire directement à l'intérieur de ce contrôleur.

Je vais vous conseiller contre cela. Même si vous pouvez, dans le monde réel, je ne peux pas penser à n'importe quel moment je le ferais.

C'est un meilleur choix (à mon avis) pour créer vos formulaires dans des classes de formes dédiées. Ces classes de formulaire se terminent toujours par le suffixe de Type . Je n'ai jamais découvert pourquoi. Si vous savez pourquoi nous utilisons le suffixe Type lorsque vous travaillez avec des formulaires dans Symfony, n'hésitez pas à me le faire savoir.

Comme notre formulaire est pour les fins de "contact", je vais avec le ContactType imaginatif pour mon nom de classe de formulaire.

Je vais utiliser la commande bin/console make:form pour me sauver un peu de frappe:

bin/console make:form

 The name of the form class (e.g. GrumpyGnomeType):
 > ContactType

 created: src/Form/ContactType.php

  Success! 

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

Jolly bien, c'est tout ce que nous devons générer.

Maintenant, commençons le vrai travail.

Construire notre formulaire de contact

Je mentirais si je disais que j'ai trouvé les formulaires de Symfony «faciles» pour les premiers mois de mon travail avec eux.

Je ne veux pas te décourager. Mais je veux aussi être complètement véridique et dire que j'ai eu du mal à apprendre comment utiliser les formulaires dans Symfony depuis longtemps. Mes luttes avec des choses comme les formulaires, la sécurité et certaines des parties les plus complexes de Symfony sont ce qui m'a poussé à créer ce site en premier lieu.

Mais, tout cela dit, nous allons nous calmer doucement, et je vais vous aider à chaque étape du chemin.

Tout d'abord, ouvrons la classe src/Form/ContactType.php que nous venons de générer, et voyons avec quoi nous devons travailler:

<?php

namespace App\Form;

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

class ContactType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('field_name')
        ;
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            // uncomment if you want to bind to a class
            //'data_class' => Contact::class,
        ]);
    }
}

Ok, d'abord, le générateur s'attend à ce que nous travaillions avec une entité. Dans ce cas, une entité Contact . Nous n'avons pas encore couvert d'entités.

A partir d'un niveau très élevé, une entité est un objet avec une forme d'identifiant unique. Nous allons travailler avec des entités dans la prochaine série. Pour l'instant, nous n'avons pas besoin de nous en préoccuper, vous pouvez donc supprimer la ligne:

use App\Entity\Contact;

La partie la plus déconcertante du travail avec les formulaires de Symfony était probablement le fait que les formes sont des classes. Ce ne sont pas des formulaires HTML que je connaissais, et j'aimerais dire que j'ai aimé, mais peut-être toléré serait une meilleure description.

La chose importante à comprendre est que nous définissons des formes dans le code. Lorsque nous utilisons un formulaire dans un template Twig, en coulisses, le composant Form de Symfony se chargera de transformer le formulaire en HTML pour nous.

Cela signifie que nous devons add tous les champs de notre formulaire à la méthode buildForm , et que le composant Form s'occupera du reste.

Si nous voulons qu'un champ de formulaire soit une input texte simple, tout ce que nous devons faire est de fournir le nom du champ.

Si nous voulions une zone de textarea , cependant, ou une zone de select , les choses deviennent un peu plus intéressantes.

C'est exactement pourquoi nous ajoutons une simple input texte pour le name notre visiteur.

Et nous aurons un input type="datetime-local" plus impliqué input type="datetime-local" pour la date de naissance de notre visiteur.

Et enfin une zone de textarea pour le message du visiteur.

Vous pouvez voir la large gamme de types de champs de formulaires que Symfony fournit pour notre utilisation. À partir de cette liste, nous utiliserons TextType , TextareaType et DateTimeType .

Ajoutons-les à notre implémentation buildForm maintenant, en supprimant l'appel existant add('field_name') , et en ajoutant trois de nos propres:

<?php

namespace App\Form;

use Symfony\Component\Form\AbstractType;
+ use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
+ use Symfony\Component\Form\Extension\Core\Type\EmailType;
+ use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class ContactType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
-           ->add('field_name')
+           ->add('name')
+           ->add('from', EmailType::class)
+           ->add('dateOfBirth', DateTimeType::class)
+           ->add('message', TextareaType::class)
        ;
    }

Remarquez comment l'appel à add('name') omet le deuxième argument, alors que dateOfBirth , from , et le message spécifient explicitement le type de champ de formulaire?

Si nous ne TextType::class pas un deuxième argument, le type de champ de formulaire est TextType::class être de type TextType::class . Cela signifie qu'il sera rendu comme une input HTML standard.

Nous pourrions être explicites:

+ use Symfony\Component\Form\Extension\Core\Type\TextType;

class ContactType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
-            ->add('name')
+            ->add('name', TextType::class)

Le résultat est le même.

Un autre point à noter: J'ai utilisé le nom de champ de dateOfBirth , pas date_of_birth , ou une autre combinaison. C'est la convention typique. Et si votre formulaire est soutenu par une entité, les propriétés de vos entités suivront probablement également cette convention de dénomination. En guise de tête, vos champs de formulaire ont tendance à mapper 1: 1 à vos noms de propriété d'entité. Vous pouvez modifier cette configuration en utilisant l'option property_path , qui est un peu plus avancée et généralement pas nécessaire. Un exemple où j'ai besoin de cela est lors de l'acceptation des données de formulaire snake_case sur une API JSON. Encore une fois, plus avancé, juste des questions intéressantes à ce stade.

L'ensemble des noms de champs sera utilisé comme noms du nom du champ d' input HTML généré et des propriétés id et label . Nous verrons cela lorsque nous afficherons la vue du formulaire.

C'est en fait assez pour obtenir notre forme là où nous en avons besoin.

À ce stade, nous avons spécifié les noms de champs que nous aimerions voir, et ce que chacun de ces champs devrait afficher en sortie au format HTML. Nous n'avons pas besoin de nous inquiéter de la façon dont cette classe de formulaire ContactType est convertie en HTML, cela sera pris en compte pour nous.

Créer le formulaire

Nous avons créé notre classe de formulaire ContactType , bien qu'elle ne soit actuellement utilisée nulle part.

Auparavant, nous avons créé une nouvelle classe de contrôleur ( ContactController ) avec une méthode générée automatiquement: index . Nous allons utiliser notre ContactType dans cette méthode de contrôleur, et voir comment Symfony convertit la classe de formulaire en HTML.

Voici notre point de départ:

<?php

namespace App\Controller;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Response;

class ContactController extends Controller
{
    /**
     * @Route("/contact", name="contact")
     */
    public function index()
    {
    }
}

Je vais faire les deux mêmes changements que nous avons faits à notre WelcomeController :

<?php

namespace App\Controller;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
-use Symfony\Bundle\FrameworkBundle\Controller\Controller;
+use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
-use Symfony\Component\HttpFoundation\Response;

-class ContactController extends Controller
+class ContactController extends AbstractController
{
    /**
     * @Route("/contact", name="contact")
     */
    public function index()
    {
    }
}

Comme notre ContactType est une ancienne classe PHP, pour utiliser cette classe, nous aurons besoin d'une new instance de cette classe. Cependant, nous new ContactType() pas directement un new ContactType() dans notre code.

Lorsque nous avons regardé pour la première fois quelques vidéos sur Twig, nous avons vu comment nous pouvions appeler $this->render(...) pour convertir un template Twig en HTML qui peut être renvoyé comme le corps de la réponse à notre visiteur du site . Nous avons accès à la méthode $this->render(...) parce que render est défini sur ControllerTrait.php de Symfony, et nos contrôleurs - ContactController , et WelcomeController - extend Controller . C'est l'héritage standard au travail, rien de spécifique à Symfony. C'est beaucoup plus facile à démontrer dans la vidéo, alors s'il vous plaît regarder si pas du tout sûr.

L'une des autres méthodes disponibles à part render est createForm . Nous pouvons appeler cette méthode comme $this->createForm(...) . Regardons rapidement la signature de la méthode:

 /**
     * Creates and returns a Form instance from the type of the form.
     *
     * @final since version 3.4
     */
    protected function createForm(string $type, $data = null, array $options = array()): FormInterface

Encore une fois, tout comme le render , le seul argument obligatoire est le $type que nous souhaitons utiliser. Nous devons passer un nom de classe ici. Nous connaissons déjà ce nom de classe, car nous venons de générer notre classe de formulaire - c'est la ContactType::class . En ajoutant ::class comme suffixe à n'importe quel nom de classe, en arrière-plan PHP étendra automatiquement le nom de la classe à son nom complet. En d'autres termes, lorsque nous ContactType::class , ce que PHP voit réellement est App\Form\ContactType , qui est l' namespace et le nom de la classe en un.

Les deuxième et troisième arguments de createForm sont facultatifs. Nous n'aurons pas besoin d'eux ici. En guise de tête, vous utiliserez le deuxième argument pour remplir votre formulaire avec des données avant qu'il ne soit rendu. Ceci est utile dans les situations où vous avez des données existantes que vous souhaitez montrer à vos visiteurs afin qu'ils puissent modifier / mettre à jour - peut-être leur profil d'utilisateur, ou similaire. Nous allons y arriver et plus encore dans la prochaine série.

Ok, créons notre formulaire:

<?php

namespace App\Controller;

+ use App\Form\ContactType;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;

class ContactController extends AbstractController
{
    /**
     * @Route("/contact", name="contact")
     */
    public function index()
    {
+        $form = $this->createForm(ContactType::class);
    }
}

Si vous deviez visiter /contact maintenant, il ne se passerait pas grand-chose. Nous ne restons rien pour l'instant.

Mais même ainsi, nous avons techniquement créé notre forme. Assurez-vous d'inclure la déclaration d' use ou cela ne fonctionnera pas.

Pour faciliter notre vie, ajoutons cette nouvelle route à notre Navbar (inside templates/base.html.twig ):

<!doctype html>
<html lang="en">
<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="">

    <title>{% block title %}Let's Explore Symfony 4{% endblock %}</title>

    <!-- Bootstrap core CSS -->
    <link rel="stylesheet"
          href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.3/css/bootstrap.min.css"
          integrity="sha384-Zug+QiDoJOrZ5t4lssLdxGhVrurbmBWopoEl+M6BdEfwnCJZtKxi1KgxUyJq13dy"
          crossorigin="anonymous">
    <link rel="stylesheet" href="{{ asset('css/custom-style.css') }}" />
    {% block stylesheets %}{% endblock %}
</head>

<body>

<header>
    <nav class="navbar navbar-expand-sm navbar-dark bg-dark">
        <div class="container">

            <a class="navbar-brand" href="{{ path('welcome') }}">Home</a>

            <div class="collapse navbar-collapse" id="navbarSupportedContent">
                <ul class="navbar-nav mr-auto">
                    <li class="nav-item">
                        <a class="nav-link" href="{{ path('hello_page') }}">Hello Page</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="{{ path('contact') }}">Contact</a>
                    </li>
                </ul>
            </div>
        </div>
    </nav>
</header>

<main role="main" class="container main">
    {% block body %}{% endblock %}
</main><!-- /.container -->

{% block javascripts %}{% endblock %}
</body>
</html>

Notez l'addition sous la section ul dans la barre de navigation.

Ok, obtenons ce formulaire affiché.

Affichage du formulaire de contact

Pour afficher le formulaire, nous devons faire deux choses:

  • Créer une "vue" du formulaire
  • Rendre la vue de formulaire à partir d'un template Twig

Aucun d'entre eux n'est trop taxant de notre point de vue. Cependant, dans les coulisses, il se passe beaucoup de choses pour que ce processus fonctionne.

Pour que cela fonctionne, nous aurons besoin d'un nouveau template Twig. Je vais suggérer de suivre ce que nous avons déjà couvert et créer un nouveau sous-répertoire de contact sous des templates , dans lequel je vais créer un fichier index.html.twig :

mkdir templates/contact 
touch templates/contact/index.html.twig 

Dans ce nouveau fichier template, j'ajoute:

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

{% block title %}Our Contact Form{% endblock title %}

{% block body %}

    <div>
        {{ form_start(our_form) }}
        {{ form_widget(our_form) }}
        {{ form_end(our_form) }}
    </div>

{% endblock %}

La seule nouvelle partie de ceci est l'utilisation des balises de form .

Ce sont des fonctions spéciales de Twig qui prennent notre vue de forme et créent une représentation de form HTML entièrement fonctionnante. Vous pouvez en savoir plus sur ce que chaque fonction fait dans les documents officiels .

Il y a un petit tweak que j'ai fait à partir de la façon dont les docs officiels affichent ceci:

J'ai changé de form à our_form .

Je vais vous expliquer pourquoi dans un instant. Pour l'instant, c'est le côté Twig des choses (presque) terminé.

Accrocher notre ContactController

Comme nous l'avons déjà mentionné, Twig ne sait pas non plus par magie ce que nous sommes en train de faire. Nous devons le dire explicitement.

Cela signifie que pour que Twig connaisse notre formulaire, nous devons informer Twig de notre formulaire.

Nous faisons cela en passant dans notre forme en tant que variable.

C'est comme passer dans n'importe quelle autre variable de notre template.

Commençons par ajouter dans l'instruction return un appel à $this->render(...) :

<?php

namespace App\Controller;

use App\Form\ContactType;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;

class ContactController extends AbstractController
{
    /**
     * @Route("/contact", name="contact")
     */
    public function index()
    {
        $form = $this->createForm(ContactType::class);

        return $this->render('contact/index.html.twig');
    }
}

Jusqu'à présent, rien de nouveau.

Ensuite, nous passerons sous notre forme au template contact/index.html.twig .

J'ai mentionné plus haut que les docs de Symfony utilisent l'exemple suivant pour leurs fonctions de template Twig form_start / form_widget / form_end :

 {{ form_start(form) }}
 {{ form_widget(form) }} 
 {{ form_end(form) }} 

Si nous utilisions ce même code, nous devions passer dans la vue de notre formulaire sous la forme d'une variable nommée.

Au lieu de cela, nous utilisons par exemple { form_start(our_form) }} . Cela signifie que nous devons utiliser la variable nommée our_form .

Je fais cela pour illustrer que les noms de variables peuvent être ce que vous voulez. Et si vous avez plusieurs formulaires par page, ils ne peuvent pas tous s'appeler form :)

<?php

namespace App\Controller;

use App\Form\ContactType;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;

class ContactController extends AbstractController
{
    /**
     * @Route("/contact", name="contact")
     */
    public function index()
    {
        $form = $this->createForm(ContactType::class);

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

Dirigez-vous vers votre site sur http://127.0.0.1:8000/contact , et que se passe-t-il?

"Erreur de type: Argument 1 passé à Symfony \ Composant \ Form \ FormRenderer :: renderBlock () doit être une instance de Symfony \ Component \ Form \ FormView, instance de Symfony \ Component \ Form \ Form donnée, appelée dans / home / chris /Development/lets-explore-symfony-4/var/cache/dev/twig/27/277e6d2e85202798cec1fe0a745133d5c7f3925b92540e36b8d1e3395807f520.php on line 72 "

Vous allez avoir un chemin légèrement différent, mais l'essentiel est le même: Cela ne fonctionne pas.

Quelle? Pourquoi je te montre même ça?

Parce que cette erreur est super facile à faire!

Nous avons transmis un objet Form , et ces fonctions form_... attendent toutes un FormView ! Mais bien sûr :)

Ok, le correctif ici est facile, et essentiel:

<?php

namespace App\Controller;

use App\Form\ContactType;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;

class ContactController extends AbstractController
{
    /**
     * @Route("/contact", name="contact")
     */
    public function index()
    {
        $form = $this->createForm(ContactType::class);

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

Maintenant, rafraîchissez la page et hoorah, nous voyons un formulaire. C'est une forme moche, mais c'est une forme.

Il y a un problème cependant:

Nous n'avons pas de bouton de submit .

Il est possible d'ajouter un bouton de soumission à notre type de formulaire. Cependant, ce faisant, nous imposons certaines restrictions sur notre forme qui limitent sa réutilisabilité. Cela devient plus évident sur des formes plus complexes.

Imaginez que nous avons un formulaire qui nous permet d'ajouter un nouveau produit. Et parce que ce formulaire est défini comme une classe distincte et autonome - tout comme notre formulaire ContactType , nous devrions être en mesure de réutiliser ce même formulaire pour l'édition / mise à jour d'un produit, aussi. Tous les champs de formulaire sont les mêmes dans les deux cas, non?

Oui. Sauf si nous utilisons le type de champ de formulaire SubmitType pour ajouter un bouton à notre classe de formulaire.

Si nous ajoutons un bouton, nous devons lui donner une étiquette. Peut-être que nous étiquetons le bouton comme "Créer".

Mais dans notre variante de formulaire Modifier / Mettre à jour, nous aimerions marquer le bouton comme "Mise à jour", à la place.

Qu'est-ce qu'on fait maintenant? Eh bien, comme toujours, il y a un tas de solutions. Et c'est super facile de trop penser ça.

Si nous allons un peu plus haut ici, nous pouvons conclure que le problème auquel nous sommes confrontés est de nature présentationnelle.

Présentation = Twig.

Ne vaudrait-il pas mieux placer les réglages spécifiques dans la couche de présentation - par exemple le template Twig pertinent - plutôt que de lier un formulaire à un type d'action spécifique ('Créer', 'Mettre à jour')?

Je le pense. Et de même que les meilleures pratiques .

Mettons à jour notre template pour ajouter un bouton de soumission simple:

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

{% block title %}Our Contact Form{% endblock title %}

{% block body %}

    <div>
        {{ form_start(our_form) }}
        {{ form_widget(our_form) }}

        <input type="submit" value="Send" />

        {{ form_end(our_form) }}
    </div>

{% endblock %}

Nous sommes à mi-chemin, continuons avec notre implémentation de formulaire de contact Symfony 4 dans le prochaine tutoriel