Pull to refresh

Comments 40

Когда открывал статью то думал что первым же пунктом увижу что-то по поводу форм. Обрадовался когда увидел хотя бы упоминания что формы юзать в контексте API не ок (да, когда-то я думал что ок).

JMS Serializer — самое распространенное решение не лишенное проблем. По сути оно навязывает решение проблем, которое само и привносит.

Далее я надеялся увидеть альтернативные подходы… но увы и ах, конец… на самом интересном месте.

Я все же думаю что стоит выкинуть JmsSerializer и выкидывать простенькие DTO из и в сервисный слой. Хоть в виде ассоциативных массивов, хоть в виде классов с сеттерами, хоть в виде stdObject.

Я бы хотел увидеть, скажем… отложенное формирование view во фронт контроллере, что бы можно было флаш доктрины туда же вынести.
А можно пример какой-нибудь, как вы работаете без JMS Serializer?

Насчет форм:
У нас все запросы, которые содержат в себе какие-либо параметры, обязательно мапятся на модели. Даже если это какой-нибудь список с фильтрами. Поэтому мы отказались полностью от форм и просто в моделях пишем что-то подобное:
class IntakeFilter
{
    protected $user;

    /**
     * @RequestMapper(type="integer")
     */
    protected $limit = 20;

    /**
     * @RequestMapper(type="integer")
     */
    protected $offset = 0;

    /**
     * @RequestMapper(type="boolean")
     */
    protected $isCompleted;

    /**
     * @RequestMapper(type="string")
     * @Assert\Length(min=2)
     */
    protected $query;
}


