Faire un formulaire de recherche avec Symfony2

Publié le Mis à jour le

Symfony2, sorti récemment, diffère de symfony 1 sur de nombreux points. Cet article va s’attarder sur les formulaires de filtrage des entités Doctrine.


Nous allons prendre comme exemple le modèle Pony représentant un poney par son nom, sa date de naissance, et sa date de décès.

./app/console doctrine:generate:entity AcmeDemoBundle:Pony "name:string(255) born_at:datetime dead_at:datetime" --mapping-type=annotation

Pour rappel, le couple symfony /Doctrine 1 générait deux classes de formulaire pour chaque modèle de notre application. Ainsi pour le modèle Pony, on trouvait les classes suivantes :

  • PonyForm
  • PonyFormFilter

Lors d’une recherche, nous n’utilisons pas forcément les mêmes champs que lors de la création ou de l’édition d’une entrée, c’est pourquoi ces deux classes représentent bien deux formulaires différents. Le premier sert à créer ou éditer un poney, alors que le second sert à filtrer une liste de poneys.

La classe PonyFormFilter contient donc la définition du formulaire de recherche (ie. les widgets et les validateurs) mais aussi la logique de construction de la requête SQL. Comme on peut le voir, il y a donc un mélange du modèle, de la présentation et de la validation avec une telle classe. Cela rend difficile la réutilisation du filtre de recherche (sa logique et sa requête SQL) dans un autre contexte. Dans une API par exemple, nous instancierions un formulaire complet pour n’utiliser que le filtre de recherche.

Avec Symfony2, il n’y a plus de classes prêtes à l’emploi, nous allons devoir coder le formulaire et la requête. Un tel mécanisme nous permettra de mieux découpler le code par rapport à ce qui était possible avec symfony 1.

Créer et éditer une entité

Le formulaire pour créer et éditer un poney est simple à réaliser, et c’est très bien documenté, contrairement aux formulaires de filtre que nous verrons après.

Attention, il s’agit ici d’un exemple très simple, pour lequel nous n’allons pas créer de classe de formulaire, même si c’est une bonne pratique à adopter normalement (voir la suite pour un exemple).

Dans le cas d’une création de Pony, il nous suffit d’utiliser le service form.factory auquel nous allons passer un objet nouvellement créé (Pour une édition, il suffirait de passer un objet récupéré via l’entityManager). À l’aide de l’entité qu’on lui passe, le formBuilder générera les champs de formulaire correspondant au type de champs, et fera le mapping entre les deux.

$pony = new Pony();
$form = $this->createFormBuilder($pony)
    ->add('name')
    ->add('born_at')
    ->add('dead_at')
    ->getForm();

Nous passons ensuite l’objet créé à notre vue avec la méthode createView() :

return $this->render('AcmeDemoBundle:Demo:new.html.twig', array(
    'form' => $form->createView(),
));

Il suffit alors d’utiliser les fonctions de Twig pour le rendu :

<form action="{{ path('pony_new') }}" method="post" {{ form_enctype(form) }}>
   {{ form_widget(form) }}

   <input type="submit" />
</form>

Notre formulaire s’affiche ! Et nos champs de type DateTime sont bien affichés sous forme de widget de date. Il ne nous reste plus qu’à stocker le poney ainsi créé en soumettant le formulaire créé.

if ($request->getMethod() == 'POST') $form->bindRequest($request); // Le Pony est hydraté ici
if ($form->isValid()) { // Si on a des Asserts sur le Pony
    $this->getDoctrine()->getEntityManager()->persist($form->getData());
    $this->getDoctrine()->getEntityManager()->flush();
    return $this->redirect($this->generateUrl('pony_success'));
}

Notre poney est alors persisté.

Rechercher une entité

Si nous recherchons sur des champs que la classe Pony possède (“name” par exemple), nous pouvons utiliser quasiment le même code que précédemment : utiliser le formBuilder qui, à la soumission du formulaire, va hydrater un objet Pony, et utiliser ces données afin de lancer la recherche à l’aide de la méthode findBy de Doctrine :

$this->getDoctrine()->getRepository('AcmeDemoBundle:Pony')
       ->findBy($form->getData()->toArray());

Notez que nous faisons appel à une méthode toArray() qui doit être développée par nos soins (findBy() prend en paramètre un tableau de clé / valeur).

Quoi qu’il en soit, cette technique ne fonctionne que si les filtres ne concernent que des champs présents dans l’entité Pony. Et dans le cas où vous avez une valeur par défaut sur une propriété de votre entité Pony, elle sera utilisée dans la recherche aussi. Ce n’est donc pas vraiment idéal. Comment faire pour chercher une entité sans utiliser celle-ci comme classe de mapping ? Il faut construire une classe de mapping dédiée !

