Повар готовит Joomla

Эта статья будет полезна, скорее, разработчикам и веб-мастерам,  работающим с Joomla. Однако, она может быть полезна также и сео-специалистам и самим владельцам бизнеса.

Современные интернет-магазины и каталоги: эволюция.

Upd 10.07.2023: Статья написана в 2020 году, однако этот подход вполне работает и 2024. Описываемые в статье расширения имеют версии для Joomla 4 и Joomla 5.

К 2020 году эволюция бизнеса в интернете привела к тому, что изменилась покупательская модель поведения. Стираются границы между онлайном и оффлайном. Мы можем придти в оффлайн-магазин, но сделать заказ через сайт и получить покупку на выдаче товара быстрее, чем стоя в очереди  у кассы. Во многих случаях заказ происходит по телефону, через мессенджеры и сообщения в социальных сетях. В то же время покупатели-компании не будут собирать корзину товаров в интернет-магазине, а скорее просто позвонят по телефону и продиктуют список артикулов и объём закупки, а реквизиты пришлют по электронной почте. Многие интернет-магазины на самом деле работают в режиме каталога, так как полноценный функционал (личный кабинет покупателя, онлайн-оплата и т.д.) для них оказывается избыточен. Для некоторых видов бизнеса более актуальным оказывается жанр интернет-витрины с возможностью быстрого заказа

Для Joomla довольно много компонентов каталогов, многие из них пользуются заслуженной популярностью, однако...

...собственно проблема

Проблема заключается в том, что использование сторонних компонентов, не входящих в ядро Joomla, накладывает на нас ограничения и дополнительную зависимость от разработчиков этого компонента.

Простой пример из личного опыта

Многие годы для создания фото-архивов или портфолио на сайтах клиентов я использовал компонент JoomGallery. В нём можно было быстро и удобно загружать фото, обрабатывать их во время загрузки, экономить тем самым место на сервере. Но, технологии не стоят на месте, а с течением времени основная команда разработчиков перестала обновлять JoomGallery. Я также столкнулся с проблемой дублей страниц при использовании нескольких пунктов меню с JoomGallery. В самой Joomla эта проблема давно решена, а вот в JoomGallery осталась. В итоге на боевом посещаемом сайте появилось около 1000 дублей, которые оказались в индексе поисковых систем и на эти страницы, порой совсем не понятные, приходили люди и как-то что-то пытались понять на сайте.

Сама Joomla регулярно обновляется, получает исправление ошибок безопасности, получает спонсорскую поддержку крупных компаний (1 октября 2020 года Google объявил о спонсорской поддержке CMS Joomla). Она способна выдерживать значительные нагрузки: есть примеры интернет-магазинов с более чем 30 000 000 товаров и сайтов-новостников с более чем 230 000 уникальных посетителей в сутки. Но не все сторонние расширения поддерживаются в актуальном состоянии их разработчиками. 

Отчасти это происходит из-за того, что многие веб-мастера и владельцы сайтов используют варёз (warez). Программирование и разработка - очень ресурсоёмкий процесс. Если он не приносит разработчику хоть какой-то доход - разработчик теряет интерес к своему детищу. И эта ситуация характерна не только для Joomla, но и для любых программных продуктов: либо продукт платный и развивается, либо продукт бесплатный и развивается за счет регулярных добровольных пожертвований и/или усилий сообщества.

Что такое варез
Варез
"Варез" не совсем понятное слово большенству пользователей. Гораздо понятнее - написать "мошенники". Расширения, были украдены, либо у авторов их нет свосем, а вместо самих расширений они публикуют на своих ресурсах предложения купить чужие разработки за смешные деньги. Пользователи, в погоне за "экономией", оплачивают покупку и получают - "ничего".
500р - не такие уж большие деньги, чтобы раздувать скандал и идти в суд. За счёт этого и живут эти сайты.
Второй вариант - у мошенников действительно есть расширение, которое вы хотите приобрести, но они, получив от вас деньги, не оказывают никакой поддержки, расширние не обновляется и, скорее всего, вы купите очень устаревшую версию, котрая содержит в себе уязвимости, ошибки и в них нет части заявленных функций.
Ещё один вариант развития событий - вам продают (или даже дают бесплатно) расшерение, котрое украли у разработчика и "взломали". Оно имеет последнюю версию и выглядит вполне "легально". Но кто же в наше время хочет обидеть себя монетой? Никто! Поэтому мошенники закладывают в код расширения зловреды, которые потом будут публиковать на вашем сайте рекламу, на которой зарабатывают именно эти мошенники. И вы, всё так же, не имете официальной поддержки разработчика и обновлений, а так же вынуждены дополнительно тратить деньги на специалиста, кторый будет чистить ваш сайт, после того, как там "нагадит" такое расширение с рекламой.

Сообщение в Telegram-чате "Joomla по-русски"

Вывод: чем меньше на сайте сторонних расширений, тем лучше для сайта.

Решение проблемы

Поиск решения проблемы приводит к созданию разных технологий и инструментов. Начиная с версии 3.7 в Joomla появился функционал custom fields - пользовательских полей. Эти поля можно добавлять к материалам, категориям материалов, пользователям, контактам. В панели администратора эти поля заполняются контент-менеджером, а снаружи для пользователя показываются как часть материала (страницы контакта или профиля пользователя). 

