Формы
Вообще использовать дефолтные формы для 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;
}
}
При таком подходе будут только необходимые поля, к тому же это можно будет использовать в других местах, где тоже нужно вывести краткую информацию о пользователе.
Конец
На самом деле, подобных советов еще очень много и если вам будет интересно, я с радостью ими поделюсь.
Комментарии (40)
sqrt
15.05.2015 08:32+1По поводу entity type. Все очень просто, вы можете указать в опциях query builder и тогда он будет вытягивать с условием in. Да не очевидный кейс но он работает
kovalevsky
15.05.2015 10:25+1Я бы в контексте Symfony + API добавил бы в статью ещё и рассказ об FOSRestBundle, о какой-нибудь JWT аутентификации, обновлении и удалении токена и прочем. Ибо когда мне пришлось выполнять данную задачу, то столкнулся с кучей проблем уже на первых парах.
Было бы интересно почитать :)sqrt
15.05.2015 15:40+2JWT аутентификации,
зачем он вам? лучше взять FOSOAuthServerBundle и проблем не будет.kovalevsky
15.05.2015 16:22+1Вот видите, об этом я и говорю — информации в статье крайне мало. Мне, как минимум, было бы интересно почитать и перенять опыт :)
За бандл отдельное спасибо ;)lowadka Автор
15.05.2015 16:42+1Согласен с тем что информации мало, в первую очередь хотел написать статью, для того, чтобы понять насколько это интересно другим. Опыта не мало, особенно в каких-то мелочах, но со временем это все кажется очевидным и просто не понимаешь о чем писать.
У нас на проектах, так исторически сложилось, что мы не используем бандлы для авторизации. Каждый запрос подписывается с помощью заголовка X-Access-Token, а в случае успеха юзер подставляется в security.context. Все это делается буквально в 1 listener, и мне до конца не понятно, зачем использовать что-то готовое, когда написать код дело 3 минут.
Если интересно, могу поделиться кодом или еще лучше написать об этом в следующей статье :)
Fesor
15.05.2015 17:21Многие считают OAuth2 избыточным для REST API. Мне этот вариант лично нравится, но не могли бы вы как-то аргументировать почему не будет проблем и почему это круто? Ну мол… можно использовать старую добрую digest авторизацию которая есть в symfony из коробки.
sqrt
15.05.2015 17:34+1Я бы не сказал что OAuth2 избыточный. Все зависит от того для чего его применять. Если вы пишите апи который используете только вы, то да возможно нет смысла тянуть туда OAuth2. digest и oauth разные цели преследую. oauth в первую очередь для того чтоб авторизировать приложение.
lowadka Автор
15.05.2015 16:50+1Про FOSRestBundle, как и про многие бандлы, которые советуют использовать при создании API, я могу сказать только одно: их очень удобно использовать первое время, пока проект очень маленький и простой. Потом начинаются проблемы, из-за того, что разработчик не понимает как это работает и начинает это использовать не как это изначально, возникают проблемы и на их решение уходит большая часть времени. Но как только схема работы становится очевидной, приходит понимание, что можно сделать и лучше, не использовать кучу кода, который будет висеть мертвым грузом.
Rest Bundle – это же по сути набор небольших скриптов и библиотек, уложенных в определенную структуру. Мне гораздо удобнее использовать их отдельно. Например: бандл для сериализации использует тот же JMS Serializer, только лишь ограничивая в его кастомизации.kovalevsky
15.05.2015 18:24Да, это выбор каждого. Я предпочитаю использовать уже написанное и покрытое тестами, а не писать велосипеды. В некоторых случаях своё, конечно, оправданно, не спорю.
lowadka Автор
15.05.2015 19:59Если будет лишнее время, на покрытие тестами, постараюсь залить куда-нибудь свои наработки. Что-то вроде своего взгляда на то, как должен выглядеть REST Bundle, ориентированный в первую очередь на скорость разработки и гибкость.
Fesor
15.05.2015 20:51Да хоть так залейте, а если будет что-то интересное с покрытием кода тестами можно помочь. Гитхаб, опенсурс, все такое.
witl
06.06.2015 22:44Разрешите поделиться нашим опытом:
1. JMSSerializer переусложнен и очень медленный. Даже опытные разработчики могли потерять часы, чтобы что-то подправить в API сгенерированном этим сервисом.
Если нужно сделать что-то сложное, например поиск, это вообще ад.
Джуниора вообще подпускать к этому бандлу нереально.
— Выход, сделали свои трансформеры, простые.
Код понятен, отлично дебажится и тестируется.
Сильно ускорили разработку, поскольку просто (KISS).
2. Symfony2 Form Component не используем.
Хороший компонент, но когда формы обычные.
Если речь идет о REST API, особенно если нужно сделать ~100 entity — формы начинают сильно замедлять разработку. Особенно для PATCH метода.
p.s. Вообще сейчас мигрируем на Spring(java).
С Symfony достаточно давно (c 2009), много кода написано.
Да еще и афилированные партнеры SensioLabs (про это могу в привате отдельно рассказать, кому интересно).
Но… java работает примерно от 10 до 200 раз быстрее.
Архитектура очень похожа на SF2, работы столько же, а выхлоп в разы круче.borNfree
08.06.2015 01:08JMSSerializer переусложнен и очень медленный. Даже опытные разработчики могли потерять часы, чтобы что-то подправить в API сгенерированном этим сервисом.
Приведите, пожалуйста, конкретный пример. К слову, с такой же ситуацией я и сам недавно столкнулся, но количество времени, которое JMSSerializer сэкономил, ставит на нет некоторые недочеты и сложности.
Если нужно сделать что-то сложное, например поиск, это вообще ад.
А причем serializer к поиску? Если и необходимо дополнительные данные вывести (количество всех записей, текущая страница/сдвиг и т.д.) — ну добавили объект-обертку ResultSet к результирующей коллекции и полет нормальный
Джуниора вообще подпускать к этому бандлу нереально.
Тут сложно не согласиться.
Но… java работает примерно от 10 до 200 раз быстрее.
До 200 раз? Приведите, пожалуйста, примерwitl
08.06.2015 17:10Приведите, пожалуйста, конкретный пример. К слову, с такой же ситуацией я и сам недавно столкнулся, но количество времени, которое JMSSerializer сэкономил, ставит на нет некоторые недочеты и сложности.
Сделайте кастомную выгрузку коллекции, чтобы там было 3-4 связи и это было скажем на хотябы на 100K записей. А также выводились данные о пагинации.
До 200 раз? Приведите, пожалуйста, пример
www.techempower.com/benchmarks — Искать Spring и Symfony2. На самом деле так и есть.
p.s. Я холиварить не особенно хочу, просто выбор сделал и делюсь с вами.borNfree
08.06.2015 18:53Сделайте кастомную выгрузку коллекции, чтобы там было 3-4 связи и это было скажем на хотябы на 100K записей. А также выводились данные о пагинации.
Ну вы же не 100К записей выводите. У нас есть пример с многомиллионной таблицей — все работает. Сами связи и степень их вложенности ведь запросто контролируются аннотациями, более того можно что-нибудь сложное выбирать в POST_SERIALIZE джоином, если много запросов не устраивает.
Про пагинацию в предыдущем комментарии ответил, нет ничего сложного и «очень медленного».
> www.techempower.com/benchmarks — Искать Spring и Symfony2. На самом деле так и есть.
Хотелось бы реальный пример, ну да ладно.
PS никто не холиварит, здоровый интерес :)
Fesor
Когда открывал статью то думал что первым же пунктом увижу что-то по поводу форм. Обрадовался когда увидел хотя бы упоминания что формы юзать в контексте API не ок (да, когда-то я думал что ок).
JMS Serializer — самое распространенное решение не лишенное проблем. По сути оно навязывает решение проблем, которое само и привносит.
Далее я надеялся увидеть альтернативные подходы… но увы и ах, конец… на самом интересном месте.
Я все же думаю что стоит выкинуть JmsSerializer и выкидывать простенькие DTO из и в сервисный слой. Хоть в виде ассоциативных массивов, хоть в виде классов с сеттерами, хоть в виде stdObject.
Я бы хотел увидеть, скажем… отложенное формирование view во фронт контроллере, что бы можно было флаш доктрины туда же вынести.
lowadka Автор
А можно пример какой-нибудь, как вы работаете без JMS Serializer?
Насчет форм:
У нас все запросы, которые содержат в себе какие-либо параметры, обязательно мапятся на модели. Даже если это какой-нибудь список с фильтрами. Поэтому мы отказались полностью от форм и просто в моделях пишем что-то подобное:
И в контроллере:
Fesor
Ну не сказать что я отказался от JmsSerializer, только на домашних проектах пока что. Но как-то так:
вот как-то так. Заметте что внутри сервисного слоя заключена все знание о том как наша бизнес логика работает и ни одного упоминания о доктрине. Так же, поскольку все энтити крутятся в unit-of-work, а коммит транзакции происходит вне оного, не очень безопасно возвращать в контроллер саму энтити, так как можно случайно там поменять состояние оной. Лучше плюнуть наружу DTO, что мы и делаем. А уж с простым массивом справится и json_encode.
Как-то так. Пока вариантов лучше я не придумал и не знаю…
Fesor
Ммм… опечатался слегка… вместо
надо
lowadka Автор
Интересный вариант, схема правда та же самая, но инструменты другие. Мне нравится, что это работает довольно быстро, ибо выбрасываем jms serializaer, но больше плюсов, честно говоря, не вижу :(
Fesor
Как же не видите? Все явно, можно полностью проследить всю логику от момента получения данных запроса до вывода наружу. И все это из сервисного слоя приложения. Полностью вся бизнес логика по одной фиче в одном месте. И все можно покрыть тестами. Как по мне это намного более весомый аргумент чем производительность.
Fesor
А ну и еще, я указывал это в комментах в коде но может вы не обратили внимание. У меня обычно flush выполняется непосредственно перед отправкой ответа, во фронт контроллере. И если мы выплюнем сущность из сервиса в контроллер, кто-то может изменить (случайно или специально) состояние сущности и мы получим баги. При моем варианте же возвращается DTO, и если мы чего поменяем в нем, то как бы и пофигу.
lowadka Автор
И еще очень интересно, о каких именно проблемах с JMS Serializer вы говорите, ибо пока что это очень удобно. Контроллеры выглядят примерно так:
Fesor
группы сериализации. Это очень мощный инструмент но он вносит в умы людей много смуты. Они начинают оперировать целыми сущностями в рамках какой-то бизнес логики, а не частью оной. То есть по сути необходимость в них есть только за счет того что никто не хочет писать скучный код формирующий DTO или вообще не понимает зачем это нужно и в чем крутость. У меня вот и symfony/validation не используется). Ну а для сложной иерархии состояний можно подключить еще и Value Object-ы, и разруливать это дело на уровне контроллера. Код можно DRY-ить, он явный, можно посадить любого человека знающего PHP и он сразу сможет писать бизнес логику. Ну и все можно покрыть юнит тестами, со штуками типа JMSSerializer, FosRest и т.д. спасают только интеграционные и функциональные тесты.
alxsad
Мне кажется у JMSSerializer слишком много плюшек, чтобы от него отказываться, а проблему с сериализационными группами надо решать не выделением имен по каким-то действиям (list, details), а используя предметную область (default, secure).
korotovsky
Мы у себя сделали _fields парметр, как во всех API. Оно ни в коем случае не заменяет группы, которые мы используем чтобы разделить рендеринг приватной информации от общей.
Fesor
Проблемы с выделением групп вообще нет, просто JmsSerializer позволяет тебе выплюнуть наружу целиком твои сущности и дальше разруливать сериализацию гурппами, вместо того что бы выплюнуть DTO где есть все что нужно и ничего лишнего. Оно конечно удобно, позволяет тебе не писать туповатый бойлерплейт, но многие слишком много на него вешают. Доходит до того что весь контроллер состоит из десериализации и персиста, а вся логика размазана по хэндлерам и т.д
ruFog
Можно вкратце объяснить почему формы в контексте API лучше не использовать? Это же просто меппер реквеста на объект. Медленно?
Fesor
Не медленно, у вас нету формы, потому использовать Symfony/forms вне форм не целесообразно. Обычно их используют только из-за хорошей интеграции с доктриной из коробки.
sqrt
эээ что вы подразумеваете под
?Fesor
Угадывание типов на основе доступной информации о мэппинге подтянутой из доктрины. Я вот о чем.
sqrt
да ну, это фича дополнительного характера. Для меня в формах в разрезе API то что я могу описать правила на получаемые данные, провалидировать их и замапить на сущность или дто.
lowadka Автор
Причин много, во первых в API нет форм, как уже говорили, во вторых, для того, чтобы формы начали более менее работать – нужно множество костылей, которых накапливается такое количество, что в какой-то момент задаешься вопросом «Ну и нафига все это?», в третьих это действительно очень медлено
sqrt
что для вас формы?
lowadka Автор
Обычные html формы, у которых другие типы данных, есть отображение, данные берутся из $_GET/$_POST
sqrt
ну вас никто не заставляет их рендерить в html. Я не вижу особых проблем почему для API не стоит использовать формы. Они хорошо подходят для маппинг, валидация реквест данных. Конечно в какой-то момент они избыточны, но согласитесь, что писать велосипед дороже. Хотя есть еще вариант использовать OptionsResolver, но не использовал, не могу по нему сказать какие плюсы или минусы.
sqrt
вот нашел линку тыц в принципе выглядит неплохо. надо попробовать
Fesor
Ок, допустим мы выбрали формы. И флоу будет таким:
— Приняли запрос
— Во фронт контроллере сделали json_encode тела запроса
— Забиндили результат работы json_encode на энтити через формы
— Валидируем состояние нашей сущности
— забираем из формы готовую сущность, куда формы запихали все через сеттеры. (ненавижу тупые сеттеры в сущностях)
А как можно:
— Приняли запрос
— Во фронт контроллере сделали десериализацию напрямую в сущность через JmsSerializer тела запроса, При этом JmsSerializer достаточно гибок, и имеет меньше ограничений.
— Валидируем состояние нашей сущности
— Как бы все, можно это делать в ParamConverter, и тогда в контроллер придет уже все готовое
(хоть я и не одобряю такой подход)
Вообще если вы берете формы, то работать напрямую с сущностью не очень хорошая идея. Да, на маленьких проектах норм, но чем сложнее проект тем больше боли они приносят. Если работать с DTO то и боли меньше с формами, и сущности красивые выходят.
sqrt
не вы один
Ну а если помимо API у вас есть веб сайт, где есть такие же сущности, то тогда легче юзать форму для двух вариантов.
Основной мой посыл в том что формы это не панацея. Все зависит от разработчика, который должен понимать что делает, а то потом может быть очень плохо)