SonataAdminBundle: создание объекта из List View (часть 2)

    Постановка задачи


    В первой части статьи мы создали кнопку в строке List View писем, которая переводит нас на форму создания ответа. Однако остались нерешенными по крайней мере два важных вопроса:
    1. автоматическая привязка ответа к письму
    2. проверка прав пользователя на создание ответа


    Автоматическая привязка ответа к письму


    Существует как минимум три способа автоматической привязки ответа к письму на уровне SonataAdminBundle:
    1) После создания формы перед сохранением сущности ответа в базу (для этого можно использовать метод prePersist объекта Admin). Главной особенностью этого способа является «непрозрачность» прикрепления одной сущности к другой для пользователя, работающего с формой, что может служить как достоинством, так и недостатком в зависимости от поставленных целей.
    2) Во время создания формы путем настройки значения необходимого поля с помощью formBuilder. Этот подход может привести к нежелательному загромождению админ-класса кодом, реализующим логику формирования значения поля по умолчанию.
    3) До создания формы. Для этого можно воспользоваться подходом, предлагаемым, например в [1], который заключается в переопределении родительского метода getNewInstance Admin-класса. Альтернативой такому решению может являться наследование от CRUD-контроллера (процесс наследования подробно описан в [2]) и определение в нем операции-зацепки [3] preCreate. Одним из преимуществ такой альтернативы является «штатная» возможность возвратить полноценный объект \Symfony\Component\HttpFoundation\Response() в случае, если это необходимо.
    Какой из способов использовать, зависит от решаемой задачи, мы же рассмотрим третий с использованием наследования от CRUD-контроллера, поскольку, на наш взгляд, он является наименее очевидным с точки зрения реализации и наиболее гибким с точки зрения архитектуры.
    В первую очередь, добавим третий аргумент в функцию admin.getRouteGenerator.generateUrl(). Это должен быть массив параметров запроса и мы добавим в него идентификатор письма, из строки которого создаем ответ. Для этого воспользуемся функцией admin.getUrlsafeIdentifier()

    {# src/AppBundle/Resources/views/CRUD/list__action_create_other_admin.html.twig #}
    <a href="{{ admin.getRouteGenerator.generateUrl(template_variables.otherAdmin, 'create', {'incoming_id': admin.getUrlsafeIdentifier(object)}) }}" class="btn btn-sm btn-default edit_link" title="Создать ответ">
        <i class="fa fa-plus"></i>
        Создать ответ
    </a>
    

    Таким образом, переход по ссылке позволит обратиться к createAction CRUD-контроллера, передав в качестве параметра запроса incoming_id идентификатор нужного нам письма. Теперь задача сводится к получению объекта письма по идентификатору и прикреплению к нему ответа. Мы ее будем решать путем определения пустого по умолчанию метода preCreate, который может возвращать объект \Symfony\Component\HttpFoundation\Response() или не возвращать ничего, просто модифицируя $object.

    namespace Application\Sonata\AdminBundle\Controller;
    
    use AppBundle\Entity\Response;
    use Sonata\AdminBundle\Controller\CRUDController as BaseController;
    use Symfony\Component\HttpFoundation\Request;
    
    class CRUDController extends BaseController
    {
    
        public function preCreate(Request $request, $object)
        {
            // Здесь Response - сущность нашего ответа, а не объект \Symfony\Component\HttpFoundation\Response()
            if ($object instanceof Response) {
                // Если передан идентификатор письма
                if ($incomingId = $request->get('incoming_id')) {
                    // Если по идентификатору получен объект письма
                     if($incoming = $this->getDoctrine()->getRepository('AppBundle:Incoming')->find($incomingId)) {
                         // Прикрепление письма к ответу
                         $object->setIncoming($incoming);
    
                         // Или ответа к письму, оба варианта ПРИКРЕПЛЕНИЯ равнозначны
                         $incoming->setResponse($object);
                     }
                }
            }
        }
    
    }
    

    Главная задача, поставленная в начале статьи, а именно: создание сущности ответа из ListView писем — решена. Рассмотрим вопрос проверки прав пользователя на создание ответа.

    Проверка прав пользователя на создание ответа


    Непосредственно вопрос установки прав пользователя на создание ответа лежит вне темы данной статьи и может быть решен на основе подробной информации, изложенной в [4]. Мы же рассмотрим вопрос отображения или скрытия кнопки «Добавить ответ» в ListView ПИСЕМ в зависимости от наличия или отсутствия у пользователя прав на создание ОТВЕТА. Для этого нам снова придется внести изменения в файл list__action_create_other_admin.html.twig

    {# src/AppBundle/Resources/views/CRUD/list__action_create_other_admin.html.twig #}
    {% if template_variables.otherAdmin.securityHandler.isGranted(template_variables.otherAdmin, 'CREATE', template_variables.otherAdmin) and template_variables.otherAdmin.hasRoute('create') %}
     <a href="{{ admin.getRouteGenerator.generateUrl(template_variables.otherAdmin, 'create', {'incoming_id': admin.getUrlsafeIdentifier(object)}) }}" class="btn btn-sm btn-default edit_link" title="Создать ответ">
         <i class="fa fa-plus"></i>
         Создать ответ
     </a>
    {% endif %}
    

    При этом мы проверяем возможность создания ответа не только на уровне системы безопасности, но и на уровне системы роутинга.

    Резюме


    В статье изложен вариант решения задачи по созданию в SonataAdminBundle некоторой сущности из ListView второй сущности, связанной с первой, с учетом наличия или отсутствия у пользователя соответствующих прав.

    Ссылки на используемые ресурсы


    1. Populate resp. set default values on form resp. object or instance in SonataAdminBundle
    2. CREATING A CUSTOM ADMIN ACTION
    3. Операции-зацепки
    4. Security
    Поделиться публикацией
    Ой, у вас баннер убежал!

    Ну. И что?
    Реклама
    Комментарии 12
    • +2
      И в этом вся саната. Распихать всю логику по контроллерам. Размазать доктрину по проекту. Забыть что такое «поддерживаемый и тестируемый код».

      В статье изложен вариант решения задачи по созданию в SonataAdminBundle некоторой сущности из ListView второй сущности, связанной с первой, с учетом наличия или отсутствия у пользователя соответствующих прав.


      Переходите на схему «админка как SPA + API», тогда для такого простого CRUD можно вообще прослойку из PHP убрать и оставить тупо CRUD на монге. А работать с формами на клиенте как-то удобнее и функциональность больше. А с учетом фреймворков (angular/ember/react/vue), в воторых реюзать UI элементы намного проще.

      Если же такой возможности нет — уж лучше свои шаблоны для CRUD генератора сделать один раз. Ибо расширять сонату и не превращая код в кучу кастылей практически невозможно.
      • 0
        API+SPA это отличный подход. Но это не серебряная пуля.
        Например простейшая с точки зрения адмики вещь как проверка прав доступа к ресурсу, для отображения или нет в списке элементов, ссылки на редактирование или просмотр. Для такой простой вещи нужно передавать права либо вместе с ресурсами, либо отдельно, либо реализовывать первичную проверку на стороне фронта.
        И в этот момент этот простой CRUD уже не так элегантно выглядит в связке API+SPA.
        • 0
          проверка прав доступа к ресурсу, для отображения или нет в списке элементов


          1) список ресурсов приходит с сервера, а стало быть сокрытие «ненужных» айтемов это задача сервера и там она прекрасно решается.
          2) если надо разграничивать доступ к функционалу — JWT, в токене аутентификации уже есть вся необходимая инфа

          И в этот момент этот простой CRUD уже не так элегантно выглядит в связке API+SPA.


          Почму же? более чем элегантно. Проверки прав на сервере в этом случае в большинстве своем ограничиваются простыми воутерами на уровне экшенов запросов (или просто ограничения по ролям/пермишенам). А на уровне UI нам в любом случае нужно делать свои проверки. В связке с JWT можно спокойно это делать.
          • 0
            Если нужно просто отфильтровать список по правам, понятно что никакй пролемы нет. Я говорил как раз о другом случае.

            Представим что у нас есть список элементов E, к этим елементам можно применять действия С и В. При этом у пользовател могут быть права на действиа С только для некоторых элементов, а для других прав на действие С у него нет.

            Как быть в таком случае?
            • 0
              Повторюсь. JWT. На клиенте уже хранится детальная информация о уровне прав пользователя. Стало быть можно реализовать (сильно сказано, есть готовые модули как правило) проверки точно так же, как если бы мы делали это на сервере. Все тот же механизм воутеров. Просто придется продублировать логику на клиенте. Точно так же как нам уже приходится дублировать логику валидации на клиенте и сервере.

              Для простых случаев с ролями все это можно автоматизировать, а вот в приведенном вами — все чуть сложнее но не сильно. С другой стороны этот незначительный оверхэд окупается слихвой упрощением бэкэнда в плане UI (теперь у нас нет никаких форм, шаблонов и т.д., и весь UI это просто HTTP API), а на клиенте можно реализовать много вкусностей.
              • 0
                Немного холиварщины, но это может быть интересно и другим, думаю. Мне вобще кажется, что для PHP хорошо, что он стал почти Java в плане ООП: наследование, неймспейсы, трейты, интерфейсы и т.д. Его основаная идея — шаблонизатор с вкраплениями кода — почти отжила своё. Назревает следующий вопрос: какой ЯП и сопутствующая экосистема наиболее хорошо (быстро/дешево/гибко) может решать задачи создания API для клиентов (браузер/мобильные устройства/что-то еще)? Понятно, что здесь нет описания проекта, но если предположить, что этот абстрактный проект похож на некий проект, в котором могут использовать SonataAdminBundle, CRUD-генераторы и т.д. Что-то вроде middleware между абстрактной БД и абстрактным клиентом.
                • 0
                  в плане ООП: наследование, неймспейсы, трейты, интерфейсы и т.д.


                  Все то что вы описали это не про ООП а про организацию кода и устранение дублирования. Для православного ООП достаточно возможности определять объекты своего типа, разграничивать доступ к деталям реализации (например что бы только объекты одного типа имели доступ к деталям реализации друг-друга) и собственно все. Даже интерфейсы — это уже для удобства, хотя с ними лучше.

                  какой ЯП и сопутствующая экосистема


                  Для андроида — своя, для ios — своя, для браузеров — javascript и там уже зависит от ваших предпочтений. Ну то есть вы поняли думаю. Скажем я использую angular и у меня накопилась куча реюзабельных ui компонентов. Потому накидывтаь crud мне легко и быстро. Можно облениться и сделать крудо-генератор, с тулами вроде yo это вообще не проблема.
        • 0
          А есть сейчас достойные решения (и не обязательно даже на Symfony), которые будут стройные по архитектуре, поддерживаемые, и реализовывать хотя бы малую часть из того, что уже реализовано и работает в сонате?
          Да, в ней порядочно недостатков, срашного кода, и сообщество только формируется. Но она отлично выполняет свою задачу — иметь быстро поднимаемую и достаточно гибкую админку для проекта, который можно строить по любым канонам и убеждениям.
          Когда админка — это всего лишь задний двор проекта, можно сосредоточиться на основной работе и использовать уже готовое решение.
          • 0
            Вопрос требований. Если вам нужно админку CRUD-о подобную поднять быстренько, и в дальшейнем не планируется ее поддерживать — соната ок. Но в моей практике как правило не бывает что бы все так просто. Да и что бы с сонатой эффективно работать надо времени в нее порядочно инвестировать.

            Когда админка — это всего лишь задний двор проекта, можно сосредоточиться на основной работе и использовать уже готовое решение.

            Ну так CRUD генераторов полно, шаблоны свои тоже можно определить, генерим апишку, накидываем UI на ангулярах, использует готовые UI-компоненты. Это все кажется возможно сложным и долгим, но по факту оверхэд по времени не такой большой и долгосрочной перспективе выходит выгоднее.

            Но если админка нужна временно — то тут без разницы.
            • 0
              Не могли бы вы привести примеры случаев из практики, в которых соната оказалась неподъемной/неоправданной? Спрашиваю не из праздного любопытства, а поскольку есть планы на её использование. Предыдущий мой опыт был положительным, но дальше многосвязанного CRUD дело не пошло.
              • +1
                Честно, я уже не вспомню. Я помню ужасный контроллер на 1.5К строк кода, в которм половина магия и любые кастомные вещи надо делать через хуки.

                У меня просто задачи частенько бывают простыми первое время, а потом «а мы тут подумали, нам тут нужна возможность запоминать кто делал такие-то и такие-то действия», или «а вот тут вот эту штуку могут делать только такие-то чуваки при таких условиях». И как бы, все это на сонате сделать конечно можно, но по трудозатратам профита по сравнению комбинаций CRUD генераторов + подправить код где надо вообще нет, а читаемость и поддерживаемость кода сильно страдает.

                Года 3 назад мы в компании, в которой я работаю, решили в принципе отказаться от всей этой дури с формами для админок (да и в целом), и просто перешли на ангуляр. В итоге простой круд в виде апишки делается дико быстро и удобно, да и UI админки можно лепить быстро и выходит он более отзычивым, можно делать множество вещей, которые с обычными формами было бы тяжело сделать. Типа понадобился мне компонент для фильтров по дате — не вопрос, просто подключаем модуль и реюзаем из проекта в проект. Все красивенько, няшно, юзабилити неплохое, да и оверхэда на время разработки. А самое главное, рисков в долгосрочной перспективе намного меньше (хотя тут кто как сделает конечно).
                • +2
                  Поделюсь своим опытом, у нас есть достаточно крупный проект с несколькими десятками моделей и кучей бизнес логики, который мы переводили на Symfony.

                  По первости когда реализацию админки откладывать было уже просто некуда, я тоже набрел на Sonata, но очень быстро столкнулся с тем что любой шаг в сторону приводит к отвратительным костылям и жрет кучу времени на то что-бы разобраться. Дальше мы пошли по пути «написать свой CRUD генератор и успокоиться», но заниматься этим было откровенно лень. В итоге мы остановились на связке rest-bundle + SPA ng-admin. ng-admin показал себя очень хорошо, т.к. имеет отличную документацию и очень легкую кастомизацию под свои нужды.

                  С серверной стороны, единственная проблема была заставить в нужных местах rest-bundle использовать наш сервисный слой, а не на прямую ходить в БД, что-бы допустим когда я в админке создаю пользователя, выполнялись все связанные с этим вещи: отправка email'ов, регистрация в мессенджере и прочих микросервисах. Но с rest-bundle получилось сделать это достаточно легко и просто.

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

          Самое читаемое