Нас спасает то, что Joomla - это не просто CMS (система управления контентом), а фреймворк (PHP framework, а-ля Yii, Laravel и т.д.) со стандартами PSR. Это означает, что Joomla удобна для программирования. Заложенная в ядро системы возможность переопределения позволяет полностью переосмыслить возможности применения стандартных компонентов.

С помощью компонента материалов Joomla стало возможно собрать:

  • каталог,
  • интернет-витрину,
  • простой интернет-магазин,
  • сервис по продаже доступа к видео-курсам
  • и многое-многое другое.

Такой подход избавляет от проблем с безопасностью, дублями страниц, позволяет сделать именно то. что нужно клиенту, не погружаясь в тонкости архитектуры и реализации её в сторонних компонентах. 

Как создать каталог или интернет-магазин?

Мы понимаем, что количество разнообразнейших интернет-магазинов и каталогов просто огромно. Поэтому заранее определим необходимый функционал. В качестве примера будет разбираться сайт одних моих клиентов, изготавливающих памятники.

Исходные данные

Существует каталог-портфолио выполненных работ. В карточке установки изложена информация о памятнике и проделанных работах, оказанных услугах. Есть форма захвата контакта - "Заказать такой же".

Задача

Перенести существующий каталог-портфолио с JoomGallery на материалы Joomla.

Необходимый функционал:

  • набор специфических характеристик для установок памятников,
  • наличие фильтра по этим характеристиками,
  • внешний вид как у интернет-магазина,
  • возможность быстрого заказа как из карточки установки, так и из категории,
  • карусельки и всплывающие окна для фотографий,
  • важно: удобство для заполнения менеджерами, далекими от web-разработки и даже аббревиатуры "HTML" никогда не слышавшими.

Отдельно скажем о СЕО-фильтре, который позволяет делать посадочные страницы для подборок товаров по фильтрам. СЕО-специалисты любят этот инструмент потому, что благодаря ему можно быстро создать множество посадочных под низко- и среднечастотные запросы. Подобные решения есть и для других CMS: Битрикс, Wordpress. 

Необходимые расширения

Radical Multifield

Плагин поля Joomla, который добавляет возможность использовать повторяемые поля Joomla в пользовательских полях 🤪 (ох уж эта терминология!..). Плагин поддерживает создание собственных шаблонов вывода поля, что позволяет использовать его в самых разных случаях:

  • простая фотогалерея или слайдер, заполняемые прямо при редактировании материала;
  • небольшая таблица или прайс-лист для статьи;
  • аудио-плеер со списокм записей.

Все зависит от Вашей фантазии. Скачать можно:

GitHub Сайт разработчика

Можно обойтись без него, но с ним удобнее.


Quantum Manager

Бесплатный файловый менеджер для Joomla! с помощью которого Вы сможете загружать, редактировать и вставлять в редактор (а так же и поля) файлы. Есть возможность переопределить вызовы стандартного файлового менеджера.

У этого файлового менеджера неоспоримое преимущество перед известным JCE File Browser, не говоря о стандартном. И он полностью бесплатен. Подробное описание по ссылкам. Скачать можно:

GitHub Сайт разработчика Joomla Extensions Directory

Также - что очень удобно - у Radical Multifield есть интеграция с Quantum Manager, что позволяет загружать файлы прямо из материала простым перетаскиванием - drag'n'drop. При загрузке происходит автоматическое изменение размера изображений и создание миниатюр.

Можно обойтись без него, но с ним удобнее.


JL Content Fields Filter

Это бесплатный модуль, который фильтрует материалы Joomla согласно настраиваемым полям. Фильтрация происходит без перезагрузки страницы (по ajax).

GitHub Сайт разработчика Joomla Extensions Directory


Создание полей

Нам потребуется отдельная категория материалов для каталога-портфолио. Назовем её "Установки". В ней находятся 4 подкатегории, по ценовым диапазонам - бюджетные, элитные, премиум и эксклюзив.

Создадим группу полей "Установки" и "Изображения установок". В первой будут находится характеристики установки памятинка: цена, год. вид стелы, материал (гранит/мрамор) и т.д. Во второй - фотографии установки и скриншоты 3D-проекта.

А так это будет выглядеть в материале:

Группы полей в материале

В нашем случае создано 11 полей в группе "Установки" и одно поле в группе "Изображения установок". Поскольку мы не хотим, чтобы значения полей отображались автоматически. в настройках каждого поля нужно указать в параметрах отображения Отображение поля ⇒ Не отображать автоматически.

field settings

Теперь все данные полей полностью в нашей власти и мы вольны выводить их как нам угодно. Типы используемых полей - текст. выпадающий список, чекбоксы - не вызывают вопросов. Поскольку мы предварительно установили JL Content Fields Filter, в настройках каждого поля добавляется следующее:

Параметры фильтрации в настройках полей Joomla

"Вид на фильтре" и "Индивидуальный макет" позволяют управлять внешним видом выводимых полей. Подберите неободимый для себя вид отображения. Также Вы можете настроить вид элементов фильтра. Для этих целей создайте переопределение макетов модуля в папке Вашего шаблона: копируем файлы из папки modules/mod_jlcontentfieldsfilter/layouts/mod_jlcontentfieldsfilter/ в папку templates/ВАШ_ШАБЛОН/html/layouts/mod_jlcontentfieldsfilter/ и можем изменять выводимый в фильтре html-код характеристик. При обновлении модуля Ваши изменения не потеряются. Я таким образом изменил часть используемых модулем стилей на стили Bootstrap 4.

