Речь пойдет о бандле для Symfony2, первую версию которого я написал более двух лет назад. Всё это время я и мои коллеги активно его использовали, бандл периодически улучшался. Решил поделиться им с сообществом.
Практически в любом приложении требуется выводить табличный список сущностей, обязательно должна быть пагинация, неплохо также иметь возможность сортировки по всем полям и гибкую фильтрацию. Именно эти задачи и решает предоставленный на ваш суд AdminPanelBundle. Конечно, это не что-то новое — та же SonataAdminBundle предоставляет подобный функционал, но Соната — это монстр (в хорошем смысле этого слова), с кучей настроек и зависимостей, а моей целью было реализация быстрой и гибкой навигации по большим табличным массивам.
Что может бандл:
Демонстрацию можно посмотреть здесь, исходный код здесь.
Как обычно — запускаем
Бандл использует knplabs/knp-paginator-bundle и braincrafted/bootstrap-bundle, если они отсутствуют в вашем приложении, то будут установлены
В app/AppKernel.php инициализируем бандл, в app/config/config.yml дописываем необходимые настройки:
И подгружаем стили, иконки и пр.
Продемонстрирую на примере небольшого приложения «Автомобили».
Структура классическая — Страна -> Бренд -> Модель
Не судите строго за заполненные данные — всё «от фонаря».
Контроллер должен наследоваться от Zk2\Bundle\AdminPanelBundle\AdminPanel\AdminPanelController
Родительский конструктор принимает:
listAction — основной метод
Построение колонок таблицы:
Метод addInList принимает массив:
Дефолтные значения массива опций:
Подробнее про опции и их использование можно посмотреть в исходном коде AdminPanelBundle/Resources/views/AdminPanel/adminList.html.twig
Можно передавать любые свои опции, но тогда нужно переопределить шаблон adminList.html.twig одним из способов переопределения в Symfony и обрабатывать их на своё усмотрение:
Построение фильтров:
Метод addInFilter принимает массив:
Типы фильтров:
Методы для форм
Ну и очень простой шаблон
Практически в любом приложении требуется выводить табличный список сущностей, обязательно должна быть пагинация, неплохо также иметь возможность сортировки по всем полям и гибкую фильтрацию. Именно эти задачи и решает предоставленный на ваш суд AdminPanelBundle. Конечно, это не что-то новое — та же SonataAdminBundle предоставляет подобный функционал, но Соната — это монстр (в хорошем смысле этого слова), с кучей настроек и зависимостей, а моей целью было реализация быстрой и гибкой навигации по большим табличным массивам.
Что может бандл:
- На входе может быть array, Doctrine\ORM\Query, Doctrine\ORM\QueryBuilder, Doctrine\Common\Collection\ArrayCollection
- Выводятся только определённые поля (свойства)
- Для любого поля (свойства) можно определить неограниченное кол-во фильтров (AND, OR) с выбором оператора (=, >, <, LIKE, etc...)
- Для любого поля можно включить/отключить сортировку
- При применении фильтра параметры фильтрации запоминаются в сессии, и при повторном посещении страницы применяются
- Есть возможность выводить автосумму по любому числовому столбцу
Демонстрацию можно посмотреть здесь, исходный код здесь.
Установка и базовая конфигурация
Как обычно — запускаем
composer require "zk2/admin-panel-bundle:dev-master"
Бандл использует knplabs/knp-paginator-bundle и braincrafted/bootstrap-bundle, если они отсутствуют в вашем приложении, то будут установлены
Настройка KnpPaginatorBundle
В app/AppKernel.php инициализируем бандл
// app/AppKernel.php public function registerBundles() { return array( // ... new Knp\Bundle\PaginatorBundle\KnpPaginatorBundle(), // ... ); }
Настройка BraincraftedBootstrapBundle
В app/AppKernel.php инициализируем бандл
Настройка хорошо описана здесь, если по быстрому, то:
Далее выполняем:
// app/AppKernel.php public function registerBundles() { return array( // ... new Braincrafted\Bundle\BootstrapBundle\BraincraftedBootstrapBundle(), // ... ); }
Настройка хорошо описана здесь, если по быстрому, то:
# app/config/config.yml ....... # Assetic Configuration assetic: debug: "%kernel.debug%" use_controller: false bundles: [ ] filters: # с использованием node less: node: /usr/bin/node # путь узнать можно выполнив $ whereis node node_paths: [/usr/lib/node_modules] # $ whereis node_modules apply_to: "\.less$" cssrewrite: ~ braincrafted_bootstrap: less_filter: less jquery_path: %kernel.root_dir%/../web/js/jquery-1.11.1.js # путь к jQuery
Далее выполняем:
php app/console braincrafted:bootstrap:install php app/console assetic:dump
В app/AppKernel.php инициализируем бандл, в app/config/config.yml дописываем необходимые настройки:
// app/AppKernel.php public function registerBundles() { return array( // ... new Zk2\Bundle\AdminPanelBundle\Zk2AdminPanelBundle(), // ... ); }
# app/config/config.yml ...... twig: ...... form: resources: - "Zk2AdminPanelBundle:AdminPanel:bootstrap_form_div_layout.html.twig" # настройки бандла по умолчанию zk2_admin_panel: check_flag_super_admin: false # -- если true, то сущность пользователя должна иметь метод "flagSuperAdmin()", возвращающий булево значение pagination_template: Zk2AdminPanelBundle:AdminPanel:pagination.html.twig # - шаблон блока пагинации sortable_template: Zk2AdminPanelBundle:AdminPanel:sortable.html.twig # - шаблон ссылки для сортировки в колонках таблицы
И подгружаем стили, иконки и пр.
php app/console asset:install web --symlink
Использование
Продемонстрирую на примере небольшого приложения «Автомобили».
Структура классическая — Страна -> Бренд -> Модель
Не судите строго за заполненные данные — всё «от фонаря».
Контроллер должен наследоваться от Zk2\Bundle\AdminPanelBundle\AdminPanel\AdminPanelController
Родительский конструктор принимает:
- Основную сущность
- Алиас для этой сущности
- Необязательный параметр «название entity_manager» — по умолчанию «default»
namespace AppBundle\Controller; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; use Zk2\Bundle\AdminPanelBundle\AdminPanel\AdminPanelController; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Security\Core\Exception\AccessDeniedException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; class DefaultController extends AdminPanelController { /** * Constructor */ public function __construct() { parent::__construct('AppBundle\Entity\Model','m'); }
listAction — основной метод
/** * listAction * * @Route("/", name="model_list") * * @return renderView */ public function listAction( Request $request ) { // Если есть разграничение прав доступа // Метод isZk2Granded приинимает роль или массов ролей // если в app/config.yml параметр zk2_admin_panel.check_flag_super_admin == true, // метод проверяет наличие "полного доступа" /* if ( false === $this->isZk2Granded(array('ROLE_LIST')) ) { throw new AccessDeniedException(); }*/ // при сбросе всех фильтров if( $this->isReset() ) { return $this->redirect( $this->generateUrl( $this->get('request')->get('_route') ) ); } // построение колонок таблицы $this->buildListFields(); // структура колонок таблицы для передачи в шаблон $items = $this->getListFields(); // инициализируем запрос $this->getEm()->buildQuery(); // сам запрос может содержать как обращение к объектам -- "m,b,c" , // так и к конкретным свойствам объектов -- "b.id AS brand_id,b.name AS brand_name,m.name,m.color" // разница в том, что в первом случае запрос возвращает коллекцию объектов, а это может быть накладно // а во втором случае возвращается обычный массив массивов $this->getQuery() ->select( 'b.id AS brand_id,b.name AS brand_name,c.name AS country_name,b.logo,m.id AS id,m.name,' .'m.color,m.airbag,m.sales,m.speed,m.price,m.dateView') ->leftJoin('m.brand','b') ->leftJoin('b.country','c') ; // сортировка по умолчанию if( !$this->get('request')->query->has('sort') ) { $this->getQuery()->orderBy('m.id','DESC'); } // строим фильтры $this->buildFilterFields(); // применяем фильтры $this->checkFilters(); // инициализируем KnpPaginator $pagination = $this->getPaginator(30); // форма фильтров для передачи в шаблон $filter_form = $this->getViewFiltersForm(); // если необходима автосумма каких-то колонок $this->initAutosum(); $autosum = $this->getSumColumns(); return $this->render('AppBundle:Model:list.html.twig', array( 'results' => $pagination, 'items' => $items, 'filter_form' => $filter_form, // будет ли кнопка для создания новой сущности 'is_new' => false, //$this->isZk2Granded(array('ROLE_NEW_ITEM')), 'autosum' => $autosum, // формат чисел по умолчанию (PHP::number_format), можно переопределять для каждой колонки 'zkNumberFormat' => array('0','.',' '), )); }
Построение колонок таблицы:
Метод addInList принимает массив:
- свойство сущности
- заголовок колонки (метод trans — аналог стандартной функции Symfony. Принимает значение, домен, массив параметров)
- алиас сущности
- массив опций
Дефолтные значения массива опций:
- 'sort' => true, — сортировка столбца
- 'func' => null, — функции (dateTimeFormat)
- 'filter' => null, — фильтры (yes_no)
- 'method' => null, — название свойства или метода
- 'autosum' => null, — уникальный алиас для автосуммы
- Так-же в массиве опций могут присутствовать:
- 'link_id' => 'brand_edit' — имя роута
- 'lid' => 'brand_id' — свойство или название метода для передачи ID в роут
- 'style' => 'text-align:center' — любой css стиль (применится к ячейке таблицы)
- 'icon_path' => '/img/' — обернётся в тэг img src="{icon_path}значение"
- 'icon_width' => 24 — используется с icon_path (ширина картинки)
- 'zkNumberFormat' => array(2,'.',' ') — PHP::number_format
- 'dateTimeFormat' => 'Y-m-d' — используется для func::dateTimeFormat
Подробнее про опции и их использование можно посмотреть в исходном коде AdminPanelBundle/Resources/views/AdminPanel/adminList.html.twig
Можно передавать любые свои опции, но тогда нужно переопределить шаблон adminList.html.twig одним из способов переопределения в Symfony и обрабатывать их на своё усмотрение:
/** * Построение колонок таблицы */ public function buildListFields() { $this ->addInList(array( 'name', // свойство сущности $this->trans('Brand','messages'), // заголовок колонки 'b', // алиас сущности array( // если наш запрос возвращает простой массив, то здесь алиас ( b.name AS brand_name ) // иначе здесь дложно быть название метода, который определён в базовой сущности // ( в нашем случае Model::getBrandName() ) 'method' => 'brand_name', // Название бренда будет ссылкой ( @Route("/brand/{id}/edit", name="brand_edit") ) 'link_id' => 'brand_edit', // если наш запрос возвращает простой массив, то здесь алиас ( b.id AS brand_id ) // иначе здесь дложно быть название метода, который определён в базовой сущности // ( в нашем случае Model::getBrandId() ) // если link_id определён, а lid нет, то в роут подставится ID из базовой сущности 'lid' => 'brand_id' ), )) ->addInList(array( 'name', $this->trans('Country','messages'), 'c', array( 'method' => 'country_name', ), )) ->addInList(array( 'logo', $this->trans('Logo','messages'), 'b', array( 'sort' => false, 'style' => 'text-align:center', 'icon_path' => '/img/' ), )) ->addInList(array( 'name', $this->trans('Model','messages'), 'm', array( 'link_id' => 'model_edit', ), )) ->addInList(array( 'color', $this->trans('Color','messages'), 'm', array( 'style' => 'text-align:center' ), )) ->addInList(array( 'airbag', $this->trans('Airbag','messages'), 'm', array( 'filter' => 'yes_no', // Будет выводиться "Да" или "Нет" 'style' => 'text-align:center' ), )) ->addInList(array( 'sales', $this->trans('Sales','messages'), 'm', array( 'autosum' => 'sales_sum', // Будет подсчитана сумма колонки 'style' => 'text-align:center' ), )) ->addInList(array( 'speed', $this->trans('Max speed','messages'), 'm', array( 'style' => 'text-align:center' ), )) ->addInList(array( 'price', $this->trans('Price','messages'), 'm', array( 'style' => 'text-align:center', 'zkNumberFormat' => array(2,'.',' ') ), )) ->addInList(array( 'dateView', $this->trans('Date','messages'), 'm', array( 'func' => 'dateTimeFormat', // Для DateTime 'dateTimeFormat' => 'Y-m-d', 'style' => 'text-align:center' ), )) ; }
Построение фильтров:
Метод addInFilter принимает массив:
- 'b_name' — алиас и название свойства через нижнее подчёркивание
- 'zk2_admin_panel_XXXXX_filter' — тип фильтра
- Название фильтра
- количество фильтров для поля
- набор доступных операторов (LIKE, =, >, <, etc...). Подробнее — AdminPanel/ConditionOperator.php
- массив параметров
Типы фильтров:
- 'zk2_admin_panel_boolean_filter' — булев фильтр (да/нет)
- 'zk2_admin_panel_choice_filter' — выпадаючий список, определённый тут-же
- 'zk2_admin_panel_date_filter' — фильтр по дате
- 'zk2_admin_panel_entity_filter' — выпадаючий список, содержащий сущности (выполняется запрос к БД)
- 'zk2_admin_panel_text_filter' — обычное текстовое поле
/** * Построение фильтров */ public function buildFilterFields() { $this ->addInFilter(array( // -- выпадаючий список, содержащий сущности 'b_name', 'zk2_admin_panel_entity_filter', $this->trans('Brand','messages'), 5, 'smal_int', array( 'entity_type' => 'entity', 'entity_class' => 'AppBundle\Entity\Brand', 'property' => 'name', 'sf_query_builder' => array( // Если необходимо ограничить запрос условием 'alias' => 'b', 'where' => 'b.id IS NOT NULL', 'order_field' => 'b.name', 'order_type' => 'ASC', ) ))) ->addInFilter(array( 'm_name', 'zk2_admin_panel_text_filter', $this->trans('Model','messages'), 5, 'light_text' )) ->addInFilter(array( // выпадаючий список, определённый тут-же 'm_color', 'zk2_admin_panel_choice_filter', $this->trans('Color','messages'), 5, 'smal_int', array('sf_choice' => array( 'black' => 'black', 'blue' => 'blue', 'brown' => 'brown', 'green' => 'green', 'red' => 'red', 'silver' => 'silver', 'white' => 'white', 'yellow' => 'yellow', )), )) ->addInFilter(array( 'm_airbag', 'zk2_admin_panel_boolean_filter', $this->trans('Airbag','messages'), )) ->addInFilter(array( 'm_door', 'zk2_admin_panel_text_filter', $this->trans('Number of doors','messages'), 5, 'medium_int' )) ->addInFilter(array( 'm_speed', 'zk2_admin_panel_text_filter', $this->trans('Max speed','messages'), 5, 'medium_int' )) ->addInFilter(array( 'm_prise', 'zk2_admin_panel_text_filter', $this->trans('Price','messages'), 5, 'medium_int' )) ->addInFilter(array( // фильтр по дате 'm_dateView', 'zk2_admin_panel_date_filter', $this->trans('Date','messages'), 2 )) ; }
Методы для форм
/** * edit Brand Action * * @Route("/brand/{id}/edit", name="brand_edit") * * @param Request $request * @param integer $id * * @return renderView */ public function editBrandAction( Request $request, $id ) { ............ } /** * edit Action * * @Route("/model/{id}/edit", name="model_edit") * * @param Request $request * @param integer $id * * @return renderView */ public function editAction( Request $request, $id ) { ..... } }
Ну и очень простой шаблон
# AppBundle:Model:list.html.twig {% extends "Zk2AdminPanelBundle::base.html.twig" %} {% block zk2_title %}Models list{% endblock %} {% block zk2_h %}<h1>General list</h1>{% endblock %} {% block zk2_body %} {% if filter_form %} {% include 'Zk2AdminPanelBundle:AdminPanel:adminFilter.html.twig' with { 'filter_form': filter_form, 'colspan': 2, {# кол-во колонок в таблице фильтра #} 'this_path': path('model_list') } %} {% endif %} {% include 'Zk2AdminPanelBundle:AdminPanel:adminList.html.twig' with { 'items': items, 'results': results, 'Zk2NumberFormat': zkNumberFormat } %} {% if is_new %} Кнопка "Создать" {% endif %} {% endblock %}
