Создание компонента "Каталог" для 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: Формирование сайдбара, добавляем списки фильтров категорий и состояний записей, чтобы была возможность выводить удаленные объекты или товары из определенной категории каталога.