Поле Joomla для изображений

Вариант 1. Без использования Quantum Manager и  Radical Multifield.

Мы создаем поле типа "Повторяемая форма". Внизу у нас появляется зеленая кнопка для добавления полей.

скриншот настроек пользовательских полей Joomla для каталога

 Для каждого изображения нам нужны 2 параметра: путь к картинке (тип поля медиа) и название (тип поля текст), которое мы будем помещать в атрибут alt изображения.

настройки поля повторяемая форма joomla скриншот

Вот так происходит добавление списка изображений контент-менеджером. Аналогичным образом можно сделать и заполнение новостного сайта. Минус заключается в том, что изображения должны быть заранее подготовлены: изменен размер изображения, оптимизирован "вес" картинок. Оригиналы изображений весят несколько мегабайт, размер их несколько тысяч пикселей по обеим сторонам. В web, как правило, такие размеры и вес не нужны, так как при большом количестве подобных фотографий загрузка происходит крайне медленно и долго. Для пакетной (массовой) обработки изображений подходит программа ImBatch.

Загрузка изображений в пользовательские поля Joomla в материалах. Quantum Manager.

В данном видео уже показан Quantum Manager, однако его необычный функционал не используется. Изображения подготовлены заранее.

Вариант 2. Настройка drag'n'drop загрузки изображений в статьи/карточки каталога с использованием Quantum Manager и  Radical Multifield.

Обзор настроек плагина поля Radical Multifield для Joomla 3. 

Даже беглый обзор показывает, что плагин поля Radical Multifield для Joomla сильно расширяет диапазон возможностей для разработчика. В повторяемых полях больше типов полей, по сравнению со стандартным. Для типа "custom" можно напрямую указывать свой xml-код поля, что позволяет отображать данные сторонних библиотек, API, сервисов, свои выгрузки из баз данных и т.д. Для поля возможно создавать свои макеты вывода: аудио или видео плеер, карсусель или плитка изображений и т.д.

Quantum manager при загрузке изображений позволяет на лету изменять размеры изображений, есть встроенный редактор изображений, интеграция с бесплатными фотостоками. Интеграция Radical Multifield с Quantum Manager позволяет использовать этот функционал при заполнении материала.

Для нас сейчас важен функционал drag'n'drop загрузки изображений и автоматической их обработки: изменения размера и создания миниатюр. Так как основная задача - максимально упростить работу для контент-менеджеров, которые могут не обладать должной компьюетрной грамотностью.

Переопределение шаблона вывода материала Joomla

Это, пожалуй, одна из важнейших особенностей Joomla - возможность изменить под себя (кастомизировать) вывод любого компонента, модуля, макета. Весь пользовательский код находится в папке с шаблоном и не зависит от основного кода. То есть при обновлении CMS или отдельных расширений все изменения сохранятся.

Для каталога нам нужны 2 вида: вид категории и вид карточки товара. Поэтому мы копируем в папку с нашим шаблоном соответствующие view компонента материалов com_content

Переопределение вида категории материалов Joomla

Файлы из папки components/com_content/views/category/tmpl/ мы копируем в папку templates/ВАШ_ШАБЛОН/html/com_content/category/. Мы создали переопределение. Если мы оставим имена файлов такими же, то будут использоваться файлы из переопределения в шаблоне, а не из папки с компонентом.

Теперь нам нужно создать свой собственный тип пункта меню.

 Пользовательский тип пункта меню в Joomla 3
Пользовательский тип пункта меню в Joomla 3. Позволит отобразить нам наши макеты для каталога установок.

 Для этого мы скопированные в templates/ВАШ_ШАБЛОН/html/com_content/category/ файлы переименовываем. В нашем случае мы создаем тип пункта меню "Установки". За основу берем файлы блога категории, "blog" заменяем на "ustanovki".

 Свой тип пункта меню Joomla 3. Названия файлов.
Свой тип пункта меню Joomla 3. Названия файлов.
  • blog.php  - основной файл макета - заменяем на ustanovki.php
  • blog_children.php - макет вывода дочерних, вложенных категорий - этот файл вызывается в blog.php - заменяем на ustanovki_children.php
  • blog_item.php - макет вывода статьи в блоге (заголовок, изображение, вводный текст, кнопка "подробнее", поля) - заменяем на ustanovki_item.php
  • blog_links.php - макет вывода ссылок на статьи перед пагинацией. Если необходимо, то переименовываем в ustanovki_links.php

Анализ кода стандартного файла blog.php

Здесь представлен код стандартного макета блога материалов. Комментарии в коде описывают его назначение и отделяют от других фрагментов. Конструкция echo $this->loadTemplate('item'); загружает файл с именем blog_item.php. Аналогично загружаются макеты children, links. Мы вольны создавать свои макеты и называть их так, как нам вздумается.

Код файла blog.php

<?php
/**
 * @package     Joomla.Site
 * @subpackage  com_content
 *
 * @copyright   Copyright (C) 2005 - 2017 Open Source Matters, Inc. All rights reserved.
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 */

defined('_JEXEC') or die;

JHtml::addIncludePath(JPATH_COMPONENT . '/helpers');

JHtml::_('behavior.caption');

$dispatcher = JEventDispatcher::getInstance();

