• 30 heures
  • Facile

Ce cours est visible gratuitement en ligne.

course.header.alt.is_video

Ce cours existe en livre papier.

course.header.alt.is_certifying

Vous pouvez être accompagné et mentoré par un professeur particulier par visioconférence sur ce cours.

J'ai tout compris !

Mis à jour le 04/09/2017

Convertir les paramètres de requêtes

Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

L'objectif des ParamConverters, ou « convertisseurs de paramètres », est de vous faire gagner du temps et des lignes de code. Sympa, non ? :)

Il s'agit de transformer automatiquement un paramètre de route, comme{id}par exemple, en un objet, une entité$advert par exemple. Vous ne pourrez plus vous en passer ! Et bien entendu, il est possible de créer vos propres convertisseurs, qui n'ont de limite que votre imagination. ;)

Théorie : pourquoi un ParamConverter ?

Récupérer des entités Doctrine avant même le contrôleur

Sur la page d'affichage d'une annonce par exemple, n'êtes-vous pas fatigués de toujours devoir vérifier l'existence de l'annonce demandée, et de l'instancier vous-mêmes ? N'avez-vous pas l'impression d'écrire toujours et encore les mêmes lignes ?

<?php
// src/OC/PlatformBundle/Controller/AdvertController.php

// …

public function viewAction($id)
{
  $em     = $this->getDoctrine()->getManager();
  $advert = $em->find('OC\PlatformBundle\Entity\Advert', $id);

  if (null !== $advert) {
    throw $this->createNotFoundException("L'annonce demandée [id=".$id."] n'existe pas.");
  }

  // Ici seulement votre vrai code…

  return $this->render('OCPlatformBundle:Advert:view.html.twig', array(
      'advert' => $advert
  ));
}

Pour enfin vous concentrer sur votre code métier, Symfony2 a évidemment tout prévu !

Les ParamConverters

Vous pouvez créer ou utiliser des ParamConverters qui vont agir juste avant le contrôleur. Comme son nom l'indique, un ParamConverter convertit les paramètres de votre route au format que vous préférez. En effet, depuis la route, vous ne pouvez pas tellement agir sur vos paramètres. Tout au plus, vous pouvez leur imposer des contraintes via des expressions régulières. Les ParamConverters pallient cette limitation en agissant après le routeur, mais avant le contrôleur, pour venir transformer à souhait ces paramètres.

Le résultat des ParamConverters est stocké dans les attributs de requête, c'est-à-dire qu'on peut les injecter dans les arguments de l'action du contrôleur.

Un ParamConverter utile :DoctrineParamConverter

Vous l'aurez deviné, ce ParamConverter va nous convertir nos paramètres directement en entités Doctrine ! L'idée est la suivante : dans le contrôleur, au lieu de récupérer le paramètre de route{id}sous forme de variable$id, on va récupérer directement une entitéAdvert sous la forme d'une variable$advert, qui correspond à l'annonce portant l'id$id.

Et un bonus en prime : on veut également que, s'il n'existe pas d'annonce portant l'id$iddans la base de données, alors une exception 404 soit levée. Après tout, c'est comme si l'on mettait dans la route :requirements: Advert exists!

Un peu de théorie sur les ParamConverters

Comment fonctionne un ParamConverter ?

Un ParamConverter est en réalité un simple listener, qui écoute l'évènementkernel.controller.

On l'a vu dans le chapitre sur les évènements, cet évènement est déclenché lorsque le noyau de Symfony2 sait quel contrôleur exécuter (après le routeur, donc), mais avant d'exécuter effectivement le contrôleur. Ainsi, lors de cet évènement, le ParamConverter va lire la signature de la méthode du contrôleur pour déterminer le type de variable que vous voulez. Cela lui permet de créer un attribut de requête du même type, à partir du paramètre de la route, que vous récupérez ensuite dans votre contrôleur.

Pour déterminer le type de variable que vous voulez, le ParamConverter a deux solutions. La première consiste à regarder la signature de la méthode du contrôleur, c'est-à-dire le typage que vous définissez pour les arguments :

<?php
public function testAction(Advert $advert)

