Hello,
Un rapide article sur symfony 3 et les possibilités de webservices proposé par le FOSRestBundle ainsi que JMSSerializer pour gérer un service REST en JSON.
L’idée est de créer un webservice en json pour la récupération de divers objets depuis ma base de données, ici rien de complexe, aucun retraitement des données, ce sera un simple controller avec un objet issus de la base de données.
Installation des composants
composer require FOSRestBundle composer require JMSSerializerBundle
Configuration
La configuration contiendra plus de lignes que notre développement ;).
Modification de AppKernel ajout des bundles suivants dans $bundles.
new JMS\SerializerBundle\JMSSerializerBundle(), new FOS\UserBundle\FOSUserBundle(),
Ajout de la configuration propre au module fosrest :
app/config/config.yml:
fos_rest: body_listener: true format_listener: true view: view_response_listener: 'force' formats: xml: true json : true templating_formats: html: true force_redirects: html: true failed_validation: HTTP_BAD_REQUEST default_engine: twig routing_loader: default_format: json disable_csrf_role: null access_denied_listener: enabled: false service: null formats: unauthorized_challenge: null param_fetcher_listener: enabled: true
Ne pas oublier d’activer le serializer pour le framework :
app/config/config.yml:
framework: serializer: enabled: true enable_annotations: true
Ajout des routes automatiques du Bundle :
app/config/routing.yml:
task_api: type: rest resource: TaskBundle\Controller\ApiController prefix: /api
Ici, j’aimerais que mon service renvoi des éléments de type rest sur des urls commençant par /api/.
Une fois la configuration terminé on peut passer à la programmation.
Création d’une classe Métier
Ici mon but est d’afficher des Catégories (avec un nom et un id).
On peut utiliser ici plusieurs annotations bien sympatique, qui permettent :
- Dégager toutes les propriétés de bases (ExclusionPolicy)
- Choisir les éléments à afficher (Expose)
- Choisir leurs groupes d’affichages cf. le controlleur (Groups)
Le gros interêt de JMS Serializer ici est de pouvoir choisir les éléments que l’on veut afficher en fonction de leur contexte (default, category).
namespace TaskBundle\Entity; use Doctrine\ORM\Mapping as ORM; use JMS\Serializer\Annotation as Serializer; /** * Category * * @Serializer\ExclusionPolicy("all") * @ORM\Table(name="category") * @ORM\Entity(repositoryClass="TaskBundle\Repository\CategoryRepository") */ class Category { /** * @var int * * @Serializer\Expose * @Serializer\Groups({"default"}) * @ORM\Column(name="id", type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") */ private $id; /** * @var string * * @Serializer\Expose * @Serializer\Groups({"default"}) * @ORM\Column(name="name", type="string", length=255) */ private $name; }
Création d’un controller REST
use FOS\RestBundle\Controller\FOSRestController; use FOS\RestBundle\Controller\Annotations\Get; use FOS\RestBundle\Controller\Annotations\View; class ApiController extends FOSRestController { /** * @return Category * @View(serializerGroups={"default", "category"}) */ public function getCategoryAction(Request $req,$id){ return $this->getDoctrine()->getRepository('TaskBundle:Category')->find($id); } }
Dans mon cas ce controller me crée une nouvelle route :
ce que l’on peut vérifier avec la commande « console debug:router »
get_category GET ANY ANY /api/categories/{id}.{_format}
Les routes sont crée automatiquement par le controller.
On peut désormais accéder à notre url de service : /api/categories/1
Ce qui nous renverra dans mon cas :
{"id":1,"name":"D\u00e9veloppement"}
A noter que l’on peut rajouter un format dérrière notre url :
/api/categories/1.json
/api/categories/1.xml
/api/categories/1.html
Gestion des posts
Il est possible de modifier nos objets directement depuis le controller rest, en respectant le format demandé (si l’on est en XML, il faut faire un post d’un objet xml, en json, il faut post un objet json, …).
On ajoute donc une action a notre Controller :
/** * @return Category * @View(serializerGroups={"default", "category"}) */ public function postCategoryAction(Request $req){ return $this->processFromType($req,CategoryType::class,new Category()); } private function processFromType($req,$type,$elem){ if($elem==null)return null; $form = $this->createForm($type, $elem); $form->handleRequest($req); if ($form->isValid()) { $elem = $form->getData(); $em = $this->getDoctrine()->getManager(); $em->persist($elem); $em->flush(); return $elem; } return $this->handleView($this->view($form, 400)); }
Un petit coup de « console debug:router » pour vérifier la création de notre route d’édition :
get_category GET ANY ANY /api/categories/{id}.{_format} post_category POST ANY ANY /api/categories.{_format}
Il vous faudra bien entendu un objet pour gérer le type du formulaire, ici un CategoryType :
namespace TaskBundle\Form; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\OptionsResolver\OptionsResolver; class CategoryType extends AbstractType { /** * @param FormBuilderInterface $builder * @param array $options */ public function buildForm(FormBuilderInterface $builder, array $options) { $builder ->add('name') ->add('id') ; } /** * @param OptionsResolver $resolver */ public function configureOptions(OptionsResolver $resolver) { $resolver->setDefaults(array( 'data_class' => 'TaskBundle\Entity\Category', 'csrf_protection' => false )); } /** * @return string */ public function getName() { return 'category'; } }
Le CategoryType est important, il permet de choisir le nom de notre container ici : « category », utile pour notre requête de création/modification :
curl -v -H "Accept: application/json" -H "Content-type: application/json" -X POST -d '{"category":{"name":"foo"}}' http://monurl/api/categories
Et voilà, vous savez tout … vous pouvez tenter de jouer sur les Groups et serializerGroups={« default », « category »} pour cacher des éléments en fonction des actions de votre controller (utile pour afficher des objets enfants ou non).