Так вышло, что всю свою не долгую карьеру я занимаюсь разработкой API для мобильных приложений и сайтов на Symfony2. Каждый раз открываю для себя все новые знания, которые кому-то покажутся очевидными, а кому-то помогут сэкономить не мало времени. Об этих знаниях и пойдет речь.
Формы
Вообще использовать дефолтные формы для API не лучшая идея, но если вы все же решились, то вам необходимо не забывать о некоторых особенностях. Изначально формы в symfony делались для обычных сайтов, где фронтенд и бекенд объединены.
Первая проблема возникает с entity type. Когда вы отсылаете запрос к методу, который использует entity type в формах – сначала достаются все сущности указанного класса, и только потом запрос на получение нужной сущности по отправленному id. Многие не знают об этом и очень удивляются, почему метод работает так долго.
<?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.
<?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, и начинают у каждой сущности использовать именно эти названия и все замечательно работает, пока не попадется какая-нибудь связанная сущность, у которой группы названы точно так же и выводится очень много лишней, не нужной информации. Так же это может уводить в бесконечный цикл при сериализации, если обе модели выводят связь друг с другом.
<?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. Последняя нужна как раз для тех случаев, когда есть связанные сущности и мы хотим вывести какую-то связанную сущность в списке.
<?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/
Добавить комментарий