Ici, le typage estAdvert, devant le nom de la variable. Le ParamConverter sait alors qu'il doit créer une entitéAdvert.

La deuxième solution consiste à utiliser une annotation@ParamConverter, ce qui nous permet de définir nous-mêmes les informations dont il a besoin.

Au final, depuis votre contrôleur, vous avez en plus du paramètre original de la route ($id) un nouveau paramètre ($advert) créé par votre ParamConverter qui s'est exécuté avant votre contrôleur. Et bien entendu, il sera possible de créer vos propres ParamConverters ! ;)

Pratique : utilisation des ParamConverters existants

Utiliser le ParamConverterDoctrine

Ce ParamConverter fait partie du bundleSensio\FrameworkBundle. C'est un bundle activé par défaut avec la distribution standard de Symfony2, que vous avez si vous suivez ce cours depuis le début.

Vous pouvez donc vous servir duDoctrineParamConverter. Il existe plusieurs façon de l'utiliser, avec ou sans expliciter l'annotation. Voyons ensemble les différentes méthodes.

1. S'appuyer sur l'id et le typage de l'argument

C'est la méthode la plus simple, et peut-être la plus utilisée. Reprenons notre route pour afficher une vue :

# src/OC/PlatformBundle/Resources/config/routing.yml

oc_platform_view:
    path:      /advert/{id}
    defaults:  { _controller: OCPlatformBundle:Advert:view }
    requirements:
        id: \d+

Une route somme toute classique, dans laquelle figure un paramètre{id}. On a mis une contrainte pour que cet id soit un nombre, très bien. Le seul point important est que le paramètre s'appelle « id », ce qui est aussi le nom d'un attribut de l'entitéAdvert.

Maintenant, la seule chose à changer pour utiliser leDoctrineParamConverterest côté contrôleur, où il faut typer un argument de la méthode, comme ceci :

<?php
// src/OC/PlatformBundle/Controller/AdvertController.php

namespace OC\PlatformBundle\Controller;

use OC\PlatformBundle\Entity\Advert;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;

class AdvertController extends Controller
{
  public function viewAction(Advert $advert, $id)
  {
    // Ici, $advert est une instance de l'entité Advert, portant l'id $id
  }
}

Faites le test ! Vous verrez que$advert est une entité pleinement opérationnelle. Vous pouvez l'afficher, créer un formulaire avec, etc. Bref, vous venez d'économiser le$em->find()nécessaire pour récupérer manuellement l'entité, ainsi que leif (null !== $advert) pour vérifier qu'elle existe bien !

De plus, si vous mettez dans l'URL un id qui n'existe pas, alors leDoctrineParamConvertervous lèvera une exception, résultant en une page d'erreur 404 comme dans la figure suivante.

J'ai tenté d'afficher une annonce qui n'existe pas, voici la page d'erreur 404
J'ai tenté d'afficher une annonce qui n'existe pas, voici la page d'erreur 404

Avec cette méthode, la seule information que le ParamConverter utilise est le typage de l'argument de la méthode, et non le nom de l'argument. Par exemple, vous pourriez tout à fait avoir ceci :

<?php
public function viewAction(Advert $bidule)

Cela ne change en rien le comportement, et la variable$bidulecontiendra une instance de l'entitéAdvert.

Une dernière note sur cette méthode. Ici cela a fonctionné car le paramètre de la route s'appelle « id », et que l'entitéAdvert a un attributid.En fait, cela fonctionne avec tous les attributs de l'entitéAdvert ! Appelez votre paramètre de routeslug, et accédez à une URL de type /platform/advert/slug-existant, cela fonctionne exactement de la même manière ! Cependant, pour l'utilisation d'autres attributs que l'id, je vous conseille d'utiliser les méthodes suivantes.

2. Utiliser l'annotation pour faire correspondre la route et l'entité

Il s'agit maintenant d'utiliser explicitement l'annotation deDoctrineParamConverter, afin de personnaliser au mieux le comportement. Considérons maintenant que vous avez la route suivante :

# src/OC/PlatformBundle/Resources/config/routing.yml

