📜 ⬆️ ⬇️

Features of API development on symfony2

So it turned out that all my short career I have been developing API for mobile applications and sites on Symfony2. Every time I discover all the new knowledge that will seem obvious to someone, and someone will help save a lot of time. About this knowledge and will be discussed.

Forms


In general, using default API forms is not the best idea, but if you decide to do it, then you need to not forget about some features. Initially, forms in symfony were made for ordinary sites where the front end and backend are merged.

The first problem occurs with the entity type . When you send a request to a method that uses an entity type in forms, first all the entities of the specified class are obtained, and only then the request to get the necessary entity by the sent id. Many do not know about it and are very surprised why the method works so long.

Solution example
EntityType.php
<?php namespace App\CommonBundle\Form\Type; use App\CommonBundle\Form\DataTransformer\EntityDataTransformer; use Doctrine\ORM\EntityManager; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolverInterface; class EntityType extends AbstractType { private $em; public function __construct(EntityManager $em) { $this->em = $em; } public function setDefaultOptions(OptionsResolverInterface $resolver) { $resolver->setDefaults([ 'field' => 'id', 'class' => null, 'compound' => false ]); $resolver->setRequired([ 'class', ]); } public function buildForm(FormBuilderInterface $builder, array $options) { $builder->addModelTransformer(new EntityDataTransformer($this->em, $options['class'], $options['field'])); } public function getName() { return 'entity'; } } 

')
EntityDataTransformer.php
 <?php namespace App\CommonBundle\Form\DataTransformer; use Doctrine\ORM\EntityManager; use Symfony\Component\Form\DataTransformerInterface; class EntityDataTransformer implements DataTransformerInterface { private $em; private $entityName; private $fieldName; public function __construct(EntityManager $em, $entityName, $fieldName) { $this->em = $em; $this->entityName = $entityName; $this->fieldName = $fieldName; } public function transform($value) { return null; } public function reverseTransform($value) { if (!$value) { return null; } return $this->em->getRepository($this->entityName)->findOneBy([$this->fieldName => $value]); } } 


services.yml
  common.form.type.entity: class: App\CommonBundle\Form\Type\EntityType arguments: [@doctrine.orm.entity_manager] tags: - { name: form.type, alias: entity } 



The second problem arises from the checkbox type , which they try to use for boolean values, but the peculiarity of this type of operation is such that if the key exists and it is not empty, then it returns true.

Solution example
BooleanType.php
 <?php namespace App\CommonBundle\Form\Type; use App\CommonBundle\Form\DataTransformer\BooleanDataTransformer; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; class BooleanType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) { $builder->addViewTransformer(new BooleanDataTransformer()); } public function getParent() { return 'text'; } public function getName() { return 'boolean'; } } 


BooleanDataTransformer.php
 <?php namespace App\CommonBundle\Form\DataTransformer; use Symfony\Component\Form\DataTransformerInterface; use Symfony\Component\Form\Exception\TransformationFailedException; class BooleanDataTransformer implements DataTransformerInterface { public function transform($value) { return null; } public function reverseTransform($value) { if ($value === "false" || $value === "0" || $value === "" || $value === 0) { return false; } return true; } } 


services.yml
  common.form.type.boolean: class: App\CommonBundle\Form\Type\BooleanType tags: - { name: form.type, alias: boolean } 



Jms serializer


In all the articles on creating an API, it is this remarkable extension that is recommended. People look at a simple example where entities have two serialization groups: details and list, and each entity begins to use these particular names and everything works fine until there is some related entity whose groups are named the same way and very a lot of unnecessary, not necessary information. It can also lead to an infinite loop during serialization, if both models display a connection with each other.

Example of misuse
News.php
 <?php use JMS\Serializer\Annotation as Serialization; class News { /** * @Serialization\Groups({"details", "list"}) */ protected $id; /** * @Serialization\Groups({"details", "list"}) */ protected $title; /** * @Serialization\Groups({"details", "list"}) */ protected $text; /** *    User * * @Serialization\Groups({"details", "list"}) */ protected $author; } 


User.php
 <?php use JMS\Serializer\Annotation as Serialization; class User { /** * @Serialization\Groups({"details", "list"}) */ protected $id; /** * @Serialization\Groups({"details", "list"}) */ protected $name; /**      list  details */ } 


NewsController.php
 <?php class NewsController extends BaseController { /** * @SerializationGroups({"details"}) * @Route("/news/{id}", requirements={"id": "\d+"}) */ public function detailsAction(Common\Entity\News $entity) { return $entity; } } 




In the example we see that when receiving news in the author field there will be all fields that are in User with the details group, which is clearly not in our plans. It would seem, obviously, that it is impossible to do this, but, to my surprise, so many do.

I suggest naming groups as% entity_name% _details,% entity_name% _list, and% entity_name% _embed. The latter is needed just for those cases when there are related entities and we want to display some related entity in the list.

Example of proper use
News.php
 <?php use JMS\Serializer\Annotation as Serialization; class News { /** * @Serialization\Groups({"news_details", "news_list"}) */ protected $id; /** * @Serialization\Groups({"news_details", "news_list"}) */ protected $title; /** * @Serialization\Groups({"news_details", "news_list"}) */ protected $text; /** *    User * * @Serialization\Groups({"news_details", "news_list"}) */ protected $author; } 


User.php
 <?php use JMS\Serializer\Annotation as Serialization; class User { /** * @Serialization\Groups({"user_details", "user_list", "user_embed"}) */ protected $id; /** * @Serialization\Groups({"user_details", "user_list", "user_embed"}) */ protected $name; /**   ,    user_list  user_details */ } 


NewsController.php
 <?php class NewsController extends BaseController { /** * @SerializationGroups({"news_details", "user_embed"}) * @Route("/news/{id}", requirements={"id": "\d+"}) */ public function detailsAction(Common\Entity\News $entity) { return $entity; } } 




With this approach there will be only the required fields, besides, it can be used in other places, where you also need to display brief information about the user.

the end


In fact, there are a lot of similar tips and if you are interested, I’ll be happy to share them.

Source: https://habr.com/ru/post/257991/


All Articles