Административный интерфейс с SonataAdminBundle

    В базовой поставке Symfony 2 предусмотрен только минимальный функционал создания CRUD интерфейса. Для реализации административного интерфейса разработан ряд бандлов, в частности SonataAdminBundle.

    Для чего это нужно?



    С помощью 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 находится Здесь
    Поделиться публикацией

    Комментарии 12

      +1
      Вот спасибочки.
        0
        спасибо, использую, очень удобно… Вот бы статические страницы добавили для контроллера, без исп. админ класса :(
          0
          SonataAdminBundle предназначен чисто для редактирования базы через веб, верно я понимаю?
            0
            А эту штуку можно использовать без симфони?
              0
              Полностью нет, но компоненты можно наверно, сам не пробовал
              0
              Как-то монструозно по сравнению с django, но сама идея отличная. Автоадминка в django очень нравится.
                0
                В Sonata очень много хороших бандлов: User, Media… sonata-project.org/bundles/
                Да, с админкой в Symfony беда… Прикручивают кто на что гаразд…
                Спасибо за подробную инструкцию!
                  0
                  Можете добавить загрузку фото?
                    0
                    Данный бандл, как говорится, маст хэв для использующих symfony 2 в разработке. Избавляет от многих рутинных операций, хотя документация хромает, порой приходится лезть в код бандла.
                      0
                      SonataPageBundle через чур заковыристо реализован. Боюсь простой смертный не разберется как создать страницу и заставить ее появиться (snapshot). Даже если разберется, то не понравиться. По моему, управление псевдо статическими страницами как-то проще должно быть. Хотя может я до конца не вкурил в этот Bundle.
                        0
                        Для работы sortable из-коробки достаточно в описание полей формы NewsLinkAdmin добавить поле
                        ->add('pos', 'hidden')
                        
                          0
                          В новой версии SonataAdminBundle (2.2.*) не получается использовать русский язык в полях label:
                                      <tag name="sonata.admin" manager_type="orm" group="Новости" label="Категории новостей"/>
                          

                          Бандл пытается транслителировать их (с помощью функции iconv,, у меня почему-то неудачно). Поэтому лучше сразу написать их латиницей, а для русификации воспользоваться файлом переводов.

                          Или только у меня с этим возникла проблема и я что-то делаю не так?

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

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