Vues, templates et formulaires

Table of Contents

1 Introduction

L'objectif est de construire un ensemble de pages et de formulaires pour :

  • consulter les circuits
  • ajouter un circuit
  • ajouter une nouvelle étape à un circuit
  • supprimer une étape d'un circuit
  • supprimer un circuit et ses étapes

2 Structure des pages Web de l'application

On va concevoir un graphe de pages à afficher pour implémenter les fonctionnalités de notre application, et les structurer dans une arborescence d'URLs.

2.1 Principe HATEOAS

Rappel d'un des principes du REST

Hypermedia As The Engine Of Application State (hay-dee-ous)

  • Une application = diagramme d'états fini
  • Transitions = hyperliens

HATEOAS-states.png

2.2 Un état

Une page Web est un état de l'application

HATEOAS-state.png

2.3 Implémentation

  • Le client doit pouvoir suivre les liens
  • Chaque réponse du serveur doit contenir les liens vers les prochaines requêtes

REST-API-HATEOAS.png

2.4 Exemple Application Web

first-tweet.png

2.5 Diagramme états Web app TWitter

  • 1 transition en entrée
  • 32 transitions en sortie

etats-twitter.png

3 Consulter les circuits

On va travailler dans un premier temps sur des pages "publiques" de l'application pour tout ce qui concerne la consultation. Ultérieurement, on pourrait implémenter une gestion de contrôle d'accès aux pages, en consultation, pour ne consulter que des circuits ayant effectivement été "approuvés", pour différencier ce que consulte l'administrateur du site, de ce que voient les clients. Mais dans un premier temps, on ne distingue pas ce qui est visible en fonction du profil de l'utilisateur.

3.1 Mise en place d'un controleur pour les circuits

On va ajouter un controleur dans src/AppBundle/Controller/CircuitController.php :

namespace AppBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Component\HttpFoundation\Request;
use AppBundle\Entity\Circuit;
use Symfony\Component\HttpFoundation\Response;

class CircuitController extends Controller
{
        /**
         * @Route("/circuit/")
         */
        public function indexAction()
        {
                $circuits = $this->getDoctrine()
                        ->getRepository('AppBundle:Circuit')
                        ->findAll();

                dump($circuits);
                return new Response(); 
        }

        /**
         * @Route("/circuit/{id}", name="circuit_show", requirements={
         *              "id": "\d+"
         *     })
         */
        public function showAction($id)
        {
                $circuit = $this->getDoctrine()
                        ->getRepository('AppBundle:Circuit')
                        ->find($id);

                dump($circuit);
                return new Response(); 
        }

}

Tester que ce controlleur basique fonctionne, et renvoie bien les valeurs de "debug" via dump(), en accédant avec le navigateur à :

  • http://localhost:8000/app_dev.php/circuit/
  • http://localhost:8000/app_dev.php/circuit/1

Rappel: on lance l'environnement de tests de Symfony avec :

php bin/console server:run

3.2 Écriture d'un template de consultation de la liste des circuits

  1. Ajouter un template Twig pour l'affichage de la liste des circuits, destiné à remplacer le dump($circuits);.

    Créer le fichier app/Resources/views/circuit/index.html.twig

    {% extends 'base.html.twig' %}
    
    {% block body %}
    
    <h1>Nos circuits</h1>
    
    {{ dump(circuits) }}
    
    {% endblock %}
    
  2. Modifier la génération de la réponse dans le controleur pour générer le HTML obtenu par l'exécution du template :

    ...
    return $this->render('circuit/index.html.twig', array(
                                    'circuits' => $circuits));
    ...
    

    Tester que l'affichage des circuits renvoie bien le debug des objets du modèle passés à la vue

  3. En s'inspirant des exemples de la documentation http://symfony.com/doc/current/book/templating.html, améliorer le template index.html.twig pour afficher une liste de circuits formatée en HTML. La liste des circuits devra ressemblera à ceci, dans un premier temps :

    ...
    <div class="circuit">
            <h2>1 : Andalousie</h2>
    
            <p>Départ de Grenade pour 1 j. de voyage à travers le Espagne, pour terminer à Séville.</p>
    </div>
    
    <div class="circuit">
            <h2>2 : Vietnam</h2>
    
            <p>Départ de Hanoi pour 3j. de voyage à travers le VietNam, pour terminer à Hô Chi Minh.</p>
    </div>
    ...
    

