В базовой поставке Symfony 2 предусмотрен только минимальный функционал создания CRUD интерфейса. Для реализации административного интерфейса разработан ряд бандлов, в частности SonataAdminBundle.
С помощью SonataAdminBundle можно быстро создать конфигурируемый интерфейс редактирования сущностей ORM-модели (также выделены бандлы для работы с MongoDb и PHPCr, но они пока находятся на раннем этапе развития). При этом любую часть интерфейса можно доработать под себя. В конце октября 2011 оформление было переведено на фреймворк Twitter Boostrap, поэтому внешний вид административного интерфейса получается довольно современным.
В файл deps нужно добавить код для установки SonataAdminBundle и дополнительных бандлов:
И затем запустить
В файл app/autoload.php нужно добавить новые пространства имен, в файл app/AppKernel.php инициализацию установленных бандлов
В файл app/config/routing.yml нужно добавить роутинг для административного интерфейса:
И записать в директорию web css,js и пр. от установленных бандлов
Чтобы добавить пароль на адмистистративный интерфейс можно воспользоваться либо штатной авторизацией Symfony 2, либо поставить дополнительный бандл FOSUserBundle
В файле app/config/config.yml можно задать заголовок и логотип, а также переопределить шаблоны административного интерфейса. Для начала добавим заголовок административного интерфейса:
Для того чтобы включить сервис translator нужно модифицировать app/config/config.yml:
После установки, при обращении по адресу http://localhost/admin/dashboard (предполагаем что Symfony 2 установлена на сайт с именем http://localhost) выводится пустой административный интерфейс, для которого пока не прописаны сервисы администрирования сущностей.
Чтобы компонент translator определял русский accept-language нужно в настройках IE добавить в разделе Свойства обозревателя/ Общие / Языки русский язык с кодом ru-Ru
В качеcтве примера сделаем административный интерфейс для редактирования новостей, сущности которых описаны в статье Создание CRUD приложения на Symfony 2. Исходники используемых сущностей можно посмотреть на Github.
SonataAdminBundle использует архитектуру, в которой описание административного интерфейса производится посредством специального класса Admin, в котором производится конфигурация формы редактирования, списка записей, формы поиска записей, страницы отображения записи. Этот принцип был заимствован из проекта Django.
Для редактирования новостей, ссылок к новостям и категорий новостей нужно создать 3 класса в директории Test/NewsBundle/Admin: NewsAdmin, NewsLinkAdmin и NewsCategoryAdmin:
Административный класс для ссылок новостей содержит только метод configureFormFields, т.к. ссылки новостей редактируются вместе с новостью:
NewsCategoryAdmin создается по аналогии с NewsAdmin. Исходники административных классов можно посмотреть на Github.
Административный класс нужно зарегистировать как сервис, для чего его нужно прописать в Test/NewsBundle/Resources/config/services.xml. Для сервисов административного интерфейса указывается тэг «sonata.admin», позволяющий отличать их от других сервисов. Также указывается название группы пунктов меню (атрибут «group») и название пункта меню (атрибут «label») — эти данные используются для построения меню административного интерфейса. В нашем случае пункт меню для редактирования ссылок к новости в главном меню показывать не нужно, т.к. они заносятся на странице редактирования новости. Поэтому для сервиса c id=«test.news.admin.newsLink» ставим атрибут show_in_dashboard=«false».
В приведенном примере сервисы используют стандартный контроллер SonataAdminBundle:CRUD, однако при необходимости можно создавать свои контроллеры.
После перезагрузки административный интерфейс выглядит так:

При нажатии на ссылку «Новости / Список» выводится список новостей с возможностью фильтрации записей:

Страница редактирования новоcти выглядит так:

Привязанные к новости ссылки добавляются без перезагрузки страницы. В сущность «NewsLink» добавлено поле pos, по которому ведется сортировка при запрашивании ссылок к новости. Указании опции 'sortable' => 'pos' для типа поля sonata_type_collection добавляет в интерфейс возможность изменения порядка новостей, путем перетаскивания строк таблицы:

