Architecture(s) et
application(s) Web

CSC4101 - Gestion avancée des routes et formulaires

25/08/2025

Plan de la séquence

Objectifs de cette séance (1h) :

  1. Formulaires améliorés
  2. Aller au-delà des CRUDs basiques
  3. Téléversement d'images

Vers des formulaires gérés avec une seule méthode

  • Affichage d’un formulaire HTML : méthode d’un contrôleur (insertion d’un formulaire dans un template)
  • Gestion de la soumission du formulaire : méthode du même contrôleur (par exemple la même méthode)

Passer à une seule méthode :

  • Rendre le code plus compact
  • Lisibilité ?

Rappel : Variante à 2 méthodes

Méthode gérant le GET : affichage du formulaire HTML

#[Route('/todo/new', name: 'todo_new_get', methods: ['GET'])]
public function newFormGet(): Response
{
    $todo = new Todo();
    // init valeurs par défaut
    $form = $this->createForm(TodoType::class, $todo);

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

Méthode gérant le POST : traitement des données soumises

#[Route('/todo/new', name: 'todo_new_post', methods: ['POST'])]
public function newFormPost(Request $request, TodoRepository $todoRepository): Response
{
    $todo = new Todo();
    $form = $this->createForm(TodoType::class, $todo);
    $form->handleRequest($request);

    if($form->isSubmitted() && $form->isValid()) {
        $todoRepository->add($todo, true);

        return $this->redirectToRoute('todo_index');
    }
    # else {
    return $this->render('todo/new.html.twig', [
        'todo' => $todo,
        'form' => $form->createView(),
    ]);
    # }
}

Formulaire moderne

Fusionne en une seule méthode gestion du GET et du POST

Cf. Forms dans la documentation Symfony.

Route et méthode unique

#[Route('/todo/new', name: 'todo_new', methods: ['GET', 'POST'])]
public function new(Request $request, TodoRepository $todoRepository): Response
{
    $todo = new Todo();
    $form = $this->createForm(TodoType::class, $todo);
    $form->handleRequest($request);
    if ($form->isSubmitted() && $form->isValid()) {
        $todoRepository->add($todo, true);

        return $this->redirectToRoute('todo_index');
    }

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

Génération des contrôleurs et formulaires

Symfony peut aussi générer des classes contenant le code des contrôleurs et les formulaires

symfony console make:crud

Exemple : nouvelle tâche

#[Route('/new', name: 'todo_new', methods: ['GET', 'POST'])]
public function new(Request $request, TodoRepository $todoRepository): Response
{
    $todo = new Todo();
    $form = $this->createForm(TodoType::class, $todo);
    $form->handleRequest($request);

    // Gestion du POST
    if ($form->isSubmitted() && $form->isValid()) {
        $todoRepository->add($todo, true);

        return $this->redirectToRoute('todo_index', [], Response::HTTP_SEE_OTHER);
    }

    // Gestion du GET ou problème validation
    return $this->renderForm('todo/new.html.twig', [
        'todo' => $todo,
        'form' => $form,
    ]);
}
  • TodoType est vue plus loin

Gabarit todo/new.html.twig :

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

{% block title %}New Todo{% endblock %}

{% block main %}
    <h1>Create new Todo</h1>

    {{ include('todo/_form.html.twig') }}

    <a href="{{ path('todo_list') }}">back to list</a>
{% endblock %}

Gabarit templates/todo/_form.html.twig :

{{ form_start(form) }}
    {{ form_widget(form) }}
    <button class="btn">{{ button_label|default('Save') }}</button>
{{ form_end(form) }}

(aussi inclus par todo/edit.html.twig)

Classe TodoType gestionnaire de formulaire

class TodoType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('title', TextType::class,
                  ['required' => false])
            ->add('completed', ChoiceType::class,
                  ['choices' => [
                      'Maybe' => null,
                      'Yes' => true,
                      'No' => false ]
                  ])
            ->add('project');
    }
    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'data_class' => Todo::class,
        ]);
    }
}

< / code formulaire moderne >

Chargement auto des instances