Prenons par exemple la recherche de poney ayant vécu à une certaine date. Nous avons donc un champ unique « date » qui ne correspond à aucune propriété de notre classe Pony. Il faut savoir qu’une classe de mapping est semblable en tous points à une entité au sens Doctrine2. Elle contient des accesseurs, ou des propriétés publiques.

Nous allons donc mettre notre classe de mapping (PonySearch) dans le même namespace que les entités que nous persistons. Ainsi, si nous souhaitons un jour sauvegarder les recherches effectuées, il suffira d’ajouter quelques annotations (@ORM et compagnie).

Voici notre classe de mapping (AcmeDemoBundle/Entity/PonySearch.php) :

<?php
namespace AcmeDemoBundleEntity;

class PonySearch
{
   public $date;
}

C’est extrêmement simple. Comme pour une entité Doctrine, nous pouvons y ajouter de la validation (@Assert) et des valeurs par défaut.

Par contre, comme nous n’avons plus d’annotations Doctrine sur les propriétés, le framework de formulaire n’est plus capable de détecter le type de notre champ. Afin de rentre le formulaire réutilisable dans d’autres contrôleurs, et pour respecter les bonnes pratiques de Symfony, nous allons créer une classe de formulaire, même si celle-ci n’est pas indispensable dans notre cas.

<?php
namespace AcmeDemoBundleFormType;

use SymfonyComponentFormAbstractType;
use SymfonyComponentFormFormBuilder;

class PonySearchType extends AbstractType
{
   public function buildForm(FormBuilder $builder, array $options)
   {
       $builder->add('date', 'date');
   }

   public function getName()
   {
       return 'pony_search';
   }
}

Nous créons une classe juste pour dire que « date » est de type « date », mais c’est l’exemple qui est simpliste et il y a bien d’autres options définissables via celle-ci.

Nous avons maintenant un formulaire (PonySearchType), une entité permettant le mapping (PonySearch), il ne nous reste plus qu’à coder la requête de recherche. Notre contrôleur fait appel à un Repository de Pony :

if ($form->isValid()) {
   $query = $this->getDoctrine()->getRepository('AcmeDemoBundle:Pony')->search($form->getData());
   $results = $query->getResult();
}

Nous passons directement notre entité de mapping PonySearch à une méthode du Repository, qui est elle aussi à développer. Comme vu au début de ce paragraphe, ce n’est pas obligatoire, et il est possible d’utiliser findBy() dans le cas où les champs font partie de l’entité utilisée.

Utiliser des fonction SQL native avec Doctrine2

Pour faire une recherche sur un jour précis, nous devons utiliser des fonctions de notre SGBD (ici, la fonction MySQL DATE()). Or, seules les fonctions communes à tous les SGBD sont supportées par le Doctrine Query Builder.

Heureusement, les NativeQuery sont beaucoup plus puissantes que la classe Doctrine_RawSql de Doctrine1. C’est un cas particulier, mais l’exemple est choisi aussi dans le but de vous le montrer. Voyez vous-même :

class PonyRepository extends EntityRepository
{
   public function search(PonySearch $search)
   {
       $rsm = new ResultSetMappingBuilder($this->getEntityManager());
       $rsm->addRootEntityFromClassMetadata('AcmeDemoBundleEntityPony', 'p');
       
       $query = 'SELECT p.* FROM Pony p WHERE 1 ';
       $params = array();

       if ($search->date instanceof DateTime) {
           $query .= 'AND (DATE(p.born_at) <= :date AND DATE(p.dead_at) >= :date) ';
           $params['date'] = $search->date->format('Y-m-d');
       }

       // Nous pouvons ajouter nos autres champs de recherches ici

       $request = $this->getEntityManager()->createNativeQuery($query, $rsm);
       $request->setParameters($params);
       return $request;
   }
}

Vous noterez l’utilisation de ResultSetMappingBuilder. C’est notre super copain qui va aller voir les annotations Doctrine de notre entité Pony et qui sait du coup comment hydrater le résultat de la requête SQL.

Conclusion

Comme on a pu le constater, le framework de formulaires de Symfony2 n’est qu’un outil permettant l’affichage et le mapping de classes en fonction d’une requête. Il n’y a rien de plus, et si nous souhaitons faire un formulaire de recherche (ou n’importe quel autre type de formulaire), il suffit de donner à ce formulaire un objet à hydrater (qu’il soit entité Doctrine ou pas). Pour un formulaire de contact par exemple, il est possible de passer un objet Mail directement à son formulaire, pour le récupérer ensuite hydraté et prêt à être envoyé.


  • Développement

  • Framework

  • PHP

  • Symfony