И в контроллере:
public function actionList() 
{
    $model = $this->handleRequest(new App\Model\IntakeFilter($this->getUser());

    return $repository->findByFilter($model);
}
Ну не сказать что я отказался от JmsSerializer, только на домашних проектах пока что. Но как-то так:

class SomeEntity {
    public function getShortInfoView() {
        
        // думаю эту штуку можно сократить для пущей читабельности. 
        // А если у нас будет много вьюшек, можно DRY-ить эти вещи
        // или если дела будут совсем плохи, делать нормальные объекты DTO.
        return [
             'id' => $this->id,
             'name' => $this->name,
             'some_info' => $this->someInfo,
             'connected_entities' => $this->connectedEntities->map(function (SomeConntectedEntity $entity) {
                  return $entity->getShortInfoView();
             })->toArray();
        ];
    }
}

class SomeController {

    public function addSomeAction(Request $request) { 
         // обычно это происходит во фронт контроллере для всех запросов с Content-type application/json
         // тут просто для наглядности
         $dto = json_encode($request->getContent());

         // у меня умерла под конец дня фантазия, так что простите за именования в духе some creator
         $shortInfoView = $this->get('app.some_creator')->createSome($dto);
 
         // это так же происходит во фронт контроллере но мне лень
         // что до ситуация с ID и их получением до flush
         // я использую postgresql и тамошние последовательности, которые прекрасно это дело разруливают 
         // а автоинкремент mysql это зло
         $this->get('doctrine.orm.entity_manager')->flush();

         // обычно из контроллера я возвращаю только данные или какой-то View объект в духе FostRest
         return new JsonResponse($shortInfoView);
    }
}

class SomeCreator 
{
     private $someRepository;

     public function __construct(SomeRepository $repository) {
          $this->someRepository = $repository;
     }

     public function create($dto) {
     
         // тут можно провалидировать $dto но лень
         // всеравто чуть что конструктор энтити бросит исключение
         // ну и лучше что бы $dto был \stdObject. 
         // Тогда потом можно будет в случае усложнения логики 
         // переделать его на нормальный DTO объект со своим типом
         $entity = new SomeEntity($dto['name'], $dto['short_info']);

         $this->someRepository->add($entity);
         
         return $entity->getShortInfoView();
     }
}


вот как-то так. Заметте что внутри сервисного слоя заключена все знание о том как наша бизнес логика работает и ни одного упоминания о доктрине. Так же, поскольку все энтити крутятся в unit-of-work, а коммит транзакции происходит вне оного, не очень безопасно возвращать в контроллер саму энтити, так как можно случайно там поменять состояние оной. Лучше плюнуть наружу DTO, что мы и делаем. А уж с простым массивом справится и json_encode.

Как-то так. Пока вариантов лучше я не придумал и не знаю…
Ммм… опечатался слегка… вместо
$entity = new SomeEntity($dto['name'], $dto['short_info']);

надо
$entity = new SomeEntity($dto['name'], $dto['some_info']);
Интересный вариант, схема правда та же самая, но инструменты другие. Мне нравится, что это работает довольно быстро, ибо выбрасываем jms serializaer, но больше плюсов, честно говоря, не вижу :(
Как же не видите? Все явно, можно полностью проследить всю логику от момента получения данных запроса до вывода наружу. И все это из сервисного слоя приложения. Полностью вся бизнес логика по одной фиче в одном месте. И все можно покрыть тестами. Как по мне это намного более весомый аргумент чем производительность.
А ну и еще, я указывал это в комментах в коде но может вы не обратили внимание. У меня обычно flush выполняется непосредственно перед отправкой ответа, во фронт контроллере. И если мы выплюнем сущность из сервиса в контроллер, кто-то может изменить (случайно или специально) состояние сущности и мы получим баги. При моем варианте же возвращается DTO, и если мы чего поменяем в нем, то как бы и пофигу.
И еще очень интересно, о каких именно проблемах с JMS Serializer вы говорите, ибо пока что это очень удобно. Контроллеры выглядят примерно так:
    /**
     * @Common\Annotation\SerializationGroup("clinic_details")
     * @Common\Annotation\HttpStatus(201)
     *
     * @Method("POST")
     * @Route
     */
    public function addAction()
    {
        $entity = $this->handleRequest(new Common\Entity\Clinic($this->getUser());
        $this->persist($entity, true);

        return $entity;
    }
группы сериализации. Это очень мощный инструмент но он вносит в умы людей много смуты. Они начинают оперировать целыми сущностями в рамках какой-то бизнес логики, а не частью оной. То есть по сути необходимость в них есть только за счет того что никто не хочет писать скучный код формирующий DTO или вообще не понимает зачем это нужно и в чем крутость. У меня вот и symfony/validation не используется). Ну а для сложной иерархии состояний можно подключить еще и Value Object-ы, и разруливать это дело на уровне контроллера. Код можно DRY-ить, он явный, можно посадить любого человека знающего PHP и он сразу сможет писать бизнес логику. Ну и все можно покрыть юнит тестами, со штуками типа JMSSerializer, FosRest и т.д. спасают только интеграционные и функциональные тесты.
Мне кажется у JMSSerializer слишком много плюшек, чтобы от него отказываться, а проблему с сериализационными группами надо решать не выделением имен по каким-то действиям (list, details), а используя предметную область (default, secure).
Мы у себя сделали _fields парметр, как во всех API. Оно ни в коем случае не заменяет группы, которые мы используем чтобы разделить рендеринг приватной информации от общей.
Проблемы с выделением групп вообще нет, просто JmsSerializer позволяет тебе выплюнуть наружу целиком твои сущности и дальше разруливать сериализацию гурппами, вместо того что бы выплюнуть DTO где есть все что нужно и ничего лишнего. Оно конечно удобно, позволяет тебе не писать туповатый бойлерплейт, но многие слишком много на него вешают. Доходит до того что весь контроллер состоит из десериализации и персиста, а вся логика размазана по хэндлерам и т.д
UFO just landed and posted this here
Не медленно, у вас нету формы, потому использовать Symfony/forms вне форм не целесообразно. Обычно их используют только из-за хорошей интеграции с доктриной из коробки.
эээ что вы подразумеваете под
хорошей интеграции с доктриной из коробки
?
Угадывание типов на основе доступной информации о мэппинге подтянутой из доктрины. Я вот о чем.
да ну, это фича дополнительного характера. Для меня в формах в разрезе API то что я могу описать правила на получаемые данные, провалидировать их и замапить на сущность или дто.
Причин много, во первых в API нет форм, как уже говорили, во вторых, для того, чтобы формы начали более менее работать – нужно множество костылей, которых накапливается такое количество, что в какой-то момент задаешься вопросом «Ну и нафига все это?», в третьих это действительно очень медлено
что для вас формы?
Обычные html формы, у которых другие типы данных, есть отображение, данные берутся из $_GET/$_POST
ну вас никто не заставляет их рендерить в html. Я не вижу особых проблем почему для API не стоит использовать формы. Они хорошо подходят для маппинг, валидация реквест данных. Конечно в какой-то момент они избыточны, но согласитесь, что писать велосипед дороже. Хотя есть еще вариант использовать OptionsResolver, но не использовал, не могу по нему сказать какие плюсы или минусы.
вот нашел линку тыц в принципе выглядит неплохо. надо попробовать
Ок, допустим мы выбрали формы. И флоу будет таким:
— Приняли запрос
— Во фронт контроллере сделали json_encode тела запроса
— Забиндили результат работы json_encode на энтити через формы
— Валидируем состояние нашей сущности
— забираем из формы готовую сущность, куда формы запихали все через сеттеры. (ненавижу тупые сеттеры в сущностях)

А как можно:
— Приняли запрос
— Во фронт контроллере сделали десериализацию напрямую в сущность через JmsSerializer тела запроса, При этом JmsSerializer достаточно гибок, и имеет меньше ограничений.
— Валидируем состояние нашей сущности
— Как бы все, можно это делать в ParamConverter, и тогда в контроллер придет уже все готовое

(хоть я и не одобряю такой подход)

Вообще если вы берете формы, то работать напрямую с сущностью не очень хорошая идея. Да, на маленьких проектах норм, но чем сложнее проект тем больше боли они приносят. Если работать с DTO то и боли меньше с формами, и сущности красивые выходят.
(ненавижу тупые сеттеры в сущностях)

не вы один

Ну а если помимо API у вас есть веб сайт, где есть такие же сущности, то тогда легче юзать форму для двух вариантов.

Основной мой посыл в том что формы это не панацея. Все зависит от разработчика, который должен понимать что делает, а то потом может быть очень плохо)
По поводу entity type. Все очень просто, вы можете указать в опциях query builder и тогда он будет вытягивать с условием in. Да не очевидный кейс но он работает
Я бы в контексте Symfony + API добавил бы в статью ещё и рассказ об FOSRestBundle, о какой-нибудь JWT аутентификации, обновлении и удалении токена и прочем. Ибо когда мне пришлось выполнять данную задачу, то столкнулся с кучей проблем уже на первых парах.

Было бы интересно почитать :)
JWT аутентификации,