$this->category->text = $this->category->description;
$dispatcher->trigger('onContentPrepare', array($this->category->extension . '.categories', &$this->category, &$this->params, 0));
$this->category->description = $this->category->text;

$results = $dispatcher->trigger('onContentAfterTitle', array($this->category->extension . '.categories', &$this->category, &$this->params, 0));
$afterDisplayTitle = trim(implode("\n", $results));

$results = $dispatcher->trigger('onContentBeforeDisplay', array($this->category->extension . '.categories', &$this->category, &$this->params, 0));
$beforeDisplayContent = trim(implode("\n", $results));

$results = $dispatcher->trigger('onContentAfterDisplay', array($this->category->extension . '.categories', &$this->category, &$this->params, 0));
$afterDisplayContent = trim(implode("\n", $results));

?>

<div class="blog<?php echo $this->pageclass_sfx; ?>" itemscope itemtype="https://schema.org/Blog">

<!-- Заголовок страницы, h1 -->


			<?php if ($this->params->get('show_page_heading')) : ?>
				<div class="page-header">
					<h1> <?php echo $this->escape($this->params->get('page_heading')); ?> </h1>
				</div>
			<?php endif; ?>

			<?php if ($this->params->get('show_category_title', 1) or $this->params->get('page_subheading')) : ?>
				<h2> <?php echo $this->escape($this->params->get('page_subheading')); ?>
					<?php if ($this->params->get('show_category_title')) : ?>
						<span class="subheading-category"><?php echo $this->category->title; ?></span>
					<?php endif; ?>
				</h2>
			<?php endif; ?>
			<?php echo $afterDisplayTitle; ?>
	
<!-- END Заголовок страницы, h1 -->
	
	
<!-- Теги категории -->
			<?php if ($this->params->get('show_cat_tags', 1) && !empty($this->category->tags->itemTags)) : ?>
				<?php $this->category->tagLayout = new JLayoutFile('joomla.content.tags'); ?>
				<?php echo $this->category->tagLayout->render($this->category->tags->itemTags); ?>
			<?php endif; ?>
	
<!-- END Теги категории -->


<!-- Описание категории -->
			<?php if ($beforeDisplayContent || $afterDisplayContent || $this->params->get('show_description', 1) || $this->params->def('show_description_image', 1)) : ?>
				<div class="category-desc clearfix">
					<?php if ($this->params->get('show_description_image') && $this->category->getParams()->get('image')) : ?>
						<img src="/<?php echo $this->category->getParams()->get('image'); ?>" alt="<?php echo htmlspecialchars($this->category->getParams()->get('image_alt'), ENT_COMPAT, 'UTF-8'); ?>"/>
					<?php endif; ?>
					<?php echo $beforeDisplayContent; ?>
					<?php if ($this->params->get('show_description') && $this->category->description) : ?>
						<?php echo JHtml::_('content.prepare', $this->category->description, '', 'com_content.category'); ?>
					<?php endif; ?>
					<?php echo $afterDisplayContent; ?>
				</div>
			<?php endif; ?>
	
<!-- END Описание категории -->



<!-- Сообщение "Нет материалов" -->
	
			<?php if (empty($this->lead_items) && empty($this->link_items) && empty($this->intro_items)) : ?>
				<?php if ($this->params->get('show_no_articles', 1)) : ?>
					<p><?php echo JText::_('COM_CONTENT_NO_ARTICLES'); ?></p>
				<?php endif; ?>
			<?php endif; ?>
	
<!-- END Сообщение "Нет материалов" -->

<!-- Материалы "во всю ширину". Настройки пункта меню - Параметры блога - Во всю ширину -->
			<?php $leadingcount = 0; ?>
			<?php if (!empty($this->lead_items)) : ?>
				<div class="items-leading clearfix">
					<?php foreach ($this->lead_items as &$item) : ?>
						<div class="my-2 leading-<?php echo $leadingcount; ?><?php echo $item->state == 0 ? ' system-unpublished' : null; ?>"
							itemprop="blogPost" itemscope itemtype="https://schema.org/BlogPosting">
							<?php
							$this->item = &$item;
							echo $this->loadTemplate('item');
							?>
						</div>
						<?php $leadingcount++; ?>
					<?php endforeach; ?>
				</div><!-- end items-leading -->
			<?php endif; ?>

<!-- END Материалы "во всю ширину". Настройки пункта меню - Параметры блога - Во всю ширину -->	
	
	
<!-- Материалы основные. Настройки пункта меню - Параметры блога - Во всю ширину, Количество колонок -->		
	<?php
	$introcount = count($this->intro_items);
	$counter = 0;
	?>

	<?php if (!empty($this->intro_items)) : ?>
		<?php foreach ($this->intro_items as $key => &$item) : ?>
			<?php $rowcount = ((int) $key % (int) $this->columns) + 1; ?>
			<?php if ($rowcount === 1) : ?>
				<?php $row = $counter / $this->columns; ?>
				<div class="mb-3 items-row cols-<?php echo (int) $this->columns; ?> <?php echo 'row-' . $row; ?> row clearfix">
			<?php endif; ?>
			<div class="col-lg-<?php echo round(12 / $this->columns); ?> col-md-<?php echo round(12 / $this->columns); ?> col-sm-<?php echo round((12 / $this->columns)*2); ?> col-xs-12 item-blog">
				<div class="item column-<?php echo $rowcount; ?><?php echo $item->state == 0 ? ' system-unpublished' : null; ?>"
					itemprop="blogPost" itemscope itemtype="https://schema.org/BlogPosting">
					<?php
					$this->item = &$item;
					echo $this->loadTemplate('item');
					?>
				</div>
				<!-- end item -->
				<?php $counter++; ?>
			</div><!-- end span -->
			<?php if (($rowcount == $this->columns) or ($counter == $introcount)) : ?>
				</div><!-- end row -->
			<?php endif; ?>
		<?php endforeach; ?>
	<?php endif; ?>

