Знакомство с системой - создание модуля «Новости»
Разберем пример самостоятельного создания (без использования scaffolding) простого модуля, используя возможности системы.
Будем создавать модуль «Новости». Пример специально задуман немного нестандартно, чтобы познакомиться с большим количеством компонент системы. Код примера есть в Demo дистрибутиве платформы.
Разработка административного интерфейса
Создание объекта ORM, отвечающего за хранение новостей
Для этого заходим в ORM (пункт меню Data Objects), нажимаем кнопку Add Object и заполняем форму создания объекта:
- Version control - флаг, определяющий будет ли использоваться версионный контроль документов;
- History log - флаг, определяющий будет ли производиться логирование действий с объектом;
- Object name - имя объекта, возможно использование alpanum (латинского алфавита) и знака «_»;
- Title - заголовок, имя объекта для отображения в пользовательских интерфейсах;
- Table name - имя таблицы базы данных (системный префикс будет добавлен автоматически);
- Table engine - тип хранилища базы данных (при использовании InnoDb ORM будет использовать транзакции во время манипуляций с объектами, в нашем случае в этом нет необходимости);
- Field used as title for link – поле, используемое как заголовок при отображении ссылок на этот объект, на текущий момент у нас нет полей, поэтому оставляем пустым.
После того как мы нажимаем кнопку «Сохранить», создается объект ORM с набором системных полей, становятся доступными новые вкладки:
Каждый объект содержит системное поле ID - unsigned integer autoincrement (идентификатор объекта).
Объекты с ревизионным контролем содержат дополнительные системные поля:
- author_id - идентификатор пользователя, создавшего запись;
- date_created - дата создания записи;
- date_published - дата публикации записи;
- editor_id - идентификатор пользователя, который редактировал документ последним;
- published - опубликован ли документ;
- published_version - версия опубликованного документа.
Теперь необходимо добавить поля для объекта news. Для этого нажимаем кнопку add Field на вкладке Fields.
Будем добавлять следующие поля:
- news_date - дата новости (datetime);
- title - заголовок новости (varchar 255);
- text - собственно сам текст (long text + allow html).
Интерфейс добавления нового поля в объект выглядит следующим образом:
Форма содержит поля:
- Field name - имя поля (alphanum + _) в базе данных;
- Field title - имя поля в интерфейсе;
- Unique Group - имя группы уникальных полей (уникальный индекс ORM может быть как для одного, так и для нескольких полей, просто указываем одну группу при необходимости);
- Field Type - тип поля;
- Standart field - стандартное поле базы данных;
- Link - ссылка, может быть ссылкой на объект, мультиссылкой на объект, ссылкой на словарное значение (некий список, определяется так же в разделе ORM);
- IsNull - может ли значение поля принимать значение null;
- Required - поле обязательно для заполнения.
Исходя из типа выбранного поля, могут появиться дополнительные настройки, например:
- isSearch - поле может быть использовано для поиска вхождения строки;
- Allow HTML - поле позволяет хранить html-код.
Вкладка «Индексы» позволяет создавать индексы таблицы базы данных, в нашем примере рассматриваться не будет.
После того как все поля добавлены переходим в основной интерфейс управления объектами и наблюдаем следующую картину:
Флаг Valid DB в виде значка кирпича означает, что структура объекта ORM не синхронизирована со структурой базы данных, исправить это положение можно нажатием на кнопку с иконкой шестеренки в первой колонке таблицы. Теперь база данных синхронизирована с новой структурой объекта.
Создание административного интерфейса
Задача разделяется на две подзадачи: создание контроллера и создание проекта интерфейса, начнем с проекта интерфейса. Интерфейсы создаются при помощи библиотеки ExtJs 4, подробную информацию об объектах и свойствах можно узнать в официальной документации на сайте разработчиков www.sencha.com.
Заходим в раздел Layout Designer, создаем новый проект под именем news в корневой папке хранения проектов (меню Interface – new):
Нам предстоит создать Data Store, Data Grid и Edit Window.
Для получения списка новостей нам потребуется контроллер, возвращающий информацию. Создадим его, добавив папку news в каталог www/dvelum/www/system/app/Backend/. В ней создаем файл Controller.php, общий путь будет www/dvelum/www/system/app/Backend/News/Controller.php.
Файл будет содержать класс контроллера, унаследованный от системного контроллера, поддерживающего версионный контроль:
class Backend_News_Controller extends Backend_Controller_Crud_Vc.
В данном примере нет необходимости переопределять логику контроллера, так что просто доопределим список полей для отображения в интерфейсе:
<?php
class Backend_News_Controller extends Backend_Controller_Crud_Vc
{
protected $_listFields = array("id" , "title" , "news_date" , "published" , "published_version" , "date_created" , "date_updated");
}
Для того, чтобы сразу активировать модуль зайдем в раздел Modules configuration (Модули административной панели):
Нажимаем «Добавить элемент», в появившейся строке выбираем контроллер, пишем заголовок, в качестве интерфейса выбираем созданный нами проект в «Layout Designer» («Дизайнер интерфейсов»):
Выставляем права группы пользователей в модуле Users (Пользователи) на вкладке Groups (Группы):
(Большая часть этих действий может быть выполнена автоматически, но об этом в другом разделе документации)
Перейдем к дизайнеру интерфейса. Создадим объект типа Store под именем dataStore, кликнув соответствующую кнопку на панели со списком компонент:
Таким же образом создадим Store для списка лет, используемого для фильтрации содержимого (подробнее позже), назовем его yearStore.
В правой панели Layout Objects перейдем на вкладку Store и кликнем двойным кликом на созданном нами объекте dataStore, загрузится панель свойств этого объекта:
Теперь необходимо импортировать поля в Store. Открываем редактор полей хранилища нажатием на кнопку, отмеченную цифрой 1 на рисунке. В открывшемся окне нажимаем кнопку Import from orm (первая в верхней панели открытого окна).
В открытом окне импорта, в левой панели находим объект news, кликаем на него, в правую панель подгружается список полей объекта news. Выбираем поля для импорта, как указано на рисунке, и нажимаем кнопку select, после чего поля импортируются в хранилище:
Метод Backend_Controller_Crud_Vc::getList (наш контроллер новостей унаследован от него) вернет дополнительную информацию, кроме указанных в классе полей будет возвращена информация о редакторе, авторе и последней версии, добавляем вручную еще три поля: user(string), updater(string), last_version (integer).
Конечный список полей должен быть следующим:
Закрываем окно редактирования полей, открываем окно редактирования Proxy для Store (кнопка 2, справа от кнопки редактирования полей Store).
Теперь начнем настраивать Store, открыв раздел proxy configuration, выставляем тип proxy: Ajax.
В панели свойства кликаем на редактирование свойства url, открывается окно выбора контроллера и action для этого Store, находим и выбираем News list как на рисунке. Если вы используете комментарии в стиле phpDoc и отключили опткод-кэшер, то рядом со списком методов контроллера появится комментарии к ним:
Далее в разделе Proxy Reader выставляем тип JSON, в панели свойства выбираем idProperty = id, root = data:
Редактирование Proxy завершено, осталось выставить свойство autoLoad = true для dataStore.
Создаем иерархию компонент, как указано на рисунке (элементы в дереве можно перемещать при помощи drug & drop):
Для этого на панели со списком компонентов кликаем Grid, именуем его dataGrid.
В свойствах dataGrid указывем store: dataStore.
Далее, выбрав Docked Items у dataGrid, кликаем на панели со списком компонентов Toolbar → Panel, назовем ее filters, в нее добавляем кнопку с именем addNews.
В Advanced Options для свойств dataGrid устанавливаем галочку Paging.
Аналогично добавляем в Panel:
Toolbar -> separator,
Toolbar -> Text Item,
Toolbar-> fill.
Добаваляем поле для фильтрации грида Component -> Field -> Search field (настроим чуть позже).
Component -> Store filter называем year, кликаем дважды на year (Component_Filter), открываем в свойствах Advanced Options (панель под основными свойствами), кликаем на кнопку Change field type и устанавливаем тип поля ComboBox.
Тут же в Advanced Options устанавливаем свойства для ComboBox:
valueField = id
displayField = title
store = yearStore
width = 80
Выставляем свойства фильтра как указано на скриншоте (на скриншоте отображены дополнительные компоненты, которые будут добавлены позже):
Устанавливаем свойства для search field в соответствии со скриншотом:
Нам понадобится нестандартная выборка данных, поэтому создаем реальную (расширенную) модель для объекта News:
www/system/app/Model/News.php
Если вы используете DVelum 0.8.x :
<?php
class Model_News extends Model
{
/**
* Получить список лет
* @return array
*/
public function getYears ($published = false)
{
$sql = $this->_db->select()
->distinct()
->from($this->table(), array('year' => 'YEAR(news_date)'))
->order('year DESC');
if($published){
$sql->where('`published` = 1');
}
return $this->_db->fetchCol($sql);
}
/**
* Переопределяем метод добавления фильтрации так, чтобы можно было фильтровать данные на
* основе года (при вызове используется позднее статическое связывание)
* @see Model::queryAddFilters()
*/
static public function queryAddFilters (Zend_Db_Select $sql, $filters)
{
if(!is_array($filters) || empty($filters)){
return;
}
$db = Application::getDbConnection();
foreach($filters as $k => $v)
{
if(empty($v)){
continue;
}
if(is_array($v)){
$sql->where($db->quoteIdentifier($k) . ' IN(?)', $v);
}else{
if ($k == 'news_date'){
$sql->where(' YEAR(`news_date`)=?', intval($v));
}else{
$sql->where($db->quoteIdentifier($k) . ' =?', $v);
}
}
}
}
}
Если вы используете DVelum 0.9.x :
<?php
class Model_News extends Model
{
/**
* Получить список лет
* @return array
*/
public function getYears ($published = false)
{
$sql = $this->_db->select()
->distinct()
->from($this->table(), array('year' => 'YEAR(news_date)'))
->order('year DESC');
if($published){
$sql->where('`published` = 1');
}
return $this->_db->fetchCol($sql);
}
/**
* (non-PHPdoc)
* @see Model::queryAddFilters()
*/
public function queryAddFilters($sql , $filters)
{
if(!is_array($filters) || empty($filters))
return;
foreach($filters as $k => $v)
{
if(empty($v))
continue;
if(is_array($v))
{
$sql->where($this->_db->quoteIdentifier($k) . ' IN(?)' , $v);
}
else
{
if($k == 'news_date')
$sql->where(' YEAR(`news_date`)=?' , intval($v));
else
$sql->where($this->_db->quoteIdentifier($k) . ' =?' , $v);
}
}
}
}
Создаем в контроллере News новый метод, возвращающий список возможных лет новостей:
public function yearlistAction ()
{
// Инстанцируем модель новостей
$model = Model::factory('News');
// Получаем список лет
$years = $model->getYears();
$result = array();
//Формируем ответ в нужном формате
$result[] = array('id' => '' , 'title' => 'All');
if(!empty($years))
foreach($years as $k => $v)
$result[] = array('id' => $v , 'title' => $v);
Response::jsonSuccess($result);
}
Настраиваем yearStore на yearlistAction по аналогии с getList для dataStore.
Добавляем в yearStore два поля: id (number) и title (string), autoLoad: true.
Переходим к редактированию колонок dataGrid (панель свойств, кнопка columns), импортируем поля из Store соответствующей кнопкой. Выставляем заголовки колонок.
Дополнительно определяем свойство renderer для колонок:
published → renderer: System/Publish
versions → renderer: System/Versions
date_created → renderer: System/Creator
date_updated → renderer: System/Updater
news_date → format: d.m.Y
Можно изменять ширину и положение колонок, перетаскиванием в основном интерфейсе.
В дизайнере интерфейса добавляем editWindow - компонент Crud Vc Window.
В свойствах окна указываем:
controllerUrl = /adminarea/news/ (В окне выбора контроллера выбираем контроллер News)
objectName = news (выбираем из выпадающего списка)
Далее нам необходимо импортировать поля для формы. Для этого на панели свойств окна нажимаем кнопку Import From ORM (слева от Show Window):
Добавляем в окно редактор HTML: Component-> field-> WYSIWYG media Field и выставляем ему свойства:
editorName = text
title = Text
Выставляем свойство title = GENERAL у editWindow_generalTab.
Теперь можно проверить, как выглядит окно.
На всякий случай обновим отображение интерфейса нажав кнопку Refresh view (верхняя панель инструментов), интерфейс не всегда атоматически перезагружается.
Нажмем кнопку Show Window в панели свойств editWindow. Должна получиться следующая картина:
Описание интерактивной логики приложения
Открываем Редактор кода и вписываем туда:
// Функция для показа окна редактирования записи
// принимает в качестве аргумента идентификатор объекта
function showPageEdit(id) {
var win = Ext.create('appClasses.editWindow', {
dataItemId : id,
canDelete : canDelete,
canEdit : canEdit,
canPublish : canPublish
});
win.on('dataSaved', function() {
appRun.dataStore.load();
appRun.yearStore.load();
}, this);
win.show();
}
Ext.onReady(function() {
/*
* Обработчик события itemdblclick для Grid
*/
appRun.dataGrid.on('itemdblclick',function(view, record, number, event, options) {
showPageEdit(record.get('id'));
});
/*
* Исходя из прав доступа, прячем кнопку добавления новости или добавляем
* обработчик для клика по ней
*/
if (!canEdit) {
appRun.addNews.hide();
} else {
appRun.addNews.on('click', function() {
showPageEdit(false);
});
}
});
Сохраняем код в редакторе и сам проект.
Модуль готов к работе. Все эти операции хоть и расписаны на много листов, на самом деле занимают немного времени.
Создание пользовательского модуля на основе pages tree
Прежде чем переходить к разработке контроллера создадим простой файл конфигурации:
www/system/config/news.php
<?php
// к-во записей на страницу
return array( 'num_in_list' => 5);
Теперь необходимо создать контроллер новостей и определить в нем логику приложения:
www/system/app/Frontend/News/Controller.php
<?php
class Frontend_News_Controller extends Frontend_Controller
{
/*
* Переопределяем конструктор, подгружаем файл конфигурации
*/
public function __construct()
{
$this->_config = Config::factory(
Config::File_Array,
Application::getConfig()->get('configs').'news.php'
);
parent::__construct();
}
/*
* Действие по умолчанию
* Страница со списком заголовков новостей
*/
public function indexAction()
{
/*
* Определяем запрашиваемую страницу списка новостей, у нас это будет второй
* параметр пути, например site.com/news/1.html
*/
$page = intval(Request::getInstance()->getPart(1));
if($page < 1)
$page = 1;
/*
* Исходя из количества записей на страницу, определяем
* с какой записи нам нужно начать выборку
*/
$from = ($page-1) * $this->_config->get('num_in_list');
// Инстанцируем модель для объекта News
$model = Model::factory('News');
// Заполняем массив параметров для выборки
$params = array(
// сортировка
'sort'=>'news_date',
// направление
'dir'=>'DESC',
// начать со строки $from
'start'=>$from,
// выбрать 'num_in_list' записей
'limit'=>$this->_config->get('num_in_list')
);
// Настраиваем фильтрацию на выборку только опубликованных объектов (материалов)
$filters = array('published'=>true);
/* Запрашиваем у модели выборку записей
* исходя из параметров сортировки пагинации и фильтрации
* запрашиваем только 3 колонки id,title,news_date
* просим по возможности применить жесткое кэширование
*/
$data = $model->getList(
$params,
$filters,
array('id','title','news_date'),
true
);
/*
* Запрашиваем у модели общее количество строк БД (кол-во объектов)
* удовлетворяющих условиям фильтрации
*/
$itemsCount = $model->getCount($filters,false,true);
// Создаем пейджер (пагинатор, элемент постраничной навигации)
$pager = new Pager();
// Сообщаем пейджеру номер текущей страницы
$pager->curPage = $page;
// Устанавливаем максимально возможное количество кнопок - ссылок
$pager->numLinks = 5;
/* Устанавливаем шаблон для гиперссылки
* запросив у роутера пользовательского интерфейса адрес страницы с
* подключенным к ней контроллером news, добавляем к ней в качестве
* дополнительного параметра шаблон, который будет заменен на номер
* страницы (шаблон определен в классе пейджера, можно заменить на свой)
*/
$pager->pageLinkTpl = Request::url(array($this->_router->findUrl('news'), '[page]'));
// Указываем пейджеру количество страниц
$pager->numPages = ceil($itemsCount / $this->_config->get('num_in_list'));
// Инициализируем объект шаблона
$template = new Template();
// Передаем данные в шаблон
$template->list = $data;
$template->page = $this->_page;
$template->pager = $pager;
// только в Dvelum 0.9.x задаем роутер
$template->router = $this->_router;
// Запускаем рендеринг шаблона списка новостей
$this->_page->text = $template->render($this->_page-> getTemplatePath('news_list.php'));
}
// просмотр новости
public function itemAction()
{
// определяем номер страницы — третий параметр пути например site.com/news/item/5.html
$id = intval(Request::getInstance()->getPart(2));
$vers = Request::get('vers', 'int', false);
$data = array();
if($id)
{
/* если запрошен превью для определенной версии документа
* и пользователь является администратором
* получаем данные конкретной версии документа
*/
if($vers && User_Admin::getInstance()->isAuthorized()){
$data = Model::factory('vc')->getData('news', $id , $vers);
}else{
$data = Model::factory('News')->getCachedItem($id);
if(!empty($data) && !$data['published'])
$data = array();
}
}
$template = new Template();
$template->page = $this->_page;
if(empty($data)){
$this->_page->text .= $template->render($this->_page->getTemplatePath('notFound.php'));
return;
}
$this->_page->page_title = $data['title'];
$this->_page->html_title = $data['title'];
$template->data = $data;
// рендерим темплейт новости в поле текст объекта Page
$this->_page->text.=$template->render($this->_page->getTemplatePath('news_item.php'));
}
}
Для того чтобы активизировать контроллер, добавляем запись о нем в разделе Frontend Modules (Модули публичной части), указываем уникальный код (например, news), выбираем контроллер, вписываем заголовок меню, нажимаем на кнопку «Сохранить».
Теперь этот контроллер можно назначать для обработки страниц, заходим в административный интерфейс, раздел Pages. Создаем страницу news и выставляем ей обработчиком наш контроллер:
Осталось опубликовать страницу (кнопка publish) и создать шаблоны отображения списка и страницы новостей:
/www/templates/public/news_list.php
<?php
$list = $this->get('list');
if(is_array($list) && !empty($list))
{
$pageUrl = $this->get('router')->findUrl('news');
foreach($list as $item)
{
$url = Request::url(array($pageUrl , 'item', $item['id']));
?>
<div class="news_list_item">
<b><?php echo date('Y.m.d' , strtotime($item['news_date']));?></b>
<a href="<?php echo $url ?>" class="news_list_item_header"><?php echo htmlspecialchars($item['title']); ?></a>
</div>
<div class="sep"></div>
<?php
}
$pager = $this->get('pager');
if($pager)
echo $pager;
}
else
{
echo '<h3>' . Lang::lang()->get('NO_RECORDS_TO_DISPLAY') . '</h3>';
}
/www/templates/public/news_item.php
<div class="news_item">
<h4><?php echo date('d.m.Y', strtotime($this->data['news_date'])); ?></h4>
<?php echo $this->data['text']?>
</div>
Все готово, не забудьте вставить ссылку на страницу в меню в одноименном разделе:
{
$data = Model::factory('vc')->getData('news', $id , $vers);
}
else
{
$newsModel = Model::factory('News');
$data = $newsModel->getCachedItem($id);
}