Особенности разработки API на Symfony2
Так вышло, что всю свою не долгую карьеру я занимаюсь разработкой API для мобильных приложений и сайтов на Symfony2. Каждый раз открываю для себя все новые знания, которые кому-то покажутся очевидными, а кому-то помогут сэкономить не мало времени. Об этих знаниях и пойдет речь.ФормыВообще использовать дефолтные формы для API не лучшая идея, но если вы все же решились, то вам необходимо не забывать о некоторых особенностях. Изначально формы в symfony делались для обычных сайтов, где фронтенд и бекенд объединены.Первая проблема возникает с entity type. Когда вы отсылаете запрос к методу, который использует entity type в формах — сначала достаются все сущности указанного класса, и только потом запрос на получение нужной сущности по отправленному id. Многие не знают об этом и очень удивляются, почему метод работает так долго.
Пример решения EntityType.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
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
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
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
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
use JMS\Serializer\Annotation as Serialization;
class User { /** * @Serialization\Groups ({«details», «list»}) */ protected $id;
/** * @Serialization\Groups ({«details», «list»}) */ protected $name;
/** Огромный список полей отмеченных группами list и details */ } NewsController.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
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
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
class NewsController extends BaseController { /** * @SerializationGroups ({«news_details», «user_embed»}) * @Route (»/news/{id}», requirements={«id»:»\d+»}) */ public function detailsAction (Common\Entity\News $entity) { return $entity; } } При таком подходе будут только необходимые поля, к тому же это можно будет использовать в других местах, где тоже нужно вывести краткую информацию о пользователе.
Конец На самом деле, подобных советов еще очень много и если вам будет интересно, я с радостью ими поделюсь.