<!-- END Материалы основные. Настройки пункта меню - Параметры блога - Во всю ширину, Количество колонок -->	
	
	
<!-- Ссылки на материалы со следующей страницы пагинации. Настройки пункта меню - Параметры блога - Количество ссылок -->
			<?php if (!empty($this->link_items)) : ?>
				<div class="items-more mt-4">
					<?php echo $this->loadTemplate('links'); ?>
				</div>
			<?php endif; ?>

<!-- END Ссылки на материалы со следующей страницы пагинации. Настройки пункта меню - Параметры блога - Количество ссылок -->

<!-- Подкатегории -->
	<?php if ($this->maxLevel != 0 && !empty($this->children[$this->category->id])) : ?>
		<div class="cat-children">
			<?php if ($this->params->get('show_category_heading_title_text', 1) == 1) : ?>
				<h3> <?php echo JText::_('JGLOBAL_SUBCATEGORIES'); ?> </h3>
			<?php endif; ?>
			<?php echo $this->loadTemplate('children'); ?> </div>
	<?php endif; ?>
	
<!-- END Подкатегории -->
	
<!-- Пагинация -->
	<?php if (($this->params->def('show_pagination', 1) == 1 || ($this->params->get('show_pagination') == 2)) && ($this->pagination->get('pages.total') > 1)) : ?>
		<div class="pagination mt-4 justify-content-around">
			<?php if ($this->params->def('show_pagination_results', 1)) : ?>
				<p class="counter"> <?php echo $this->pagination->getPagesCounter(); ?> </p>
				<div class="clearfix"></div>
			<?php endif; ?>
			<?php echo $this->pagination->getPagesLinks(); ?> 
  		</div>
	<?php endif; ?>
<!-- END Пагинация -->
</div>

Аналогичным образом происходит переопределение макета вывода материала - отдельной статьи, которая в нашем случае будет "карточкой установки".

Для Joomla 4 

(Upd:. 02.03.2023) Если Вы делаете переопределения "с нуля", то Вы скопируете файлы с новым синтаксисом и всё будет работать. Если Вы обновляете уже готовый каталог с Joomla 3 на Joomla 4, то в переопределения нужно внести изменения:

  1. Убираем строку $dispatcher = JEventDispatcher::getInstance(); вообще. В Joomla 4 убран устаревший класс JEventDispatcher
  2. В самом начале файла добавляем use Joomla\CMS\Factory;
  3. После секции с use ... добавляем $app = Factory::getApplication();
  4. Все упоминания $dispatcher->trigger заменяем на $app->triggerEvent.
  5. Использование JHtml::_('behavior.caption'); в Joomla 4 вызовет ошибку. Удаляем.
  6. Все упоминания JHtml заменяем на HTMLHelper. В этом случае в начале файла, рядом с use Joomla\CMS\Factory; (до или после) вставляем use Joomla\CMS\HTML\HTMLHelper;
  7. Все упоминания JText нужно заменить на Text. В начале файла добавляем use Joomla\CMS\Language\Text;

К слову сказать, этот код будет работать и на поздних версиях Joomla 3 - Joomla 3.9.x, 3.10.x.

XML-файл переопределения.

XML-файл ustanovki.xml - это переименованный blog.xml. В нём описаны поля настроек пункта меню

 joomla3 xml file menu item settings
Поля настроек реализованы с помощью класса API Joomla JForm. При желании, Вы можете создавать свои поля для настройки со своими названиями. Да, это потребует времени и усилий, но Joomla это позволяет.

Для того, чтобы мы увидели новый тип меню при создании нам потребуется свое название и описание для нового типа пункта меню. Это секция <layout> в самом начале файла - пишем название в title:


<layout title="Установки" option="COM_CONTENT_CATEGORY_VIEW_BLOG_OPTION">
	<help key = "JHELP_MENUS_MENU_ITEM_ARTICLE_CATEGORY_BLOG" />
	<message>
		<![CDATA[COM_CONTENT_CATEGORY_VIEW_BLOG_DESC]]>
	</message>
</layout>
Если делать канонично, по правилам, то мы должны создать свою языковую константу, указать её в языковых файлах хотя бы на английском и разрабатываемом языке и в xml-файле указывать именно константу. Но, Joomla принимает названия и напрямую из xml-файла. Поскольку файлы переопределений находятся в папке html шаблона, при обновлении CMS или расширений они не пострадают.
 Пользовательский тип пункта меню в Joomla 3
Светло-серый текст описания типа пункта меню - это языковая константа COM_CONTENT_CATEGORY_VIEW_BLOG_DESC. Вы можете указать свою константу или описание в файле.

Наш шаблон на Bootstrap 4.5 Начиная с версии 4.4.1 в Bootstrap появилась полезная серия классов row-cols-*, которая позволяет указывать количество колонок буквально добавлением одной цифры. Для этого Вы должны понимать принцип работы css flexbox. Подробнее в документации Bootstrap 4 ⇒ Grid cards