зачем он вам? лучше взять FOSOAuthServerBundle и проблем не будет.
Вот видите, об этом я и говорю — информации в статье крайне мало. Мне, как минимум, было бы интересно почитать и перенять опыт :)

За бандл отдельное спасибо ;)
Согласен с тем что информации мало, в первую очередь хотел написать статью, для того, чтобы понять насколько это интересно другим. Опыта не мало, особенно в каких-то мелочах, но со временем это все кажется очевидным и просто не понимаешь о чем писать.

У нас на проектах, так исторически сложилось, что мы не используем бандлы для авторизации. Каждый запрос подписывается с помощью заголовка X-Access-Token, а в случае успеха юзер подставляется в security.context. Все это делается буквально в 1 listener, и мне до конца не понятно, зачем использовать что-то готовое, когда написать код дело 3 минут.

Если интересно, могу поделиться кодом или еще лучше написать об этом в следующей статье :)
Многие считают OAuth2 избыточным для REST API. Мне этот вариант лично нравится, но не могли бы вы как-то аргументировать почему не будет проблем и почему это круто? Ну мол… можно использовать старую добрую digest авторизацию которая есть в symfony из коробки.
Я бы не сказал что OAuth2 избыточный. Все зависит от того для чего его применять. Если вы пишите апи который используете только вы, то да возможно нет смысла тянуть туда OAuth2. digest и oauth разные цели преследую. oauth в первую очередь для того чтоб авторизировать приложение.
Про FOSRestBundle, как и про многие бандлы, которые советуют использовать при создании API, я могу сказать только одно: их очень удобно использовать первое время, пока проект очень маленький и простой. Потом начинаются проблемы, из-за того, что разработчик не понимает как это работает и начинает это использовать не как это изначально, возникают проблемы и на их решение уходит большая часть времени. Но как только схема работы становится очевидной, приходит понимание, что можно сделать и лучше, не использовать кучу кода, который будет висеть мертвым грузом.