3.3 Ajout d'un template de consultation des détails d'un circuit

Ajouter un template circuit/circuit_show.html.twig affichant les attributs d'une instance de Circuit, qui sera affiché via CircuitController::showAction(). Dans un premier temps, on n'affichera pas les détails de toutes les étapes.

L'affichage du template donnera quelque chose du style :

<h1>Détails du circuit</h1>

<div class="circuit_details">

<p>Description: Andalousie</p>

<p>Départ de Grenade pour  j. de voyage à travers le Espagne, pour terminer à Séville.</p>

<p>Etapes :</p>
        <table>
          <tr>
            <th>Numéro</th>
            <th>Ville</th>
            <th>Nombre jours</th>
            <th></th>
          </tr>
</thead>
<tr>
  <td>1</td>
  <td>Grenade</td> 
  <td>1 j.</td>
</tr>
<tr>
  <td>2</td>
  <td>Cordoue</td> 
  <td>2 j.</td>
</tr>
<tr>
  <td>3</td>
  <td>Séville</td> 
  <td>1 j.</td>
</tr>
</table>

</div>

3.4 Ajout du lien de consultation des détails d'un circuit

Modifier le template de liste des circuits pour ajouter un lien vers les détails du circuit.

Le lien s'appuiera sur l'utilisation de la route définie pour la consultation au niveau des annotations du controleur.

...
<a href="{{ path('circuit_show', { id: circuit.id }) }}"> {{
                        circuit.description }} </a>
...

3.5 Ajout d'une gestion de cas particulier en cas d'absence de circuits en base

Essayer de consulter les détails d'un circuit inexistant en base de données, par exemple sur http://localhost:8000/app_dev.php/circuit/5

Au lieu d'un plantage de l'application (erreur 500), on souhaiterait plutôt afficher un message d'erreur.

Modifier CircuitController::showAction() pour générer une exception, et vérifier qu'elle provoque bien un code de retour HTTP 404 Not Found (cf. http://symfony.com/doc/current/book/controller.html#managing-errors-and-404-pages).

...
throw $this->createNotFoundException('The circuit does not exist');
...

On devrait ensuite ajouter le template d'erreur correspondant, si l'on veut un message d'erreur plus explicite, via un template dans app/Resources/TwigBundle/views/Exception/error404.html.twig (cf. http://symfony.com/doc/current/cookbook/controller/error_pages.html), mais il est un peu délicat de tester cela, car on doit changer d'environnement, de dev à prod

4 Modification du template de base

L'objectif est d'ajouter un menu de navigation dans les pages de l'application.

L'objectif, pour l'instant, n'est pas d'implémenter une présentation ni très riche, ni très ergonomique : on verra plus tard on verra comment utiliser des CSS, et bootstrap.

On modifiera pour cela le fichier de template twig dont on souhaite qu'"héritent" toutes les pages de l'application.

5 Ajout d'un circuit

On va travailler sur la zone /admin/ du site, donc sur /admin/circuit/, dans un premier temps. L'objectif est de se limiter pour l'instant à la partie back-office.

C'est le même contrôleur CircuitController qui gérera les actions, même si le routage met en oeuvre des URLs différentes, aussi bien pour le front-office que pour le back-office :

<?php

// ...

class CircuitController extends Controller
{
        /**
         * @Route("/circuit/")
         */
        public function indexAction()
        {
            // ...
        }

        /**
         * @Route("/circuit/{id}", name="circuit_show"...
         */
        public function showAction($id)
        {
            // ...
        }

        /**
         * @Route("/admin/circuit/new", name = "add_circuit")
         */
        public function newAction(Request $request)
        {
            // ...
        }
}

On se référera à la documentation Symfony sur les formulaires : http://symfony.com/doc/current/book/forms.html

5.1 Ajout d'une page "nouveau circuit"