Вид списка категорий

Мы используем карточки Boostrap как для вывода категорий, так и для вывода установок. В настройках пункта меню есть параметр Количество колонок. Поскольку стандартный шаблон Joomla 3 был создан во времена Bootstrap 2, то в нём ещё используются вычисления для нужного количества колонок вида <div class="span<?php echo round(12 / $this->columns); ?>">. Сейчас же мы просто можем добавить $this->columns к классу row-cols-, а лишний код удалить. Один из вариантов реализации:


<?php if ($this->maxLevel != 0 && !empty($this->children[$this->category->id])) : ?>
	<div class="row row-cols-1 row-cols-md-<?php echo $this->params->get("child_categories_columns");?> cat-children">
		<?php echo $this->loadTemplate('children'); ?>
        </div>
<?php endif; ?>

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

 Вид переопределенных категорий материалов Joomla 3
Все данные заносятся в стандартные поля при редактировании категории.

Добавить свои настройки для переопределения макета Joomla в пункт меню

(Upd:. 02.03.2023) В xml-файл для переопределения можно добавить свои поля настроек и получать к ним доступ в php коде. Например, Вы можете добавить своё количество колонок для разных Bootstrap брейкпойнтов. Для примера, это будут поля типа number (целое число). Все типы полей можно посмотреть в официальной документации Joomla.


<field
	name="bootstrap_lg_columns"
	type="number"
	label="Bootstrap LG columns"
	filter="integer"
/>

У полей должны быть уникальные названия (name). Label - это видимое название поля в админке. Добавлять поле нужно в <fieldset>. Можно в <fieldset name="basic" label="JGLOBAL_CATEGORY_OPTIONS">, либо же создать свой <fieldset>, для которого должно быть уникальное имя - name и label - это отображаемое название вкладки параметров в настройках пункта меню. Тогда все ваши настройки будут находится в отдельной вкладке. Это удобно.

В php вы будете получать значения параметров также, как и стандартные параметры: 


<div class="row row-cols-1 
		row-cols-md-<?php echo $this->params->get('num_columns'); ?> 
		row-cols-lg-<?php echo $this->params->get('bootstrap_lg_columns'); ?>
		 g-4">
	</div>

Хороший пример добавления своих параметров в переопределения Joomla можно увидеть здесь (репозиторий на GitHub одного из создателей Joomla - Brian Teeman). По этой ссылке в файле card.xml занесены дополнительные параметры настроек отображения материалов Joomla. А в дополнительных макетах видно как они применяются. Описание и скриншоты можно увидеть на главной странице репозитория Joomla Cards.

Список установок (материалов)

Для списка установок решили сделать вид, похожий на традиционные каталоги товаров: крупная цена, несколько характеристик, заказ обратного звонка с отправкой информации о том какая именно установка заинтересовала посетителя. Фотографии выводятся с помощью Owl Carousel, каждую фотографию можно увеличить с помощью Lightbox.

 

Вид материала

Для переопределения макета вывода материала нужно скопировать файлы из components/com_content/views/article/tmpl/ в templates/ВАШ_ШАБЛОН/html/com_content/article/. Файлы переименовываем по той же схеме, что и макеты для категории.

joomla3 скриншот файлов переопределения макета вывода материала

После этого в настройках пункта меню у нас появится выбор макета. Это позволит использовать собственный макет карточки "товара" (установки в нашем случае) для нашего каталога.

joomla3 скриншот выбора макета материала при редактировании пункта меню

После создания переопределения для "карточки" каталога её внешний вид становится таким:

Карточка товара в каталоге на материалах Joomla

Как вывести значения пользовательских полей Joomla в категории и материале?

Получить значение пользовательского поля можно с помощью следующего кода:

Для материала, например, в файле ustanovki_item.php или в файле ustanovka.php


<?php echo $this->item->jcfields[ID_ПОЛЯ]->value;?>

 Id поля можно узнать в менеджере полей:

joomla3 id custom field

Так, например, код файла ustanovki_item.php у меня получился следующий

Код файла ustanovki_item.php

<?php
/**
 * @package     Joomla.Site
 * @subpackage  com_content
 *
 * @copyright   Copyright (C) 2005 - 2017 Open Source Matters, Inc. All rights reserved.
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 */

defined('_JEXEC') or die;
use Joomla\CMS\Factory;
// Create a shortcut for params.
$params = $this->item->params;
JHtml::addIncludePath(JPATH_COMPONENT . '/helpers/html');
$canEdit = $this->item->params->get('access-edit');
$info    = $params->get('info_block_position', 0);

// Check if associations are implemented. If they are, define the parameter.
$assocParam = (JLanguageAssociations::isEnabled() && $params->get('show_associations'));