oc_platform_view:
    path:      /advert/{advert_id}
    defaults:  { _controller: OCPlatformBundle:Advert:view }
    requirements:
        advert_id: \d+

La seule différence est que le paramètre de la route s'appelle maintenant « advert_id ». On aurait pu tout aussi bien l'appeler « bidule », l'important est que ce soit un nom qui n'est pas également un attribut de l'entitéAdvert. Le ParamConverter ne peut alors pas faire la correspondance automatiquement, il faut donc le lui dire.

Cela ne nous fait pas peur ! Voici l'annotation à utiliser :

<?php
// src/OC/PlatformBundle/Controller/AdvertController.php

use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;

/**
 * @ParamConverter("advert", options={"mapping": {"advert_id": "id"}})
 */
public function viewAction(Advert $advert)

Il s'agit maintenant d'être un peu plus rigoureux. Dans l'annotation@ParamConverter, voici ce qu'il faut renseigner :

  • Le premier argument de l'annotation correspond au nom de l'argument de la méthode que l'on veut injecter. Leadvert de l'annotation correspond donc au$advert de la méthode.

  • Le deuxième argument correspond aux options à passer au ParamConverter. Ici, nous avons passé une seule optionmapping. Cette option fait la correspondance « paramètre de route » => « attribut de l'entité ». Dans notre exemple, c'est ce qui permet de dire au ParamConverter : « le paramètre de routeadvert_idcorrespond à l'attributidde l'Advert ».

Le ParamConverter connaît le type d'entité à récupérer (Advert,Category, etc.) en lisant, comme précédemment, le typage de l'argument.

Bien entendu, il est également possible de récupérer une entité grâce à plusieurs attributs. Prenons notre entitéAdvertSkill par exemple, qui est identifiée par deux attributs :advert etskill. Il suffit pour cela de passer les deux attributs dans l'optionmappingde l'annotation, comme suit :

<?php
// src/OC/PlatformBundle/Controller/AdvertController.php

use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;

// La route serait par exemple :
// /platform/advert/{advert_id}/{skill_id}

/**
 * @ParamConverter("advertSkill", options={"mapping": {"advert_id": "advert", "skill_id": "skill"}})
 */
public function viewAction(AdvertSkill $advertSkill)
3. Utiliser les annotations sur plusieurs arguments

Grâce à l'annotation, il est alors possible d'appliquer plusieurs ParamConverters à plusieurs arguments. Prenez la route suivante :

# src/OC/PlatformBundle/Resources/config/routing.yml

oc_platform_view:
    path:      /advert/{advert_id}/applications/{application_id}
    defaults:  { _controller: OCPlatformBundle:Advert:view }

L'idée ici est d'avoir deux paramètres dans la route, qui vont nous permettre de récupérer deux entités grâce au ParamConverter.

Avec deux paramètres à convertir il vaut mieux tout expliciter grâce aux annotations, plutôt que de reposer sur une devinette. La mise en application est très simple, il suffit de définir deux annotations, chacune comme on l'a déjà vu. Voici comment le faire :

<?php
/**
 * @ParamConverter("advert",      options={"mapping": {"advert_id": "id"})
 * @ParamConverter("application", options={"mapping": {"application_id": "id"})
 */
public function viewAction(Advert $advert, Application $application)

Utiliser le ParamConverterDatetime

Ce ParamConverter est plus simple : il se contente de convertir une date d'un format défini en un objet de typeDatetime. Très pratique !

Partons donc de cette route par exemple :

# src/OC/PlatformBundle/Resources/config/routing.yml

oc_platform_list:
    path:      /list/{date}
    defaults:  { _controller: OCPlatformBundle:Advert:viewList }

Et voici comment utiliser le convertisseur sur la méthode du contrôleur :

<?php
// src/OC/PlatformBundle/Controller/AdvertController.php

use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;

/**
 * @ParamConverter("date", options={"format": "Y-m-d"})
 */
public function viewListAction(\Datetime $date)

Ainsi, au lieu de simplement recevoir l'argument$datequi vaut « 2014-09-20 » par exemple, vous récupérez directement un objetDatetimeà cette date, vraiment sympa.