Rest Bundle – это же по сути набор небольших скриптов и библиотек, уложенных в определенную структуру. Мне гораздо удобнее использовать их отдельно. Например: бандл для сериализации использует тот же JMS Serializer, только лишь ограничивая в его кастомизации.
Да, это выбор каждого. Я предпочитаю использовать уже написанное и покрытое тестами, а не писать велосипеды. В некоторых случаях своё, конечно, оправданно, не спорю.
Если будет лишнее время, на покрытие тестами, постараюсь залить куда-нибудь свои наработки. Что-то вроде своего взгляда на то, как должен выглядеть REST Bundle, ориентированный в первую очередь на скорость разработки и гибкость.
Да хоть так залейте, а если будет что-то интересное с покрытием кода тестами можно помочь. Гитхаб, опенсурс, все такое.
Разрешите поделиться нашим опытом:
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, работы столько же, а выхлоп в разы круче.
JMSSerializer переусложнен и очень медленный. Даже опытные разработчики могли потерять часы, чтобы что-то подправить в API сгенерированном этим сервисом.

Приведите, пожалуйста, конкретный пример. К слову, с такой же ситуацией я и сам недавно столкнулся, но количество времени, которое JMSSerializer сэкономил, ставит на нет некоторые недочеты и сложности.

Если нужно сделать что-то сложное, например поиск, это вообще ад.

А причем serializer к поиску? Если и необходимо дополнительные данные вывести (количество всех записей, текущая страница/сдвиг и т.д.) — ну добавили объект-обертку ResultSet к результирующей коллекции и полет нормальный

Джуниора вообще подпускать к этому бандлу нереально.

Тут сложно не согласиться.

Но… java работает примерно от 10 до 200 раз быстрее.

До 200 раз? Приведите, пожалуйста, пример
Приведите, пожалуйста, конкретный пример. К слову, с такой же ситуацией я и сам недавно столкнулся, но количество времени, которое JMSSerializer сэкономил, ставит на нет некоторые недочеты и сложности.

Сделайте кастомную выгрузку коллекции, чтобы там было 3-4 связи и это было скажем на хотябы на 100K записей. А также выводились данные о пагинации.

До 200 раз? Приведите, пожалуйста, пример

www.techempower.com/benchmarks — Искать Spring и Symfony2. На самом деле так и есть.

p.s. Я холиварить не особенно хочу, просто выбор сделал и делюсь с вами.
Сделайте кастомную выгрузку коллекции, чтобы там было 3-4 связи и это было скажем на хотябы на 100K записей. А также выводились данные о пагинации.


Ну вы же не 100К записей выводите. У нас есть пример с многомиллионной таблицей — все работает. Сами связи и степень их вложенности ведь запросто контролируются аннотациями, более того можно что-нибудь сложное выбирать в POST_SERIALIZE джоином, если много запросов не устраивает.

Про пагинацию в предыдущем комментарии ответил, нет ничего сложного и «очень медленного».

> www.techempower.com/benchmarks — Искать Spring и Symfony2. На самом деле так и есть.

Хотелось бы реальный пример, ну да ладно.

PS никто не холиварит, здоровый интерес :)
Sign up to leave a comment.

Articles