Ajouter, en respectant l'arborescence des fichiers standard d'une application Symfony, le template admin/circuit/new.html.twig contenant le formulaire. On complétera ensuite le template pour générer une page HTML valide.

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

5.2 Ajout de l'action "nouveau circuit"

On complètera le squelette de méthode suivant, pour gérer la dynamique d'affichage des pages à la soumission du formulaire.

$circuit = new Circuit();

$form = $this->createFormBuilder($circuit)
             ->add('description')
             ->add('save', SubmitType::class, array('label' => 'Create circuit'))
             ->getForm();

$form->handleRequest($request);

if ($form->isSubmitted() && $form->isValid()) {
        $em = $this->getDoctrine()->getManager();
        $em->persist($circuit);
        $em->flush();

        return $this->redirectToRoute(...A_COMPLETER..., array( 'id' => $circuit->getId()));
}

return $this->render(...A_COMPLETER..., array(
    'form' => $form->createView(),
));

6 Ajout / suppression d'étapes dans un circuit

De façon similaire aux opérations sur les circuits, on ajoutera un controleur pour les étapes : EtapeController, gérant les actions suivantes :

  • indexAction($circuitId) : listes des étapes d'un circuit
  • newAction($circuitId, Request $request) : ajout d'une étape dans un circuit
  • deleteAction($circuitId, $etapeId) : suppression d'une étape d'un circuit

L'ajout d'une nouvelle étape se fera en fin de circuit, en s'assurant que le numéro d'étape est incrémenté par rapport à l'étape précédente, plutôt qu'en le faisant saisir par l'utilisateur.

La suppression se fera sans confirmation préalable, même si, dans une application réelle, on préfèrerait un dialogue de confirmation.

7 Mise ne place de tests systèmes de l'interface Web

Afin de garantir que l'application fonctionne de bout en bout, on va mettre en place un test des controleur afin de vérifier que les actions effectuées via l'interface Web (requêtes HTTP) sont bien prises en compte au niveau de la base de données.

On va réutiliser le bundle LiipFunctionalTestBundle afin de tester l'action de suppression d'une étape d'un circuit en injectant des requêtes destinées à déclencher les actions des controleur.

Vous ajouterez une classe EtapeControllerTest héritant de WebTestCase qui charge les fixtures de circuits et étapes, et vérifie que la suppression d'une étape (par exemple, la deuxième) du circuit "vietnam" décrémente bien le nombre d'étapes présentes dans la base pour ce circuit (initialement 4 étapes).

La méthode de test se basera sur le squelette suivant :

...
        const CIRCUIT = 'vietnam-circuit';
        const NBETAPES = 4;


        public function testRemoveEtapeWeb()
        {
                $em = $this->getContainer()->get('doctrine')->getManager();
                $circuit = $this->fixtures->getReference(EtapeControllerTest::CIRCUIT);

                $etape = $em->getRepository('AppBundle:Etape')->findOneBy(array("circuit" => $circuit->getId(), "numeroEtape" => 2));

                $client = static::makeClient();
                $crawler = $client->request('GET', ...A_COMPLETER...);
                $this->assertStatusCode(302, $client);

                unset $circuit;

                $circuit = $this->fixtures->getReference(EtapeControllerTest::CIRCUIT);

                $nbetapes = $circuit->getEtapes()->count();
                $this->assertEquals(EtapeControllerTest::NBETAPES-1, $nbetapes);
        }
...

8 Aller plus loin

Vous pouvez compléter l'application pour ajouter d'autres pages et formulaires pour les différentes actions.

Vous pouvez allez vérifier dans la documentation Symfony comment on peut mettre en oeuvre des mécanismes de vérification des données saisies dans les formulaires, en lien avec le Modèle de l'application.

Par exemple, ici, on n'a pas mis en oeuvre de controle de saisie sur les pays, ou les villes. On verra plus tard dans le cours comment mettre en place des widgets de saisie permettant par exemple une auto-complétion pour éviter des saisies erronées, en utilisant Javascript.

Author: Olivier Berger

Created: 2016-02-16 mar. 18:11

Validate