Aller plus loin : créer ses propres ParamConverters

Comment sont exécutés les ParamConverters ?

Avant de pouvoir créer notre ParamConverter, étudions comment ils sont réellement exécutés.

À l'origine de tout, il y a un listener, il s'agit deSensio\Bundle\FrameworkExtraBundle\EventListener\ParamConverterListener. Ce listener écoute l'évènementkernel.controller, ce qui lui permet de connaître le contrôleur qui va être exécuté. L'idée est qu'il parcourt les différents ParamConverters pour exécuter celui qui convient le premier. On peut synthétiser son comportement par le code suivant :

<?php

foreach ($converters as $converter) {
  if ($converter->supports($configuration)) {
    if ($converter->apply($request, $configuration)) {
      return;
    }
  }
}

Dans ce code :

  • La variable$converterscontient la liste de tous les ParamConverters, nous voyons plus loin comment elle est construite ;

  • La méthode$converter->supports()demande au ParamConverter si le paramètre actuel l'intéresse ;

  • La variable$configurationcontient les informations de l'annotation : le typage de l'argument, les options de l'annotation, etc. ;

  • La méthode$converter->apply()permet d'exécuter à proprement parler le ParamConverter.

L'ordre des convertisseurs est donc très important, car si le premier retournetruelors de l'exécution de sa méthodeapply(), alors les éventuels autres ne seront pas exécutés.

Comment Symfony2 trouve tous les convertisseurs ?

Pour connaître tous les convertisseurs, Symfony2 utilise un mécanisme que nous avons déjà utilisé : les tags des services. Vous l'aurez compris, un convertisseur est avant tout un service, sur lequel on a appliqué le tagrequest.param_converter.

Commençons donc par créer la définition d'un service, que nous allons implémenter en tant que ParamConverter :

# src/OC/PlatformBundle/Resources/config/services.yml

services:
    oc_platform.json_paramconverter:
        class: OC\PlatformBundle\ParamConverter\JsonParamConverter
        tags:
            - { name: request.param_converter }

On a ajouté le tagrequest.param_convertersur notre service, ce qui permet de l'enregistrer en tant que tel. Je n'ai pas mis de priorité, mais vous pouvez en préciser une grâce à l'attribut priority dans le tag.

Créer un convertisseur

Créons maintenant la classe du ParamConverter. Un convertisseur doit implémenter l'interface ParamConverterInterface. Commençons par créer la classe d'un convertisseurJsonParamConvertersur ce squelette, que je place dans le répertoireParamConverterdu bundle :

<?php
// src/OC/PlatformBundle/ParamConverter/JsonParamConverter.php

namespace OC\PlatformBundle\ParamConverter;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Sensio\Bundle\FrameworkExtraBundle\Request\ParamConverter\ParamConverterInterface;
use Symfony\Component\HttpFoundation\Request;

class JsonParamConverter implements ParamConverterInterface
{
  function supports(ParamConverter $configuration)
  {
  }

  function apply(Request $request, ParamConverter $configuration)
  {
  }
}

L'interface ne définit que deux méthodes :supports()etapply().

La méthodesupports()

La méthodesupports()doit retournertruelorsque le convertisseur souhaite convertir le paramètre en question,falsesinon. Les informations sur le paramètre courant sont stockées dans l'argument$configuration, et contiennent :

  • $configuration->getClass(): le typage de l'argument dans la méthode du contrôleur ;

  • $configuration->getName(): le nom de l'argument dans la méthode du contrôleur ;

  • $configuration->getOptions(): les options de l'annotation, si elles sont explicitées (vide bien sûr lorsqu'il n'y a pas l'annotation).

Vous devez, avec ces trois éléments, décider si oui ou non le convertisseur compte convertir le paramètre.

La méthodeapply()

La méthodeapply()doit effectivement créer un attribut de requête, qui sera injecté dans l'argument de la méthode du contrôleur.

Ce travail peut être effectué grâce à ses deux arguments :

  • La configuration, qui contient les informations sur l'argument de la méthode du contrôleur, que nous avons vu juste au-dessus ;

  • La requête, qui contient tout ce que vous savez, et notamment les paramètres de la route courante via$request->attributs->get('paramètre_de_route').

