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().

Многовкладочные формы

Иногда форма получается такой огромной, что ее сложно охватить взглядом. Для таких случаев предусмотрена возможность создания многовкладочных форм. Для этого нужно:

  1. создать массив, в котором перечислены вкладки
  2. указать для каждого поля в какой вкладке оно будет отображаться (параметр 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/").

И в том и в другом случае выводится форма записи. Массовое редактирование, доступное на странице списка записей, осуществляется аналогичным образом за исключением того, что формой здесь будет являться сам список.

Механизм сохранения записи следующий:

  1. Отправка формы происходит на текущую страницу, однако теперь через POST передаются введенные данные.
  2. Так как урл запроса тот же, то и срабатывает тот же метод action_edit(), action_add() или action_list(). В них всегда проверяется, является ли текущий запрос POST-запросом: если нет, то просто выводится шаблон формы, иначе, в случае успеха валидации данных из POST, update или insert записи.
  3. После этого производится редирект по адресу, определяемому в следующих методах:
// $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 в списке записей

Что делать, если для какой-то конкретной задачи тут вообще ничего никуда не годится?

Ну и не надо надевать трусы на голову. Каждой вещи - свое применение. Если данный контроллер не годится для решения вашей задачи, то лучше воспользуйтесь более подходящим средством (например, напишите свой контроллер с нуля). Вообще, прежде чем применить этот контроллер, посмотрите на задачу: если легче и быстрее написать контроллер с нуля, чем через всякие ухищрения настраивать стандартный, то лучше сразу же писать свой.

16.06.2014
Все статьи