Знакомство с системой - создание модуля «Новости»

Разберем пример самостоятельного создания (без использования 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>

 

Все готово, не забудьте вставить ссылку на страницу в меню в одноименном разделе:

 

if($showRevision)
{
$data = Model::factory('vc')->getData('news', $id , $vers);   
}
else
{
$newsModel = Model::factory('News');   
$data = $newsModel->getCachedItem($id);
}