Mécanisme MapEntity des contrôleurs

  • Paramètre {id} de l’attribut Route : extrait du chemin de la requête HTTP
  • Argument de la méthode : $todo, instance de Todo
  • Chargement auto grâce au Repository de Todo (appel à TodoRepository::find() automagique)
class TodoController extends Controller
{
    #[Route('/todo/{id}', name: 'todo_show'),
      requirements: ['id' => '\d+'], methods: ['GET'])]
    public function showAction(Todo $todo): Response
    {
        return $this->render('todo/show.html.twig', array(
            'todo' => $todo,
        ));
    }

Plan de la séquence

Objectifs de cette séance (1h) :

  1. Formulaires améliorés
  2. Aller au-delà des CRUDs basiques
  3. Téléversement d'images

Ajout entités liées

Création d’une Todo dans le contexte d’un Project

Version basique

Version /todo/new créée via make:crud

  • Formulaire TodoType :

    class TodoType extends AbstractType
    {
        public function buildForm(FormBuilderInterface $builder, array $options)
        {
            $builder
                // ...
                ->add('project');
    
    

    la référence au projet de la tâche (Todo::project) est gérée comme une propriété « ordinaire » de la tâche dans le formulaire

Résultat : liste de choix parmi les projets

ajout-popup-entite-liee.png
Figure 1 : « Sélection d’une entité liée dans un popup »

Pas ce qu’on souhaite, si une tâche ne peut pas changer de projet

Formulaire d’ajout, depuis l’affichage d’un projet

  • Dans page affichage d’un projet (project_show, sur /project/{id})
  • Sous l’affichage de la liste des tâches d’un projet : bouton/lien « ajouter nouvelle tâche »
  • Chemin du formulaire d’ajout : /project/{id}/addtodo
  • id de projet (connu) !

Code final

#[Route('/project/{id}/addtodo', name: 'todo_add',
        methods: ['GET', 'POST'])]