Однако чтобы изменения положения ссылки отражались в БД нужно дополнить класс NewsAdmin (не уверен что решение правильное, но по крайней мере работает):
В базовой поставке SonataAdminBundle есть русская локализация стандартных названий кнопок, заголовков и и т.п. Чтобы локализация была полной, для созданных разделов административного интерфейса нужно создать переводы заголовков, которые автоматически создаются на основе названий сущностей, например, News List, News Create. Для этого в директории Test/NewsBundle/Resources/translations требуется создать файл messages.ru.xliff (про сервис трансляции можно почитать здесь)
В итоге получился функциональный, расширяемый интерфейс редактирования записей. Все шаблоны и контроллеры, используемые SonataAdmin можно переопределить в конфигурации приложения. Разработчики на базе SonataAdmin сделали несколько полезных для разработки веб-приложений бандлов, реализующих ряд базовых функций: SonataUserBundle (управление пользователями), SonataNewsBundle (блог), SonataMediaBundle (управление медиа-ресурсами) и SonataPageBundle (прототип CMS). Большой проблемой является плохая документированность, особенно SonataPageBundle, хотя на первый взляд интересный продукт.
Update 2012-07-20: актуальная версия статьи с учетом нововведений Symfony 2.1 находится Здесь
Для чего это нужно?
С помощью SonataAdminBundle можно быстро создать конфигурируемый интерфейс редактирования сущностей ORM-модели (также выделены бандлы для работы с MongoDb и PHPCr, но они пока находятся на раннем этапе развития). При этом любую часть интерфейса можно доработать под себя. В конце октября 2011 оформление было переведено на фреймворк Twitter Boostrap, поэтому внешний вид административного интерфейса получается довольно современным.
Установка и базовая конфигурация
В файл deps нужно добавить код для установки SonataAdminBundle и дополнительных бандлов:
[SonataAdminBundle]
git=http://github.com/sonata-project/SonataAdminBundle.git
target=/bundles/Sonata/AdminBundle
[SonataDoctrineORMAdminBundle]
git=http://github.com/sonata-project/SonataDoctrineORMAdminBundle.git
target=/bundles/Sonata/DoctrineORMAdminBundle
[SonatajQueryBundle]
git=http://github.com/sonata-project/SonatajQueryBundle.git
target=/bundles/Sonata/jQueryBundle
[KnpMenuBundle]
git=https://github.com/KnpLabs/KnpMenuBundle.git
target=/bundles/Knp/Bundle/MenuBundle
[KnpMenu]
git=https://github.com/KnpLabs/KnpMenu.git
target=/knp/menu
И затем запустить
php bin/vendors install
В файл app/autoload.php нужно добавить новые пространства имен, в файл app/AppKernel.php инициализацию установленных бандлов
<?php
// app/autoload.php
$loader->registerNamespaces(array(
// ...
'Sonata' => __DIR__.'/../vendor/bundles',
'Knp\Bundle' => __DIR__.'/../vendor/bundles',
'Knp\Menu' => __DIR__.'/../vendor/knp/menu/src',
// ...
));
// app/AppKernel.php
public function registerBundles()
{
return array(
// ...
new Sonata\AdminBundle\SonataAdminBundle(),
new Sonata\DoctrineORMAdminBundle\SonataDoctrineORMAdminBundle(),
new Knp\Bundle\MenuBundle\KnpMenuBundle(),
new Sonata\jQueryBundle\SonatajQueryBundle(),
// ...
);
}
В файл app/config/routing.yml нужно добавить роутинг для административного интерфейса:
# app/config/routing.yml
admin:
resource: '@SonataAdminBundle/Resources/config/routing/sonata_admin.xml'
prefix: /admin
_sonata_admin:
resource: .
type: sonata_admin
prefix: /admin
И записать в директорию web css,js и пр. от установленных бандлов
php app/console assets:install web
Чтобы добавить пароль на адмистистративный интерфейс можно воспользоваться либо штатной авторизацией Symfony 2, либо поставить дополнительный бандл FOSUserBundle
В файле app/config/config.yml можно задать заголовок и логотип, а также переопределить шаблоны административного интерфейса. Для начала добавим заголовок административного интерфейса:
sonata_admin:
title: Сайт.Ру
Для того чтобы включить сервис translator нужно модифицировать app/config/config.yml:
framework:
translator: { fallback: %locale% }
После установки, при обращении по адресу http://localhost/admin/dashboard (предполагаем что Symfony 2 установлена на сайт с именем http://localhost) выводится пустой административный интерфейс, для которого пока не прописаны сервисы администрирования сущностей.
Замечание: Translator и IE
Чтобы компонент translator определял русский accept-language нужно в настройках IE добавить в разделе Свойства обозревателя/ Общие / Языки русский язык с кодом ru-Ru
Пример использования
В качеcтве примера сделаем административный интерфейс для редактирования новостей, сущности которых описаны в статье Создание CRUD приложения на Symfony 2. Исходники используемых сущностей можно посмотреть на Github.
SonataAdminBundle использует архитектуру, в которой описание административного интерфейса производится посредством специального класса Admin, в котором производится конфигурация формы редактирования, списка записей, формы поиска записей, страницы отображения записи. Этот принцип был заимствован из проекта Django.
Классы {Имя сущности}Admin
Для редактирования новостей, ссылок к новостям и категорий новостей нужно создать 3 класса в директории Test/NewsBundle/Admin: NewsAdmin, NewsLinkAdmin и NewsCategoryAdmin:
<?php
namespace Test\NewsBundle\Admin;
use Sonata\AdminBundle\Admin\Admin;
use Sonata\AdminBundle\Form\FormMapper;
use Sonata\AdminBundle\Datagrid\DatagridMapper;
use Sonata\AdminBundle\Datagrid\ListMapper;
use Sonata\AdminBundle\Show\ShowMapper;
use Knp\Menu\ItemInterface as MenuItemInterface;
class NewsAdmin extends Admin
{
/**
* Конфигурация отображения записи
*
* @param \Sonata\AdminBundle\Show\ShowMapper $showMapper
* @return void
*/
protected function configureShowField(ShowMapper $showMapper)
{
$showMapper
->add('id', null, array('label' => 'Идентификатор'))
->add('title', null, array('label' => 'Заголовок'))
->add('announce', null, array('label' => 'Анонс'))
->add('text', null, array('label' => 'Текст'))
->add('pubDate', null, array('label' => 'Дата публикации'))
->add('newsLinks', null, array('label' => 'Ссылки к новости'))
->add('newsCategory', null, array('label' => 'Идентификатор'));
}
/**
* Конфигурация формы редактирования записи
* @param \Sonata\AdminBundle\Form\FormMapper $formMapper
* @return void
*/
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
->add('title', null, array('label' => 'Заголовок'))
->add('announce', null, array('label' => 'Анонс'))
->add('text', null, array('label' => 'Текст'))
->add('pubDate', null, array('label' => 'Дата публикации'))
//by_reference используется для того чтобы при трансформации данных запроса в объект сущности
//которую выполняет Symfony Form Framework, использовался setter сущности News::setNewsLinks
->add('newsLinks', 'sonata_type_collection',
array('label' => 'Ссылки', 'by_reference' => false),
array(
'edit' => 'inline',
//В сущности NewsLink есть поле pos, отражающее положение ссылки в списке
//указание опции sortable позволяет менять положение ссылок в списке перетаскиваением
'sortable' => 'pos',
'inline' => 'table',
))
->add('newsCategory', null, array('label' => 'Категория'))
->setHelps(array(
'title' => 'Подсказка по заголовку',
'pubDate' => 'Дата публикации новости на сайте'
));
}
/**
* Конфигурация списка записей
*
* @param \Sonata\AdminBundle\Datagrid\ListMapper $listMapper
* @return void
*/
protected function configureListFields(ListMapper $listMapper)
{
$listMapper
->addIdentifier('id')
->addIdentifier('title', null, array('label' => 'Заголовок'))
->add('pubDate', null, array('label' => 'Дата публикации'))
->add('newsCategory', null, array('label' => 'Категория'));
}
/**
* Поля, по которым производится поиск в списке записей
*
* @param \Sonata\AdminBundle\Datagrid\DatagridMapper $datagridMapper
* @return void
*/
protected function configureDatagridFilters(DatagridMapper $datagridMapper)
{
$datagridMapper
->add('title', null, array('label' => 'Заголовок'));
}
/**
* Конфигурация левого меню при отображении и редатировании записи
*
* @param \Knp\Menu\ItemInterface $menu
* @param $action
* @param null|\Sonata\AdminBundle\Admin\Admin $childAdmin
*
* @return void
*/
protected function configureSideMenu(MenuItemInterface $menu, $action, Admin $childAdmin = null)
{
$menu->addChild(
$action == 'edit' ? 'Просмотр новости' : 'Редактирование новости',
array('uri' => $this->generateUrl(
$action == 'edit' ? 'show' : 'edit', array('id' => $this->getRequest()->get('id'))))
);
}
}
Административный класс для ссылок новостей содержит только метод configureFormFields, т.к. ссылки новостей редактируются вместе с новостью:
<?php
namespace Test\NewsBundle\Admin;
use Sonata\AdminBundle\Admin\Admin;
use Sonata\AdminBundle\Form\FormMapper;
class NewsLinkAdmin extends Admin
{
/**
* @param \Sonata\AdminBundle\Form\FormMapper $formMapper
* @return void
*/
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
->add('url', null, array('label' => 'URL', 'required' => true))
->add('text', null, array('label' => 'Описание'));
}
}
NewsCategoryAdmin создается по аналогии с NewsAdmin. Исходники административных классов можно посмотреть на Github.
Регистрация админстративных сервисов
Административный класс нужно зарегистировать как сервис, для чего его нужно прописать в Test/NewsBundle/Resources/config/services.xml. Для сервисов административного интерфейса указывается тэг «sonata.admin», позволяющий отличать их от других сервисов. Также указывается название группы пунктов меню (атрибут «group») и название пункта меню (атрибут «label») — эти данные используются для построения меню административного интерфейса. В нашем случае пункт меню для редактирования ссылок к новости в главном меню показывать не нужно, т.к. они заносятся на странице редактирования новости. Поэтому для сервиса c id=«test.news.admin.newsLink» ставим атрибут show_in_dashboard=«false».
В приведенном примере сервисы используют стандартный контроллер SonataAdminBundle:CRUD, однако при необходимости можно создавать свои контроллеры.
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="test.news.admin.news" class="Test\NewsBundle\Admin\NewsAdmin">
<tag name="sonata.admin" manager_type="orm" group="Новости" label="Новости"/>
<argument/>
<argument>Test\NewsBundle\Entity\News</argument>
<argument>SonataAdminBundle:CRUD</argument>
</service>
<service id="test.news.admin.newsLink" class="Test\NewsBundle\Admin\NewsLinkAdmin">
<tag name="sonata.admin" manager_type="orm" show_in_dashboard="false" />
<argument/>
<argument>Test\NewsBundle\Entity\NewsLink</argument>
<argument>SonataAdminBundle:CRUD</argument>
</service>
<service id="test.news.admin.newsCategory" class="Test\NewsBundle\Admin\NewsCategoryAdmin">
<tag name="sonata.admin" manager_type="orm" group="Новости" label="Категории новостей"/>
<argument/>
<argument>Test\NewsBundle\Entity\NewsCategory</argument>
<argument>SonataAdminBundle:CRUD</argument>
</service>
</services>
</container>
Что получилось
После перезагрузки административный интерфейс выглядит так:

