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. Шаг - Файловая структура компонента

После того как сохраним все файлы должно получиться вот так:

com catalogue step1 image 01
Структура каталога

Но это далеко не все. Вернемся немного назад и вспомним о XML структуре administrator - это ничто иное как описание административной части:

Путь в XML: extension -> administrator -> menu - это заголовок главного меню Joomla

 

com catalogue step2 image 02
Заголовок меню (тут он уже переведен на русский см. далее)

Путь в 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 части там все аналогично и даже проще, поэтому столь подробно описывать не будем, просто посмотрим на конечную структуру компонента.

 com catalogue step2 image 03

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 развернется перед нами во всей красе! Обведенная область показывает то что умеет наш компонент на данный момент.

com catalogue step4 image 04
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);
		}
    }



Время нарисовать диаграмму что бы понять что мы сделали

com catalogue step5 image 05




7. Шаг - создание класса JViewLegacy

Сначала надо создать файловую структуру. Папка views у нас уже должна быть, но до сих пор пустая, сейчас там нужно создать папку с названием вида т.е. catalogue, если еще не заметили то поройтесь в компонентах Joomla и везде заметите что основной вид и сам компонент обычно совпадают по названию (переопределить конечно можно, но лучше оставить там как требует Joomla). Итак создаем в папке views папку catalogue в ней файл view.html.php и папку tmpl, а в ней default.php. Папка tmpl хранит HTML шаблоны (layouts) для вывода информации полученной из модели. В итоге должна получится такая структура:

com catalogue step7 image 06
Ну а теперь код файла 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.

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

Никаких мыслей по поводу “Создание компонента "Каталог" для Joomla 3”