Постановка задачи
Имеется набор некоторых объектов, например, входящих писем, связанных связью один-к-одному (для целей данной статьи вид связи значения не имеет) с объектами из другого набора, например, ответами на письма. Для управления сущностями используется SonataAdminBundle (т.е. для каждой сущности определен Admin-класс). Необходимо создавать новые ответы непосредственно из списка (List View) писем.
Сущности (entity) соответственно письма и ответа могут выглядеть следующим образом:
Сущность входящего письма
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @ORM\Table(name="incoming", options={"comment":"Входящее письмо"})
*/
class Incoming
{
/**
* @ORM\Column(type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
protected $incomingTitle;
/**
* @ORM\OneToOne(targetEntity="Response", mappedBy="incoming")
*/
protected $response;
// другие поля
/**
* Добавление ответа на входящее письмо
*
* @param \AppBundle\Entity\Response $response
*
* @return \AppBundle\Entity\Incoming
*/
public function setResponse( \AppBundle\Entity\Response $response)
{
$this->response = $response;
return $this;
}
}
Сущность ответа на входящее письмо
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @ORM\Table(name="response", options={"comment":"Ответ на входящее письмо"})
*/
class Response
{
/**
* @ORM\Column(type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* @ORM\Column(type="text", options={"comment":"Заголовок ответа"})
*/
protected $responseTitle;
/**
* @ORM\OneToOne(targetEntity="Incoming", inversedBy="response")
* @ORM\JoinColumn(name="incoming_id", referencedColumnName="id")
*/
protected $incoming;
/**
* @ORM\Column(type="text", options={"comment":"Текст ответа"})
*/
protected $text;
/**
* Добавление входящего письма
*
* @param \AppBundle\Entity\Incoming $incoming
*
* @return \AppBundle\Entity\Response
*/
public function setResponse( \AppBundle\Entity\Incoming $incoming)
{
$this->incoming = $incoming;
return $this;
}
//Другие действия
}
Варианты решения
Решение задачи на первый взгляд аналогично созданию Custom Admin Action в SonataAdminBundle, процесс которого описан в [1]. Следуя данному руководству, мы могли бы реализовать действие, которое создает и сохраняет объект ответа, прикрепляет его к текущему объекту письма и перенаправляет пользователя на форму редактирования сохраненного ответа для ввода его заголовка и текста.
При этом нам было бы необходимо:
- Создать действие по созданию ответа (например, createResponseAction) в CRUD контроллере, унаследовавшись от Sonata\AdminBundle\Controller\CRUDController
- Скопировать содержимое createAction из Sonata\AdminBundle\Controller\CRUDController в наше действие, внеся в него изменения, выполняющее функции, которые описаны выше
Такой подход привлекателен тем, что для его реализации достаточно строго следовать руководству по созданию Custom Admin Action, однако ведет к дублированию кода и чреват непредвиденными ошибками при доработке createAction до createResponseAction. Избежать недостатков подхода можно, напрямую используя существующие, а также переопределяя предназначенные для этого действия Sonata\AdminBundle\Controller\CRUDController.
Для этого будем решать задачу поэтапно:
- обеспечим переход на форму создания нового ответа по нажатию элемента управления (например, кнопки) в строке List View писем;
- реализуем автоматическую связь между создаваемым ответом и письмом, в строке которого был нажат элемент управления, до отображения формы создания.
Переход на форму создания нового ответа
Процесс создания элемента управления в строке ListView подробно описан в [1]. Остановимся на особенностях, касающихся решения нашей задачи, а именно — генерации url для перехода на форму создания нового объекта. Предлагаемый в [1] вариант
{# src/AppBundle/Resources/views/CRUD/list__action_create_other_admin.html.twig #}
<a class="btn btn-sm" href="{{ admin.generateObjectUrl('create', object) }}">Создать ответ</a>
не подходит, поскольку функция admin.generateObjectUrl генерирует url для создания объекта текущего Admin-класса; в нашем случае это письмо (Incoming), а нужно, чтобы был ответ (Response). Поэтому используем следующий вариант, украсив кнопку иконкой:
{# src/AppBundle/Resources/views/CRUD/list__action_create_other_admin.html.twig #}
<a href="{{ admin.getRouteGenerator.generateUrl(template_variables.otherAdmin, 'create'}" class="btn btn-sm btn-default edit_link" title="Создать ответ">
<i class="fa fa-plus"></i>
Создать ответ
</a>
Ключевым моментом здесь является использование функции admin.getRouteGenerator.generateUrl, принимающей в качестве аргумента Admin-сервис, для создания объекта которого необходимо сгенерировать url. Теперь задача состоит в том, чтобы передать нужный Admin-сервис в шаблон. Это можно сделать, обратившись к контейнеру Symfony2 прямо из list__action_create_other_admin.html.twig, что лишит подход универсальности, поэтому мы использовали переменную template_variables.otherAdmin, которая передается в шаблон описанным ниже способом.
Шаблоны, соответствующие кнопкам _actions в строке ListView, отображаются посредством twig-функции include шаблона SonataAdminBundle CRUD\list__action.html.twig, а именно:
{% include actions.template %}
где actions.template — переменная, которая определеяется в Admin-классе в секции configureListFields.
protected function configureListFields(ListMapper $listMapper)
{
$listMapper
// other fields...
->add('_action', 'actions', array(
'actions' => array(
// ...
'createOtherAdmin' => array(
// ВОТ ЭТА ПЕРЕМЕННАЯ
'template' => 'AppBundle:CRUD:list__action_create_other_admin.html.twig'
)
)
))
;
}
Таким образом, нам нужно добавить в CRUD\list__action.html.twig ключевое слово with, чтобы обеспечить передачу переменной в дочерние шаблоны. Поскольку не все из них будут использовать данную переменную, следует сделать проверку на ее наличие:
{% include actions.template with {template_variables : (actions.template_variables is defined ? actions.template_variables : null)} %}
Теперь можно определить переменную template_variables.otherAdmin в Admin-классе, присвоив ей нужный Admin-сервис (в нашем случае это sonata.admin.response) и она станет доступна в шаблоне list__action_create_other_admin.html.twig.
protected function configureListFields(ListMapper $listMapper)
{
$listMapper
// other fields...
->add('_action', 'actions', array(
'actions' => array(
// ...
'createOtherAdmin' => array(
'template' => 'AppBundle:CRUD:list__action_create_other_admin.html.twig',
// Передаем Admin-сервис в качестве аргумента в шаблон
'template_variables' => array('otherAdmin'=> $this->getConfigurationPool()->getContainer()->get('sonata.admin.response');)
)
)
))
;
}
Теперь при нажатии на кнопку в строке List View писем открывается форма для создания ответа на письмо.
Во второй части статьи будет рассмотрена реализация автоматического установления связи между письмом и ответом на него, а также вопросы отображения кнопки в зависимости от наличия у пользователя прав на создание ответа.