Особенности разработки API на Symfony2

от автора

Так вышло, что всю свою не долгую карьеру я занимаюсь разработкой API для мобильных приложений и сайтов на Symfony2. Каждый раз открываю для себя все новые знания, которые кому-то покажутся очевидными, а кому-то помогут сэкономить не мало времени. Об этих знаниях и пойдет речь.

Формы

Вообще использовать дефолтные формы для API не лучшая идея, но если вы все же решились, то вам необходимо не забывать о некоторых особенностях. Изначально формы в symfony делались для обычных сайтов, где фронтенд и бекенд объединены.

Первая проблема возникает с entity type. Когда вы отсылаете запрос к методу, который использует entity type в формах – сначала достаются все сущности указанного класса, и только потом запрос на получение нужной сущности по отправленному id. Многие не знают об этом и очень удивляются, почему метод работает так долго.

Пример решения

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 } 

Вторая проблема возникает с checkbox type, который пытаются использовать для булевых значений, но особенность работы этого типа такова, что если ключ существует и он не пустой, то вернется true.

Пример решения

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

Во всех статьях про создание API советуется именно это замечательное расширение. Люди смотрят простенький пример, где у сущностей есть две serialization groups: details и list, и начинают у каждой сущности использовать именно эти названия и все замечательно работает, пока не попадется какая-нибудь связанная сущность, у которой группы названы точно так же и выводится очень много лишней, не нужной информации. Так же это может уводить в бесконечный цикл при сериализации, если обе модели выводят связь друг с другом.

Пример

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;     } } 

В примере видно, что при получении новости в поле author будут все поля, которые в User с группой details, что явно не входит в наши планы. Казалось бы очевидно, что так делать нельзя, но к моему удивлению так делают многие.

Я советую именовать группы как %entity_name%_details, %entity_name%_list и %entity_name%_embed. Последняя нужна как раз для тех случаев, когда есть связанные сущности и мы хотим вывести какую-то связанную сущность в списке.

Пример

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;     } } 

При таком подходе будут только необходимые поля, к тому же это можно будет использовать в других местах, где тоже нужно вывести краткую информацию о пользователе.

Конец

На самом деле, подобных советов еще очень много и если вам будет интересно, я с радостью ими поделюсь.

ссылка на оригинал статьи http://habrahabr.ru/post/257991/