Создание компонента "Каталог" для Joomla 3
1. Шаг - установщик компонента.
Начнем с самого начала! Это файл установщик компонента, не стоит им пренебрегать, нужно чтобы все ваши разработки имели опрятный вид, да и к тому же в последствии это с экономит уйму времени, ну а если выкладывать свое ваяние на JED то установшик это уже не рекомендация, а требование!
Наш компонент будет называться catalogue поэтому название компонента в структуре Joomla будет соответственно com_catalogue. Создаем файл с названием com_catalogue.xml и пока просто копируем листинг ниже.
<?xml version="1.0" encoding="utf-8"?> <extension type="component" version="2.5.0" method="upgrade"> <name>com_catalogue</name> <creationDate>20.02.2013</creationDate> <author>Buyanov Danila</author> <authorEmail>info[at]saity74.ru</authorEmail> <authorUrl>http://saity74.ru</authorUrl> <copyright>© Saity74 LLC, 2013</copyright> <license>GNU General Public License version 2 or later;</license> <version>1.0.0</version> <description>COM_CATALOGUE_MANAGER</description> <install> <sql> <file driver="mysql" charset="utf8">sql/install.mysql.utf8.sql</file> </sql> </install> <uninstall> <sql> <file driver="mysql" charset="utf8">sql/uninstall.mysql.utf8.sql</file> </sql> </uninstall> <administration> <menu>Catalogue</menu> <files folder="admin"> <filename>index.html</filename> <filename>catalogue.php</filename> <filename>controller.php</filename> <filename>access.xml</filename> <filename>config.xml</filename> <folder>controllers</folder> <folder>helpers</folder> <folder>models</folder> <folder>views</folder> <folder>tables</folder> <folder>sql</folder> </files> <languages folder="admin"> <language tag="ru-RU">language/ru-RU.com_catalogue.ini</language> <language tag="ru-RU">language/ru-RU.com_catalogue.sys.ini</language> </languages> </administration> <files> <filename>index.html</filename> <filename>catalogue.php</filename> <filename>com_catalogue.xml</filename> <filename>controller.php</filename> <filename>helper.php</filename> <filename>thumbnail.php</filename> <filename>router.php</filename> <folder>models</folder> <folder>views</folder> </files> </extension>
Теперь подробно: если вы уже создавали установщики шаблона или модуля то верхняя часть понятна, в ней указывается название компонента, дата создания, автор, почтовый ящик и прочая информация о разработчике тут все просто.
Далее идет секция install - эта секция отвечает за выполнение SQL конструкций во время установки и удаления компонента, как правило компоненты Joomla имеют свою собственную таблицу в базе данных где хранят информацию в нашем случае это будут товары каталога и категории. В этом месте часто начинаются проблемы в основном из-за невнимательности. Посмотрим листинг этих файлов:
install.mysql.utf8.sql
DROP TABLE IF EXISTS `#__catalogue_categories`; CREATE TABLE `#__catalogue_categories` ( `id` int(11) NOT NULL AUTO_INCREMENT, `title` varchar(255) NOT NULL, `alias` varchar(255) NOT NULL, `state` tinyint(3) NOT NULL, `published` tinyint(1) NOT NULL, `ordering` int(11) NOT NULL, `image` varchar(255) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8; DROP TABLE IF EXISTS `#__catalogue_items`; CREATE TABLE `#__catalogue_items` ( `id` int(11) NOT NULL AUTO_INCREMENT, `cat_id` int(11) NOT NULL, `name` varchar(255) NOT NULL, `intro` text NOT NULL, `desc` text NOT NULL, `state` tinyint(3) NOT NULL, `published` tinyint(1) NOT NULL, `sticker` tinyint(3) NOT NULL, `ishot` tinyint(1) NOT NULL, `ordering` int(11) NOT NULL, `price` varchar(30) NOT NULL, `image` varchar(255) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8;
uninstall.mysql.utf8.ini
DROP TABLE `#__catalogue_categories`, `#__catalogue_items`;
Те кто знает MySQL тому все понятно, кто не знает может просто скопировать, на самом деле ничего особенного тут не происходит. Создается две таблицы: таблица товаров и таблица категорий.
Внимание стоит уделить только тому, что названия таблиц должны иметь префикс т.е. записываются по правилам Joomla например #__catalogue_items.
Далее идут две очень похожие XML структуры: administrator и files первая отвечает за административную часть компонента - BackEnd, а вторая за пользовательскую - FrontEnd. И тут мы плавно переходим к следующему шагу.
2. Шаг - Файловая структура компонента
После того как сохраним все файлы должно получиться вот так:
Структура каталога
Но это далеко не все. Вернемся немного назад и вспомним о XML структуре administrator - это ничто иное как описание административной части:
Путь в XML: extension -> administrator -> menu - это заголовок главного меню Joomla
Заголовок меню (тут он уже переведен на русский см. далее)
Путь в XML: extension -> administrator -> files - этот блок отвечает за копирование файлов и папок компонента из установочного архива в систему. Начинается все с ключа folder="admin" он указывает что все что перечислено внутри блока нужно искать в этой папке (т.е. admin). Далее файлы заключаем в тег file, дериктории соответственно в folder.
Название файла | Описание |
---|---|
index.html | Должен быть в каждой папке чтобы ограничить к ней доступ из браузера |
catalogue.php | Основной файл компонента, который осуществляет загрузку дополнительных модулей и функций, а также создает контроллер компонента. |
controller.php | Файл основного контроллера (бывают еще и дополнительные) создает экземпляр класса JControllerLegacy |
access.xml | Управление доступом пользоватей к компонету и отдельным его функциям |
config.xml | Файл глобальных настроек компонента |
controllers | Папка в которой будут храниться дополнительные контроллеры JControllerAdmin и JControllerForm, они отвечают за вывод списков, сохранение, публикацию, сортировку отдельных записей |
helpers | Папка содержащая вспомогательные классы компонента |
models | Папка с моделями JModelList и JModelForm в месте с контроллерами они выполняют операции над записями в таблице. |
views | В этой папке лежат виды т.е. шаблоны вывода информации в виде HTML (прим. не всегда HTML, бывает и PDF, и многое другое) |
tables | Папка содержащая классы таблиц, они необходимы чтобы модель могла оперировать записями в таблице базы данных |
sql | Ну и наконец здесь лежат SQL-скрипты для создания/удаления таблиц при установке компонента |
Путь в XML: extension -> administrator -> languages - языковые настройки или файлы локализации нашего компонента. Настоятельно рекомендую их использовать если вы хотите создать хорошее дополнение к Joomla, да и в конце концов потом будет проще вносить исправления.
Обращаю внимание на атрибут folder="admin" папка languages должна лежать внутри папки admin в структуре нашего компонента
Далее перейдем к FrontEnd части там все аналогично и даже проще, поэтому столь подробно описывать не будем, просто посмотрим на конечную структуру компонента.
3. Шаг - загрузка компонента
Вспомнив, что за это отвечает файл catalogue.php откроем его в редакторе и вставим туда следующие строки:
<?php defined('_JEXEC') or die; if (!JFactory::getUser()->authorise('core.manage', 'com_catalogue')) { return JError::raiseWarning(404, JText::_('JERROR_ALERTNOAUTHOR')); } // Execute the task. $controller = JControllerLegacy::getInstance('Catalogue'); $controller->execute(JFactory::getApplication()->input->get('task')); $controller->redirect();
Теперь разберем по порядку каждую строчку этого файла.
1: проверка контекста запуска, Joomla определяет константу _JEXEC при инициализации приложения, таким образом файл компонента невозможно запустить отдельно от системы.
3 - 6: Проверка прав пользователя, хватает ли у пользователя прав чтобы администрировать данный компонент. Права устанавливаются в глобальных настройках.
9: Создаем экземпляр класса JControllerLegacy.
10: В значении переменной task будет храниться команда контроллеру (по умолчанию display).
11: Осуществляем редирект!
На этом все, дальше работает контроллер.
4. Шаг - Контроллер JControllerLegacy
Создадим файл controller.php и внесем в него такие строки.
<?php defined('_JEXEC') or die; class CatalogueController extends JControllerLegacy { protected $default_view = 'catalogue'; public function display($cachable = false, $urlparams = false) { require_once JPATH_COMPONENT.'/helpers/catalogue.php'; $view = $this->input->get('view', 'catalogue'); $layout = $this->input->get('layout', 'default'); $id = $this->input->getInt('id'); if ($view == 'item' && $layout == 'edit' && !$this->checkEditId('com_catalogue.edit.item', $id)) { $this->setError(JText::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id)); $this->setMessage($this->getError(), 'error'); $this->setRedirect(JRoute::_('index.php?option=com_catalogue&view=catalogue', false)); return false; } if ($view == 'category' && $layout == 'edit' && !$this->checkEditId('com_catalogue.edit.category', $id)) { // Somehow the person just went to the form - we don't allow that. $this->setError(JText::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id)); $this->setMessage($this->getError(), 'error'); $this->setRedirect(JRoute::_('index.php?option=com_catalogue&view=categories', false)); return false; } parent::display(); return $this; } }
И опять также разбираем по строчно.
1: Запре доступа вне контекста приложения.
2: Объявляем класс CatalogueController.
3: Устанавливаем свойство default_view, оно обычно необходимо если у вас по каким то причинам отличается название стандартного view. Пока может быть не совсем понятно, но это не страшно.
5: Определяем действие контроллера по умолчанию функцию display().
7: Подключаем класс-помошник. Пока он нам не нужен, но потом мы его будем использовать почти во всех view. Так что позаботимся об этом заранее.
9 - 11: Собираем данные из формы. Если вдруг пользователь решил отредактировать какую-то запись то мы должны его перенаправить на соответствующий вид, а точнее на форму редактирования, для это нам понадобятся эти переменные. Более подробно о view и layout поговорим позже.
13 и 21: Два условия для перенаправления пользователя в зависимости от того что он хочет редактировать, запись или категорию. Тут стоит обратить внимание на функцию JController::checkEditId($context, $id) она проверяет доступность записи для редактирования это необходимо для того чтобы два пользователя не редактировали одну и туже запись.
15 - 17 и 24 - 26: Выполняют переадресацию пользователя и вывод ошибки в случае несанкционированного доступа к формам редактирования.
29: Выполняем родительский метод JController::display чтобы подгрузить соответствующий вид и показать пользователю.
30: Возвращаем ссылку на объект класса CatalogueController.
Вывод: контроллер служит пользователю своебразным переключателем между видами и моделями вашего компонента. Благодаря ему компонент может выполнять различные действия в зависимости от запроса пользователя.
Если взглянуть на картинку то станет понятно как работает контроллер, пока что она очень простая, но вскоре мы ее усложним и MVC развернется перед нами во всей красе! Обведенная область показывает то что умеет наш компонент на данный момент.
MVC паттерн на примере компонента Joomla
5. Шаг - создание модели JModelList
Итак приступим к созданию модели, для нее у нас уже подготовлена папка models в структуре нашего компонента, там нужно создать файл с названием catalogue.php. Основной функцией модели является обращение к базе данных и вытаскивание из нее записей в зависимости от запроса пользователей, также в модели есть функция для хранения текущего состояния компонента в сессии. Обо всем об этом по порядку!
<?php defined('_JEXEC') or die; class CatalogueModelCatalogue extends JModelList { protected function getListQuery() { $db = $this->getDbo(); $query = $db->getQuery(true); $query->select('c.id, c.cat_id, c.name, c.intro, c.desc, c.price, c.ordering, c.state, c.image, c.sticker, c.published, c.params'); $query->from($db->quoteName('#__catalogue_items').' AS c'); $query->select('cat.title AS category_title'); $query->join('LEFT', '#__catalogue_categories AS cat ON cat.id = c.cat_id'); $published = $this->getState('filter.state'); if (is_numeric($published)) { $query->where('c.state = '.(int) $published); } elseif ($published === '') { $query->where('(c.state IN (0, 1))'); } $categoryId = $this->getState('filter.cat_id'); if (is_numeric($categoryId)) { $query->where('c.cat_id = '.(int) $categoryId); } $published = $this->getState('filter.published'); if (is_numeric($published)) { $query->where('c.published = ' . (int) $published); } elseif ($published === '') { $query->where('(c.published = 0 OR c.published = 1)'); } $search = $this->getState('filter.search'); if (!empty($search)) { if (stripos($search, 'id:') === 0) { $query->where('c.id = '.(int) substr($search, 3)); } else { $search = $db->Quote('%'.$db->escape($search, true).'%'); $query->where('(c.name LIKE '.$search.' OR c.intro LIKE '.$search.')'); } } $orderCol = $this->state->get('list.ordering', 'ordering'); $orderDirn = $this->state->get('list.direction', 'ASC'); if ($orderCol == 'ordering') { $orderCol = 'c.name '.$orderDirn.', c.ordering'; } $query->order($db->escape($orderCol.' '.$orderDirn)); return $query; } protected function getStoreId($id = '') { $id .= ':'.$this->getState('filter.search'); $id .= ':'.$this->getState('filter.access'); $id .= ':'.$this->getState('filter.state'); $id .= ':'.$this->getState('filter.published'); $id .= ':'.$this->getState('filter.cat_id'); return parent::getStoreId($id); } public function getTable($type = 'Catalogue', $prefix = 'CatalogueTable', $config = array()) { return JTable::getInstance($type, $prefix, $config); } protected function populateState($ordering = null, $direction = null) { $app = JFactory::getApplication('administrator'); $search = $this->getUserStateFromRequest($this->context.'.filter.search', 'filter_search'); $this->setState('filter.search', $search); $state = $this->getUserStateFromRequest($this->context.'.filter.state', 'filter_state', '', 'string'); $this->setState('filter.state', $state); $access = $this->getUserStateFromRequest($this->context.'.filter.access', 'filter_access', 0, 'int'); $this->setState('filter.access', $access); $published = $this->getUserStateFromRequest($this->context.'.filter.published', 'filter_published', ''); $this->setState('filter.published', $published); $categoryId = $this->getUserStateFromRequest($this->context.'.filter.cat_id', 'filter_cat_id', ''); $this->setState('filter.cat_id', $categoryId); $id = $this->getUserStateFromRequest($this->context.'.item.id', 'id', 'int', 0); $this->setState('item.id', $id); $params = JComponentHelper::getParams('com_catalogue'); $this->setState('params', $params); parent::populateState('c.name', 'asc'); } }
1: не нуждается в комментариях;
3: Объявляем класс CatalogueModelCatalogue именно так и не как иначе. Если будет ошибка в названии ничего не получится (Fatal Error! и все);
5: Функция в которой мы составим запрос к базе данных;
7: Получаем объект базы данных и сохраняем в переменную $db;
8: Создаем объект JDataBaseQuery - это конструктор запросов функция getQuery имеет единственный параметр, который указывает на то что это должен быть новый запрос - true, если напишем false (по умолчанию) то наш запрос попадет в стек - это нужно когда делаем много INSERT чтобы сложить их в кучу, а потом выполнить за один заход, но нам это не пригодится;
10: Выполняем метод JDataBaseQuery::select() и передаем ему все поля, по которым делаем выборку из таблицы;
11: Методом JDataBaseQuery::from() указываем таблицу (не забываем про префикс #__);
12: Дальше докидываем еще немного в select (этим и удобен объект $query можно вызывать select хоть на каждое поле);
13: Тут выполняем Join чтобы получить в из таблицы категорий названия;
15 - 20: Первый фильтр - состояние state (бывает -2 - в корзине ,-1 - в архиве ,0 - не опубликовано ,1 - опубликовано) значение фильра хранится в сессии, о том как оно там очутилось обсудим позже.
22 - 25: Опять фильтр теперь по категории;
27 - 33: Теперь фильтруем по полю published (0 - не опубликовано, 1 - опубликовано)
35 - 42: Реализация поиска по имени;
45: Поле сортировки;
46: Направление сортировки
47 - 48: Небольшая плюшка - если сортировка по умолчанию (т.е. по полю ordering) то добавляем еще и поле name;
51: Применяем наши манипуляции с сортировкой с помощью JDataBaseQuery::order();
53: Возвращаем модели наш собранный запрос!
57 - 66: Функция для сохранения Stored ID в справке написано что он нужен для формирования уникального ключа (хеша) для каждого состояния модели ее сильно описывать не будем в ней просто идет формирование $id;
68 - 71: Функция getTable она возвращает объект класса JCatalogueTableCatalogue с помощью, которого можно выполнить запрос к базе и получить данные. О самом классе чуть позже.
73: Функция populateState - очень хорошая функция, которой очень не хватало в J1.5, в ней реализуется сохранение состояния модели в сессии (т.е. значение фильтров, постраничной навигации, поиска и прочих переменных, которые могут понадобиться от запроса к запросу). Каждую строку описывать смысла нет. Сделаю акцент только на функцию getUserStateFromRequest($context, $name, $default) она возвращает значение переменной, которое ищет либо в сессии либо берет из запроса и если нет ничего берет его из третьего аргумента (т.е. предусмотренное по умолчанию) - это очень удобно! А функция setState сохраняет найденное значение в сессию таким образом даже при первой загрузке модели мы уже имеем вполне определенное состояние компонента и можем прогнозировать запрос к базе.
6. Шаг - создание класса таблицы JTable
Пока что он выглядит совсем смешно, но в нем можно реализовать очень много полезных вещей. Дело в том что модель при выполнении метода store выполняет различные проверки поступивших данных и с ними можно проделывать различные манипуляции перед сохранением. Но об этом подробно поговорим позже.
<?php defined('_JEXEC') or die; class CatalogueTableCatalogue extends JTable { public function __construct(&$_db) { parent::__construct('#__catalogue_items', 'id', $_db); } }
Время нарисовать диаграмму что бы понять что мы сделали
7. Шаг - создание класса JViewLegacy
Сначала надо создать файловую структуру. Папка views у нас уже должна быть, но до сих пор пустая, сейчас там нужно создать папку с названием вида т.е. catalogue, если еще не заметили то поройтесь в компонентах Joomla и везде заметите что основной вид и сам компонент обычно совпадают по названию (переопределить конечно можно, но лучше оставить там как требует Joomla). Итак создаем в папке views папку catalogue в ней файл view.html.php и папку tmpl, а в ней default.php. Папка tmpl хранит HTML шаблоны (layouts) для вывода информации полученной из модели. В итоге должна получится такая структура:
Ну а теперь код файла view.html.php
<?php defined('_JEXEC') or die; JLoader::register('CatalogueHelper', JPATH_COMPONENT.'/helpers/catalogue.php'); class CatalogueViewCatalogue extends JViewLegacy { protected $categories; protected $items; protected $pagination; protected $state; public function display($tpl = null) { $this->items = $this->get('Items'); $this->pagination = $this->get('Pagination'); $this->state = $this->get('State'); $this->categories = $this->get('Categories'); if (count($errors = $this->get('Errors'))) { JError::raiseError(500, implode("\n", $errors)); return false; } CatalogueHelper::addSubmenu('catalogue'); $this->addToolbar(); $this->sidebar = JHtmlSidebar::render(); parent::display($tpl); } protected function addToolbar() { $canDo = CatalogueHelper::getActions(); $bar = JToolBar::getInstance('toolbar'); JToolbarHelper::title(JText::_('COM_CATALOGUE_MANAGER'), 'component.png'); if ($canDo->get('core.create')) { JToolbarHelper::addNew('item.add'); } if (($canDo->get('core.edit'))) { JToolbarHelper::editList('item.edit'); } if ($canDo->get('core.edit.state')) { if ($this->state->get('filter.state') != 2) { JToolbarHelper::publish('item.publish', 'JTOOLBAR_PUBLISH', true); JToolbarHelper::unpublish('item.unpublish', 'JTOOLBAR_UNPUBLISH', true); } if ($this->state->get('filter.state') != -1) { if ($this->state->get('filter.state') != 2) { JToolbarHelper::archiveList('item.archive'); } elseif ($this->state->get('filter.state') == 2) { JToolbarHelper::unarchiveList('item.publish'); } } } if ($canDo->get('core.edit.state')) { JToolbarHelper::checkin('item.checkin'); } if ($this->state->get('filter.state') == -2 && $canDo->get('core.delete')) { JToolbarHelper::deleteList('', 'items.delete', 'JTOOLBAR_EMPTY_TRASH'); } elseif ($canDo->get('core.edit.state')) { JToolbarHelper::trash('catalogue.trash'); } if ($canDo->get('core.admin')) { JToolbarHelper::preferences('com_catalogue'); } JHtmlSidebar::setAction('index.php?option=com_catalogue&view=catalogue'); JHtmlSidebar::addFilter( JText::_('JOPTION_SELECT_PUBLISHED'), 'filter_published', JHtml::_( 'select.options', JHtml::_('jgrid.publishedOptions'), 'value', 'text', $this->state->get('filter.published'), true ) ); JHtmlSidebar::addFilter( JText::_( 'JOPTION_SELECT_CATEGORY'), 'filter_cat_id', JHtml::_( 'select.options', CatalogueHelper::getCategoriesOptions(), 'value', 'text', $this->state->get('filter.cat_id') ) ); } }
Теперь разберемся с каждой строчкой:
1: Все стандартно;
3: Загрузка класса помошника (вернемся к нему чуть позже);
5: Объявление класса CatalogueViewCatalogue;
7 - 10: Подготовка переменных;
12: Функция JView::display() - если помните у контроллера тоже есть эта функция при ее вызове срабатывает и эта функция! В данном случае она позволяет нам обратиться к модели, загрузить нужные данные и отправиться дальше;
14 - 17: Вызовы функций Модели JModelList, которые мы писали в прошлой статье, обратите внимание на функцию getCategories() ее не было чтобы не усложнять модель, но сейчас она нужно и мы ее добавим в модель чуть позже;
19 - 23: Функция класса JObject getErrors() возвращает список всех ошибок и если таковые есть, то нужно сказать об этом пользователю;
25: Вызов функции из класса-помошника;
27: Вызов функции addToolbar() - она формирует панель кнопок Joomla.
29: Формируем боковую понель;
30: Действие по умолчанию для класса JViewLegacy;
33: Объявление функции addToolbar();
34: Получаем объект класса JAccess чтобы проверять права пользователя и в зависимости от них решаем показывать или скрывать определенные действия;
37: Создаем заголовок;
39 - 41: Проверяем права на создание новой записи и если все ок то добавляем кнопку в тулбар;
43 - 46: Проверяем права на редактирование;
48 - 67: Проверка прав на изменение состояния (опубликовано или не опубликовано и т.д.)
69 - 72: Кнопка "Разблокировать";
74 - 81: Условие для показа кнопок "В корзину" и "Очистить корзину". В Joomla объект считается в корзине если поле state = -2. Данное условие как раз и проверяет сначало состояние поля state а потом права пользователя на удаление.
84 - 87: Проверка прав на администрирование компонента и добавление кнопки "Настройки" если это администратор;
92 - 102: Формирование сайдбара, добавляем списки фильтров категорий и состояний записей, чтобы была возможность выводить удаленные объекты или товары из определенной категории каталога.