Так вышло, что всю свою недолгую карьеру я занимаюсь разработкой API для мобильных приложений и сайтов на Symfony2. Каждый раз открываю для себя все новые знания, которые кому-то покажутся очевидными, а кому-то помогут сэкономить не мало времени. Об этих знаниях и пойдет речь.
Вообще использовать дефолтные формы для API не лучшая идея, но если вы все же решились, то вам необходимо не забывать о некоторых особенностях. Изначально формы в symfony делались для обычных сайтов, где фронтенд и бекенд объединены.
Первая проблема возникает с entity type. Когда вы отсылаете запрос к методу, который использует entity type в формах – сначала достаются все сущности указанного класса, и только потом запрос на получение нужной сущности по отправленному id. Многие не знают об этом и очень удивляются, почему метод работает так долго.
Вторая проблема возникает с checkbox type, который пытаются использовать для булевых значений, но особенность работы этого типа такова, что если ключ существует и он не пустой, то вернется true.
Во всех статьях про создание API советуется именно это замечательное расширение. Люди смотрят простенький пример, где у сущностей есть две serialization groups: details и list, и начинают у каждой сущности использовать именно эти названия и все замечательно работает, пока не попадется какая-нибудь связанная сущность, у которой группы названы точно так же и выводится очень много лишней, не нужной информации. Также это может уводить в бесконечный цикл при сериализации, если обе модели выводят связь друг с другом.
В примере видно, что при получении новости в поле author будут все поля, которые в User с группой details, что явно не входит в наши планы. Казалось бы, очевидно, что так делать нельзя, но, к моему удивлению, так делают многие.
Я советую именовать группы как %entity_name%_details, %entity_name%_list и %entity_name%_embed. Последняя нужна как раз для тех случаев, когда есть связанные сущности и мы хотим вывести какую-то связанную сущность в списке.
При таком подходе будут только необходимые поля, к тому же это можно будет использовать в других местах, где тоже нужно вывести краткую информацию о пользователе.
На самом деле, подобных советов еще очень много и если вам будет интересно, я с радостью ими поделюсь.
Формы
Вообще использовать дефолтные формы для API не лучшая идея, но если вы все же решились, то вам необходимо не забывать о некоторых особенностях. Изначально формы в symfony делались для обычных сайтов, где фронтенд и бекенд объединены.
Первая проблема возникает с entity type. Когда вы отсылаете запрос к методу, который использует entity type в формах – сначала достаются все сущности указанного класса, и только потом запрос на получение нужной сущности по отправленному id. Многие не знают об этом и очень удивляются, почему метод работает так долго.
Пример решения
EntityType.php
EntityDataTransformer.php
services.yml
<?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
BooleanDataTransformer.php
services.yml
<?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
User.php
NewsController.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
User.php
NewsController.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;
}
}
При таком подходе будут только необходимые поля, к тому же это можно будет использовать в других местах, где тоже нужно вывести краткую информацию о пользователе.
Конец
На самом деле, подобных советов еще очень много и если вам будет интересно, я с радостью ими поделюсь.