При нажатии на ссылку «Новости / Список» выводится список новостей с возможностью фильтрации записей:

Страница редактирования новоcти выглядит так:

Изменение позиции привязанных сущностей
Привязанные к новости ссылки добавляются без перезагрузки страницы. В сущность «NewsLink» добавлено поле pos, по которому ведется сортировка при запрашивании ссылок к новости. Указании опции 'sortable' => 'pos' для типа поля sonata_type_collection добавляет в интерфейс возможность изменения порядка новостей, путем перетаскивания строк таблицы:

Однако чтобы изменения положения ссылки отражались в БД нужно дополнить класс NewsAdmin (не уверен что решение правильное, но по крайней мере работает):
#src/Test/NewsBundle/Admin/NewsAdmin.php
class NewsAdmin
{
..
/**
* Метод вызывается перед обновлением записи
* @param $news Редактируемый объект
* @return void
*/
public function preUpdate($news)
{
//Создаем новый экземпляр редактируемой сущности
$emptyObj = $this->getNewInstance();
//Создаем форму, которая описана в методе сonfigureFormFields класса NewsAdmin,
//привязываем к ней пустой объект
//наполняем пустой объект данными из запроса - это позволяет добиться того, что
//порядок привязанных NewsLink будет таким, как определено в html-форме
//(учитывая возможные перемещения строк таблицы с полями редактирования NewsLink)
//В отличии от порядка записей NewsLink редактируемого объекта - он такой, как возвращает Doctrine
$this->getForm()->setData($emptyObj)->bindRequest($this->getRequest());
$newLinkPos = array();
//Запоминаем положение NewsLink
foreach ($emptyObj->getNewsLinks() as $link) $newLinkPos[] = $link->getUrl();
$newLinkPos = array_flip($newLinkPos);
//Выставляем позиции для редактируемого объекта
foreach ($news->getNewsLinks() as $pos => $link)
$link->setPos($newLinkPos[$link->getUrl()]);
}
..
}
Навигация
В базовой поставке SonataAdminBundle есть русская локализация стандартных названий кнопок, заголовков и и т.п. Чтобы локализация была полной, для созданных разделов административного интерфейса нужно создать переводы заголовков, которые автоматически создаются на основе названий сущностей, например, News List, News Create. Для этого в директории Test/NewsBundle/Resources/translations требуется создать файл messages.ru.xliff (про сервис трансляции можно почитать здесь)
Внимание! В тэгах </sourсe> в примере ниже заменена английская c на русскую с, иначе слетает форматирование кода
<?xml version="1.0"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="ru" datatype="plaintext" original="" >
<body>
<trans-unit id="News List">
<source>News List</sourсe>
<target>Список новостей</target>
</trans-unit>
<trans-unit id="News Create">
<source>News Create</sourсe>
<target>Создание новости</target>
</trans-unit>
<trans-unit id="News Edit">
<source>News Edit</sourсe>
<target>Редактирование новости</target>
</trans-unit>
<trans-unit id="News Category List">
<source>News Category List< sourсe>
<target>Список категорий новостей</target>
</trans-unit>
<trans-unit id="News Category Create">
<source>News Category Create</sourсe>
<target>Создание категории новости</target>
</trans-unit>
<trans-unit id="News Category Edit">
<source>News Category Edit</sourсe>
<target>Редактирование категории новостей</target>
</trans-unit>
</body>
</file>
</xliff>
Заключение
В итоге получился функциональный, расширяемый интерфейс редактирования записей. Все шаблоны и контроллеры, используемые SonataAdmin можно переопределить в конфигурации приложения. Разработчики на базе SonataAdmin сделали несколько полезных для разработки веб-приложений бандлов, реализующих ряд базовых функций: SonataUserBundle (управление пользователями), SonataNewsBundle (блог), SonataMediaBundle (управление медиа-ресурсами) и SonataPageBundle (прототип CMS). Большой проблемой является плохая документированность, особенно SonataPageBundle, хотя на первый взляд интересный продукт.
Update 2012-07-20: актуальная версия статьи с учетом нововведений Symfony 2.1 находится Здесь