• 30 hours
  • Medium

Free online content available in this course.

Videos available in this course

Certificate of achievement available at the end this course

Got it!

Last updated on 5/13/19

Convertir les paramètres de requêtes

Log in or subscribe for free to enjoy all this course has to offer!

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->getRepository('OCPlatformBundle:Advert')->find($id);

  if (null === $advert) {
    throw new NotFoundHttpException("L'annonce d'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, Symfony a évidemment tout prévu !

Les ParamConverters

Vous pouvez créer ou utiliser des ParamConverters qui vont agir juste avant l'appel du 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 $id dans 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ènement kernel.controller.

On l'a vu dans le chapitre sur les évènements, cet évènement est déclenché lorsque le noyau de Symfony sait quel contrôleur exécuter (après le routeur, donc), mais avant d'exécuter effectivement le contrôleur. Lors de cet évènement, les listeners ont la possibilité de modifier la Request. Ainsi, 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 est Advert, devant le nom de la variable. Le ParamConverter sait alors qu'il doit créer une entité de classe 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, cela permet plus de flexibilité.

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 ParamConverter Doctrine

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

Vous pouvez donc vous servir du DoctrineParamConverter. 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 le DoctrineParamConverter est 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 le if (null !== $advert) pour vérifier qu'elle existe bien !

De plus, si vous mettez dans l'URL un id qui n'existe pas, alors le DoctrineParamConverter vous 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 $bidule contiendra 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 attribut id. En fait, cela fonctionne avec tous les attributs de l'entité Advert ! Appelez votre paramètre de route slug, 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 de DoctrineParamConverter, 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 variable que l'on veut injecter. Le advert 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 option mapping. 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 route advert_id correspond à l'attribut id de 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 et skill. Il suffit pour cela de passer les deux attributs dans l'option mapping de 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 différentes 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 ParamConverter Datetime

Ce ParamConverter est plus simple : il se contente de convertir une date d'un format défini en un objet de type Datetime. 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 $date qui vaut la chaîne de caractères « 2014-09-20 » par exemple, vous récupérez directement un objet Datetime à 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 de Sensio\Bundle\FrameworkExtraBundle\EventListener\ParamConverterListener. Ce listener écoute l'évènement kernel.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 $converters contient 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 $configuration contient 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 retourne true lors de l'exécution de sa méthode apply(), alors les éventuels autres ne seront pas exécutés.

Comment Symfony trouve tous les convertisseurs ?

Pour connaître tous les convertisseurs, Symfony 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 tag request.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.paramconverter.json:
    class: OC\PlatformBundle\ParamConverter\JsonParamConverter
    tags:
      - { name: request.param_converter }

On a ajouté le tag request.param_converter sur 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 convertisseur JsonParamConverter sur ce squelette, que je place dans le répertoire ParamConverter du 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() et apply().

La méthode supports()

La méthode supports() doit retourner true lorsque le convertisseur souhaite convertir le paramètre en question, false sinon. 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éthode apply()

La méthode apply() 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 notre JsonParamConverter

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 fonction json_decode.

Évitons de convertir tous les paramètres de routes, c'est pourquoi on n'appliquera notre convertisseur que sur les paramètre de route ayant 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, où nous avions typé avec Advert  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;
// N'oubliez pas le use pour l'annotation :
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 de renseigner d'options dans l'annotation ici, car nous n'avons aucune option particulière à passer au ParamConverter. Mais si vous en aviez, c'est ici qu'il faut 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 le fonction print_r d'un tableau en pur PHP, qui a donc é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 Symfony : Doctrine et Datetime ;

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

  • Le code du cours tel qu'il doit être à ce stade est disponible sur la branche iteration-20 du dépot Github.

Example of certificate of achievement
Example of certificate of achievement