CMS.Controller.Table: Стандартный контроллер администрирования таблиц
Задача, возникающая постоянно: есть табличные данные (чаще всего - одна простая таблица в БД), требуется организовать их администрирование. Как это проще всего сделать?
Конечно, можно пойти обычным путем: сделать контроллер, в котором будут реализованы методы запроса данных, изменения данных, постраничной навигации для вывода таблицы, создать шаблоны для отрисовки формы редактирования и таблицы.
Один раз это сделать нетрудно. Однако в большинстве случаев на проекте есть куча подобных режимов, причем они очень однообразны и отличаются друг от друга лишь заголовками и набором редактируемых/отображаемых полей.
Разумеется, встречаются случаи, когда механизм редактирования весьма сложен и не вписывается в стандартные рамки, но подобное мы не рассматриваем, т.к. общего решения здесь не может быть вообще.
Однако для простых режимов, которые и составляют большинство, можно описать стандартную схему.
Хотелось бы иметь механизм, который самостоятельно мог бы организовать администрирование табличных данных, и чтобы ему в качестве конфигурации нужно было указать следующее:
- Источник данных (как читать табличные данные и как их сохранять)
- Список полей для вывода в виде таблицы
- Список полей для редактирования в форме
- Заголовки
- Прочие возможности: фильтрация, поиск, сортировка, дополнительные обработчики (перед выводом, записью) и т.д.
В качестве реализации такого механизма был создан контроллер табличного администрирования CMS.Controller.Table.
Пользовательский контроллер представляет собой контроллер, наследуемый от CMS_Controller_Table следующим образом:
Core::load('CMS.Controller.Table'); class Component_MyComponent_MyTableAdminController extends CMS_Controller_Table { // .......... // Здесь размещаются методы и переменные класса, // которые выступают в качестве конфигурационной информации }
Основные аспекты работы контроллера
Контроллер администрирования табличных данных предназначен для различных режимов обработки данных или действий: добавления (add), редактирования (edit), удаления (delete), фильтрации данных (filter), а также отображения нескольких записей в виде списка (list).
Действие является ключевым понятием - от его типа зависит поведение контроллера.
Общий алгоритм работы CMS.Controller.Table будет следующим:
- Диспетчеризация
- Соединение с источником данных
- Вывод данных в виде списка или формы
В пользовательском контроллере можно вмешаться в каждый из этих этапов и изменить реализацию, переопределив соответствующие поля или методы.
Диспетчеризация
Входная точка CMS.Controller.Table одна - метод index().
// $func - выполняемое действие (list,edit,add,delete и пр.) // $parms - параметр. Для list - это номер страницы, для edit,delete - идентификатор записи, для add - пустая строка; также могут быть указаны параметры сортировки записей. public function index($func,$parms) { .... }
Роутер, работающий с данным контроллером, обязан обеспечить вызов index() с параметрами, а также иметь метод admin_url, создающий uri для вызова. Например:
public function admin_url($func,$parms) { return "/admin/mytable/$func/$parms"; }
В случае если используется роутер, наследуемый от CMS_Router, все эти средства уже есть. Достаточно лишь задать базовый uri для контроллера, и указать, что будет использоваться стандартная для Table диспетчеризация:
class Component_MyComponent_Router extends CMS_Router { .... protected $controllers = array( 'Component.MyComponent.MyTableAdminController' => array( 'path' => '/admin/mytable/', 'table-admin' => true, ) ); .... }
Таким образом, для любого uri вида "/admin/mytable/..." контроллер выполнит метод index().
Например, для "/admin/mytable/edit/id-7/" вызовется index() с параметрами: edit - действие, 7 - идентификатор записи.
Далее внутри index(), в зависимости от обрабатываемого действия, контроллер вызовет соответствующий метод с префиксом action_ (action_add, action_edit, action_delete, action_list, action_filter), в котором организуется связь с источником данных, выполняются необходимые действия и возвращается, если нужно, шаблон списка или формы редактирования.
Источник данных
Связь с источником данных в CMS.Controller.Table организуется на основе модели DB.ORM. При этом доступны как дефолтные запросы (count, select, find, insert, update, delete), так и фильтрация, сортировка и пр.
DB.ORM
Контроллеру необходимо указать лишь имя ORM-маппера, который будет использоваться для работы с данными:
protected $orm_name = 'items';
Также, если требуется, можно указать имя ORM-субмаппера для выборки списка записей:
protected $orm_for_select = 'my_items';
Параметр $orm_for_select по умолчанию равен false. Он должен содержать имя метода/методов, применяемых к основному мапперу $orm_name, для получения другой выборки.
Например:
protected $orm_for_select = 'active/idtypes';
- в этом случае для выборки списка записей будет использоваться базовый маппер, у которого последовательно вызваны методы active() и idtypes(). Разумеется, эти методы должны существовать.
Методы, возвращающие нужный маппер:
// входные параметры методов: // $mapper - экземпляр маппера // $parms - массив параметров и фильтров для маппера // возвращает основной ORM-маппер protected function orm_mapper() // возвращает ORM-маппер для выборки строк protected function orm_mapper_for_select($parms=array()) // возвращает ORM-маппер для подсчета строк protected function orm_mapper_for_count($parms=array()) // применяет к ORM-мапперу параметры и фильтры и возвращает результат применения protected function orm_mapper_for_parms($mapper,$parms=array())
Самостоятельная настройка источника данных
Если же вышеописанный способ по каким-то причинам не годится, то можно переопределить методы для доступа к данным непосредственно в пользовательском контроллере:
// создает и возвращает новый экземпляр объекта - редактируемой сущности. protected function new_object(); // возвращает количество записей, удовлетворяющих переданному фильтру. protected function count_all($parms); // возвращает список записей, удовлетворяющих переданному фильтру. protected function select_all($parms); // возвращает экземпляр записи protected function load($id); // удаляет запись protected function delete($item); // вставляет запись protected function insert($item); // изменяет запись protected function update($item); // возвращает идентификатор ключевого поля записи в таблице protected function item_key() // возвращает числовой идентификатор записи protected function item_id($item); // вовзращает путь к каталогу для прикрепленных к записи файлов public function item_homedir($item,$private=false) // возвращает путь к каталогу для кэшированных данных записи public function item_cachedir($item,$private=false)
Настройка табличного вывода
В CMS.Controller.Table есть ряд методов и свойств для настройки постраничного вывода информации в виде таблицы. Их можно переопределять в пользовательском контроллере. В первую очередь - это метод list_fields(). Рассмотрим на примере:
protected function list_fields() { // получаем результат работы метода родителя - CMS.Controller.Table $fields = parent::list_fields(); // как-то изменяем данные для настройки пользовательского контроллера $fields['id'] = array( 'caption' => 'ID', 'weight' => 4, ); $fields['isadmin'] = array( 'caption' => 'Админ', 'edit' => 'checkbox', 'td' => array('align' => 'center','width'=>'60'), 'weight' => 3, ); $fields['name'] = array( 'caption' => 'Имя', 'edit' => array( 'type' => 'input', 'style' => 'width: 200px;', 'onChange' => 'checkMyField(this)', ), 'weight' => 2, ); $fields['email'] = array( 'type' => 'textarea', 'caption' => 'E-Mail', 'order_by' => 'email', 'weight' => 1, ); // возвращаем конечный результат return $fields; }
Как видим, должен возвращаться ассоциативный массив, в котором ключ - имя выводимого поля, а значение - массив параметров. Рассмотрим допустимые параметры:
- type - тип поля (если не указан, то значение будет выведено "как есть")
- caption - заголовок столбца (TH)
- td - массив атрибутов тега TD, в котором будет выводиться значение поля
- order_by - сортировка; если параметр указан (имя поля, по которому будет производиться сортировка), то нажав на заголовок столбца можно менять сортировку листинга
- edit - способ редактирования (для массового изменения строк). Если не указан, то поле - нередактируемое. Может принимать строковое значение (checkbox, select или input) или ассоциативный массив, в котором 'type' - тип поля (checkbox, select или input), а остальные элементы - атрибуты тега INPUT. Для типа select следует указать также параметр items.
- weight - порядок расположения столбцов таблицы (по возрастанию)
Типы полей, указываемых в параметре type, определены в модуле CMS.Fields. Некоторые из них допускают (или даже требуют) наличия дополнительных параметров. Подробное описание полей и их настройки доступно в статье CMS.Fields: типы полей. Можно также создать свой тип поля.
Есть также и другие методы, влияющие на табличный вывод. Для некоторых из них в скобках показаны возвращаемые по умолчанию значения:
// входные параметры методов: // $row - объект, представляющий запись в таблице // $action - выполняемое действие: list, edit, add, delete и пр. // количество записей на странице (20) protected function per_page() // заголовок страницы при листинге ('lang:_common:ta_list') // 'Список записей' protected function title_list() // сообщение, выводящееся вместо таблицы, если нет записей или нет удовлетворяющих запросу - при фильтрации ('lang:_common:ta_norows') // 'Записи отсутствуют' protected function message_norows() // текст на кнопке массового изменения ('lang:_common:ta_submit_mass_edit') // 'Изменить данные' protected function submit_massupdate() // текст на кнопке добавления; при нажатии - переход на страницу с формой добавления ('lang:_common:ta_button_add') // 'Добавить запись' protected function button_add() // возвращает массив сообщений, выпадающих при нажатии на кнопку удаления ('lang:_common:ta_del_confirm') или копирования (false) // 'Вы действительно хотите удалить эту запись?' public function row_actions() // разрешено ли добавлять записи (true) protected function access_add() // разрешено ли удалять записи (true) protected function row_can_delete($row) // разрешено ли редактировать записи (true) protected function row_can_edit($row) // разрешено ли копировать записи (false) protected function row_can_copy($row) // разрешено ли массовое изменение записей (true) protected function access_massupdate() // возвращает поле, по которому будет вестись сортировка public function sort_param($field,$direction) // считывание параметров фильтрации из GET-запроса protected function prepare_filter() // получение набора выводимых записей protected function get_rows() // создание формы массового редактирования в постраничном выводе списка записей protected function massupdate_form($rows)
Замечание. Значения по умолчанию некоторых из перечисленных полей выглядят как "lang:_common:...", например, "lang:_common:ta_norows" или "lang:_common:ta_del_confirm". Это означает, что они подставляются в зависимости от языковых настроек и будут браться из модуля CMS.Lang.Common.код_локали.
Существуют также и свойства контроллера, такие как, например:
protected $list_fields; protected $per_page; protected $title_list; ... protected $can_edit;
- они используются одноименными методами, при отображении списка записей. Можно переопределять и конкретно их, однако, рекомендуется действовать по схеме, показанной в примере выше для list_fields().
Настройка формы редактирования
Параметры отображения
Форма редактирования отображается при редактировании и добавлении записи. Она настраивается аналогично табличному выводу - в первую очередь через метод form_fields().
protected function form_fields($action) { // получаем результат работы родительского метода $fields = parent::form_fields($action); // как-то изменяем данные для настройки пользовательского контроллера $fields['title'] = array( 'caption' => 'Название', 'style' => 'width:98%', 'in_list' => true, 'weight' => 1, ); $fields['file'] = array( 'rcaption' => 'Текст справа от поля', 'type' => 'upload', 'dir' => '../files/testupload', 'weight' => 3, ); $fields['description'] = array( 'caption' => 'Описание', 'type' => 'textarea', 'style' => 'width:98%;height:200px;', 'if_component_exists' => 'Nodus', 'in_list' => array( 'caption' => 'test', 'td' => array('align' => 'center','width'=>'60'), ), 'weight' => 2, ); // возвращаем конечный результат return $fields; }
Допустимые параметры для каждого поля:
- type - тип поля, по умолчанию - input.
- caption - заголовок (текст слева от поля)
- rcaption - заголовок (текст справа от поля; заменяет caption)
- comment - комментарий (поясняющий текст)
- tab - имя вкладки (для многовкладочных форм)
- if_component_exists - поле будет отображено только, если загружен компонент с данным именем (имя должно быть без префикса "Component.", например, "Nodus" или "Forms")
- in_list - поле будет отображаться при постраничном выводе списка записей (параметр аналогичен и используется вместо указания поля в list_fields())
- weight - порядок расположения полей в форме (по возрастанию)
- любой другой параметр будет подставлен в качеств параметра тега. Наример, так установлен параметр style в приведенном примере.
Типы полей здесь аналогичны указанным в list_fields() и описываются в модуле CMS.Fields.
Помимо form_fields() есть и другие методы, влияющие на отображение формы (для некоторых в скобках показаны значения по умолчанию):
// входные параметры методов: // $action - выполняемое действие: list, edit, add, delete и пр. // $item - объект, представляющий запись в таблице // важный параметр, отвечающий за многовкладочные формы; подробное описание - в следующем пункте (array()) protected function form_tabs($action,$item=false) // заголовок на странице редактирования ('lang:_common:ta_title_edit') // 'Редактирование записи' protected function title_edit($item) // заголовок на странице добавления ('lang:_common:ta_title_add') // 'Добавление записи' protected function title_add() // текст на submit-кнопке в форме добавления ('lang:_common:ta_submit_add') // 'Добавить' protected function submit_add() // текст на submit-кнопке в форме редактирования ('lang:_common:ta_submit_edit') // 'Сохранить' protected function submit_edit($item) // текст на кнопке справа над формой - переход на страницу со списком ('lang:_common:ta_button_list') // 'К списку' protected function button_list()
Дополнительные методы:
// входные параметры методов: // $action - выполняемое действие (list, edit, add, delete и пр.) // $item - объект, представляющий запись в таблице // $name - имя поля // $parms - параметры поля // $url - адрес перехода после отправки формы // $path - путь к каталогу // вывод кнопки "Сохранить и остаться" для многовкладочной формы (false) protected function with_save_button() // текст на кнопке "Сохранить и остаться" ('lang:_common:ta_save_button') protected function save_button_text() // возвращает массив вкладок формы редактирования protected function get_form_tabs($action,$item=false) // проверка существования компонента, указанного в настройке поля "if_component_exist" protected function form_field_exists($name,$parms,$action) // фильтрация выводимых в форме полей на основе метода form_field_exists() protected function filter_form_fields($action) // создание объекта, соответствующего форме редактирования public function create_form($url,$action) // копирование данных из формы в объект-запись БД перед сохранением protected function form_to_item($item) // копирование данных из объекта-записи БД в форму перед выводом protected function item_to_form($item) // валидация формы после отправки protected function process_form($item) // формирование пути к директории для upload-полей protected function uploaded_path($path) // преобразование имени закачиваемого файла (замена " " на "_"; транслитерация) protected function validate_filename($name) // управление закачкой файлов через upload-поле protected function process_uploads($item) // обработка записи перед сохранением при добавлении или редактировании protected function process_inserted_item($item)
Как и для вывода списка, для формы редактирования существуют свойства контроллера, такие как:
protected $form_fields; protected $form_tabs; ... protected $title_edit;
Также рекомендуется переопределять не их, а соответствующие методы как показано в примере для form_fields().
Многовкладочные формы
Иногда форма получается такой огромной, что ее сложно охватить взглядом. Для таких случаев предусмотрена возможность создания многовкладочных форм. Для этого нужно:
- создать массив, в котором перечислены вкладки
- указать для каждого поля в какой вкладке оно будет отображаться (параметр tab)
protected function form_tabs($action,$item=false) { $tabs = parent::form_tabs($action,$item=false); $tabs['common'] = 'Общие параметры'; $tabs['descr'] = 'Описание'; $tabs['files'] = 'Файлы и картинки'; return $tabs; } protected function form_fields($action) { $fields = parent::form_fields($action); $fields['title'] = array( 'caption' => 'Название', 'style' => 'width:98%', 'tab' => 'common', ); $fields['file'] = array( 'caption' => 'Файл', 'type' => 'upload', 'dir' => 'files/testupload', 'tab' => 'common', ); $fields['description'] = array( 'caption' => 'Описание', 'type' => 'html', 'style' => 'width:98%;height:400px;', 'tab' => 'descr', ); $fields['attaches'] = array( 'style' => 'width:98%;height:300px;', 'type' => 'attaches', 'tab' => 'files', ); return $fields; }
В form_tabs() для каждой вкладки можно указать всего лишь ключ (машинное имя вкладки) и значение (выводимый заголовок). В расширенном варианте параметры вкладки описываются так:
protected function form_tabs($action,$item=false) { .... $tabs['files'] = array( 'caption' => 'Файлы и картинки', 'edit_only' => true, 'weight' => 1, ); .... }
- edit_only - будет ли вкладка доступна только в режиме редактирования записи (и недоступна при добавлении);
- weight - определяет порядок расположения вкладок (сортируются по возрастанию).
Замечание. Обратите внимание, что если форма многовкладочная, то поля, у которых имя вкладки не указано, вообще отображаться не будут!
Дополнительные возможности
Есть ещё несколько возможностей CMS.Controller.Table, о которых стоит упомянуть:
- проверка доступа
- генерация событий
- вызов методов-событий
- добавление, удаление, редактирование, массовое изменение записей
- копирование записей
- обработка действий, связанных с отдельным полем
- фильтрация данных
- валидация ввода
- управление шаблонами
- формирование урлов
Проверка доступа
В CMS.Controller.Table есть возможность определять доступ при запросе. Это происходит до срабатывания контроллера по какому-либо $action, в методе index():
public function index($action,$args) { .... if (!$this->check_access()) { return $this->access_denied(); } .... }
По умолчанию эти методы выглядят так:
protected function access_denied() { return $this->page_not_found(); } protected function check_access() { return $this->access('list'); } // $action - выполняемое действие (list, edit, add, delete и пр.) // $item - объект, представляющий запись в таблице protected function access($action, $item = null) {...}
Их можно переопределить, в том числе и вместе с методом index(), передавая какие-либо параметры, относительно которых будет определяться доступ, например, $action. В конечном счете доступ определяется в методе access(), где используются события, о них речь пойдет в следующем пункте. Здесь же скажем, что вызов каждого из этих событий может вернуть true или false, т.е. разрешен доступ или нет.
Также происходит проверка доступа на выполнение некоторых действий:
// входные параметры методов: // $item - объект, представляющий запись в таблице // доступ к редактированию записи protected function access_edit($item) { return $this->can_edit && $this->access('edit', $item); } // доступ к добавлению записи protected function access_add() { return $this->can_add && $this->access('add'); } // доступ к удалению записи protected function access_delete($item) { return $this->can_delete && $this->access('delete', $item); } // доступ к массовому изменению записи protected function access_massupdate() { return $this->can_massupdate && $this->access('massupdate'); }
В CMS.Controller.Table есть проверка доступа на отображение вкладки формы редактирования:
// $tab - имя вкладки // $data - настройки вкладки из массива $form_tabs // $action - выполняемое действие (list, edit, add, delete и пр.) // $item - объект, представляющий запись в таблице protected function access_tab($tab, $data, $action, $item = false) {...}
- если метод возвращает false, то вкладка формы просто не будет отображаться. Внутри данного метода также используются события.
Генерация событий
В CMS.Controller.Table реализована генерация некоторых событий. Подробнее о том, как использовать события, описано в статье Events.
admin.change
Происходит после удаления, добавления, редактирования (в том числе и массового) записей.
В методе action_list():
Events::call('admin.change');
В методах action_add(), action_edit(), action_delete():
Events::call('admin.change',$item);
- в качестве аргумента будет передаваться $item - объект, представляющий запись в базе.
cms.table.access
События, связанные с проверкой доступа. Вызываются в методе access():
// $action - выполняемое действие (list, edit, add, delete и пр.) // $item - объект, представляющий запись в таблице protected function access($action, $item = null) { $rc = Events::call('cms.table.access.' . $action, $item, $this); if (!is_null($rc)) return $rc; $rc = Events::call('cms.table.access', $action, $item, $this); if (!is_null($rc)) return $rc; return true; }
Здесь есть как общее для всех действий событие cms.table.access, так и отдельные cms.tables.access.edit, cms.table.access.delete и др. И вообще, при определении своего метода action_имя_действия будет доступно событие cms.table.access.имя_действия.
cms.table.tabs
Событие, связанное с проверкой доступа на отображение вкладки формы редактирования:
// $tab - имя вкладки // $data - настройки вкладки из массива $form_tabs // $action - выполняемое действие (list, edit, add, delete и пр.) // $item - объект, представляющий запись в таблице protected function access_tab($tab, $data, $action, $item = false) { $er = Events::call('cms.table.tabs', $tab, $data, $action, $item, $this); if (!is_null($er)) return $er; return true; }
Вызов методов-событий
В стандартном контроллере есть ряд методов, которые запускаются в определенные моменты. Они изначально пусты, но могут быть переопределены в пользовательском контроллере для создания дополнительной функциональности. В результате получаем удобное средство дополнительной обработки данных перед выводом на страницу или записью в базу.
При выводе списка
Следующие методы вызываются при выводе данных в табличном виде:
// вызывается перед действием, связанным с отрисовкой таблицы protected function on_before_list() // вызывается перед отрисовкой в таблице каждой записи // $row - объект, представляющий запись в таблице protected function on_row($row)
В методе on_row() доступен каждый объект-запись, поля которого будут выведены как строка в таблице. Соответственно, перед самым выводом можно изменить значение поля, например:
protected function on_row($row) { $row->title = "<b>".$row->title."</b>"; $row->count = 10; }
При выводе формы
Методы, связанные с выводом формы редактирования, вызываемые до начала какого-либо действия:
// входные параметры методов: // $func - выполняемое действие (list, edit, add, delete и пр.) // $item - объект, представляющий запись в таблице // вызывается перед началом любого действия (action, field_action). protected function on_before_action($func) // перед редактированием одиночной записи protected function on_before_action_edit($item) // перед добавлением записи protected function on_before_action_add() // перед удалением записи protected function on_before_delete_item($item) // перед добавлением и редактированием одиночной записи protected function on_before_change_item($item) // перед редактированием одиночной записи (при добавлениии не вызывается) protected function on_before_update_item($item) // вызывается перед добавлением одиночной записи (при редактировании не вызывается) protected function on_before_insert_item($item)
Методы, вызываемые после совершения какого-либо действия:
// входные параметры методов: // $func - выполняемое действие: list,edit,add,delete и пр. // $item - объект, представляющий запись в таблице // после удаления записи protected function on_after_delete_item($item) // вызывается после любого изменения в редктируемом объекте (добавления, изменения записи, удаления, ??массового изменения??) protected function on_after_change($func) // вызывается после добавления и редактирования одиночной записи protected function on_after_change_item($item) // вызывается после редактирования одиночной записи (при добавлениии не вызывается) protected function on_after_update_item($item) // вызывается после добавления одиночной записи (при редактировании не вызывается) protected function on_after_insert_item($item)
Методы on_before_change_item(), on_before_update_item() и on_before_insert_item() вызываются перед сохранением записи в БД, а т.к. в них доступен объект $item, то можно непосредственно изменить значения полей, которые будут сохранены в базе, например:
protected function on_before_change_item($item) { $item->is_active = 1; }
Важно возвращаемое значение метода on_before_action: если это false, то контроллер сразу же отдаст 404 ошибку, если это строка или объект, то он сразу же выведется вместо шаблона, в остальных случаях по-прежнему запустится соответствующий action.
Добавление, удаление, редактирование, массовое изменение записей
Режимы добавления, удаления и редактирования записей схожи. Они реализованы в соответствующих методах action_add(), action_delete(), action_edit() и action_list().
Стандартно добавление записи срабатывает по урлу вида "/admin/mytable/add/$parms" (где $parms - может содержать номер страницы в постраничном выводе, на которую вернется пользователь после добавления, например, "/admin/mytable/add/page-7/").
Редактирование срабатывает по урлам вида "/admin/mytable/edit/$parms" (где $parms обязательно содержит id записи, например, "/admin/mytable/edit/id-221/" или "/admin/mytable/edit/page-12/id-221/").
И в том и в другом случае выводится форма записи. Массовое редактирование, доступное на странице списка записей, осуществляется аналогичным образом за исключением того, что формой здесь будет являться сам список.
Механизм сохранения записи следующий:
- Отправка формы происходит на текущую страницу, однако теперь через POST передаются введенные данные.
- Так как урл запроса тот же, то и срабатывает тот же метод action_edit(), action_add() или action_list(). В них всегда проверяется, является ли текущий запрос POST-запросом: если нет, то просто выводится шаблон формы, иначе, в случае успеха валидации данных из POST, update или insert записи.
- После этого производится редирект по адресу, определяемому в следующих методах:
// $item - только что сохраненная запись в таблице protected function redirect_after_add($item) protected function redirect_after_edit()
Удаление записи проще - нужен только её id, который считывается из урла ("/admin/mytable/delete/$parms" - $parms обязательно содержит id копируемой записи), поэтому она сразу удаляется, и затем происходит редирект по адресу, определяемому методом:
// $item - объект записи перед её удалением protected function redirect_after_delete($item)
Копирование записей
Если в пользовательском контроллере установить следующий параметр:
// разрешено копировать записи protected function row_can_copy($row) { return true; }
- то у каждой записи в постраничном выводе будет отображаться кнопка копирования. По нажатию будет происходить редирект по урлу вида "/admin/mytable/copy/$parms" ($parms обязательно содержит id копируемой записи). Далее должен вызваться action_copy(), однако в CMS.Controller.Table его нет, потому что общего подхода здесь не существует. Программисту предлагается самому реализовать его в пользовательском контроллере, исходя из конкретной задачи. Стандартно методы с префиксом action возвращают какой-либо отрендеренный шаблон или редирект на другую страницу - для копирования можно поступить аналогичным образом.
Свойства и методы, относящиеся к копированию:
// описаны выше в разделе "Настройка табличного вывода" // $row - объект, представляющий запись в таблице public function row_actions() protected function row_can_copy($row)
В action_copy() можно также для удобства использования реализовать вызов:
// событие перед копированием записи protected function on_before_action_copy() // событие после копирования записи protected function on_after_copy_item($from,$to)
Обработка действий, связанных с отдельным полем
В CMS.Controller.Table есть действия, связанные не только с одной записью или их списком, но и с отдельным полем формы. Это относится лишь к некоторым полям.
В частности, в form_fields() можно указать тип gallery:
protected function form_fields($action) { .... $fields['gallery'] = array( 'type' => 'gallery', 'caption' => 'Галерея', 'tab' => 'default' ); .... }
- это галерея изображений, использующая ajax. Каждый ajax-запрос представляет собой действие, направленное на изменение этого поля. В урле запроса при этом будет присутствовать параметр field, например:
/admin/mytable/right/field-gallery/page-1/id-1/?filename=1-1354271879.jpg
Для подобных действий метод index() контроллера вызывает field_action().
Непосредственно перед этим отрабатывает следующее событие:
// $func - выполняемое действие (list,edit,add,delete и пр.) // возвращаемое значение проверяется так же как и у on_before_action() protected function on_before_field_action($func)
- оно как и другие пусто и создано для переопределения в пользовательском контроллере.
Также можно проверить доступ к действию над полем, переопределив метод:
// $item - объект-запись в БД, над полем которой проводится действие protected function check_item_access($item) { return true; }
Фильтрация данных
В самом простом случае фильтрация - это возможность передать через URL какой-то параметр или несколько параметров, которые являются фильтром и передаются в виде ассоциативного массива в методы count_all и select_all, в результате чего выборка при показе списка может быть разной. Основная проблема при этом - обеспечить, чтобы переменные фильтра перманентно прикреплялись в качестве GET-параметров ко всем урлам при навигации по режиму администрирования.
Чтобы организовать фильтрованный вывод записей, нужно следующее:
- Определить переменные фильтра.
- Обеспечить, чтобы источник данных реагировал на переданные значения и выдавал разную выборку в зависимости от состояния фильтра.
- Передать значения фильтра как параметры GET-запроса.
Параметры фильтрации
Выборка записей осуществляется маппером. Есть сразу несколько возможностей задать для него параметры фильтрации:
1) filters
protected function filters() { $filters = parent::filters(); $filters[] = 'search'; $filters[] = 'type_id'; return $filters; }
Параметры (имена фильтров), перечисленные в этом массиве, будут браться из GET-запроса, например, "/admin/mytable/?search=test&type_id=1". Если какой-либо из них не передан, то соответствующий фильтр не применится.
2) force_filters
protected function force_filters() { $force_filters = parent::force_filters(); $force_filters['type'] = 'all'; $force_filters['active'] = true; return $force_filters; }
Здесь содержатся параметры, всегда используемые в качестве фильтра. Они никак не передаются, а просто используются маппером при каждом запросе, поэтому и указывается пара ключ - значение.
3) exclude_filters
protected function exclude_filters() { $exclude_filters = parent::exclude_filters(); $exclude_filters[] = 'type'; return $force_filters; }
Массив содержит параметры, по которым никогда не будет осуществляться фильтрация.
Все эти возможности расположены в порядке увеличения приоритета, т.е. при совместном использовании сначала принимаются значения для filters из GET-запроса, затем берутся значения из предустановленного массива force_filters, которые в случае совпадения по имени фильтра перезапишут их, а затем из всего получившегося маппер применяет только то, чего нет в exclude_filters.
По умолчанию все параметры фильтрации представляют из себя пустой массив. Они используются в методах count_all() и select_all() маппера.
Источник данных должен реагировать на переданные ему значения фильтра. Если указан массив:
array('type' => 1, 'search' => 'search_str')
то CMS.Controller.Table вызовет у маппера соответствующие методы type() и search(), которые должны существовать, причем в качестве параметра передастся значение.
Таким образом, при указании фильтра, необходимо создать и соответствующий метод маппера, причем имя фильтра не обязательно должно совпадать с именем поля в таблице, например:
// фильтр - type protected function map_type($type) { // имя поля - item_type return $this->where('item_type=:type', $type); }
Форма фильтра
Часто необходимо фильтровать вывод, выбирая параметры фильтра непосредственно в режиме администрирования. Для этого служит форма фильтра - она находится над таблицей и содержит поля ввода (input и/или select) и submit-кнопку. Для создания такой формы нужно переопределить метод filters_form:
protected function filters_form() { $form = parent::filters_form(); $form['search'] = array( 'caption' => 'Поиск', 'type' => 'input', ); $form['city_id'] = array( 'caption' => 'Город', 'type' => 'select', 'items' => array(0=>'Все города','db:cities'), ); return $form; }
В данном примере будет создана форма фильтрации по одному из полей и поле поиска.
Замечание. Описанные в этом массиве переменные фильтра не нужно повторно указывать в методе filters. Обратите внимание, что параметр items формируется по тем же правилам, что и для поля формы редактирования (типы select, multilink).
Создания формы фильтра можно добиться, используя не filters_form, а form_fields:
protected function form_fields($action) { .... $fields['title'] = array( 'caption' => 'Название', 'style' => 'width:98%', 'in_filters' => true ); .... $fields['type'] = array( 'caption' => 'Название', 'style' => 'width:98%', 'in_filters' => array( 'caption' => 'Тип', 'type' => 'select', 'items' => array('Тип1','Тип2'), ), ); .... }
- параметр in_filters аналогичен описанию поля в filters_form().
Фильтрация в данном случае будет происходит в 2 шага:
1) После отправки данных с формы фильтрации, происходит переход по урлу вида "/admin/mytable/filter/$parms". В процессе его обработки контроллер вызывает метод action_filter(), куда попадают данные с формы. Он формирует урл, в который подставляет их в качестве параметров GET-запроса, например, "/admin/mytable/?type=all&search=test". Далее происходит переход по этому адресу.
2) Контроллер снова срабатывает и на втором шаге вызывает уже action_list(), где данные считываются из $_GET и учитываются в выборке записей.
Помимо данной формы можно легко добавить кнопки фильтрации с помощью метода filters_buttons():
protected function filters_buttons() { $buttons = parent::filters_buttons(); .... $buttons[$caption] = $url; .... return $buttons; }
- здесь $caption - надпись на кнопке, а $url - адрес ссылки.
По умолчанию кнопок фильтрации нет, но переопределив можно, например, в $url передавать GET-параметры фильтрации:
protected function filters_buttons() { $buttons = parent::filters_buttons(); $buttons['Активные'] = '/admin/mytable/?is_active=1'; $buttons['Неактивные'] = '/admin/mytable/?is_active=0'; return $buttons; }
- которые, разумеется, должны быть указаны в методе filters().
Другие методы CMS.Controller.Table, относящиеся к фильтрации:
// создание объекта, соответствующего форме фильтрации protected function create_filters_form() // создание урла с GET-параметрами и осуществление редиректа protected function action_filter()
Валидация ввода
CMS.Controller.Table позволяет настроить валидацию ввода для полей в форме редактирования/добавления записи. Настройка осуществляется в form_fields().
Доступны для использования несколько типов валидации:
1) проверка на непустой ввод:
protected function form_fields($action) { .... $fields['title'] = array( 'caption' => 'Название', 'style' => 'width:98%', 'tab' => 'default', 'validate_presence' => 'Пожалуйста, заполните поле!' ); .... }
Здесь значение опции validate_presence - это всплывающий текст при ошибке валидации.
2) проверка на ввод email-адреса:
protected function form_fields($action) { .... $fields['title'] = array( 'caption' => 'Название', 'style' => 'width:98%', 'tab' => 'default', 'validate_email' => 'Пожалуйста, введите правильный email-адрес!' ); .... }
3) проверка на соответствие регулярному выражению:
protected function form_fields($action) { .... $fields['title'] = array( 'caption' => 'Название', 'style' => 'width:98%', 'tab' => 'default', 'validate_match' => '{^\d+$}', 'validate_match_message' => 'Пожалуйста, введите число!' ); .... }
Здесь в validate_match содержится рег. выражение, а в validate_match_message - текст всплывающего при ошибке сообщения. Чтобы валидация работала, необходимо указать оба этих параметра.
4) проверка на повторный ввод данных:
protected function form_fields($action) { .... $fields['password'] = array( 'caption' => 'Пароль', 'type' => 'password', 'tab' => 'default', ); $fields['password_confirm'] = array( 'caption' => 'Подтвердите пароль', 'type' => 'password', 'validate_confirmation' => 'password', 'validate_confirmation_message' => 'Введенные пароли не совпадают!', 'tab' => 'default', ); .... }
Данный тип валидации предназначен для полей, которые требуют подтверждения, например, повторный ввод пароля. В одном из них нужно задать параметры: validate_confirmation - имя сравниваемого поля, validate_confirmation_message - текст сообщения об ошибке. Таким образом, чтобы пройти валидацию, нужно будет ввести одинаковые значения.
Все типы валидации можно комбинировать друг с другом.
Управление шаблонами
Общие шаблоны
Для отображения данных в CMS.Controller.Table применяются стандартные библиотечные шаблоны. При необходимости, их можно переопределить.
Вывод шаблонов осуществляется при отображении списка записей и формы редактирования записи.
Шаблон должен иметь расширение .phtml. А путь к нему может быть указан несколькими способами:
'edit' 'edit.phtml' 'custom_templates/edit' 'custom_templates/edit.phtml'
- т.е. расширение писать необязательно.
Для отображения какой-либо страницы CMS.Controller.Table использует несколько различных шаблонов. По умолчанию выводятся библиотечные, которые лежат в ../tao/views/admin/table2/. Например, "edit-header.phtml" - шаблон, выводимый перед формой редактирования записи, а "list-table-row.phtml" - шаблон строки при отображении списка записей. Шаблоны подключают друг друга, образуя иерархию.
Чтобы переопределить любой из них, достаточно создать в определенном месте файл с таким же именем. Находиться он может по пути, определяемому методом templates_dir():
protected function templates_dir()
- возвращает путь к каталогу в котором могут находиться предопределенные шаблоны; значение по умолчанию false означает, что такой каталог не установлен.
Параметр указывается относительно DOCROOT, например:
protected function templates_dir() { return '../app/custom_templates/'; }
- в этом каталоге и будет искаться заданный шаблон.
Если templates_dir не задан, то пользовательские шаблоны можно положить в директорию "../app/components/имя_компонента/views/admin/". Там они будут искаться автоматически.
На вершине иерархии шаблонов находятся "list.phtml", "add.phtml" и "edit.phtml" - они подключают все остальное. Начать кастомизировать внешний вид можно и с них.
Для этого надо либо, как описано выше, создать одноименный шаблон, либо вообще переопределить метод, с которого начинается рендеринг:
protected function render_list($parms) { return $this->render('list',$parms); } protected function render_add($parms) { return $this->render('add',$parms); } protected function render_edit($parms) { return $this->render('edit',$parms); }
- все они вызывают render(), передавая имя шаблона и массив параметров. Соответственно, в пользовательском контроллере можно указать другое имя.
Сам контроллер осуществляет поиск всех шаблонов (неважно, пользовательских или стандартных) определенным образом.
Алгоритм поиска шаблонов CMS.Controller.Table:
1) В первую очередь проверяется существует ли шаблон с таким именем относительно DOCROOT. При этом имя шаблона должно начинаться с "./".
2) Если шаблон не найден, то вызывается метод контроллера templates_dir() и поиск осуществляется в соответствующей директории. Если метод возвращает false, то такой каталог не был установлен и этот шаг пропускается.
3) Если шаблон до сих пор не найден, то далее поиск осуществляется внутри компонента, а именно в директории "../app/components/имя_компонента/views/admin/".
4) В конце концов при отсутствии других будет использован стандартный библиотечный шаблон, находящийся в "../tao/views/admin/table2/".
Вышеописанный алгоритм реализуется методами:
// входные параметры методов: // $template - имя шаблона // $parms - массив доступных в шаблоне переменных // возвращает отрендеренный шаблон protected function render($template,$parms=array()) // возвращает путь к найденному шаблону public function template($template) // возвращает путь к найденному шаблону public function redefined_template($template) // возвращает параметр $templates_dir protected function templates_dir()
- их тоже можно переопределить для своих нужд.
Пользовательские шаблоны логичнее всего держать именно в директории самого компонента как описано в п.3.
Если требуется подключить свой css- или js-файл, то проще всего это сделать в одном из render-методов так:
protected function render_list($parms) { // получение шаблона $t = $this->render('list',$parms); // добавление к нему стилей и скриптов $t->use_styles('my_style'); $t->use_scripts('my_script'); return $t; }
Шаблоны полей
Иногда может потребоваться изменить шаблон только одного или нескольких полей в форме редактирования, не трогая другие. Для этого есть специальный параметр template, который указывается для конкретного поля в form_fields():
protected function form_fields($action) { .... $fields['title'] = array( 'caption' => 'Название', 'style' => 'width:98%', 'template' => './my_template.phtml' ); .... }
- путь указывается относительно DOCROOT.
Значение данного поля в файле шаблона можно получить, например, так:
$value = $item->$name;
Замечание. Код, содержащийся в шаблоне, будет вставлен вместо тега input в соответствующем месте формы.
Их также логичнее всего хранить в директории самого компонента, например, в "../app/components/имя_компонента/views/admin/fields":
protected function form_fields($action) { .... $path = CMS::current_component_dir()."/views/admin/fields/"; .... $fields['title'] = array( 'caption' => 'Название', 'style' => 'width:98%', 'tab' => 'default', 'template' => $path.'my_template.phtml', ); .... }
Замечание. При использовании собственного шаблона нужно быть уверенным, что введенные в это поле данные будут отправлены и сохранены. Поэтому лучше взять за основу стандартный. Здесь все зависит от типа поля, например, для input достаточно правильно указать атрибут name.
Формирование урлов
Контроллер CMS.Controller.Table срабатывает по определенным урлам. Например, если в роутере, наследуемом от CMS_Router прописано:
protected $controllers = array( 'MyTableAdminController' => array( 'path' => '/admin/mytable/', 'table-admin' => true, ) );
то контроллер будет отзываться по урлам вида "/admin/mytable/...". Какое действие при этом нужно выполнять - определяется в методе index() контроллера.
Урлы для изменения, удаления записей, фильтрации и пр. формируются самим контроллером. В этом принимают участие следующие методы:
// $action - выполняемое действие (list, edit, add, delete, ...) // $p - если объект, то используется его id для подстановки в урл; если число, то используется как номер страницы // $args - дополнительные параметры GET-запроса, например, параметры фильтрации // $extra - параметры сортировки public function action_url($action,$p=false,$args=false,$extra=false) // $page - номер страницы, отображаемой в постраничном выводе табличных данных public function list_url($page) // $args - дополнительные параметры GET-запроса protected function args_to_query_string($args=false)
Кратко опишем их назначение:
- action_url() - формирует урл для действий добавления, удаления, редактирования, фильтрации записей и табличного вывода
- list_url() - просто вызывает action_url для вывода конкретной страницы табличных данных
- args_to_query_string() - осуществляет подстановку параметров GET-запроса, таких как id записи, номер страницы, параметры фильтрации и др. при его формировании
Для переопределения системы урлов, следует знать, что:
1) Парсинг урла начинается в роутере. Там можно указать массив rules:
class Component_MyComponent_Router extends CMS_Router { .... protected $controllers = array( 'MyTableAdminController' => array( 'path' => '/admin/mytable/', 'table-admin' => true, 'rules' => array( '{^test$}' => array('add', 1), ) ) ); .... }
Если роутер наследуется от CMS_Router, то в метод index() контроллера передадутся первый параметр в качестве action, а второй - в качестве args:
$rules = array( '{^test$}' => array('add', 1) ); public function index($action,$args)
Если при этом параметр table-admin установлен в true, то дополнительно из CMS.Router добавится стандартный набор правил, которого достаточно для определения типа действия и передаваемых параметров.
2) Далее происходит парсинг переменной $args в index() по правилам:
- если $args - целое число, то контроллер воспримет его как номер страницы (параметр page в урле);
- иначе $args будет разбиваться на подстроки по символу "/" (следовательно, обязан быть строкой), которые дальше проверяются на соответствие строке вида "имя_параметра-значение_параметра" и считываются в случае успеха
- стандартные параметры (page, id, sort, sortdesc, field), найденные таким образом, будут использоваться при дальнейшей обработке запроса
В итоге, вместе с формированием (в первую очередь в action_url()) может потребоваться переопределение разбора урла (в роутере и в методе index() контроллера).
Примеры параметров, передаваемых в урле:
"/admin/mytable/edit/page-3/id-50/" - редактирование записи с id=50, находящейся на третьей странице в постраничном выводе списка. "/admin/mytable/?type=page&search=1" - фильтрация по параметрам type и search "/admin/mytable/sortdesc-type" - сортировка по убыванию по полю type в списке записей
Что делать, если для какой-то конкретной задачи тут вообще ничего никуда не годится?
Ну и не надо надевать трусы на голову. Каждой вещи - свое применение. Если данный контроллер не годится для решения вашей задачи, то лучше воспользуйтесь более подходящим средством (например, напишите свой контроллер с нуля). Вообще, прежде чем применить этот контроллер, посмотрите на задачу: если легче и быстрее написать контроллер с нуля, чем через всякие ухищрения настраивать стандартный, то лучше сразу же писать свой.