L'exemple de notreJsonParamConverter

Histoire de bien comprendre ce que chaque méthode et chaque variable doit faire, je vous propose un petit exemple. Imaginons que, d'une façon ou d'une autre, vous avez un paramètre de route qui contient un tableau en JSON, par exemple {"a":1,"b":2,"c":3}. On souhaite simplement transformer cette chaîne de caractères JSON en un tableau PHP, via la fonctionjson_decode.

Évitons de convertir tous les paramètres, c'est pourquoi on n'appliquera notre convertisseur que sur le paramètre de route avec pour nom "json".

 

La classe

Nous avons déjà le squelette du service de notre convertisseur, il ne manque plus que l'implémentation concrète. Voici la mienne :

<?php
// src/OC/PlatformBundle/ParamConverter/JsonParamConverter.php

namespace OC\PlatformBundle\ParamConverter;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Sensio\Bundle\FrameworkExtraBundle\Request\ParamConverter\ParamConverterInterface;
use Symfony\Component\HttpFoundation\Request;

class JsonParamConverter implements ParamConverterInterface
{
  function supports(ParamConverter $configuration)
  {
    // Si le nom de l'argument du contrôleur n'est pas "json", on n'applique pas le convertisseur
    if ('json' !== $configuration->getName()) {
      return false;
    }

    return true;
  }

  function apply(Request $request, ParamConverter $configuration)
  {
    // On récupère la valeur actuelle de l'attribut
    $json = $request->attributes->get('json');

    // On effectue notre action : le décoder
    $json = json_decode($json, true);

    // On met à jour la nouvelle valeur de l'attribut
    $request->attributes->set('json', $json);
  }
}
Le contrôleur

Pour utiliser votre convertisseur flambant neuf, il faut explicitement appliquer le convertisseur via une annotation.

Pourquoi ? Car le listener, lorsqu'il n'y a pas d'annotation pour un argument de contrôleur, n'applique les ParamConverter que si l'argument est typé. C'était le cas tout à l'heure avec les convertisseurs Doctrine et Datetime. Mais ici, ce n'est pas le cas, on n'attend pas un objet mais un simple tableau PHP.

Ce n'est pas grave, ajoutons donc l'annotation nécessaire (simple car pas d'option particulière) :

<?php
// src/OC/PlatformBundle/Controller/AdvertController.php

namespace OC\PlatformBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;

class AdvertController extends Controller
{
  /**
   * @ParamConverter("json")
   */
  public function ParamConverterAction($json)
  {
    return new Response(print_r($json, true));
  }
}

Pas besoin d'expliciter l'annotation ici, car nous n'avons aucune option particulière à passer au ParamConverter. Mais si vous en aviez, c'est le seul moyen de le faire donc gardez-le en tête. ;)

Essayez le ! Créez cette route par exemple :

# app/config/routing_dev.yml

oc_platform_paramconverter:
    path: /test/{json}
    defaults: { _controller: "OCPlatformBundle:Advert:ParamConverter" }

Et accédez à la page /test/{"a":1,"b":2,"c":3}. Le résultat est l'affichage par la fonctionprint_r d'un tableau en pur PHP, qui a été décodé depuis le JSON initial.

Bien sûr l'exemple est plutôt simple, ici en une ligne vous convertissez le paramètre de JSON en PHP. Mais laissez libre cours à votre imagination, et vos convertisseurs peuvent être complexes. N'oubliez pas, ils sont avant tout des services, dans lesquels vous pouvez y injecter d'autres services pour faire des actions plus complexe que simplement décoder du JSON.

  • Un ParamConverter vous permet de créer un attribut de requête, que vous récupérez ensuite en argument de vos méthodes de contrôleur ;

  • Il existe deux ParamConverters par défaut avec Symfony2 :DoctrineetDatetime;

  • Il est facile de créer ses propres convertisseurs pour accélérer votre développement.

Exemple de certificat de réussite
Exemple de certificat de réussite