?>
<div class="card item-blog ustanovka<?php echo $this->item->id;?>">

    <div class="<?php echo $item->state == 0 ? ' system-unpublished' : null; ?> card-body p-1" itemprop="blogPost" itemscope itemtype="https://schema.org/BlogPosting">
        <?php if ($this->item->state == 0 || strtotime($this->item->publish_up) > strtotime(JFactory::getDate())
            || ((strtotime($this->item->publish_down) < strtotime(JFactory::getDate())) && $this->item->publish_down != JFactory::getDbo()->getNullDate())) : ?>
            <h3 class="system-unpublished">
        <?php endif; ?>
                <p class="text-muted d-flex"><span class="mr-auto ustanovkaTitle"><?php echo $this->item->title;?></span> <span class="ml-auto"><?php echo $this->item->jcfields[5]->label.": <span class='ustanovkaYear'>". $this->item->jcfields[5]->value; ?></span></span></p>
                <?php if (!$params->get('show_intro')) : ?>
                    <?php // Content is generated by content plugin event "onContentAfterTitle" ?>
                    <?php echo $this->item->event->afterDisplayTitle; ?>
                <?php endif; ?>

                <?php // Content is generated by content plugin event "onContentBeforeDisplay" ?>
                <?php echo $this->item->event->beforeDisplayContent; ?>

                <?php //echo $this->item->introtext; ?>
                <h3 class="h1 text-center mx-1 ustanovkaPrice"><?php echo $this->item->jcfields[4]->value;?>р.</h3>
                <?php if($this->item->jcfields[6]->value):?>
                    <p class="text-muted text-center"><small><strong>+ <?php echo mb_strtolower($this->item->jcfields[6]->label)." ".$this->item->jcfields[6]->value; ?>р.</strong></small></p>
                <?php endif;?>
	            <?php
                if(!empty($this->item->jcfields[12]->value) && $this->item->jcfields[12]->rawvalue[0] != "other") :?>
                    <p class="text-center"><i class="fas fa-map-marker-alt"></i> <?php echo mb_strtolower($this->item->jcfields[12]->label)." ".$this->item->jcfields[12]->value; ?></p>
	            <?php elseif($this->item->jcfields[12]->rawvalue[0] == "other" && !empty($this->item->jcfields[13]->value)):?>
                    <p class="text-center"><i class="fas fa-map-marker-alt"></i> <?php echo mb_strtolower($this->item->jcfields[13]->label)." ".$this->item->jcfields[13]->value; ?></p>
	            <?php endif;?>


        </div>

        <div class="card-footer">
            <?php
            $menu = JFactory::getApplication()->getMenu();
            $active = $menu->getActive();
            $itemId = $active->id;
            $link = SEFLink("index.php?option=com_content&view=article&id=".$this->item->id."&catid=".$this->item->catid."&Itemid=".$itemId);
            if($link):?>
                <a class="btn btn-sin-blue text-white" href="/<?php echo $link;?>" itemprop="url" aria-label="Цена с установкой <?php echo htmlspecialchars($this->item->title, ENT_QUOTES, 'UTF-8'); ?>"><?php echo '<span class="fas fa-chevron-right" aria-hidden="true"></span> '; echo JText::_('Подробнее'); ?></a>
            <?php endif;?>
            <button class="btn btn-sin-blue-outline" data-simplecallback-open="211" data-ustanovka-callback-btn="<?php echo $this->item->id;?>" data-toggle="modal" data-target="#ustanovka-callback-form-modal"><i class="fa fa-phone"></i></button>

        </div>

        <?php if ($this->item->state == 0 || strtotime($this->item->publish_up) > strtotime(JFactory::getDate())
            || ((strtotime($this->item->publish_down) < strtotime(JFactory::getDate())) && $this->item->publish_down != JFactory::getDbo()->getNullDate())) : ?>
            </div>
        <?php endif;  ?>

        <?php // Content is generated by content plugin event "onContentAfterDisplay" ?>

        <?php echo $this->item->event->afterDisplayContent; ?>
</div>

 Для того, чтобы работали Owl Carousel и Lightbox в файлах category/ustanovki.php и article/ustanovka.php подключаем нужные css и js. 


<?php 
$doc = \Joomla\CMS\Factory::getDocument();
$script ='
	jQuery(document).ready(function($) {
		$(".owl-carousel").owlCarousel({
            items:1,
            loop:true,
            nav:true,
            dots:false
        })
	});

document.addEventListener("JlContentFieldsFilterLoadDataSuccess", function(event) {
    jQuery(".owl-carousel").owlCarousel({
            items:1,
            loop:true,
            nav:true,
            dots:false
        });
});
	';
$doc->addStylesheet("templates/YOUR_TEMPLATE/js/assets/owl.carousel.min.css");
$doc->addStylesheet("templates/YOUR_TEMPLATE/js/assets/owl.theme.sinay.min.css");
$doc->addStylesheet("templates/YOUR_TEMPLATE/js/lightbox2/css/lightbox.css");
$doc->addScript("templates/YOUR_TEMPLATE/js/owl.carousel.min.js");
$doc->addScript("templates/YOUR_TEMPLATE/js/lightbox2/js/lightbox.js");
$doc->addScriptDeclaration($script);

Как сделать код более читаемым спустя время?

Upd. 14.03.2024: Когда мы возвращаемся к написанному своими же руками коду спустя некоторое время - порой бывает трудно понять какими причинами мы руководствовались, совершая то или иное действие. Пусть это в меньшей степени относится к созданию своих макетов, но даже в них стоит делать код более информативным. 

Предлагаемый ниже фрагмент кода для материалов позволяет "пересобрать" массив с пользовательскми полями так, чтобы в коде они были доступны не по id, а по алиасу (системному имени) поля. Таким образом echo $this->item->customFields['ustanovka']->value является более интуитивно понятным, чем echo $this->item->jcfields[15]->value.