public function addTodo(Request $request, Project $project,
                        TodoRepository $todoRepository): Response
{
        $todo = new Todo();
        $todo->setProject($project);

        $form = $this->createForm(TodoType::class, $todo);
        $form->handleRequest($request);
        if ($form->isSubmitted() && $form->isValid()) {

            $todoRepository->add($todo, true);
            // ...

Contexte :

  • chargement automagique du projet d’après l’{id} de la route
  • tâche créée dans son projet :

    $todo->setProject($project)

Supprimer le champ project du formulaire TodoType

Éviter la modification du champ project.

Comportement optionnel (garde fonctionnement initial formulaire nouvelle tâche isolée).

$form = $this->createForm(TodoType::class, $todo,
        ['display_project' => false]);

public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('title')
            ->add('created', DateType::class, [
                  'widget' => 'single_text'])
            ->add('project', null, [
                  'disabled' => $options['display_project'] ]);
    }

</formulaires>

Plan de la séquence

Objectifs de cette séance (1h) :

  1. Formulaires améliorés
  2. Aller au-delà des CRUDs basiques
  3. Téléversement d'images

Format données POST contenant fichier

POST /paste/new HTTP/1.1
Host: example.com
Content-Type: multipart/form-data; boundary=---------------------------1493984614149677742994700663
Content-Length: 1013137

-----------------------------1493984614149677742994700663
Content-Disposition: form-data; name="paste[content]"

Joulis
-----------------------------1493984614149677742994700663
Content-Disposition: form-data; name="paste[imageFile][file]"; filename="joulis.png"
Content-Type: image/png

89504e470d0a1a0a0000000d49484452000003ef000003500802000000d6
84c9d20000000373424954080808dbe14fe00000001974455874536f6674
7761726500676e6f6d652d73637265656e73686f74ef03bf3e0000200049
444154789cacbddb92f3489226f6b97b440024f3f49faaaa0fd333bd6b2b
e95ab7b27d1999ccf4b27a00ad64265977cf6cef7457fd87cc240920c2dd
75118120c8fcab6d2f848b4c12040271f0e3e71e0efae31fffe8eecc0c80
88fe873ffecfcc0c220070777783c13de7cccc2272f9898d880227df1cb5
1100214562e4bc48e2a2cb3c2fbbfdf89ffff3fff2bfffafffdbbb770f0e
64982948782986c00e14c000000410e06887030a38dc9cb09e37adffe006
38dc1d4a00dc6005aa0a804988e00a33a742eeee4eb5930048090c765b07
ea06ad9f01981a0017eb67ea35446e6666d61aa1d6a0885cee3533337737
b37ac1767e4c0840ce595555b54e3e337b7d84998830b3aa1e4fc7799ee1
1c63bc74d2cc8d6afb00587ac7a8ce7c3ddf6680b9b656cf9b1933b74636
5d0220ad016a73be36d8afec2b6ba4ee9e735ed707ed4e0611c1c9ddad0d
c500b85d7ad5db717737727714ab679899888cbc4e0b33d7079b50049b50
7052e218060e29c6c8297064621611171e778999534a87fdb8dbedd22024
8d7a94200e1308c10402982031dc51668401309419d3abfdebbffe2b8fe9
a7dffc187794cb3c1cc22e88c2046ce60202371a3586aae5a594a9308518
024706508a91331138801444200013b4a8bb47b0bb0f7b91082b30852be6
d37c3a9dbe7dfb0620c6e8ee9ffffd97bffce52fcfcfcf089e4f131179d1

Outil pour upload d’image dans formulaire Symfony

Exemple : utilisation de VichUploaderBundle pour mettre des photos dans les Pastes

  • Formulaire upload de photo
  • Stockage dans public/
  • Affichage dans gabarit

Entité Paste

Ajout propriété à entité Doctrine (décorée par annotation Vich\Uploadable)

#[ORM\Entity(repositoryClass: PasteRepository::class)]
#[Vich\Uploadable]
class Paste
{
    //...
    #[ORM\Column(nullable: true)]
    private ?string $imageName = null;

    #[Vich\UploadableField(mapping: 'pastes',
                           fileNameProperty: 'imageName')]
    private $imageFile;

    //           .../...
  • imageName stocké en base (Doctrine)
  • imageFile objet « fichier téléversé » Symfony
          /**
           * @param File|\Symfony\Component\HttpFoundation\File\UploadedFile $imageFile
           */
          public function setImageFile(?File $imageFile = null): void
          {
                  $this->imageFile = $imageFile;

                  //...
          }

}

Formulaire Paste

class PasteType extends AbstractType
{
        public function buildForm(FormBuilderInterface $builder,
                              array $options)
        {
                $builder
                        ->add('content')
                        ->add('created')
                        ->add('content_type')
                        ->add('imageName', TextType::class,
                              ['disabled' => true])
                        ->add('imageFile', VichImageType::class,
                              [
                                'required' => false,
                                'delete_label' => 'Delete image ?'
                              ])
                ;
        }

Résultat

profiler-upload-vich.png
{% extends 'base.html.twig' %}

{% block title %}Paste{% endblock %}

{% block main %}
    <h1>Paste</h1>

    <table class="table">
        <tbody>
            <tr>
                <th>Id</th>
                <td>{{ paste.id }}</td>
            </tr>
            ...
            <tr>
                    <th>Image</th>
                    <td>
                       <img src="{{ vich_uploader_asset(paste, 'imageFile') }}"/>
                    </td>
            </tr>
        </tbody>
    </table>

    <a href="{{ path('app_paste_index') }}">back to list</a>

{% endblock %}

Fichier déposé : public/images/pastes/joulis-68ac7be5bf930013195640.png

joulis.png

</upload_images>

Take Away

  • Gestion optimale intégrée entre formulaires et Doctrine
  • Ne pas se limiter à make:crud
  • Gestion des données liées dans les associations
  • Bundle pour téléversement d’images simple

Copyright

  • Document propriété de ses auteurs et de Télécom SudParis (sauf exceptions explicitement mentionnées).
  • Réservé à l’utilisation pour la formation initiale à Télécom SudParis.