Речь пойдет о бандле для Symfony2, первую версию которого я написал более двух лет назад. Всё это время я и мои коллеги активно его использовали, бандл периодически улучшался. Решил поделиться им с сообществом.
Практически в любом приложении требуется выводить табличный список сущностей, обязательно должна быть пагинация, неплохо также иметь возможность сортировки по всем полям и гибкую фильтрацию. Именно эти задачи и решает предоставленный на ваш суд AdminPanelBundle. Конечно, это не что-то новое — та же SonataAdminBundle предоставляет подобный функционал, но Соната — это монстр (в хорошем смысле этого слова), с кучей настроек и зависимостей, а моей целью было реализация быстрой и гибкой навигации по большим табличным массивам.
Что может бандл:
- На входе может быть array, DoctrineORMQuery, DoctrineORMQueryBuilder, DoctrineCommonCollectionArrayCollection
- Выводятся только определённые поля (свойства)
- Для любого поля (свойства) можно определить неограниченное кол-во фильтров (AND, OR) с выбором оператора (=, >, <, LIKE, etc...)
- Для любого поля можно включить/отключить сортировку
- При применении фильтра параметры фильтрации запоминаются в сессии, и при повторном посещении страницы применяются
- Есть возможность выводить автосумму по любому числовому столбцу
Демонстрацию можно посмотреть здесь, исходный код здесь.
Установка и базовая конфигурация
Как обычно — запускаем
composer require "zk2/admin-panel-bundle:dev-master"
Бандл использует knplabs/knp-paginator-bundle и braincrafted/bootstrap-bundle, если они отсутствуют в вашем приложении, то будут установлены
// app/AppKernel.php
public function registerBundles()
{
return array(
// ...
new KnpBundlePaginatorBundleKnpPaginatorBundle(),
// ...
);
}
// app/AppKernel.php
public function registerBundles()
{
return array(
// ...
new BraincraftedBundleBootstrapBundleBraincraftedBootstrapBundle(),
// ...
);
}
Настройка хорошо описана здесь, если по быстрому, то:
# 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 Zk2BundleAdminPanelBundleZk2AdminPanelBundle(),
// ...
);
}
# 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
Использование
Продемонстрирую на примере небольшого приложения «Автомобили».
Структура классическая — Страна -> Бренд -> Модель
Не судите строго за заполненные данные — всё «от фонаря».
Контроллер должен наследоваться от Zk2BundleAdminPanelBundleAdminPanelAdminPanelController
Родительский конструктор принимает:
- Основную сущность
- Алиас для этой сущности
- Необязательный параметр «название entity_manager» — по умолчанию «default»
namespace AppBundleController;
use SensioBundleFrameworkExtraBundleConfigurationRoute;
use Zk2BundleAdminPanelBundleAdminPanelAdminPanelController;
use SymfonyComponentHttpFoundationRequest;
use SymfonyComponentSecurityCoreExceptionAccessDeniedException;
use SymfonyComponentHttpKernelExceptionNotFoundHttpException;
class DefaultController extends AdminPanelController
{
/**
* Constructor
*/
public function __construct()
{
parent::__construct('AppBundleEntityModel','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' => 'AppBundleEntityBrand',
'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 %}
Автор: zk-zeka