<?php
/**
 * Переписываем поля, чтоб можно было получить доступ по имени поля
 */

// подключаем допполя для материала, пропускаем в объекте материала - они там уже есть
$this->item->customFields = [];
foreach ($this->item->jcfields as $f)
{
	$this->item->customFields[$f->name] = $f;
}

Что в итоге получилось

Свой макет для пользовательского поля Joomla

Помните, выше по тексту я говорил о том, что галерею изображений можно сделать с помощью стандартного поля? Для этого, также нужно сделать его переопределение. В таком случае мы можем создавать свои HTML-макеты вывода пользовательских полей.

Переопределение стандартных полей Joomla

Файл components/com_fields/layouts/field/render.php копируем в templates/YOUR_TEMPLATE/html/layouts/com_fields/field/my-layout.php. В нём мы сможем получить список путей к изображениям, которые загрузили через панель администратора в карточку каталога (материал). Соответственно подключение Owl Carousel потребует некоторых усилий. Но, тем не менее, изменить макет поля Joomla можно и без сторонних расширений. Тем не менее перед нами встает задача предварительной подготовки изображений, подготовки миниатюр и указания пути к ним. Эти задачи решает плагин поля Radical MultiField.

Переопределение макета Radical Multifield

В этом плагине есть ещё одно преимущество: возможность создавать собственные макеты. Находятся они в plugins/fields/radicalmultifield/tmpl/. Для того, чтобы создать свой макет сделайте копию одного из предлагаемых разработчиками макетов и измените для своих задач. Выбрать макет поля можно при редактировании поля.

Вот таким образом, например, выглядит код для ShadowBox



<?php
/**
 * @package    Radical MultiField
 *
 * @author     delo-design.ru <info[at]delo-design.ru>
 * @copyright  Copyright (C) 2018 "Delo Design". All rights reserved.
 * @license    GNU General Public License version 2 or later; see LICENSE.txt
 * @link       https://delo-design.ru
 */

defined('_JEXEC') or die;
jimport('radicalmultifieldhelper', JPATH_ROOT . DIRECTORY_SEPARATOR . implode(DIRECTORY_SEPARATOR, ['plugins', 'fields', 'radicalmultifield'])); //подключаем хелпер
if (!$field->value)
{
	return;
}

$values = json_decode($field->value, JSON_OBJECT_AS_ARRAY);
$listtype = $this->getListTypeFromField($field);

?>
<div class="row">
	    <?php
	    $k=0;
         foreach ($values as $key => $row):

//Проверка наличия и создание, если нет, миниатюр изображений. $preview = RadicalmultifieldHelper::generateThumb($field, $row['image']); ?> <div class="col-xs-12 col-sm-6 col-md-4 col-lg-4" style="margin-bottom:15px;"> <a href="/<?php echo $row['image'];?>" rel="shadowbox[article];player=img" title="<?php echo $row['alt']; ?>"> <img src="/<?php echo $preview;?>" alt="<?php echo $row['alt']; ?>"/> </a> </div> <?php endforeach;?> </div>

 Пример для Owl Carousel + LightBox


<?php
/**
 * @package    Radical MultiField
 *
 * @author     delo-design.ru <info[at]delo-design.ru>
 * @copyright  Copyright (C) 2018 "Delo Design". All rights reserved.
 * @license    GNU General Public License version 2 or later; see LICENSE.txt
 * @link       https://delo-design.ru
 */

defined('_JEXEC') or die;
jimport('radicalmultifieldhelper', JPATH_ROOT . DIRECTORY_SEPARATOR . implode(DIRECTORY_SEPARATOR, ['plugins', 'fields', 'radicalmultifield'])); //подключаем хелпер
if (!$field->value)
{
	return;
}

$values = json_decode($field->value, JSON_OBJECT_AS_ARRAY);
$listtype = $this->getListTypeFromField($field);

?>
<div class="owl-carousel owl-theme">
	    <?php
	    $k=0;
         foreach ($values as $key => $row):
			$preview = RadicalmultifieldHelper::generateThumb($field, $row['image']);
             ?>

            <div class="item">
				<a href="/<?php echo $row['image'];?>" data-lightbox="<?php  echo substr($row['alt'],0,-1);?>" data-title="<?php echo $row['alt']; ?>">
					<img src="/<?php echo $preview;?>" alt="<?php echo $row['alt']; ?>" class=""/>
				</a>
            </div>
	    <?php endforeach;?>
</div>

Вместо заключения

Данная статья не претендует на статус единственно верного решения и подхода. Joomla - это стабильный инструмент, который можно использовать разными способами, один из которых автор попытался раскрыть. С радостью приму замечания и предложения по улучшению статьи. 

Толкачев Сергей Юрьевич
Толкачев Сергей Юрьевич

Joomla-разработчик. Контрибьютер ядра Joomla. Один из ведущих Telegram-канала русскоязычного Joomla-сообщества JoomlaFeed, один из модераторов чата русскоязычного Joomla-сообщества. Мои расширения в официальном маркетплейсе расширений Joomla - Joomla Extensions Directory. Имею публикации в официальном журнале международного Joomla-сообщества - Joomla Community Magazine.

Муж. Отец 3 детей.

Россия, Саратов.

Расширения Joomla WebTolk

89 Всего расширений
11 Категорий
391 Выпущено версий
379392 Всего скачиваний
Корзина
Корзина пуста