Постановка задачи
В первой части статьи мы создали кнопку в строке List View писем, которая переводит нас на форму создания ответа. Однако остались нерешенными по крайней мере два важных вопроса:
- автоматическая привязка ответа к письму
- проверка прав пользователя на создание ответа
Автоматическая привязка ответа к письму
Существует как минимум три способа автоматической привязки ответа к письму на уровне 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 второй сущности, связанной с первой, с учетом наличия или отсутствия у пользователя соответствующих прав.
Fesor
И в этом вся саната. Распихать всю логику по контроллерам. Размазать доктрину по проекту. Забыть что такое «поддерживаемый и тестируемый код».
Переходите на схему «админка как SPA + API», тогда для такого простого CRUD можно вообще прослойку из PHP убрать и оставить тупо CRUD на монге. А работать с формами на клиенте как-то удобнее и функциональность больше. А с учетом фреймворков (angular/ember/react/vue), в воторых реюзать UI элементы намного проще.
Если же такой возможности нет — уж лучше свои шаблоны для CRUD генератора сделать один раз. Ибо расширять сонату и не превращая код в кучу кастылей практически невозможно.
Fedot
API+SPA это отличный подход. Но это не серебряная пуля.
Например простейшая с точки зрения адмики вещь как проверка прав доступа к ресурсу, для отображения или нет в списке элементов, ссылки на редактирование или просмотр. Для такой простой вещи нужно передавать права либо вместе с ресурсами, либо отдельно, либо реализовывать первичную проверку на стороне фронта.
И в этот момент этот простой CRUD уже не так элегантно выглядит в связке API+SPA.
Fesor
1) список ресурсов приходит с сервера, а стало быть сокрытие «ненужных» айтемов это задача сервера и там она прекрасно решается.
2) если надо разграничивать доступ к функционалу — JWT, в токене аутентификации уже есть вся необходимая инфа
Почму же? более чем элегантно. Проверки прав на сервере в этом случае в большинстве своем ограничиваются простыми воутерами на уровне экшенов запросов (или просто ограничения по ролям/пермишенам). А на уровне UI нам в любом случае нужно делать свои проверки. В связке с JWT можно спокойно это делать.
Fedot
Если нужно просто отфильтровать список по правам, понятно что никакй пролемы нет. Я говорил как раз о другом случае.
Представим что у нас есть список элементов E, к этим елементам можно применять действия С и В. При этом у пользовател могут быть права на действиа С только для некоторых элементов, а для других прав на действие С у него нет.
Как быть в таком случае?
Fesor
Повторюсь. JWT. На клиенте уже хранится детальная информация о уровне прав пользователя. Стало быть можно реализовать (сильно сказано, есть готовые модули как правило) проверки точно так же, как если бы мы делали это на сервере. Все тот же механизм воутеров. Просто придется продублировать логику на клиенте. Точно так же как нам уже приходится дублировать логику валидации на клиенте и сервере.
Для простых случаев с ролями все это можно автоматизировать, а вот в приведенном вами — все чуть сложнее но не сильно. С другой стороны этот незначительный оверхэд окупается слихвой упрощением бэкэнда в плане UI (теперь у нас нет никаких форм, шаблонов и т.д., и весь UI это просто HTTP API), а на клиенте можно реализовать много вкусностей.
ruFog
Немного холиварщины, но это может быть интересно и другим, думаю. Мне вобще кажется, что для PHP хорошо, что он стал почти Java в плане ООП: наследование, неймспейсы, трейты, интерфейсы и т.д. Его основаная идея — шаблонизатор с вкраплениями кода — почти отжила своё. Назревает следующий вопрос: какой ЯП и сопутствующая экосистема наиболее хорошо (быстро/дешево/гибко) может решать задачи создания API для клиентов (браузер/мобильные устройства/что-то еще)? Понятно, что здесь нет описания проекта, но если предположить, что этот абстрактный проект похож на некий проект, в котором могут использовать SonataAdminBundle, CRUD-генераторы и т.д. Что-то вроде middleware между абстрактной БД и абстрактным клиентом.
Fesor
Все то что вы описали это не про ООП а про организацию кода и устранение дублирования. Для православного ООП достаточно возможности определять объекты своего типа, разграничивать доступ к деталям реализации (например что бы только объекты одного типа имели доступ к деталям реализации друг-друга) и собственно все. Даже интерфейсы — это уже для удобства, хотя с ними лучше.
Для андроида — своя, для ios — своя, для браузеров — javascript и там уже зависит от ваших предпочтений. Ну то есть вы поняли думаю. Скажем я использую angular и у меня накопилась куча реюзабельных ui компонентов. Потому накидывтаь crud мне легко и быстро. Можно облениться и сделать крудо-генератор, с тулами вроде yo это вообще не проблема.
nitso
А есть сейчас достойные решения (и не обязательно даже на Symfony), которые будут стройные по архитектуре, поддерживаемые, и реализовывать хотя бы малую часть из того, что уже реализовано и работает в сонате?
Да, в ней порядочно недостатков, срашного кода, и сообщество только формируется. Но она отлично выполняет свою задачу — иметь быстро поднимаемую и достаточно гибкую админку для проекта, который можно строить по любым канонам и убеждениям.
Когда админка — это всего лишь задний двор проекта, можно сосредоточиться на основной работе и использовать уже готовое решение.
Fesor
Вопрос требований. Если вам нужно админку CRUD-о подобную поднять быстренько, и в дальшейнем не планируется ее поддерживать — соната ок. Но в моей практике как правило не бывает что бы все так просто. Да и что бы с сонатой эффективно работать надо времени в нее порядочно инвестировать.
Ну так CRUD генераторов полно, шаблоны свои тоже можно определить, генерим апишку, накидываем UI на ангулярах, использует готовые UI-компоненты. Это все кажется возможно сложным и долгим, но по факту оверхэд по времени не такой большой и долгосрочной перспективе выходит выгоднее.
Но если админка нужна временно — то тут без разницы.
nitso
Не могли бы вы привести примеры случаев из практики, в которых соната оказалась неподъемной/неоправданной? Спрашиваю не из праздного любопытства, а поскольку есть планы на её использование. Предыдущий мой опыт был положительным, но дальше многосвязанного CRUD дело не пошло.
Fesor
Честно, я уже не вспомню. Я помню ужасный контроллер на 1.5К строк кода, в которм половина магия и любые кастомные вещи надо делать через хуки.
У меня просто задачи частенько бывают простыми первое время, а потом «а мы тут подумали, нам тут нужна возможность запоминать кто делал такие-то и такие-то действия», или «а вот тут вот эту штуку могут делать только такие-то чуваки при таких условиях». И как бы, все это на сонате сделать конечно можно, но по трудозатратам профита по сравнению комбинаций CRUD генераторов + подправить код где надо вообще нет, а читаемость и поддерживаемость кода сильно страдает.
Года 3 назад мы в компании, в которой я работаю, решили в принципе отказаться от всей этой дури с формами для админок (да и в целом), и просто перешли на ангуляр. В итоге простой круд в виде апишки делается дико быстро и удобно, да и UI админки можно лепить быстро и выходит он более отзычивым, можно делать множество вещей, которые с обычными формами было бы тяжело сделать. Типа понадобился мне компонент для фильтров по дате — не вопрос, просто подключаем модуль и реюзаем из проекта в проект. Все красивенько, няшно, юзабилити неплохое, да и оверхэда на время разработки. А самое главное, рисков в долгосрочной перспективе намного меньше (хотя тут кто как сделает конечно).